diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index b567129a..00000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,3 +0,0 @@ -github: [rom1v] -liberapay: rom1v -custom: ["https://paypal.me/rom2v"] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 576d4666..00000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: '' -assignees: '' - ---- - -_Please read the [prerequisites] to run scrcpy._ - -[prerequisites]: https://github.com/Genymobile/scrcpy#prerequisites - -_Also read the [FAQ] and check if your [issue][issues] already exists._ - -[FAQ]: https://github.com/Genymobile/scrcpy/blob/master/FAQ.md -[issues]: https://github.com/Genymobile/scrcpy/issues - -## Environment - - - **OS:** [e.g. Debian, Windows, macOS...] - - **Scrcpy version:** [e.g. 2.5] - - **Installation method:** [e.g. manual build, apt, snap, brew, Windows release...] - - **Device model:** - - **Android version:** [e.g. 14] - -## 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. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 524c370f..00000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -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. diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md deleted file mode 100644 index 14dc373a..00000000 --- a/.github/ISSUE_TEMPLATE/question.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: Question -about: Ask a question about scrcpy -title: '' -labels: '' -assignees: '' - ---- diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 49402a6e..00000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,514 +0,0 @@ -name: Build - -on: - workflow_dispatch: - inputs: - name: - description: 'Version name (default is ref name)' - -env: - # $VERSION is used by release scripts - VERSION: ${{ github.event.inputs.name || github.ref_name }} - -jobs: - test-scrcpy-server: - runs-on: ubuntu-latest - env: - GRADLE: gradle # use native gradle instead of ./gradlew in scripts - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup JDK - uses: actions/setup-java@v4 - with: - distribution: 'zulu' - java-version: '17' - - - name: Test scrcpy-server - run: release/test_server.sh - - build-scrcpy-server: - runs-on: ubuntu-latest - env: - GRADLE: gradle # use native gradle instead of ./gradlew in scripts - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup JDK - uses: actions/setup-java@v4 - with: - distribution: 'zulu' - java-version: '17' - - - name: Build - run: release/build_server.sh - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: scrcpy-server - path: release/work/build-server/server/scrcpy-server - - test-build-scrcpy-server-without-gradle: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup JDK - uses: actions/setup-java@v4 - with: - distribution: 'zulu' - java-version: '17' - - - name: Build without gradle - run: server/build_without_gradle.sh - - test-client: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install dependencies - run: | - sudo apt update - sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ - libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ - libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ - libv4l-dev - - - name: Test - run: release/test_client.sh - - build-linux-x86_64: - runs-on: ubuntu-22.04 - steps: - - name: Check architecture - run: | - arch=$(uname -m) - if [[ "$arch" != x86_64 ]] - then - echo "Unexpected architecture: $arch" >&2 - exit 1 - fi - - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install dependencies - run: | - sudo apt update - sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ - libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ - libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ - libv4l-dev - - - name: Build - run: release/build_linux.sh x86_64 - - # upload-artifact does not preserve permissions - - name: Tar - run: | - cd release/work/build-linux-x86_64 - mkdir dist-tar - cd dist-tar - tar -C .. -cvf dist.tar.gz dist/ - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: build-linux-x86_64-intermediate - path: release/work/build-linux-x86_64/dist-tar/ - - build-win32: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install dependencies - run: | - sudo apt update - sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ - libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ - libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ - mingw-w64 mingw-w64-tools libz-mingw-w64-dev - - - name: Build - run: release/build_windows.sh 32 - - # upload-artifact does not preserve permissions - - name: Tar - run: | - cd release/work/build-win32 - mkdir dist-tar - cd dist-tar - tar -C .. -cvf dist.tar.gz dist/ - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: build-win32-intermediate - path: release/work/build-win32/dist-tar/ - - build-win64: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install dependencies - run: | - sudo apt update - sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ - libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ - libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ - mingw-w64 mingw-w64-tools libz-mingw-w64-dev - - - name: Build - run: release/build_windows.sh 64 - - # upload-artifact does not preserve permissions - - name: Tar - run: | - cd release/work/build-win64 - mkdir dist-tar - cd dist-tar - tar -C .. -cvf dist.tar.gz dist/ - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: build-win64-intermediate - path: release/work/build-win64/dist-tar/ - - build-macos-aarch64: - runs-on: macos-latest - steps: - - name: Check architecture - run: | - arch=$(uname -m) - if [[ "$arch" != arm64 ]] - then - echo "Unexpected architecture: $arch" >&2 - exit 1 - fi - - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install dependencies - run: | - brew install meson ninja nasm libiconv zlib automake autoconf \ - libtool - - - name: Build - env: - # the default Xcode (and macOS SDK) version can be found at - # - # - # then the minimal supported deployment target of that macOS SDK can be found at - # - MACOSX_DEPLOYMENT_TARGET: 10.13 - run: release/build_macos.sh aarch64 - - # upload-artifact does not preserve permissions - - name: Tar - run: | - cd release/work/build-macos-aarch64 - mkdir dist-tar - cd dist-tar - tar -C .. -cvf dist.tar.gz dist/ - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: build-macos-aarch64-intermediate - path: release/work/build-macos-aarch64/dist-tar/ - - build-macos-x86_64: - runs-on: macos-13 - steps: - - name: Check architecture - run: | - arch=$(uname -m) - if [[ "$arch" != x86_64 ]] - then - echo "Unexpected architecture: $arch" >&2 - exit 1 - fi - - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install dependencies - run: brew install meson ninja nasm libiconv zlib automake - # autoconf and libtool are already installed on macos-13 - - - name: Build - env: - # the default Xcode (and macOS SDK) version can be found at - # - # - # then the minimal supported deployment target of that macOS SDK can be found at - # - MACOSX_DEPLOYMENT_TARGET: 10.13 - run: release/build_macos.sh x86_64 - - # upload-artifact does not preserve permissions - - name: Tar - run: | - cd release/work/build-macos-x86_64 - mkdir dist-tar - cd dist-tar - tar -C .. -cvf dist.tar.gz dist/ - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: build-macos-x86_64-intermediate - path: release/work/build-macos-x86_64/dist-tar/ - - package-linux-x86_64: - needs: - - build-scrcpy-server - - build-linux-x86_64 - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Download scrcpy-server - uses: actions/download-artifact@v4 - with: - name: scrcpy-server - path: release/work/build-server/server/ - - - name: Download build-linux-x86_64 - uses: actions/download-artifact@v4 - with: - name: build-linux-x86_64-intermediate - path: release/work/build-linux-x86_64/dist-tar/ - - # upload-artifact does not preserve permissions - - name: Detar - run: | - cd release/work/build-linux-x86_64 - tar xf dist-tar/dist.tar.gz - - - name: Package - run: release/package_client.sh linux-x86_64 tar.gz - - - name: Upload release - uses: actions/upload-artifact@v4 - with: - name: release-linux-x86_64 - path: release/output/ - - package-win32: - needs: - - build-scrcpy-server - - build-win32 - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Download scrcpy-server - uses: actions/download-artifact@v4 - with: - name: scrcpy-server - path: release/work/build-server/server/ - - - name: Download build-win32 - uses: actions/download-artifact@v4 - with: - name: build-win32-intermediate - path: release/work/build-win32/dist-tar/ - - # upload-artifact does not preserve permissions - - name: Detar - run: | - cd release/work/build-win32 - tar xf dist-tar/dist.tar.gz - - - name: Package - run: release/package_client.sh win32 zip - - - name: Upload release - uses: actions/upload-artifact@v4 - with: - name: release-win32 - path: release/output/ - - package-win64: - needs: - - build-scrcpy-server - - build-win64 - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Download scrcpy-server - uses: actions/download-artifact@v4 - with: - name: scrcpy-server - path: release/work/build-server/server/ - - - name: Download build-win64 - uses: actions/download-artifact@v4 - with: - name: build-win64-intermediate - path: release/work/build-win64/dist-tar/ - - # upload-artifact does not preserve permissions - - name: Detar - run: | - cd release/work/build-win64 - tar xf dist-tar/dist.tar.gz - - - name: Package - run: release/package_client.sh win64 zip - - - name: Upload release - uses: actions/upload-artifact@v4 - with: - name: release-win64 - path: release/output - - package-macos-aarch64: - needs: - - build-scrcpy-server - - build-macos-aarch64 - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Download scrcpy-server - uses: actions/download-artifact@v4 - with: - name: scrcpy-server - path: release/work/build-server/server/ - - - name: Download build-macos-aarch64 - uses: actions/download-artifact@v4 - with: - name: build-macos-aarch64-intermediate - path: release/work/build-macos-aarch64/dist-tar/ - - # upload-artifact does not preserve permissions - - name: Detar - run: | - cd release/work/build-macos-aarch64 - tar xf dist-tar/dist.tar.gz - - - name: Package - run: release/package_client.sh macos-aarch64 tar.gz - - - name: Upload release - uses: actions/upload-artifact@v4 - with: - name: release-macos-aarch64 - path: release/output/ - - package-macos-x86_64: - needs: - - build-scrcpy-server - - build-macos-x86_64 - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Download scrcpy-server - uses: actions/download-artifact@v4 - with: - name: scrcpy-server - path: release/work/build-server/server/ - - - name: Download build-macos - uses: actions/download-artifact@v4 - with: - name: build-macos-x86_64-intermediate - path: release/work/build-macos-x86_64/dist-tar/ - - # upload-artifact does not preserve permissions - - name: Detar - run: | - cd release/work/build-macos-x86_64 - tar xf dist-tar/dist.tar.gz - - - name: Package - run: release/package_client.sh macos-x86_64 tar.gz - - - name: Upload release - uses: actions/upload-artifact@v4 - with: - name: release-macos-x86_64 - path: release/output/ - - release: - needs: - - build-scrcpy-server - - package-linux-x86_64 - - package-win32 - - package-win64 - - package-macos-aarch64 - - package-macos-x86_64 - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Download scrcpy-server - uses: actions/download-artifact@v4 - with: - name: scrcpy-server - path: release/work/build-server/server/ - - - name: Download release-linux-x86_64 - uses: actions/download-artifact@v4 - with: - name: release-linux-x86_64 - path: release/output/ - - - name: Download release-win32 - uses: actions/download-artifact@v4 - with: - name: release-win32 - path: release/output/ - - - name: Download release-win64 - uses: actions/download-artifact@v4 - with: - name: release-win64 - path: release/output/ - - - name: Download release-macos-aarch64 - uses: actions/download-artifact@v4 - with: - name: release-macos-aarch64 - path: release/output/ - - - name: Download release-macos-x86_64 - uses: actions/download-artifact@v4 - with: - name: release-macos-x86_64 - path: release/output/ - - - name: Package server - run: release/package_server.sh - - - name: Generate checksums - run: release/generate_checksums.sh - - - name: Upload release artifact - uses: actions/upload-artifact@v4 - with: - name: scrcpy-release-${{ env.VERSION }} - path: release/output diff --git a/.gitignore b/.gitignore index 26d977ac..59bc840d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,4 @@ build/ /dist/ -/build-*/ -/build_*/ -/release-*/ .idea/ .gradle/ -/x/ -local.properties -/scrcpy-server diff --git a/doc/build.md b/BUILD.md similarity index 55% rename from doc/build.md rename to BUILD.md index 7f76b4fd..a9a2e4a2 100644 --- a/doc/build.md +++ b/BUILD.md @@ -2,25 +2,17 @@ Here are the instructions to build _scrcpy_ (client and server). -If you just want to build and install the latest release, follow the simplified -process described in [doc/linux.md](linux.md). +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). -## Branches - -There are two main branches: - - `master`: contains the latest release. It is the home page of the project on - GitHub. - - `dev`: the current development branch. Every commit present in `dev` will be - in the next release. - -If you want to contribute code, please base your commits on the latest `dev` -branch. +[prebuilt server]: #prebuilt-server ## Requirements You need [adb]. It is available in the [Android SDK platform -tools][platform-tools], or packaged in your distribution (`adb`). +tools][platform-tools], or packaged in your distribution (`android-adb-tools`). On Windows, download the [platform-tools][platform-tools-windows] and extract the following files to a directory accessible from your `PATH`: @@ -28,8 +20,6 @@ the following files to a directory accessible from your `PATH`: - `AdbWinApi.dll` - `AdbWinUsbApi.dll` -It is also available in scrcpy releases. - The client requires [FFmpeg] and [LibSDL2]. Just follow the instructions. [adb]: https://developer.android.com/studio/command-line/adb.html @@ -50,15 +40,15 @@ Install the required packages from your package manager. ```bash # runtime dependencies -sudo apt install ffmpeg libsdl2-2.0-0 adb libusb-1.0-0 +sudo apt install ffmpeg libsdl2-2.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 \ - libswresample-dev libusb-1.0-0-dev +sudo apt install make gcc pkg-config meson ninja-build \ + libavcodec-dev libavformat-dev libavutil-dev \ + libsdl2-dev # server build dependencies -sudo apt install openjdk-17-jdk +sudo apt install openjdk-8-jdk ``` On old versions (like Ubuntu 16.04), `meson` is too old. In that case, install @@ -77,10 +67,10 @@ pip3 install meson sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm # client build dependencies -sudo dnf install SDL2-devel ffms2-devel libusb1-devel libavdevice-free-devel meson gcc make +sudo dnf install SDL2-devel ffms2-devel meson gcc make # server build dependencies -sudo dnf install java-devel +sudo dnf install java ``` @@ -94,19 +84,19 @@ 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 libz-mingw-w64-dev +sudo apt install mingw-w64 mingw-w64-tools ``` You also need the JDK to build the server: ```bash -sudo apt install openjdk-17-jdk +sudo apt install openjdk-8-jdk ``` Then generate the releases: ```bash -./release.sh +make -f Makefile.CrossWindows ``` It will generate win32 and win64 releases into `dist/`. @@ -122,8 +112,7 @@ install the required packages: ```bash # runtime dependencies pacman -S mingw-w64-x86_64-SDL2 \ - mingw-w64-x86_64-ffmpeg \ - mingw-w64-x86_64-libusb + mingw-w64-x86_64-ffmpeg # client build dependencies pacman -S mingw-w64-x86_64-make \ @@ -137,8 +126,7 @@ 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 + mingw-w64-i686-ffmpeg # client build dependencies pacman -S mingw-w64-i686-make \ @@ -162,19 +150,19 @@ Install the packages with [Homebrew]: ```bash # runtime dependencies -brew install sdl2 ffmpeg libusb +brew install sdl2 ffmpeg # client build dependencies brew install pkg-config meson ``` -Additionally, if you want to build the server, install Java 17 from Caskroom, and -make it available from the `PATH`: +Additionally, if you want to build the server, install Java 8 from Caskroom, and +make it avaliable from the `PATH`: ```bash -brew tap homebrew/cask-versions -brew install adoptopenjdk/openjdk/adoptopenjdk17 -export JAVA_HOME="$(/usr/libexec/java_home --version 1.17)" +brew tap caskroom/versions +brew cask install java8 +export JAVA_HOME="$(/usr/libexec/java_home --version 1.8)" export PATH="$JAVA_HOME/bin:$PATH" ``` @@ -185,44 +173,30 @@ See [pierlon/scrcpy-docker](https://github.com/pierlon/scrcpy-docker). ## Common steps -**As a non-root user**, clone the project: +If you want to build the server, install the [Android SDK] (_Android Studio_), +and set `ANDROID_HOME` to its directory. For example: + +[Android SDK]: https://developer.android.com/studio/index.html + +```bash +export ANDROID_HOME=~/android/sdk +``` + +If you don't want to build the server, use the [prebuilt server]. + +Clone the project: ```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 +meson x --buildtype release --strip -Db_lto=true +cd x +ninja ``` _Note: `ninja` [must][ninja-user] be run as a non-root user (only `ninja @@ -231,27 +205,9 @@ install` must be run as root)._ [ninja-user]: https://github.com/Genymobile/scrcpy/commit/4c49b27e9f6be02b8e63b508b60535426bd0291a -#### Option 2: Use prebuilt server +### Run - - [`scrcpy-server-v3.3.1`][direct-scrcpy-server] - SHA-256: `a0f70b20aa4998fbf658c94118cd6c8dab6abbb0647a3bdab344d70bc1ebcbb8` - -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-server-v3.3.1 - -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: +To run without installing: ```bash ./run x [options] @@ -263,23 +219,33 @@ The server only works with a matching client version (this server works with the After a successful build, you can install _scrcpy_ on the system: ```bash -sudo ninja -Cx install # without sudo on Windows +sudo ninja install # without sudo on Windows ``` -This installs several files: +This installs two files: - - `/usr/local/bin/scrcpy` (main app) - - `/usr/local/share/scrcpy/scrcpy-server` (server to push to the device) - - `/usr/local/share/man/man1/scrcpy.1` (manpage) - - `/usr/local/share/icons/hicolor/256x256/apps/icon.png` (app icon) - - `/usr/local/share/zsh/site-functions/_scrcpy` (zsh completion) - - `/usr/local/share/bash-completion/completions/scrcpy` (bash completion) + - `/usr/local/bin/scrcpy` + - `/usr/local/share/scrcpy/scrcpy-server.jar` -You can then run `scrcpy`. +Just remove them to "uninstall" the application. + +You can then [run](README.md#run) _scrcpy_. -### Uninstall +## Prebuilt server + + - [`scrcpy-server-v1.8.jar`][direct-scrcpy-server] + _(SHA-256: 839055ef905903bf98ead1b9b8a127fe402b39ad657a81f9a914b2dbcb2ce5c0)_ + +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.8/scrcpy-server-v1.8.jar + +Download the prebuilt server somewhere, and specify its path during the Meson +configuration: ```bash -sudo ninja -Cx uninstall # without sudo on Windows +meson x --buildtype release --strip -Db_lto=true \ + -Dprebuilt_server=/path/to/scrcpy-server.jar +cd x +ninja +sudo ninja install ``` diff --git a/DEVELOP.md b/DEVELOP.md new file mode 100644 index 00000000..dea8137d --- /dev/null +++ b/DEVELOP.md @@ -0,0 +1,270 @@ +# scrcpy for developers + +## Overview + +This application is composed of two parts: + - the server (`scrcpy-server.jar`), 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.jar`). + +[dex]: https://en.wikipedia.org/wiki/Dalvik_(software) +[apk]: https://en.wikipedia.org/wiki/Android_application_package + + +### Hidden methods + +Although compiled against the Android framework, [hidden] methods and classes are +not directly accessible (and they may differ from one Android version to +another). + +They can be called using reflection though. The communication with hidden +components is provided by [_wrappers_ classes][wrappers] and [aidl]. + +[hidden]: https://stackoverflow.com/a/31908373/1987178 +[wrappers]: https://github.com/Genymobile/scrcpy/tree/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 messges_ + 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 client. + +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 immediatly 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 its 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 ;-) diff --git a/FAQ.md b/FAQ.md index 24722c74..4b04d228 100644 --- a/FAQ.md +++ b/FAQ.md @@ -1,204 +1,46 @@ # Frequently Asked Questions -[Read in another language](#translations) +## Common issues + +The application is very young, it is not unlikely that you encounter problems +with it. Here are the common reported problems and their status. -If you encounter any error, the first step is to upgrade to the latest version. +### On Windows, my device is not detected -## `adb` and USB issues +The most common is your device not being detected by `adb`, or is unauthorized. +Check everything is ok by calling: -`scrcpy` execute `adb` commands to initialize the connection with the device. If -`adb` fails, then scrcpy will not work. + adb devices -This is typically not a bug in _scrcpy_, but a problem in your environment. +Windows may need some [drivers] to detect your device. - -### `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 - - -## OTG issues on Windows - -On Windows, if `scrcpy --otg` (or `--keyboard=aoa`/`--mouse=aoa`) results in: - -> ERROR: Could not find any USB device - -(or if only unrelated USB devices are detected), there might be drivers issues. - -Please read [#3654], in particular [this comment][#3654-comment1] and [the next -one][#3654-comment2]. - -[#3654]: https://github.com/Genymobile/scrcpy/issues/3654 -[#3654-comment1]: https://github.com/Genymobile/scrcpy/issues/3654#issuecomment-1369278232 -[#3654-comment2]: https://github.com/Genymobile/scrcpy/issues/3654#issuecomment-1369295011 - - -## Control issues - -### Mouse and keyboard do not work +### Mouse clicks 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_ - -Rebooting the device is necessary once this option is set. [simulating input]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 -### Special characters do not work +### Mouse clicks at wrong location -The default text injection method is limited to ASCII characters. A trick allows -to also inject some [accented characters][accented-characters], -but that's all. See [#37]. +On MacOS, with HiDPI support and multiple screens, input location are wrongly +scaled. See [issue 15]. -To avoid the problem, [change the keyboard mode to simulate a physical -keyboard][hid]. +[issue 15]: https://github.com/Genymobile/scrcpy/issues/15 -[accented-characters]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-accented-characters -[#37]: https://github.com/Genymobile/scrcpy/issues/37 -[hid]: doc/keyboard.md#physical-keyboard-simulation - - -## Client issues - -### 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 +A workaround is to build with HiDPI support disabled: ```bash -export SDL_VIDEODRIVER=wayland -scrcpy +meson x --buildtype release -Dhidpi_support=false ``` -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 +However, the video will be displayed at lower resolution. ### KWin compositor crashes @@ -208,27 +50,3 @@ 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 - -If you get any exception related to `MediaCodec`: - -``` -ERROR: Exception on thread Thread[main,5,main] -java.lang.IllegalStateException - at android.media.MediaCodec.native_dequeueOutputBuffer(Native Method) -``` - -then try with another [encoder](doc/video.md#encoder). - - -## Translations - -Translations of this FAQ in other languages are available in the [wiki]. - -[wiki]: https://github.com/Genymobile/scrcpy/wiki - -Only this FAQ file is guaranteed to be up-to-date. diff --git a/LICENSE b/LICENSE index 1196b3da..cea43741 100644 --- a/LICENSE +++ b/LICENSE @@ -188,7 +188,6 @@ identification within third-party archives. Copyright (C) 2018 Genymobile - Copyright (C) 2018-2025 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Makefile.CrossWindows b/Makefile.CrossWindows new file mode 100644 index 00000000..7955c544 --- /dev/null +++ b/Makefile.CrossWindows @@ -0,0 +1,137 @@ +# 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.jar). +# +# In particular, this implies to change the location from where the client push +# the server to the device. + +.PHONY: default clean \ + build-server \ + prepare-deps-win32 prepare-deps-win64 \ + build-win32 build-win32-noconsole \ + build-win64 build-win64-noconsole \ + dist-win32 dist-win64 \ + zip-win32 zip-win64 \ + sums release + +GRADLE ?= ./gradlew + +SERVER_BUILD_DIR := build-server +WIN32_BUILD_DIR := build-win32 +WIN32_NOCONSOLE_BUILD_DIR := build-win32-noconsole +WIN64_BUILD_DIR := build-win64 +WIN64_NOCONSOLE_BUILD_DIR := build-win64-noconsole + +DIST := dist +WIN32_TARGET_DIR := scrcpy-win32 +WIN64_TARGET_DIR := scrcpy-win64 + +VERSION := $(shell git describe --tags --always) +WIN32_TARGET := $(WIN32_TARGET_DIR)-$(VERSION).zip +WIN64_TARGET := $(WIN64_TARGET_DIR)-$(VERSION).zip + +release: clean zip-win32 zip-win64 sums + @echo "Windows archives generated in $(DIST)/" + +clean: + $(GRADLE) clean + rm -rf "$(SERVER_BUILD_DIR)" "$(WIN32_BUILD_DIR)" "$(WIN64_BUILD_DIR)" \ + "$(WIN32_NOCONSOLE_BUILD_DIR)" "$(WIN64_NOCONSOLE_BUILD_DIR)" "$(DIST)" + +build-server: + [ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \ + meson "$(SERVER_BUILD_DIR)" \ + --buildtype release -Dbuild_app=false ) + ninja -C "$(SERVER_BUILD_DIR)" + +prepare-deps-win32: + -$(MAKE) -C prebuilt-deps prepare-win32 + +build-win32: prepare-deps-win32 + [ -d "$(WIN32_BUILD_DIR)" ] || ( mkdir "$(WIN32_BUILD_DIR)" && \ + meson "$(WIN32_BUILD_DIR)" \ + --cross-file cross_win32.txt \ + --buildtype release --strip -Db_lto=true \ + -Dcrossbuild_windows=true \ + -Dbuild_server=false \ + -Dportable=true ) + ninja -C "$(WIN32_BUILD_DIR)" + +build-win32-noconsole: prepare-deps-win32 + [ -d "$(WIN32_NOCONSOLE_BUILD_DIR)" ] || ( mkdir "$(WIN32_NOCONSOLE_BUILD_DIR)" && \ + meson "$(WIN32_NOCONSOLE_BUILD_DIR)" \ + --cross-file cross_win32.txt \ + --buildtype release --strip -Db_lto=true \ + -Dcrossbuild_windows=true \ + -Dbuild_server=false \ + -Dwindows_noconsole=true \ + -Dportable=true ) + ninja -C "$(WIN32_NOCONSOLE_BUILD_DIR)" + +prepare-deps-win64: + -$(MAKE) -C prebuilt-deps prepare-win64 + +build-win64: prepare-deps-win64 + [ -d "$(WIN64_BUILD_DIR)" ] || ( mkdir "$(WIN64_BUILD_DIR)" && \ + meson "$(WIN64_BUILD_DIR)" \ + --cross-file cross_win64.txt \ + --buildtype release --strip -Db_lto=true \ + -Dcrossbuild_windows=true \ + -Dbuild_server=false \ + -Dportable=true ) + ninja -C "$(WIN64_BUILD_DIR)" + +build-win64-noconsole: prepare-deps-win64 + [ -d "$(WIN64_NOCONSOLE_BUILD_DIR)" ] || ( mkdir "$(WIN64_NOCONSOLE_BUILD_DIR)" && \ + meson "$(WIN64_NOCONSOLE_BUILD_DIR)" \ + --cross-file cross_win64.txt \ + --buildtype release --strip -Db_lto=true \ + -Dcrossbuild_windows=true \ + -Dbuild_server=false \ + -Dwindows_noconsole=true \ + -Dportable=true ) + ninja -C "$(WIN64_NOCONSOLE_BUILD_DIR)" + +dist-win32: build-server build-win32 build-win32-noconsole + mkdir -p "$(DIST)/$(WIN32_TARGET_DIR)" + cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(DIST)/$(WIN32_TARGET_DIR)/" + cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/" + cp "$(WIN32_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/scrcpy-noconsole.exe" + cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/SDL2-2.0.8/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + +dist-win64: build-server build-win64 build-win64-noconsole + mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)" + cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(DIST)/$(WIN64_TARGET_DIR)/" + cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/" + cp "$(WIN64_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/scrcpy-noconsole.exe" + cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/SDL2-2.0.8/x86_64-w64-mingw32/bin/SDL2.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)" + +sums: + cd "$(DIST)"; \ + sha256sum *.zip > SHA256SUMS.txt diff --git a/README.md b/README.md index d886d23c..5026ebe2 100644 --- a/README.md +++ b/README.md @@ -1,216 +1,374 @@ -**This GitHub repo () is the only official -source for the project. Do not download releases from random websites, even if -their name contains `scrcpy`.** +# scrcpy (v1.8) -# scrcpy (v3.3.1) - -scrcpy - -_pronounced "**scr**een **c**o**py**"_ - -This application mirrors Android devices (video and audio) connected via USB or -[TCP/IP](doc/connection.md#tcpip-wireless) and allows control using the -computer's keyboard and mouse. It does not require _root_ access or an app -installed on the device. It works on _Linux_, _Windows_, and _macOS_. +This application provides display and control of Android devices connected on +USB (or [over TCP/IP][article-tcpip]). It does not require any _root_ access. +It works on _GNU/Linux_, _Windows_ and _MacOS_. ![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 +## Requirements -[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 +The Android part requires at least API 21 (Android 5.0). -Its features include: - - [audio forwarding](doc/audio.md) (Android 11+) - - [recording](doc/recording.md) - - [virtual display](doc/virtual_display.md) - - mirroring with [Android device screen off](doc/device.md#turn-screen-off) - - [copy-paste](doc/control.md#copy-paste) in both directions - - [configurable quality](doc/video.md) - - [camera mirroring](doc/camera.md) (Android 12+) - - [mirroring as a webcam (V4L2)](doc/v4l2.md) (Linux-only) - - physical [keyboard][hid-keyboard] and [mouse][hid-mouse] simulation (HID) - - [gamepad](doc/gamepad.md) support - - [OTG mode](doc/otg.md) - - and more… +Make sure you [enabled adb debugging][enable-adb] on your device(s). -[hid-keyboard]: doc/keyboard.md#physical-keyboard-simulation -[hid-mouse]: doc/mouse.md#physical-mouse-simulation +[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling -## Prerequisites - -The Android device requires at least API 21 (Android 5.0). - -[Audio forwarding](doc/audio.md) is supported for API >= 30 (Android 11+). - -Make sure you [enabled USB debugging][enable-adb] on your device(s). - -[enable-adb]: https://developer.android.com/studio/debug/dev-options#enable - -On some devices (especially Xiaomi), you might get the following error: - -``` -Injecting input events requires the caller (or the source of the instrumentation, if any) to have the INJECT_EVENTS permission. -``` - -In that case, you need to enable [an additional option][control] `USB debugging -(Security Settings)` (this is an item different from `USB debugging`) to control -it using a keyboard and mouse. Rebooting the device is necessary once this -option is set. +On some devices, you also need to enable [an additional option][control] to +control it using keyboard and mouse. [control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 -Note that USB debugging is not required to run scrcpy in [OTG mode](doc/otg.md). - ## Get the app - - [Linux](doc/linux.md) - - [Windows](doc/windows.md) (read [how to run](doc/windows.md#run)) - - [macOS](doc/macos.md) + +### Linux + +On Linux, you typically need to [build the app manually][BUILD]. Don't worry, +it's not that hard. + +For Arch Linux, an [AUR] package is available: [`scrcpy`][aur-link]. + +[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository +[aur-link]: https://aur.archlinux.org/packages/scrcpy/ + +For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link]. + +[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild +[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy -## Must-know tips +### Windows - - [Reducing resolution](doc/video.md#size) may greatly improve performance - (`scrcpy -m1024`) - - [_Right-click_](doc/mouse.md#mouse-bindings) triggers `BACK` - - [_Middle-click_](doc/mouse.md#mouse-bindings) triggers `HOME` - - Alt+f toggles [fullscreen](doc/window.md#fullscreen) - - There are many other [shortcuts](doc/shortcuts.md) +For Windows, for simplicity, prebuilt archives with all the dependencies +(including `adb`) are available: + + - [`scrcpy-win32-v1.8.zip`][direct-win32] + _(SHA-256: c0c29ed1c66deaa73bdadacd09e598aafb3a117929cf7a314cce1cc45e34de53)_ + - [`scrcpy-win64-v1.8.zip`][direct-win64] + _(SHA-256: 9cc980d07bd8f036ae4e91d0bc6fc3281d7fa8f9752d4913b643c0fb72a19fb7)_ + +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.8/scrcpy-win32-v1.8.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.8/scrcpy-win64-v1.8.zip + +You can also [build the app manually][BUILD]. -## Usage examples +### Mac OS -There are a lot of options, [documented](#user-documentation) in separate pages. -Here are just some common examples. +The application is available in [Homebrew]. Just install it: - - Capture the screen in H.265 (better quality), limit the size to 1920, limit - the frame rate to 60fps, disable audio, and control the device by simulating - a physical keyboard: +[Homebrew]: https://brew.sh/ - ```bash - scrcpy --video-codec=h265 --max-size=1920 --max-fps=60 --no-audio --keyboard=uhid - scrcpy --video-codec=h265 -m1920 --max-fps=60 --no-audio -K # short version - ``` +```bash +brew install scrcpy +``` - - Start VLC in a new virtual display (separate from the device display): +You need `adb`, accessible from your `PATH`. If you don't have it yet: - ```bash - scrcpy --new-display=1920x1080 --start-app=org.videolan.vlc - ``` +```bash +brew cask install android-platform-tools +``` - - Record the device camera in H.265 at 1920x1080 (and microphone) to an MP4 - file: - - ```bash - scrcpy --video-source=camera --video-codec=h265 --camera-size=1920x1080 --record=file.mp4 - ``` - - - Capture the device front camera and expose it as a webcam on the computer (on - Linux): - - ```bash - scrcpy --video-source=camera --camera-size=1920x1080 --camera-facing=front --v4l2-sink=/dev/video2 --no-playback - ``` - - - Control the device without mirroring by simulating a physical keyboard and - mouse (USB debugging not required): - - ```bash - scrcpy --otg - ``` - - - Control the device using gamepad controllers plugged into the computer: - - ```bash - scrcpy --gamepad=uhid - scrcpy -G # short version - ``` - -## User documentation - -The application provides a lot of features and configuration options. They are -documented in the following pages: - - - [Connection](doc/connection.md) - - [Video](doc/video.md) - - [Audio](doc/audio.md) - - [Control](doc/control.md) - - [Keyboard](doc/keyboard.md) - - [Mouse](doc/mouse.md) - - [Gamepad](doc/gamepad.md) - - [Device](doc/device.md) - - [Window](doc/window.md) - - [Recording](doc/recording.md) - - [Virtual display](doc/virtual_display.md) - - [Tunnels](doc/tunnels.md) - - [OTG](doc/otg.md) - - [Camera](doc/camera.md) - - [Video4Linux](doc/v4l2.md) - - [Shortcuts](doc/shortcuts.md) +You can also [build the app manually][BUILD]. -## Resources +## Run - - [FAQ](FAQ.md) - - [Translations][wiki] (not necessarily up to date) - - [Build instructions](doc/build.md) - - [Developers](doc/develop.md) +Plug an Android device, and execute: -[wiki]: https://github.com/Genymobile/scrcpy/wiki +```bash +scrcpy +``` + +It accepts command-line arguments, listed by: + +```bash +scrcpy --help +``` + +## Features -## Articles +### Reduce size -- [Introducing scrcpy][article-intro] -- [Scrcpy now works wirelessly][article-tcpip] -- [Scrcpy 2.0, with audio][article-scrcpy2] +Sometimes, it is useful to mirror an Android device at a lower definition to +increase performances. -[article-intro]: https://blog.rom1v.com/2018/03/introducing-scrcpy/ -[article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/ -[article-scrcpy2]: https://blog.rom1v.com/2023/03/scrcpy-2-0-with-audio/ +To limit both width and height to some value (e.g. 1024): -## Contact +```bash +scrcpy --max-size 1024 +scrcpy -m 1024 # short version +``` -You can open an [issue] for bug reports, feature requests or general questions. - -For bug reports, please read the [FAQ](FAQ.md) first, you might find a solution -to your problem immediately. - -[issue]: https://github.com/Genymobile/scrcpy/issues - -You can also use: - - - Reddit: [`r/scrcpy`](https://www.reddit.com/r/scrcpy) - - BlueSky: [`@scrcpy.bsky.social`](https://bsky.app/profile/scrcpy.bsky.social) - - Twitter: [`@scrcpy_app`](https://twitter.com/scrcpy_app) +The other dimension is computed to that the device aspect-ratio is preserved. +That way, a device in 1920×1080 will be mirrored at 1024×576. -## Donate +### Change bit-rate -I'm [@rom1v](https://github.com/rom1v), the author and maintainer of _scrcpy_. +The default bit-rate is 8Mbps. To change the video bitrate (e.g. to 2Mbps): -If you appreciate this application, you can [support my open source -work][donate]: - - [GitHub Sponsors](https://github.com/sponsors/rom1v) - - [Liberapay](https://liberapay.com/rom1v/) - - [PayPal](https://paypal.me/rom2v) +```bash +scrcpy --bit-rate 2M +scrcpy -b 2M # short version +``` -[donate]: https://blog.rom1v.com/about/#support-my-open-source-work -## License +### Crop + +The device screen may be cropped to mirror only part of the screen. + +This is useful for example to mirror only 1 eye of the Oculus Go: + +```bash +scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0) +scrcpy -c 1224:1440:0:0 # short version +``` + +If `--max-size` is also specified, resizing is applied after cropping. + + +### Wireless + +_Scrcpy_ uses `adb` to communicate with the device, and `adb` can [connect] to a +device over TCP/IP: + +1. Connect the device to the same Wi-Fi as your computer. +2. Get your device IP address (in Settings → About phone → Status). +3. Enable adb over TCP/IP on your device: `adb tcpip 5555`. +4. Unplug your device. +5. Connect to your device: `adb connect DEVICE_IP:5555` _(replace `DEVICE_IP`)_. +6. Run `scrcpy` as usual. + +It may be useful to decrease the bit-rate and the definition: + +```bash +scrcpy --bit-rate 2M --max-size 800 +scrcpy -b2M -m800 # short version +``` + +[connect]: https://developer.android.com/studio/command-line/adb.html#wireless + + +### Record screen + +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 +# Ctrl+C does not terminate properly on Windows, so disconnect the device +``` + +"Skipped frames" are recorded, even if they are not displayed in real time (for +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 + + +### Multi-devices + +If several devices are listed in `adb devices`, you must specify the _serial_: + +```bash +scrcpy --serial 0123456789abcdef +scrcpy -s 0123456789abcdef # short version +``` + +You can start several instances of _scrcpy_ for several devices. + + +### Fullscreen + +The app may be started directly in fullscreen: + +```bash +scrcpy --fullscreen +scrcpy -f # short version +``` + +Fullscreen can then be toggled dynamically with `Ctrl`+`f`. + + +### Always on top + +The window of app can always be above others by: + +```bash +scrcpy --always-on-top +scrcpy -T # short version +``` + + +### 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 disable on exit: + +```bash +scrcpy --show-touches +scrcpy -t +``` + +Note that it only shows _physical_ touches (with the finger on the device). + + +### 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/` 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. + + +### 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 +``` + +### 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 `Ctrl`+`o` at any time. + +To turn it back on, press `POWER` (or `Ctrl`+`p`). + + +### Render expired frames + +By default, to minimize latency, _scrcpy_ always renders the last decoded frame +available, and drops any previous one. + +To force the rendering of all frames (at a cost of a possible increased +latency), use: + +```bash +scrcpy --render-expired-frames +``` + + +### Forward audio + +Audio is not forwarded by _scrcpy_. + +There is a limited solution using [AOA], implemented in the [`audio`] branch. If +you are interested, see [issue 14]. + + +[AOA]: https://source.android.com/devices/accessories/aoa2 +[`audio`]: https://github.com/Genymobile/scrcpy/commits/audio +[issue 14]: https://github.com/Genymobile/scrcpy/issues/14 + + +## Shortcuts + + | Action | Shortcut | + | -------------------------------------- |:---------------------------- | + | switch fullscreen mode | `Ctrl`+`f` | + | resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` | + | resize window to remove black borders | `Ctrl`+`x` \| _Double-click¹_ | + | click on `HOME` | `Ctrl`+`h` \| _Middle-click_ | + | click on `BACK` | `Ctrl`+`b` \| _Right-click²_ | + | click on `APP_SWITCH` | `Ctrl`+`s` | + | click on `MENU` | `Ctrl`+`m` | + | click on `VOLUME_UP` | `Ctrl`+`↑` _(up)_ (`Cmd`+`↑` on MacOS) | + | click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ (`Cmd`+`↓` on MacOS) | + | click on `POWER` | `Ctrl`+`p` | + | power on | _Right-click²_ | + | turn device screen off (keep mirroring)| `Ctrl`+`o` | + | expand notification panel | `Ctrl`+`n` | + | collapse notification panel | `Ctrl`+`Shift`+`n` | + | copy device clipboard to computer | `Ctrl`+`c` | + | paste computer clipboard to device | `Ctrl`+`v` | + | copy computer clipboard to device | `Ctrl`+`Shift+`v` | + | enable/disable FPS counter (on stdout) | `Ctrl`+`i` | + +_¹Double-click on black borders to remove them._ +_²Right-click turns the screen on if it was off, presses BACK otherwise._ + + +## Custom paths + +To use a specific _adb_ binary, configure its path in the environment variable +`ADB`: + + ADB=/path/to/adb scrcpy + +To override the path of the `scrcpy-server.jar` file, configure its path in +`SCRCPY_SERVER_PATH`. + +[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345 + + +## Why _scrcpy_? + +A colleague challenged me to find a name as unpronounceable as [gnirehtet]. + +[`strcpy`] copies a **str**ing; `scrcpy` copies a **scr**een. + +[gnirehtet]: https://github.com/Genymobile/gnirehtet +[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html + + +## How to build? + +See [BUILD]. + +[BUILD]: BUILD.md + + +## Common issues + +See the [FAQ](FAQ.md). + + +## Developers + +Read the [developers page]. + +[developers page]: DEVELOP.md + + +## Licence Copyright (C) 2018 Genymobile - Copyright (C) 2018-2025 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -223,3 +381,11 @@ work][donate]: 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/ diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy deleted file mode 100644 index a49da8ca..00000000 --- a/app/data/bash-completion/scrcpy +++ /dev/null @@ -1,227 +0,0 @@ -_scrcpy() { - local cur prev words cword - local opts=" - --always-on-top - --angle - --audio-bit-rate= - --audio-buffer= - --audio-codec= - --audio-codec-options= - --audio-dup - --audio-encoder= - --audio-source= - --audio-output-buffer= - -b --video-bit-rate= - --camera-ar= - --camera-id= - --camera-facing= - --camera-fps= - --camera-high-speed - --camera-size= - --capture-orientation= - --crop= - -d --select-usb - --disable-screensaver - --display-id= - --display-ime-policy= - --display-orientation= - -e --select-tcpip - -f --fullscreen - --force-adb-forward - -G - --gamepad= - -h --help - -K - --keyboard= - --kill-adb-on-close - --legacy-paste - --list-apps - --list-camera-sizes - --list-cameras - --list-displays - --list-encoders - -m --max-size= - -M - --max-fps= - --mouse= - --mouse-bind= - -n --no-control - -N --no-playback - --new-display - --new-display= - --no-audio - --no-audio-playback - --no-cleanup - --no-clipboard-autosync - --no-downsize-on-error - --no-key-repeat - --no-mipmaps - --no-mouse-hover - --no-power-on - --no-vd-destroy-content - --no-vd-system-decorations - --no-video - --no-video-playback - --orientation= - --otg - -p --port= - --pause-on-exit - --pause-on-exit= - --power-off-on-close - --prefer-text - --print-fps - --push-target= - -r --record= - --raw-key-events - --record-format= - --record-orientation= - --render-driver= - --require-audio - --rotation= - -s --serial= - -S --turn-screen-off - --screen-off-timeout= - --shortcut-mod= - --start-app= - -t --show-touches - --tcpip - --tcpip= - --time-limit= - --tunnel-host= - --tunnel-port= - --v4l2-buffer= - --v4l2-sink= - -v --version - -V --verbosity= - --video-buffer= - --video-codec= - --video-codec-options= - --video-encoder= - --video-source= - -w --stay-awake - --window-borderless - --window-title= - --window-x= - --window-y= - --window-width= - --window-height=" - - _init_completion -s || return - - case "$prev" in - --video-codec) - COMPREPLY=($(compgen -W 'h264 h265 av1' -- "$cur")) - return - ;; - --audio-codec) - COMPREPLY=($(compgen -W 'opus aac flac raw' -- "$cur")) - return - ;; - --video-source) - COMPREPLY=($(compgen -W 'display camera' -- "$cur")) - return - ;; - --audio-source) - COMPREPLY=($(compgen -W 'output playback mic mic-unprocessed mic-camcorder mic-voice-recognition mic-voice-communication voice-call voice-call-uplink voice-call-downlink voice-performance' -- "$cur")) - return - ;; - --camera-facing) - COMPREPLY=($(compgen -W 'front back external' -- "$cur")) - return - ;; - --keyboard) - COMPREPLY=($(compgen -W 'disabled sdk uhid aoa' -- "$cur")) - return - ;; - --mouse) - COMPREPLY=($(compgen -W 'disabled sdk uhid aoa' -- "$cur")) - return - ;; - --gamepad) - COMPREPLY=($(compgen -W 'disabled uhid aoa' -- "$cur")) - return - ;; - --capture-orientation) - COMPREPLY=($(compgen -W '0 90 180 270 flip0 flip90 flip180 flip270 @0 @90 @180 @270 @flip0 @flip90 @flip180 @flip270' -- "$cur")) - return - ;; - --orientation|--display-orientation) - COMPREPLY=($(compgen -W '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur")) - return - ;; - --display-ime-policy) - COMPREPLY=($(compgen -W 'local fallback hide' -- "$cur")) - return - ;; - --record-orientation) - COMPREPLY=($(compgen -W '0 90 180 270' -- "$cur")) - return - ;; - --pause-on-exit) - COMPREPLY=($(compgen -W 'true false if-error' -- "$cur")) - return - ;; - -r|--record) - COMPREPLY=($(compgen -f -- "$cur")) - return - ;; - --record-format) - COMPREPLY=($(compgen -W 'mp4 mkv m4a mka opus aac flac wav' -- "$cur")) - return - ;; - --render-driver) - COMPREPLY=($(compgen -W 'direct3d opengl opengles2 opengles metal software' -- "$cur")) - return - ;; - --shortcut-mod) - # Only auto-complete a single key - COMPREPLY=($(compgen -W 'lctrl rctrl lalt ralt lsuper rsuper' -- "$cur")) - return - ;; - -V|--verbosity) - COMPREPLY=($(compgen -W 'verbose debug info warn error' -- "$cur")) - return - ;; - -s|--serial) - # Use 'adb devices' to list serial numbers - COMPREPLY=($(compgen -W "$("${ADB:-adb}" devices | awk '$2 == "device" {print $1}')" -- ${cur})) - return - ;; - --audio-bit-rate \ - |--audio-buffer \ - |-b|--video-bit-rate \ - |--audio-codec-options \ - |--audio-encoder \ - |--audio-output-buffer \ - |--camera-ar \ - |--camera-id \ - |--camera-fps \ - |--camera-size \ - |--crop \ - |--display-id \ - |--max-fps \ - |-m|--max-size \ - |--new-display \ - |-p|--port \ - |--push-target \ - |--rotation \ - |--screen-off-timeout \ - |--tunnel-host \ - |--tunnel-port \ - |--v4l2-buffer \ - |--v4l2-sink \ - |--video-buffer \ - |--video-codec-options \ - |--video-encoder \ - |--tcpip \ - |--window-*) - # Option accepting an argument, but nothing to auto-complete - return - ;; - esac - - COMPREPLY=($(compgen -W "$opts" -- "$cur")) - [[ $COMPREPLY == *= ]] && compopt -o nospace -} - -complete -F _scrcpy scrcpy diff --git a/app/data/icon.ico b/app/data/icon.ico deleted file mode 100644 index b3238778..00000000 Binary files a/app/data/icon.ico and /dev/null differ diff --git a/app/data/icon.png b/app/data/icon.png deleted file mode 100644 index b96a1aff..00000000 Binary files a/app/data/icon.png and /dev/null differ diff --git a/app/data/icon.svg b/app/data/icon.svg deleted file mode 100644 index 0ab92c2a..00000000 --- a/app/data/icon.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/app/data/open_a_terminal_here.bat b/app/data/open_a_terminal_here.bat deleted file mode 100644 index 24d557f3..00000000 --- a/app/data/open_a_terminal_here.bat +++ /dev/null @@ -1 +0,0 @@ -@cmd diff --git a/app/data/scrcpy-console.bat b/app/data/scrcpy-console.bat deleted file mode 100644 index 0ea7619f..00000000 --- a/app/data/scrcpy-console.bat +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -scrcpy.exe --pause-on-exit=if-error %* diff --git a/app/data/scrcpy-console.desktop b/app/data/scrcpy-console.desktop deleted file mode 100644 index fccd42b7..00000000 --- a/app/data/scrcpy-console.desktop +++ /dev/null @@ -1,13 +0,0 @@ -[Desktop Entry] -Name=scrcpy (console) -GenericName=Android Remote Control -Comment=Display and control your Android device -# For some users, the PATH or ADB environment variables are set from the shell -# startup file, like .bashrc or .zshrc… Run an interactive shell to get -# environment correctly initialized. -Exec=/bin/sh -c "\\$SHELL -i -c 'scrcpy --pause-on-exit=if-error'" -Icon=scrcpy -Terminal=true -Type=Application -Categories=Utility;RemoteAccess; -StartupNotify=false diff --git a/app/data/scrcpy-noconsole.vbs b/app/data/scrcpy-noconsole.vbs deleted file mode 100644 index d509ad7f..00000000 --- a/app/data/scrcpy-noconsole.vbs +++ /dev/null @@ -1,7 +0,0 @@ -strCommand = "cmd /c scrcpy.exe" - -For Each Arg In WScript.Arguments - strCommand = strCommand & " """ & replace(Arg, """", """""""""") & """" -Next - -CreateObject("Wscript.Shell").Run strCommand, 0, false diff --git a/app/data/scrcpy.desktop b/app/data/scrcpy.desktop deleted file mode 100644 index 9fb81d47..00000000 --- a/app/data/scrcpy.desktop +++ /dev/null @@ -1,13 +0,0 @@ -[Desktop Entry] -Name=scrcpy -GenericName=Android Remote Control -Comment=Display and control your Android device -# For some users, the PATH or ADB environment variables are set from the shell -# startup file, like .bashrc or .zshrc… Run an interactive shell to get -# environment correctly initialized. -Exec=/bin/sh -c "\\$SHELL -i -c scrcpy" -Icon=scrcpy -Terminal=false -Type=Application -Categories=Utility;RemoteAccess; -StartupNotify=false diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy deleted file mode 100644 index 04ffb8f1..00000000 --- a/app/data/zsh-completion/_scrcpy +++ /dev/null @@ -1,113 +0,0 @@ -#compdef scrcpy 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\)]' - '--angle=[Rotate the video content by a custom angle, in degrees]' - '--audio-bit-rate=[Encode the audio at the given bit-rate]' - '--audio-buffer=[Configure the audio buffering delay \(in milliseconds\)]' - '--audio-codec=[Select the audio codec]:codec:(opus aac flac raw)' - '--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]' - '--audio-dup=[Duplicate audio]' - '--audio-encoder=[Use a specific MediaCodec audio encoder]' - '--audio-source=[Select the audio source]:source:(output playback mic mic-unprocessed mic-camcorder mic-voice-recognition mic-voice-communication voice-call voice-call-uplink voice-call-downlink voice-performance)' - '--audio-output-buffer=[Configure the size of the SDL audio output buffer (in milliseconds)]' - {-b,--video-bit-rate=}'[Encode the video at the given bit-rate]' - '--camera-ar=[Select the camera size by its aspect ratio]' - '--camera-high-speed=[Enable high-speed camera capture mode]' - '--camera-id=[Specify the camera id to mirror]' - '--camera-facing=[Select the device camera by its facing direction]:facing:(front back external)' - '--camera-fps=[Specify the camera capture frame rate]' - '--camera-size=[Specify an explicit camera capture size]' - '--capture-orientation=[Set the capture video orientation]:orientation:(0 90 180 270 flip0 flip90 flip180 flip270 @0 @90 @180 @270 @flip0 @flip90 @flip180 @flip270)' - '--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-id=[Specify the display id to mirror]' - '--display-ime-policy[Set the policy for selecting where the IME should be displayed]' - '--display-orientation=[Set the initial display orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)' - {-e,--select-tcpip}'[Use TCP/IP device]' - {-f,--fullscreen}'[Start in fullscreen]' - '--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]' - '-G[Use UHID/AOA gamepad \(same as --gamepad=uhid or --gamepad=aoa, depending on OTG mode\)]' - '--gamepad=[Set the gamepad input mode]:mode:(disabled uhid aoa)' - {-h,--help}'[Print the help]' - '-K[Use UHID/AOA keyboard \(same as --keyboard=uhid or --keyboard=aoa, depending on OTG mode\)]' - '--keyboard=[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)' - '--kill-adb-on-close[Kill adb when scrcpy terminates]' - '--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]' - '--list-apps[List Android apps installed on the device]' - '--list-camera-sizes[List the valid camera capture sizes]' - '--list-cameras[List cameras available on the device]' - '--list-displays[List displays available on the device]' - '--list-encoders[List video and audio encoders available on the device]' - {-m,--max-size=}'[Limit both the width and height of the video to value]' - '-M[Use UHID/AOA mouse \(same as --mouse=uhid or --mouse=aoa, depending on OTG mode\)]' - '--max-fps=[Limit the frame rate of screen capture]' - '--mouse=[Set the mouse input mode]:mode:(disabled sdk uhid aoa)' - '--mouse-bind=[Configure bindings of secondary clicks]' - {-n,--no-control}'[Disable device control \(mirror the device in read only\)]' - {-N,--no-playback}'[Disable video and audio playback]' - '--new-display=[Create a new display]' - '--no-audio[Disable audio forwarding]' - '--no-audio-playback[Disable audio playback]' - '--no-cleanup[Disable device cleanup actions on exit]' - '--no-clipboard-autosync[Disable automatic clipboard synchronization]' - '--no-downsize-on-error[Disable lowering definition on MediaCodec error]' - '--no-key-repeat[Do not forward repeated key events when a key is held down]' - '--no-mipmaps[Disable the generation of mipmaps]' - '--no-mouse-hover[Do not forward mouse hover events]' - '--no-power-on[Do not power on the device on start]' - '--no-vd-destroy-content[Disable virtual display "destroy content on removal" flag]' - '--no-vd-system-decorations[Disable virtual display system decorations flag]' - '--no-video[Disable video forwarding]' - '--no-video-playback[Disable video playback]' - '--orientation=[Set the video orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)' - '--otg[Run in OTG mode \(simulating physical keyboard and mouse\)]' - {-p,--port=}'[\[port\[\:port\]\] Set the TCP port \(range\) used by the client to listen]' - '--pause-on-exit=[Make scrcpy pause before exiting]:mode:(true false if-error)' - '--power-off-on-close[Turn the device screen off when closing scrcpy]' - '--prefer-text[Inject alpha characters and space as text events instead of key events]' - '--print-fps[Start FPS counter, to print frame logs to the console]' - '--push-target=[Set the target directory for pushing files to the device by drag and drop]' - {-r,--record=}'[Record screen to file]:record file:_files' - '--raw-key-events[Inject key events for all input keys, and ignore text events]' - '--record-format=[Force recording format]:format:(mp4 mkv m4a mka opus aac flac wav)' - '--record-orientation=[Set the record orientation]:orientation values:(0 90 180 270)' - '--render-driver=[Request SDL to use the given render driver]:driver name:(direct3d opengl opengles2 opengles metal software)' - '--require-audio=[Make scrcpy fail if audio is enabled but does not work]' - {-s,--serial=}'[The device serial number \(mandatory for multiple devices only\)]:serial:($("${ADB-adb}" devices | awk '\''$2 == "device" {print $1}'\''))' - {-S,--turn-screen-off}'[Turn the device screen off immediately]' - '--screen-off-timeout=[Set the screen off timeout in seconds]' - '--shortcut-mod=[\[key1,key2+key3,...\] Specify the modifiers to use for scrcpy shortcuts]:shortcut mod:(lctrl rctrl lalt ralt lsuper rsuper)' - '--start-app=[Start an Android app]' - {-t,--show-touches}'[Show physical touches]' - '--tcpip[\(optional \[ip\:port\]\) Configure and connect the device over TCP/IP]' - '--time-limit=[Set the maximum mirroring time, in seconds]' - '--tunnel-host=[Set the IP address of the adb tunnel to reach the scrcpy server]' - '--tunnel-port=[Set the TCP port of the adb tunnel to reach the scrcpy server]' - '--v4l2-buffer=[Add a buffering delay \(in milliseconds\) before pushing frames]' - '--v4l2-sink=[\[\/dev\/videoN\] Output to v4l2loopback device]' - {-v,--version}'[Print the version of scrcpy]' - {-V,--verbosity=}'[Set the log level]:verbosity:(verbose debug info warn error)' - '--video-buffer=[Add a buffering delay \(in milliseconds\) before displaying video frames]' - '--video-codec=[Select the video codec]:codec:(h264 h265 av1)' - '--video-codec-options=[Set a list of comma-separated key\:type=value options for the device video encoder]' - '--video-encoder=[Use a specific MediaCodec video encoder]' - '--video-source=[Select the video source]:source:(display camera)' - {-w,--stay-awake}'[Keep the device on while scrcpy is running, when the device is plugged in]' - '--window-borderless[Disable window decorations \(display borderless window\)]' - '--window-title=[Set a custom window title]' - '--window-x=[Set the initial window horizontal position]' - '--window-y=[Set the initial window vertical position]' - '--window-width=[Set the initial window width]' - '--window-height=[Set the initial window height]' -) - -_arguments -s $arguments diff --git a/app/deps/.gitignore b/app/deps/.gitignore deleted file mode 100644 index ccf6a49e..00000000 --- a/app/deps/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/work diff --git a/app/deps/README b/app/deps/README deleted file mode 100644 index 9cfb5c06..00000000 --- a/app/deps/README +++ /dev/null @@ -1,27 +0,0 @@ -This directory (app/deps/) contains: - -*.sh : shell scripts to download and build dependencies - -patches/ : patches to fix dependencies (used by scripts) - -work/sources/ : downloaded tarballs and extracted folders - ffmpeg-6.1.1.tar.xz - ffmpeg-6.1.1/ - libusb-1.0.27.tar.gz - libusb-1.0.27/ - ... -work/build/ : build dirs for each dependency/version/architecture - ffmpeg-6.1.1/win32/ - ffmpeg-6.1.1/win64/ - libusb-1.0.27/win32/ - libusb-1.0.27/win64/ - ... -work/install/ : install dirs for each architexture - win32/bin/ - win32/include/ - win32/lib/ - win32/share/ - win64/bin/ - win64/include/ - win64/lib/ - win64/share/ diff --git a/app/deps/adb_linux.sh b/app/deps/adb_linux.sh deleted file mode 100755 index a3e339ec..00000000 --- a/app/deps/adb_linux.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash -set -ex -DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) -cd "$DEPS_DIR" -. common - -VERSION=36.0.0 -FILENAME=platform-tools_r$VERSION-linux.zip -PROJECT_DIR=platform-tools-$VERSION-linux -SHA256SUM=0ead642c943ffe79701fccca8f5f1c69c4ce4f43df2eefee553f6ccb27cbfbe8 - -cd "$SOURCES_DIR" - -if [[ -d "$PROJECT_DIR" ]] -then - echo "$PWD/$PROJECT_DIR" found -else - get_file "https://dl.google.com/android/repository/$FILENAME" "$FILENAME" "$SHA256SUM" - mkdir -p "$PROJECT_DIR" - cd "$PROJECT_DIR" - ZIP_PREFIX=platform-tools - unzip "../$FILENAME" "$ZIP_PREFIX"/adb - mv "$ZIP_PREFIX"/* . - rmdir "$ZIP_PREFIX" -fi - -mkdir -p "$INSTALL_DIR/adb-linux" -cd "$INSTALL_DIR/adb-linux" -cp -r "$SOURCES_DIR/$PROJECT_DIR"/. "$INSTALL_DIR/adb-linux/" diff --git a/app/deps/adb_macos.sh b/app/deps/adb_macos.sh deleted file mode 100755 index 36f5df89..00000000 --- a/app/deps/adb_macos.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash -set -ex -DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) -cd "$DEPS_DIR" -. common - -VERSION=36.0.0 -FILENAME=platform-tools_r$VERSION-darwin.zip -PROJECT_DIR=platform-tools-$VERSION-darwin -SHA256SUM=b241878e6ec20650b041bf715ea05f7d5dc73bd24529464bd9cf68946e3132bd - -cd "$SOURCES_DIR" - -if [[ -d "$PROJECT_DIR" ]] -then - echo "$PWD/$PROJECT_DIR" found -else - get_file "https://dl.google.com/android/repository/$FILENAME" "$FILENAME" "$SHA256SUM" - mkdir -p "$PROJECT_DIR" - cd "$PROJECT_DIR" - ZIP_PREFIX=platform-tools - unzip "../$FILENAME" "$ZIP_PREFIX"/adb - mv "$ZIP_PREFIX"/* . - rmdir "$ZIP_PREFIX" -fi - -mkdir -p "$INSTALL_DIR/adb-macos" -cd "$INSTALL_DIR/adb-macos" -cp -r "$SOURCES_DIR/$PROJECT_DIR"/. "$INSTALL_DIR/adb-macos/" diff --git a/app/deps/adb_windows.sh b/app/deps/adb_windows.sh deleted file mode 100755 index de37162c..00000000 --- a/app/deps/adb_windows.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash -set -ex -DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) -cd "$DEPS_DIR" -. common - -VERSION=36.0.0 -FILENAME=platform-tools_r$VERSION-win.zip -PROJECT_DIR=platform-tools-$VERSION-windows -SHA256SUM=24bd8bebbbb58b9870db202b5c6775c4a49992632021c60750d9d8ec8179d5f0 - -cd "$SOURCES_DIR" - -if [[ -d "$PROJECT_DIR" ]] -then - echo "$PWD/$PROJECT_DIR" found -else - get_file "https://dl.google.com/android/repository/$FILENAME" "$FILENAME" "$SHA256SUM" - mkdir -p "$PROJECT_DIR" - cd "$PROJECT_DIR" - ZIP_PREFIX=platform-tools - unzip "../$FILENAME" \ - "$ZIP_PREFIX"/AdbWinApi.dll \ - "$ZIP_PREFIX"/AdbWinUsbApi.dll \ - "$ZIP_PREFIX"/adb.exe - mv "$ZIP_PREFIX"/* . - rmdir "$ZIP_PREFIX" -fi - -mkdir -p "$INSTALL_DIR/adb-windows" -cd "$INSTALL_DIR/adb-windows" -cp -r "$SOURCES_DIR/$PROJECT_DIR"/. "$INSTALL_DIR/adb-windows/" diff --git a/app/deps/common b/app/deps/common deleted file mode 100644 index daaa96c0..00000000 --- a/app/deps/common +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env bash -# This file is intended to be sourced by other scripts, not executed - -process_args() { - if [[ $# != 3 ]] - then - # : win32 or win64 - # : native or cross - # : static or shared - echo "Syntax: $0 " >&2 - exit 1 - fi - - HOST="$1" - BUILD_TYPE="$2" # native or cross - LINK_TYPE="$3" # static or shared - DIRNAME="$HOST-$BUILD_TYPE-$LINK_TYPE" - - if [[ "$BUILD_TYPE" != native && "$BUILD_TYPE" != cross ]] - then - echo "Unsupported build type (expected native or cross): $BUILD_TYPE" >&2 - exit 1 - fi - - if [[ "$LINK_TYPE" != static && "$LINK_TYPE" != shared ]] - then - echo "Unsupported link type (expected static or shared): $LINK_TYPE" >&2 - exit 1 - fi - - if [[ "$BUILD_TYPE" == cross ]] - then - if [[ "$HOST" = win32 ]] - then - HOST_TRIPLET=i686-w64-mingw32 - elif [[ "$HOST" = win64 ]] - then - HOST_TRIPLET=x86_64-w64-mingw32 - else - echo "Unsupported cross-build to host: $HOST" >&2 - exit 1 - fi - fi -} - -DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) -cd "$DEPS_DIR" - -PATCHES_DIR="$PWD/patches" - -WORK_DIR="$PWD/work" -SOURCES_DIR="$WORK_DIR/sources" -BUILD_DIR="$WORK_DIR/build" -INSTALL_DIR="$WORK_DIR/install" - -mkdir -p "$INSTALL_DIR" "$SOURCES_DIR" "$WORK_DIR" - -checksum() { - local file="$1" - local sum="$2" - echo "$file: verifying checksum..." - echo "$sum $file" | shasum -a256 -c -} - -get_file() { - local url="$1" - local file="$2" - local sum="$3" - if [[ -f "$file" ]] - then - echo "$file: found" - else - echo "$file: not found, downloading..." - wget "$url" -O "$file" - fi - checksum "$file" "$sum" -} diff --git a/app/deps/dav1d.sh b/app/deps/dav1d.sh deleted file mode 100755 index 3069b6fe..00000000 --- a/app/deps/dav1d.sh +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env bash -set -ex -DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) -cd "$DEPS_DIR" -. common -process_args "$@" - -VERSION=1.5.0 -FILENAME=dav1d-$VERSION.tar.gz -PROJECT_DIR=dav1d-$VERSION -SHA256SUM=78b15d9954b513ea92d27f39362535ded2243e1b0924fde39f37a31ebed5f76b - -cd "$SOURCES_DIR" - -if [[ -d "$PROJECT_DIR" ]] -then - echo "$PWD/$PROJECT_DIR" found -else - get_file "https://code.videolan.org/videolan/dav1d/-/archive/$VERSION/$FILENAME" "$FILENAME" "$SHA256SUM" - tar xf "$FILENAME" # First level directory is "$PROJECT_DIR" -fi - -mkdir -p "$BUILD_DIR/$PROJECT_DIR" -cd "$BUILD_DIR/$PROJECT_DIR" - -if [[ -d "$DIRNAME" ]] -then - echo "'$PWD/$DIRNAME' already exists, not reconfigured" - cd "$DIRNAME" -else - mkdir "$DIRNAME" - cd "$DIRNAME" - - conf=( - --prefix="$INSTALL_DIR/$DIRNAME" - --libdir=lib - -Denable_tests=false - -Denable_tools=false - # Always build dav1d statically - --default-library=static - ) - - if [[ "$BUILD_TYPE" == cross ]] - then - case "$HOST" in - win32) - conf+=( - --cross-file="$SOURCES_DIR/$PROJECT_DIR/package/crossfiles/i686-w64-mingw32.meson" - ) - ;; - - win64) - conf+=( - --cross-file="$SOURCES_DIR/$PROJECT_DIR/package/crossfiles/x86_64-w64-mingw32.meson" - ) - ;; - - *) - echo "Unsupported host: $HOST" >&2 - exit 1 - esac - fi - - meson setup . "$SOURCES_DIR/$PROJECT_DIR" "${conf[@]}" -fi - -ninja -ninja install diff --git a/app/deps/ffmpeg.sh b/app/deps/ffmpeg.sh deleted file mode 100755 index fb8b9a25..00000000 --- a/app/deps/ffmpeg.sh +++ /dev/null @@ -1,144 +0,0 @@ -#!/usr/bin/env bash -set -ex -DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) -cd "$DEPS_DIR" -. common -process_args "$@" - -VERSION=7.1.1 -FILENAME=ffmpeg-$VERSION.tar.xz -PROJECT_DIR=ffmpeg-$VERSION -SHA256SUM=733984395e0dbbe5c046abda2dc49a5544e7e0e1e2366bba849222ae9e3a03b1 - -cd "$SOURCES_DIR" - -if [[ -d "$PROJECT_DIR" ]] -then - echo "$PWD/$PROJECT_DIR" found -else - get_file "https://ffmpeg.org/releases/$FILENAME" "$FILENAME" "$SHA256SUM" - tar xf "$FILENAME" # First level directory is "$PROJECT_DIR" -fi - -mkdir -p "$BUILD_DIR/$PROJECT_DIR" -cd "$BUILD_DIR/$PROJECT_DIR" - -if [[ -d "$DIRNAME" ]] -then - echo "'$PWD/$DIRNAME' already exists, not reconfigured" - cd "$DIRNAME" -else - mkdir "$DIRNAME" - cd "$DIRNAME" - - if [[ "$HOST" == win* ]] - then - # -static-libgcc to avoid missing libgcc_s_dw2-1.dll - # -static to avoid dynamic dependency to zlib - export CFLAGS='-static-libgcc -static' - export CXXFLAGS="$CFLAGS" - export LDFLAGS='-static-libgcc -static' - elif [[ "$HOST" == "macos" ]] - then - export PKG_CONFIG_PATH="/opt/homebrew/opt/zlib/lib/pkgconfig" - fi - - export PKG_CONFIG_PATH="$INSTALL_DIR/$DIRNAME/lib/pkgconfig:$PKG_CONFIG_PATH" - - conf=( - --prefix="$INSTALL_DIR/$DIRNAME" - --pkg-config-flags="--static" - --extra-cflags="-O2 -fPIC" - --disable-programs - --disable-doc - --disable-swscale - --disable-postproc - --disable-avfilter - --disable-network - --disable-everything - --disable-vulkan - --disable-vaapi - --disable-vdpau - --enable-swresample - --enable-libdav1d - --enable-decoder=h264 - --enable-decoder=hevc - --enable-decoder=av1 - --enable-decoder=libdav1d - --enable-decoder=pcm_s16le - --enable-decoder=opus - --enable-decoder=aac - --enable-decoder=flac - --enable-decoder=png - --enable-protocol=file - --enable-demuxer=image2 - --enable-parser=png - --enable-zlib - --enable-muxer=matroska - --enable-muxer=mp4 - --enable-muxer=opus - --enable-muxer=flac - --enable-muxer=wav - ) - - if [[ "$HOST" == linux ]] - then - conf+=( - --enable-libv4l2 - --enable-outdev=v4l2 - --enable-encoder=rawvideo - ) - else - # libavdevice is only used for V4L2 on Linux - conf+=( - --disable-avdevice - ) - fi - - if [[ "$LINK_TYPE" == static ]] - then - conf+=( - --enable-static - --disable-shared - ) - else - conf+=( - --disable-static - --enable-shared - ) - fi - - if [[ "$BUILD_TYPE" == cross ]] - then - conf+=( - --enable-cross-compile - --cross-prefix="${HOST_TRIPLET}-" - --cc="${HOST_TRIPLET}-gcc" - ) - - case "$HOST" in - win32) - conf+=( - --target-os=mingw32 - --arch=x86 - ) - ;; - - win64) - conf+=( - --target-os=mingw32 - --arch=x86_64 - ) - ;; - - *) - echo "Unsupported host: $HOST" >&2 - exit 1 - esac - fi - - "$SOURCES_DIR/$PROJECT_DIR"/configure "${conf[@]}" -fi - -make -j -make install diff --git a/app/deps/libusb.sh b/app/deps/libusb.sh deleted file mode 100755 index 887a2a77..00000000 --- a/app/deps/libusb.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env bash -set -ex -DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) -cd "$DEPS_DIR" -. common -process_args "$@" - -VERSION=1.0.29 -FILENAME=libusb-$VERSION.tar.gz -PROJECT_DIR=libusb-$VERSION -SHA256SUM=7c2dd39c0b2589236e48c93247c986ae272e27570942b4163cb00a060fcf1b74 - -cd "$SOURCES_DIR" - -if [[ -d "$PROJECT_DIR" ]] -then - echo "$PWD/$PROJECT_DIR" found -else - get_file "https://github.com/libusb/libusb/archive/refs/tags/v$VERSION.tar.gz" "$FILENAME" "$SHA256SUM" - tar xf "$FILENAME" # First level directory is "$PROJECT_DIR" -fi - -mkdir -p "$BUILD_DIR/$PROJECT_DIR" -cd "$BUILD_DIR/$PROJECT_DIR" - -export CFLAGS='-O2' -export CXXFLAGS="$CFLAGS" - -if [[ -d "$DIRNAME" ]] -then - echo "'$PWD/$DIRNAME' already exists, not reconfigured" - cd "$DIRNAME" -else - mkdir "$DIRNAME" - cd "$DIRNAME" - - conf=( - --prefix="$INSTALL_DIR/$DIRNAME" - ) - - if [[ "$LINK_TYPE" == static ]] - then - conf+=( - --enable-static - --disable-shared - ) - else - conf+=( - --disable-static - --enable-shared - ) - fi - - if [[ "$BUILD_TYPE" == cross ]] - then - conf+=( - --host="$HOST_TRIPLET" - ) - fi - - "$SOURCES_DIR/$PROJECT_DIR"/bootstrap.sh - "$SOURCES_DIR/$PROJECT_DIR"/configure "${conf[@]}" -fi - -make -j -make install-strip diff --git a/app/deps/sdl.sh b/app/deps/sdl.sh deleted file mode 100755 index 54fee12b..00000000 --- a/app/deps/sdl.sh +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env bash -set -ex -DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) -cd "$DEPS_DIR" -. common -process_args "$@" - -VERSION=2.32.8 -FILENAME=SDL-$VERSION.tar.gz -PROJECT_DIR=SDL-release-$VERSION -SHA256SUM=dd35e05644ae527848d02433bec24dd0ea65db59faecf1a0e5d1880c533dac2c - -cd "$SOURCES_DIR" - -if [[ -d "$PROJECT_DIR" ]] -then - echo "$PWD/$PROJECT_DIR" found -else - get_file "https://github.com/libsdl-org/SDL/archive/refs/tags/release-$VERSION.tar.gz" "$FILENAME" "$SHA256SUM" - tar xf "$FILENAME" # First level directory is "$PROJECT_DIR" -fi - -mkdir -p "$BUILD_DIR/$PROJECT_DIR" -cd "$BUILD_DIR/$PROJECT_DIR" - -export CFLAGS='-O2' -export CXXFLAGS="$CFLAGS" - -if [[ -d "$DIRNAME" ]] -then - echo "'$PWD/$HDIRNAME' already exists, not reconfigured" - cd "$DIRNAME" -else - mkdir "$DIRNAME" - cd "$DIRNAME" - - conf=( - --prefix="$INSTALL_DIR/$DIRNAME" - ) - - if [[ "$HOST" == linux ]] - then - conf+=( - --enable-video-wayland - --enable-video-x11 - ) - fi - - if [[ "$LINK_TYPE" == static ]] - then - conf+=( - --enable-static - --disable-shared - ) - else - conf+=( - --disable-static - --enable-shared - ) - fi - - if [[ "$BUILD_TYPE" == cross ]] - then - conf+=( - --host="$HOST_TRIPLET" - ) - fi - - "$SOURCES_DIR/$PROJECT_DIR"/configure "${conf[@]}" -fi - -make -j -# There is no "make install-strip" -make install -# Strip manually -if [[ "$LINK_TYPE" == shared && "$HOST" == win* ]] -then - ${HOST_TRIPLET}-strip "$INSTALL_DIR/$DIRNAME/bin/SDL2.dll" -fi diff --git a/app/meson.build b/app/meson.build index f7df69eb..02d24a34 100644 --- a/app/meson.build +++ b/app/meson.build @@ -1,156 +1,91 @@ src = [ 'src/main.c', - 'src/adb/adb.c', - 'src/adb/adb_device.c', - 'src/adb/adb_parser.c', - 'src/adb/adb_tunnel.c', - 'src/audio_player.c', - 'src/audio_regulator.c', - 'src/cli.c', - 'src/clock.c', - 'src/compat.c', + 'src/command.c', 'src/control_msg.c', 'src/controller.c', + 'src/convert.c', 'src/decoder.c', - 'src/delay_buffer.c', - 'src/demuxer.c', + 'src/device.c', 'src/device_msg.c', - 'src/display.c', - 'src/events.c', - 'src/icon.c', - 'src/file_pusher.c', + 'src/file_handler.c', 'src/fps_counter.c', - 'src/frame_buffer.c', 'src/input_manager.c', - 'src/keyboard_sdk.c', - 'src/mouse_capture.c', - 'src/mouse_sdk.c', - 'src/opengl.c', - 'src/options.c', - 'src/packet_merger.c', + 'src/net.c', 'src/receiver.c', 'src/recorder.c', 'src/scrcpy.c', 'src/screen.c', 'src/server.c', - 'src/version.c', - 'src/hid/hid_gamepad.c', - 'src/hid/hid_keyboard.c', - 'src/hid/hid_mouse.c', - 'src/trait/frame_source.c', - 'src/trait/packet_source.c', - 'src/uhid/gamepad_uhid.c', - 'src/uhid/keyboard_uhid.c', - 'src/uhid/mouse_uhid.c', - 'src/uhid/uhid_output.c', - 'src/util/acksync.c', - 'src/util/audiobuf.c', - 'src/util/average.c', - 'src/util/env.c', - 'src/util/file.c', - 'src/util/intmap.c', - 'src/util/intr.c', - 'src/util/log.c', - 'src/util/memory.c', - 'src/util/net.c', - 'src/util/net_intr.c', - 'src/util/process.c', - 'src/util/process_intr.c', - 'src/util/rand.c', - 'src/util/strbuf.c', - 'src/util/str.c', - 'src/util/term.c', - 'src/util/thread.c', - 'src/util/tick.c', - 'src/util/timeout.c', + 'src/str_util.c', + 'src/tiny_xpm.c', + 'src/stream.c', + 'src/video_buffer.c', ] -conf = configuration_data() +if not get_option('crossbuild_windows') -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'), + # native build + dependencies = [ + dependency('libavformat'), + dependency('libavcodec'), + dependency('libavutil'), + dependency('sdl2'), ] - 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 + # cross-compile mingw32 build (from Linux to Windows) + cc = meson.get_compiler('c') -usb_support = get_option('usb') -if usb_support - src += [ - 'src/usb/aoa_hid.c', - 'src/usb/gamepad_aoa.c', - 'src/usb/keyboard_aoa.c', - 'src/usb/mouse_aoa.c', - 'src/usb/scrcpy_otg.c', - 'src/usb/screen_otg.c', - 'src/usb/usb.c', + prebuilt_sdl2 = meson.get_cross_property('prebuilt_sdl2') + sdl2_bin_dir = meson.current_source_dir() + '/../prebuilt-deps/' + prebuilt_sdl2 + '/bin' + sdl2_lib_dir = meson.current_source_dir() + '/../prebuilt-deps/' + prebuilt_sdl2 + '/lib' + sdl2_include_dir = '../prebuilt-deps/' + prebuilt_sdl2 + '/include' + + sdl2 = declare_dependency( + dependencies: [ + cc.find_library('SDL2', dirs: sdl2_bin_dir), + cc.find_library('SDL2main', dirs: sdl2_lib_dir), + ], + include_directories: include_directories(sdl2_include_dir) + ) + + prebuilt_ffmpeg_shared = meson.get_cross_property('prebuilt_ffmpeg_shared') + prebuilt_ffmpeg_dev = meson.get_cross_property('prebuilt_ffmpeg_dev') + ffmpeg_bin_dir = meson.current_source_dir() + '/../prebuilt-deps/' + prebuilt_ffmpeg_shared + '/bin' + ffmpeg_include_dir = '../prebuilt-deps/' + prebuilt_ffmpeg_dev + '/include' + ffmpeg = declare_dependency( + dependencies: [ + cc.find_library('avcodec-58', dirs: ffmpeg_bin_dir), + cc.find_library('avformat-58', dirs: ffmpeg_bin_dir), + cc.find_library('avutil-56', dirs: ffmpeg_bin_dir), + ], + include_directories: include_directories(ffmpeg_include_dir) + ) + + dependencies = [ + ffmpeg, + sdl2, + cc.find_library('mingw32') ] + endif cc = meson.get_compiler('c') -static = get_option('static') - -dependencies = [ - dependency('libavformat', version: '>= 57.33', static: static), - dependency('libavcodec', version: '>= 57.37', static: static), - dependency('libavutil', static: static), - dependency('libswresample', static: static), - dependency('sdl2', version: '>= 2.0.5', static: static), -] - -if v4l2_support - dependencies += dependency('libavdevice', static: static) -endif - -if usb_support - dependencies += dependency('libusb-1.0', static: static) -endif - if host_machine.system() == 'windows' - dependencies += cc.find_library('mingw32') + src += [ 'src/sys/win/command.c' ] + src += [ 'src/sys/win/net.c' ] dependencies += cc.find_library('ws2_32') +else + src += [ 'src/sys/unix/command.c' ] + src += [ 'src/sys/unix/net.c' ] endif -check_functions = [ - 'strdup', - 'asprintf', - 'vasprintf', - 'nrand48', - 'jrand48', - 'reallocarray', -] +conf = configuration_data() -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')) +# expose the build type +conf.set('BUILD_DEBUG', get_option('buildtype') == 'debug') # the version, updated on release conf.set_quoted('SCRCPY_VERSION', meson.project_version()) @@ -158,130 +93,72 @@ 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 +# build a "portable" version (with scrcpy-server.jar 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 +# the default client TCP port 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') +conf.set('DEFAULT_LOCAL_PORT', '27183') -# run a server debugger and wait for a client to be attached -conf.set('SERVER_DEBUGGER', get_option('server_debugger')) +# the default max video size for both dimensions, in pixels +# overridden by option --max-size +conf.set('DEFAULT_MAX_SIZE', '0') # 0: unlimited -# enable V4L2 support (linux only) -conf.set('HAVE_V4L2', v4l2_support) +# the default video bitrate, in bits/second +# overridden by option --bit-rate +conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps -# enable HID over AOA support (linux only) -conf.set('HAVE_USB', usb_support) +# enable High DPI support +conf.set('HIDPI_SUPPORT', get_option('hidpi_support')) + +# disable console on Windows +conf.set('WINDOWS_NOCONSOLE', get_option('windows_noconsole')) configure_file(configuration: conf, output: 'config.h') src_dir = include_directories('src') +if get_option('windows_noconsole') + c_args = [ '-mwindows' ] + link_args = [ '-mwindows' ] +else + c_args = [] + link_args = [] +endif + 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: datadir / 'icons/hicolor/256x256/apps') -install_data('data/zsh-completion/_scrcpy', - install_dir: datadir / 'zsh/site-functions') -install_data('data/bash-completion/scrcpy', - install_dir: 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: datadir / 'applications') - install_data('data/scrcpy-console.desktop', - install_dir: datadir / 'applications') -endif + c_args: c_args, + link_args: link_args) ### 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_audiobuf', [ - 'tests/test_audiobuf.c', - 'src/util/audiobuf.c', - 'src/util/memory.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_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_orientation', [ - 'tests/test_orientation.c', - 'src/options.c', - ]], - ['test_strbuf', [ - 'tests/test_strbuf.c', - 'src/util/strbuf.c', - ]], - ['test_str', [ - 'tests/test_str.c', - 'src/util/str.c', - 'src/util/strbuf.c', - ]], - ['test_vecdeque', [ - 'tests/test_vecdeque.c', - 'src/util/memory.c', - ]], - ['test_vector', [ - 'tests/test_vector.c', - ]], - ] +tests = [ + ['test_cbuf', [ + 'tests/test_cbuf.c', + ]], + ['test_control_event_serialize', [ + 'tests/test_control_msg_serialize.c', + 'src/control_msg.c', + 'src/str_util.c' + ]], + ['test_device_event_deserialize', [ + 'tests/test_device_msg_deserialize.c', + 'src/device_msg.c' + ]], + ['test_strutil', [ + 'tests/test_strutil.c', + 'src/str_util.c' + ]], +] - foreach t : tests - sources = t[1] + ['src/compat.c'] - exe = executable(t[0], sources, - include_directories: src_dir, - dependencies: dependencies, - c_args: ['-DSDL_MAIN_HANDLED', '-DSC_TEST']) - test(t[0], exe) - endforeach -endif - -if meson.version().version_compare('>= 0.58.0') - devenv = environment() - devenv.set('SCRCPY_ICON_PATH', meson.current_source_dir() / 'data/icon.png') - meson.add_devenv(devenv) -endif +foreach t : tests + exe = executable(t[0], t[1], + include_directories: src_dir, + dependencies: dependencies) + test(t[0], exe) +endforeach diff --git a/app/scrcpy-windows.manifest b/app/scrcpy-windows.manifest deleted file mode 100644 index f2708ecb..00000000 --- a/app/scrcpy-windows.manifest +++ /dev/null @@ -1,9 +0,0 @@ - - - - - true - PerMonitorV2 - - - diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc deleted file mode 100644 index 9c5374ae..00000000 --- a/app/scrcpy-windows.rc +++ /dev/null @@ -1,23 +0,0 @@ -#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", "3.3.1" - END - END - BLOCK "VarFileInfo" - BEGIN - VALUE "Translation", 0x409, 1252 - END -END diff --git a/app/scrcpy.1 b/app/scrcpy.1 deleted file mode 100644 index d72fda13..00000000 --- a/app/scrcpy.1 +++ /dev/null @@ -1,860 +0,0 @@ -.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 "\-\-angle " degrees -Rotate the video content by a custom angle, in degrees (clockwise). - -.TP -.BI "\-\-audio\-bit\-rate " value -Encode the audio at the given bit rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000). - -Default is 128K (128000). - -.TP -.BI "\-\-audio\-buffer " ms -Configure the audio buffering delay (in milliseconds). - -Lower values decrease the latency, but increase the likelihood of buffer underrun (causing audio glitches). - -Default is 50. - -.TP -.BI "\-\-audio\-codec " name -Select an audio codec (opus, aac, flac or raw). - -Default is opus. - -.TP -.BI "\-\-audio\-codec\-options " key\fR[:\fItype\fR]=\fIvalue\fR[,...] -Set a list of comma-separated key:type=value options for the device audio encoder. - -The possible values for 'type' are 'int' (default), 'long', 'float' and 'string'. - -The list of possible codec options is available in the Android documentation: - - - -.TP -.B \-\-audio\-dup -Duplicate audio (capture and keep playing on the device). - -This feature is only available with --audio-source=playback. - -.TP -.BI "\-\-audio\-encoder " name -Use a specific MediaCodec audio encoder (depending on the codec provided by \fB\-\-audio\-codec\fR). - -The available encoders can be listed by \fB\-\-list\-encoders\fR. - -.TP -.BI "\-\-audio\-source " source -Select the audio source. Possible values are: - - - "output": forwards the whole audio output, and disables playback on the device. - - "playback": captures the audio playback (Android apps can opt-out, so the whole output is not necessarily captured). - - "mic": captures the microphone. - - "mic-unprocessed": captures the microphone unprocessed (raw) sound. - - "mic-camcorder": captures the microphone tuned for video recording, with the same orientation as the camera if available. - - "mic-voice-recognition": captures the microphone tuned for voice recognition. - - "mic-voice-communication": captures the microphone tuned for voice communications (it will for instance take advantage of echo cancellation or automatic gain control if available). - - "voice-call": captures voice call. - - "voice-call-uplink": captures voice call uplink only. - - "voice-call-downlink": captures voice call downlink only. - - "voice-performance": captures audio meant to be processed for live performance (karaoke), includes both the microphone and the device playback. - -Default is output. - -.TP -.BI "\-\-audio\-output\-buffer " ms -Configure the size of the SDL audio output buffer (in milliseconds). - -If you get "robotic" audio playback, you should test with a higher value (10). Do not change this setting otherwise. - -Default is 5. - -.TP -.BI "\-b, \-\-video\-bit\-rate " value -Encode the video at the given bit rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000). - -Default is 8M (8000000). - -.TP -.BI "\-\-camera\-ar " ar -Select the camera size by its aspect ratio (+/- 10%). - -Possible values are "sensor" (use the camera sensor aspect ratio), "\fInum\fR:\fIden\fR" (e.g. "4:3") and "\fIvalue\fR" (e.g. "1.6"). - -.TP -.BI "\-\-camera\-facing " facing -Select the device camera by its facing direction. - -Possible values are "front", "back" and "external". - -.TP -.BI "\-\-camera\-fps " fps -Specify the camera capture frame rate. - -If not specified, Android's default frame rate (30 fps) is used. - -.TP -.B \-\-camera\-high\-speed -Enable high-speed camera capture mode. - -This mode is restricted to specific resolutions and frame rates, listed by \fB\-\-list\-camera\-sizes\fR. - -.TP -.BI "\-\-camera\-id " id -Specify the device camera id to mirror. - -The available camera ids can be listed by \fB\-\-list\-cameras\fR. - -.TP -.BI "\-\-camera\-size " width\fRx\fIheight -Specify an explicit camera capture size. - -.TP -.BI "\-\-capture\-orientation " value -Possible values are 0, 90, 180, 270, flip0, flip90, flip180 and flip270, possibly prefixed by '@'. - -The number represents the clockwise rotation in degrees; the "flip" keyword applies a horizontal flip before the rotation. - -If a leading '@' is passed (@90) for display capture, then the rotation is locked, and is relative to the natural device orientation. - -If '@' is passed alone, then the rotation is locked to the initial device orientation. - -Default is 0. - -.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). - -.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 " id -Specify the device display id to mirror. - -The available display ids can be listed by \fB\-\-list\-displays\fR. - -Default is 0. - -.TP -.BI "\-\-display\-ime\-policy " value -Set the policy for selecting where the IME should be displayed. - -Possible values are "local", "fallback" and "hide": - - - "local" means that the IME should appear on the local display. - - "fallback" means that the IME should appear on a fallback display (the default display). - - "hide" means that the IME should be hidden. - -By default, the IME policy is left unchanged. - - -.TP -.BI "\-\-display\-orientation " value -Set the initial display orientation. - -Possible values are 0, 90, 180, 270, flip0, flip90, flip180 and flip270. The number represents the clockwise rotation in degrees; the "flip" keyword applies a horizontal flip before the rotation. - -Default is 0. - -.TP -.B \-e, \-\-select\-tcpip -Use TCP/IP device (if there is exactly one, like adb -e). - -Also see \fB\-d\fR (\fB\-\-select\-usb\fR). - -.TP -.B \-f, \-\-fullscreen -Start in fullscreen. - -.TP -.B \-\-force\-adb\-forward -Do not attempt to use "adb reverse" to connect to the device. - -.TP -.B \-G -Same as \fB\-\-gamepad=uhid\fR, or \fB\-\-keyboard=aoa\fR if \fB\-\-otg\fR is set. - -.TP -.BI "\-\-gamepad " mode -Select how to send gamepad inputs to the device. - -Possible values are "disabled", "uhid" and "aoa": - - - "disabled" does not send gamepad inputs to the device. - - "uhid" simulates physical HID gamepads using the Linux HID kernel module on the device. - - "aoa" simulates physical HID gamepads using the AOAv2 protocol. It may only work over USB. - -Also see \fB\-\-keyboard\f and R\fB\-\-mouse\fR. -.TP -.B \-h, \-\-help -Print this help. - -.TP -.B \-K -Same as \fB\-\-keyboard=uhid\fR, or \fB\-\-keyboard=aoa\fR if \fB\-\-otg\fR is set. - -.TP -.BI "\-\-keyboard " mode -Select how to send keyboard inputs to the device. - -Possible values are "disabled", "sdk", "uhid" and "aoa": - - - "disabled" does not send keyboard inputs to the device. - - "sdk" uses the Android system API to deliver keyboard events to applications. - - "uhid" simulates a physical HID keyboard using the Linux HID kernel module on the device. - - "aoa" simulates a physical HID keyboard using the AOAv2 protocol. It may only work over USB. - -For "uhid" and "aoa", the keyboard layout must be configured (once and for all) on the device, via Settings -> System -> Languages and input -> Physical keyboard. This settings page can be started directly using the shortcut MOD+k (except in OTG mode), or by executing: - - adb shell am start -a android.settings.HARD_KEYBOARD_SETTINGS - -This option is only available when the HID keyboard is enabled (or a physical keyboard is connected). - -Also see \fB\-\-mouse\fR and \fB\-\-gamepad\fR. - -.TP -.B \-\-kill\-adb\-on\-close -Kill adb when scrcpy terminates. - -.TP -.B \-\-legacy\-paste -Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+Shift+v). - -This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically. - -.TP -.B \-\-list\-apps -List Android apps installed on the device. - -.TP -.B \-\-list\-camera\-sizes -List the valid camera capture sizes. - -.TP -.B \-\-list\-cameras -List cameras available on the device. - -.TP -.B \-\-list\-encoders -List video and audio encoders available on the device. - -.TP -.B \-\-list\-displays -List displays available on the device. - -.TP -.BI "\-m, \-\-max\-size " value -Limit both the width and height of the video to \fIvalue\fR. The other dimension is computed so that the device aspect\-ratio is preserved. - -Default is 0 (unlimited). - -.TP -.B \-M -Same as \fB\-\-mouse=uhid\fR, or \fB\-\-mouse=aoa\fR if \fB\-\-otg\fR is set. - -.TP -.BI "\-\-max\-fps " value -Limit the framerate of screen capture (officially supported since Android 10, but may work on earlier versions). - -.TP -.BI "\-\-mouse " mode -Select how to send mouse inputs to the device. - -Possible values are "disabled", "sdk", "uhid" and "aoa": - - - "disabled" does not send mouse inputs to the device. - - "sdk" uses the Android system API to deliver mouse events to applications. - - "uhid" simulates a physical HID mouse using the Linux HID kernel module on the device. - - "aoa" simulates a physical mouse using the AOAv2 protocol. It may only work over USB. - -In "uhid" and "aoa" modes, the computer mouse is captured to control the device directly (relative mouse mode). - -LAlt, LSuper or RSuper toggle the capture mode, to give control of the mouse back to the computer. - -Also see \fB\-\-keyboard\fR and \fB\-\-gamepad\fR. - -.TP -.BI "\-\-mouse\-bind " xxxx[:xxxx] -Configure bindings of secondary clicks. - -The argument must be one or two sequences (separated by ':') of exactly 4 characters, one for each secondary click (in order: right click, middle click, 4th click, 5th click). - -The first sequence defines the primary bindings, used when a mouse button is pressed alone. The second sequence defines the secondary bindings, used when a mouse button is pressed while the Shift key is held. - -If the second sequence of bindings is omitted, then it is the same as the first one. - -Each character must be one of the following: - - - '+': forward the click to the device - - '-': ignore the click - - 'b': trigger shortcut BACK (or turn screen on if off) - - 'h': trigger shortcut HOME - - 's': trigger shortcut APP_SWITCH - - 'n': trigger shortcut "expand notification panel" - -Default is 'bhsn:++++' for SDK mouse, and '++++:bhsn' for AOA and UHID. - - -.TP -.B \-n, \-\-no\-control -Disable device control (mirror the device in read\-only). - -.TP -.B \-N, \-\-no\-playback -Disable video and audio playback on the computer (equivalent to \fB\-\-no\-video\-playback \-\-no\-audio\-playback\fR). - -.TP -\fB\-\-new\-display\fR[=[\fIwidth\fRx\fIheight\fR][/\fIdpi\fR]] -Create a new display with the specified resolution and density. If not provided, they default to the main display dimensions and DPI. - -Examples: - - \-\-new\-display=1920x1080 - \-\-new\-display=1920x1080/420 - \-\-new\-display # main display size and density - \-\-new\-display=/240 # main display size and 240 dpi - -.TP -.B \-\-no\-audio -Disable audio forwarding. - -.TP -.B \-\-no\-audio\-playback -Disable audio playback on the computer. - -.TP -.B \-\-no\-cleanup -By default, scrcpy removes the server binary from the device and restores the device state (show touches, stay awake and power mode) on exit. - -This option disables this cleanup. - -.TP -.B \-\-no\-clipboard\-autosync -By default, scrcpy automatically synchronizes the computer clipboard to the device clipboard before injecting Ctrl+v, and the device clipboard to the computer clipboard whenever it changes. - -This option disables this automatic synchronization. - -.TP -.B \-\-no\-downsize\-on\-error -By default, on MediaCodec error, scrcpy automatically tries again with a lower definition. - -This option disables this behavior. - -.TP -.B \-\-no\-key\-repeat -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\-mouse\-hover -Do not forward mouse hover (mouse motion without any clicks) events. - -.TP -.B \-\-no\-power\-on -Do not power on the device on start. - -.TP -.B \-\-no\-vd\-destroy\-content -Disable virtual display "destroy content on removal" flag. - -With this option, when the virtual display is closed, the running apps are moved to the main display rather than being destroyed. - -.TP -.B \-\-no\-vd\-system\-decorations -Disable virtual display system decorations flag. - -.TP -.B \-\-no\-video -Disable video forwarding. - -.TP -.B \-\-no\-video\-playback -Disable video playback on the computer. - -.TP -.B \-\-no\-window -Disable scrcpy window. Implies --no-video-playback. - -.TP -.BI "\-\-orientation " value -Same as --display-orientation=value --record-orientation=value. - -.TP -.B \-\-otg -Run in OTG mode: simulate physical keyboard and mouse, as if the computer keyboard and mouse were plugged directly to the device via an OTG cable. - -In this mode, adb (USB debugging) is not necessary, and mirroring is disabled. - -LAlt, LSuper or RSuper toggle the mouse capture mode, to give control of the mouse back to the computer. - -If any of \fB\-\-hid\-keyboard\fR or \fB\-\-hid\-mouse\fR is set, only enable keyboard or mouse respectively, otherwise enable both. - -It may only work over USB. - -See \fB\-\-keyboard\fR, \fB\-\-mouse\fR and \fB\-\-gamepad\fR. - -.TP -.BI "\-p, \-\-port " port\fR[:\fIport\fR] -Set the TCP port (range) used by the client to listen. - -Default is 27183:27199. - -.TP -\fB\-\-pause\-on\-exit\fR[=\fImode\fR] -Configure pause on exit. Possible values are "true" (always pause on exit), "false" (never pause on exit) and "if-error" (pause only if an error occurred). - -This is useful to prevent the terminal window from automatically closing, so that error messages can be read. - -Default is "false". - -Passing the option without argument is equivalent to passing "true". - -.TP -.B \-\-power\-off\-on\-close -Turn the device screen off when closing scrcpy. - -.TP -.B \-\-prefer\-text -Inject alpha characters and space as text events instead of key events. - -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 -.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. - -.TP -.B \-\-raw\-key\-events -Inject key events for all input keys, and ignore text events. - -.TP -.BI "\-\-record\-format " format -Force recording format (mp4, mkv, m4a, mka, opus, aac, flac or wav). - -.TP -.BI "\-\-record\-orientation " value -Set the record orientation. - -Possible values are 0, 90, 180 and 270. The number represents the clockwise rotation in degrees. - -Default is 0. - -.TP -.BI "\-\-render\-driver " name -Request SDL to use the given render driver (this is just a hint). - -Supported names are currently "direct3d", "opengl", "opengles2", "opengles", "metal" and "software". - - - -.TP -.B \-\-require\-audio -By default, scrcpy mirrors only the video if audio capture fails on the device. This option makes scrcpy fail if audio is enabled but does not work. - -.TP -.BI "\-s, \-\-serial " number -The device serial number. Mandatory only if several devices are connected to adb. - -.TP -.B \-S, \-\-turn\-screen\-off -Turn the device screen off immediately. - -.TP -.B "\-\-screen\-off\-timeout " seconds -Set the screen off timeout while scrcpy is running (restore the initial value on exit). - -.TP -.BI "\-\-shortcut\-mod " key\fR[+...]][,...] -Specify the modifiers to use for scrcpy shortcuts. Possible keys are "lctrl", "rctrl", "lalt", "ralt", "lsuper" and "rsuper". - -Several shortcut modifiers can be specified, separated by ','. - -For example, to use either LCtrl or LSuper for scrcpy shortcuts, pass "lctrl,lsuper". - -Default is "lalt,lsuper" (left-Alt or left-Super). - -.TP -.BI "\-\-start\-app " name -Start an Android app, by its exact package name. - -Add a '?' prefix to select an app whose name starts with the given name, case-insensitive (retrieving app names on the device may take some time): - - scrcpy --start-app=?firefox - -Add a '+' prefix to force-stop before starting the app: - - scrcpy --new-display --start-app=+org.mozilla.firefox - -Both prefixes can be used, in that order: - - scrcpy --start-app=+?firefox - -.TP -.B \-t, \-\-show\-touches -Enable "show touches" on start, restore the initial value on exit. - -It only shows physical touches (not clicks from scrcpy). - -.TP -.BI "\-\-tcpip\fR[=[+]\fIip\fR[:\fIport\fR]] -Configure and connect 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. - -Prefix the address with a '+' to force a reconnection. - -.TP -.BI "\-\-time\-limit " seconds -Set the maximum mirroring time, in seconds. - -.TP -.BI "\-\-tunnel\-host " ip -Set the IP address of the adb tunnel to reach the scrcpy server. This option automatically enables \fB\-\-force\-adb\-forward\fR. - -Default is localhost. - -.TP -.BI "\-\-tunnel\-port " port -Set the TCP port of the adb tunnel to reach the scrcpy server. This option automatically enables \fB\-\-force\-adb\-forward\fR. - -Default is 0 (not forced): the local port used for establishing the tunnel will be used. - -.TP -.B \-v, \-\-version -Print the version of scrcpy. - -.TP -.BI "\-V, \-\-verbosity " value -Set the log level ("verbose", "debug", "info", "warn" or "error"). - -Default is "info" for release builds, "debug" for debug builds. - -.TP -.BI "\-\-v4l2-sink " /dev/videoN -Output to v4l2loopback device. - -.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\-\-video\-buffer\fR, but specific to V4L2 sink. - -Default is 0 (no buffering). - -.TP -.BI "\-\-video\-buffer " ms -Add a buffering delay (in milliseconds) before displaying video frames. - -This increases latency to compensate for jitter. - -Default is 0 (no buffering). - -.TP -.BI "\-\-video\-codec " name -Select a video codec (h264, h265 or av1). - -Default is h264. - -.TP -.BI "\-\-video\-codec\-options " key\fR[:\fItype\fR]=\fIvalue\fR[,...] -Set a list of comma-separated key:type=value options for the device video encoder. - -The possible values for 'type' are 'int' (default), 'long', 'float' and 'string'. - -The list of possible codec options is available in the Android documentation: - - - -.TP -.BI "\-\-video\-encoder " name -Use a specific MediaCodec video encoder (depending on the codec provided by \fB\-\-video\-codec\fR). - -The available encoders can be listed by \fB\-\-list\-encoders\fR. - -.TP -.BI "\-\-video\-source " source -Select the video source (display or camera). - -Camera mirroring requires Android 12+. - -Default is display. - -.TP -.B \-w, \-\-stay-awake -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+Shift+Left, MOD+Shift+Right -Flip display horizontally - -.TP -.B MOD+Shift+Up, MOD+Shift+Down -Flip display vertically - -.TP -.B MOD+z -Pause or re-pause display - -.TP -.B MOD+Shift+z -Unpause display - -.TP -.B MOD+Shift+r -Reset video capture/encoding - -.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+k -Open keyboard settings on the device (for HID keyboard only) - -.TP -.B MOD+i -Enable/disable FPS counter (print frames/second in logs) - -.TP -.B Ctrl+click-and-move -Pinch-to-zoom and rotate from the center of the screen - -.TP -.B Shift+click-and-move -Tilt vertically (slide with 2 fingers) - -.TP -.B Ctrl+Shift+click-and-move -Tilt horizontally (slide with 2 fingers) - -.TP -.B Drag & drop APK file -Install APK from computer - -.TP -.B Drag & drop non-APK file -Push file to device (see \fB\-\-push\-target\fR) - - -.SH Environment variables - -.TP -.B ADB -Path to adb. - -.TP -.B ANDROID_SERIAL -Device serial to use if no selector (\fB-s\fR, \fB-d\fR, \fB-e\fR or \fB\-\-tcpip=\fIaddr\fR) is specified. - -.TP -.B SCRCPY_ICON_PATH -Path to the program icon. - -.TP -.B SCRCPY_SERVER_PATH -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 . - -.SH COPYRIGHT -Copyright \(co 2018 Genymobile - -Copyright \(co 2018\-2025 Romain Vimont - -Licensed under the Apache License, Version 2.0. - -.SH WWW - diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c deleted file mode 100644 index 9e9cfd6b..00000000 --- a/app/src/adb/adb.c +++ /dev/null @@ -1,792 +0,0 @@ -#include "adb.h" - -#include -#include -#include -#include -#include - -#include "adb/adb_device.h" -#include "adb/adb_parser.h" -#include "util/env.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 char *adb_executable; - -bool -sc_adb_init(void) { - adb_executable = sc_get_env("ADB"); - if (adb_executable) { - LOGD("Using adb: %s", adb_executable); - return true; - } - -#if !defined(PORTABLE) || defined(_WIN32) - adb_executable = strdup("adb"); - if (!adb_executable) { - LOG_OOM(); - return false; - } -#else - // For portable builds, use the absolute path to the adb executable - // in the same directory as scrcpy (except on Windows, where "adb" - // is sufficient) - adb_executable = sc_file_get_local_path("adb"); - if (!adb_executable) { - // Error already logged - return false; - } - - LOGD("Using adb (portable): %s", adb_executable); -#endif - - return true; -} - -void -sc_adb_destroy(void) { - free(adb_executable); -} - -const char * -sc_adb_get_executable(void) { - return adb_executable; -} - -// serialize argv to string "[arg1], [arg2], [arg3]" -static size_t -argv_to_string(const char *const *argv, char *buf, size_t bufsize) { - size_t idx = 0; - bool first = true; - while (*argv) { - const char *arg = *argv; - size_t len = strlen(arg); - // count space for "[], ...\0" - if (idx + len + 8 >= bufsize) { - // not enough space, truncate - assert(idx < bufsize - 4); - memcpy(&buf[idx], "...", 3); - idx += 3; - break; - } - if (first) { - first = false; - } else { - buf[idx++] = ','; - buf[idx++] = ' '; - } - buf[idx++] = '['; - memcpy(&buf[idx], arg, len); - idx += len; - buf[idx++] = ']'; - argv++; - } - assert(idx < bufsize); - buf[idx] = '\0'; - return idx; -} - -static void -show_adb_installation_msg(void) { -#ifndef __WINDOWS__ - static const struct { - const char *binary; - const char *command; - } pkg_managers[] = { - {"apt", "apt install adb"}, - {"apt-get", "apt-get install adb"}, - {"brew", "brew install --cask android-platform-tools"}, - {"dnf", "dnf install android-tools"}, - {"emerge", "emerge dev-util/android-tools"}, - {"pacman", "pacman -S android-tools"}, - }; - for (size_t i = 0; i < ARRAY_LEN(pkg_managers); ++i) { - if (sc_file_executable_exists(pkg_managers[i].binary)) { - LOGI("You may install 'adb' by \"%s\"", pkg_managers[i].command); - return; - } - } -#endif -} - -static void -show_adb_err_msg(enum sc_process_result err, const char *const argv[]) { -#define MAX_COMMAND_STRING_LEN 1024 - char *buf = malloc(MAX_COMMAND_STRING_LEN); - if (!buf) { - LOG_OOM(); - LOGE("Failed to execute"); - return; - } - - switch (err) { - case SC_PROCESS_ERROR_GENERIC: - argv_to_string(argv, buf, MAX_COMMAND_STRING_LEN); - LOGE("Failed to execute: %s", buf); - break; - case SC_PROCESS_ERROR_MISSING_BINARY: - argv_to_string(argv, buf, MAX_COMMAND_STRING_LEN); - LOGE("Command not found: %s", buf); - LOGE("(make 'adb' accessible from your PATH or define its full" - "path in the ADB environment variable)"); - show_adb_installation_msg(); - break; - case SC_PROCESS_SUCCESS: - // do nothing - break; - } - - free(buf); -} - -static bool -process_check_success_internal(sc_pid pid, const char *name, bool close, - unsigned flags) { - bool log_errors = !(flags & SC_ADB_NO_LOGERR); - - if (pid == SC_PROCESS_NONE) { - if (log_errors) { - LOGE("Could not execute \"%s\"", name); - } - return false; - } - sc_exit_code exit_code = sc_process_wait(pid, close); - if (exit_code) { - if (log_errors) { - if (exit_code != SC_EXIT_CODE_NONE) { - LOGE("\"%s\" returned with value %" SC_PRIexitcode, name, - exit_code); - } else { - LOGE("\"%s\" exited unexpectedly", name); - } - } - return false; - } - return true; -} - -static bool -process_check_success_intr(struct sc_intr *intr, sc_pid pid, const char *name, - unsigned flags) { - if (intr && !sc_intr_set_process(intr, pid)) { - // Already interrupted - return false; - } - - // Always pass close=false, interrupting would be racy otherwise - bool ret = process_check_success_internal(pid, name, false, flags); - - if (intr) { - sc_intr_set_process(intr, SC_PROCESS_NONE); - } - - // Close separately - sc_process_close(pid); - - return ret; -} - -static sc_pid -sc_adb_execute_p(const char *const argv[], unsigned flags, sc_pipe *pout) { - unsigned process_flags = 0; - if (flags & SC_ADB_NO_STDOUT) { - process_flags |= SC_PROCESS_NO_STDOUT; - } - if (flags & SC_ADB_NO_STDERR) { - process_flags |= SC_PROCESS_NO_STDERR; - } - - sc_pid pid; - enum sc_process_result r = - sc_process_execute_p(argv, &pid, process_flags, NULL, pout, NULL); - if (r != SC_PROCESS_SUCCESS) { - // If the execution itself failed (not the command exit code), log the - // error in all cases - show_adb_err_msg(r, argv); - pid = SC_PROCESS_NONE; - } - - return pid; -} - -sc_pid -sc_adb_execute(const char *const argv[], unsigned flags) { - return sc_adb_execute_p(argv, flags, NULL); -} - -bool -sc_adb_start_server(struct sc_intr *intr, unsigned flags) { - const char *const argv[] = SC_ADB_COMMAND("start-server"); - - sc_pid pid = sc_adb_execute(argv, flags); - return process_check_success_intr(intr, pid, "adb start-server", flags); -} - -bool -sc_adb_kill_server(struct sc_intr *intr, unsigned flags) { - const char *const argv[] = SC_ADB_COMMAND("kill-server"); - - sc_pid pid = sc_adb_execute(argv, flags); - return process_check_success_intr(intr, pid, "adb kill-server", flags); -} - -bool -sc_adb_forward(struct sc_intr *intr, const char *serial, uint16_t local_port, - const char *device_socket_name, unsigned flags) { - char local[4 + 5 + 1]; // tcp:PORT - char remote[108 + 14 + 1]; // localabstract:NAME - - int r = snprintf(local, sizeof(local), "tcp:%" PRIu16, local_port); - assert(r >= 0 && (size_t) r < sizeof(local)); - - r = snprintf(remote, sizeof(remote), "localabstract:%s", - device_socket_name); - if (r < 0 || (size_t) r >= sizeof(remote)) { - LOGE("Could not write socket name"); - return false; - } - - assert(serial); - const char *const argv[] = - SC_ADB_COMMAND("-s", serial, "forward", local, remote); - - sc_pid pid = sc_adb_execute(argv, flags); - return process_check_success_intr(intr, pid, "adb forward", flags); -} - -bool -sc_adb_forward_remove(struct sc_intr *intr, const char *serial, - uint16_t local_port, unsigned flags) { - char local[4 + 5 + 1]; // tcp:PORT - int r = snprintf(local, sizeof(local), "tcp:%" PRIu16, local_port); - assert(r >= 0 && (size_t) r < sizeof(local)); - (void) r; - - assert(serial); - const char *const argv[] = - SC_ADB_COMMAND("-s", serial, "forward", "--remove", local); - - sc_pid pid = sc_adb_execute(argv, flags); - return process_check_success_intr(intr, pid, "adb forward --remove", flags); -} - -bool -sc_adb_reverse(struct sc_intr *intr, const char *serial, - const char *device_socket_name, uint16_t local_port, - unsigned flags) { - char local[4 + 5 + 1]; // tcp:PORT - char remote[108 + 14 + 1]; // localabstract:NAME - int r = snprintf(local, sizeof(local), "tcp:%" PRIu16, local_port); - assert(r >= 0 && (size_t) r < sizeof(local)); - - r = snprintf(remote, sizeof(remote), "localabstract:%s", - device_socket_name); - if (r < 0 || (size_t) r >= sizeof(remote)) { - LOGE("Could not write socket name"); - return false; - } - - assert(serial); - const char *const argv[] = - SC_ADB_COMMAND("-s", serial, "reverse", remote, local); - - sc_pid pid = sc_adb_execute(argv, flags); - return process_check_success_intr(intr, pid, "adb reverse", flags); -} - -bool -sc_adb_reverse_remove(struct sc_intr *intr, const char *serial, - const char *device_socket_name, unsigned flags) { - char remote[108 + 14 + 1]; // localabstract:NAME - int r = snprintf(remote, sizeof(remote), "localabstract:%s", - device_socket_name); - if (r < 0 || (size_t) r >= sizeof(remote)) { - LOGE("Device socket name too long"); - return false; - } - - assert(serial); - const char *const argv[] = - SC_ADB_COMMAND("-s", serial, "reverse", "--remove", remote); - - sc_pid pid = sc_adb_execute(argv, flags); - return process_check_success_intr(intr, pid, "adb reverse --remove", flags); -} - -bool -sc_adb_push(struct sc_intr *intr, const char *serial, const char *local, - const char *remote, unsigned flags) { -#ifdef __WINDOWS__ - // Windows will parse the string, so the paths must be quoted - // (see sys/win/command.c) - local = sc_str_quote(local); - if (!local) { - return SC_PROCESS_NONE; - } - remote = sc_str_quote(remote); - if (!remote) { - free((void *) local); - return SC_PROCESS_NONE; - } -#endif - - assert(serial); - const char *const argv[] = - SC_ADB_COMMAND("-s", serial, "push", local, remote); - - sc_pid pid = sc_adb_execute(argv, flags); - -#ifdef __WINDOWS__ - free((void *) remote); - free((void *) local); -#endif - - return process_check_success_intr(intr, pid, "adb push", flags); -} - -bool -sc_adb_install(struct sc_intr *intr, const char *serial, const char *local, - unsigned flags) { -#ifdef __WINDOWS__ - // Windows will parse the string, so the local name must be quoted - // (see sys/win/command.c) - local = sc_str_quote(local); - if (!local) { - return SC_PROCESS_NONE; - } -#endif - - assert(serial); - const char *const argv[] = - SC_ADB_COMMAND("-s", serial, "install", "-r", local); - - sc_pid pid = sc_adb_execute(argv, flags); - -#ifdef __WINDOWS__ - free((void *) local); -#endif - - return process_check_success_intr(intr, pid, "adb install", flags); -} - -bool -sc_adb_tcpip(struct sc_intr *intr, const char *serial, uint16_t port, - unsigned flags) { - char port_string[5 + 1]; - int r = snprintf(port_string, sizeof(port_string), "%" PRIu16, port); - assert(r >= 0 && (size_t) r < sizeof(port_string)); - (void) r; - - assert(serial); - const char *const argv[] = - SC_ADB_COMMAND("-s", serial, "tcpip", port_string); - - sc_pid pid = sc_adb_execute(argv, flags); - return process_check_success_intr(intr, pid, "adb tcpip", flags); -} - -bool -sc_adb_connect(struct sc_intr *intr, const char *ip_port, unsigned flags) { - const char *const argv[] = SC_ADB_COMMAND("connect", ip_port); - - sc_pipe pout; - sc_pid pid = sc_adb_execute_p(argv, flags, &pout); - if (pid == SC_PROCESS_NONE) { - LOGE("Could not execute \"adb connect\""); - return false; - } - - // "adb connect" always returns successfully (with exit code 0), even in - // case of failure. As a workaround, check if its output starts with - // "connected" or "already 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) - || !strncmp("already connected", buf, sizeof("already connected") - 1); - if (!ok && !(flags & SC_ADB_NO_STDERR)) { - // "adb connect" also prints errors to stdout. Since we capture it, - // re-print the error to stderr. - size_t len = strcspn(buf, "\r\n"); - buf[len] = '\0'; - fprintf(stderr, "%s\n", buf); - } - return ok; -} - -bool -sc_adb_disconnect(struct sc_intr *intr, const char *ip_port, unsigned flags) { - assert(ip_port); - const char *const argv[] = SC_ADB_COMMAND("disconnect", ip_port); - - sc_pid pid = sc_adb_execute(argv, flags); - return process_check_success_intr(intr, pid, "adb disconnect", flags); -} - -static bool -sc_adb_list_devices(struct sc_intr *intr, unsigned flags, - struct sc_vec_adb_devices *out_vec) { - const char *const argv[] = SC_ADB_COMMAND("devices", "-l"); - -#define BUFSIZE 65536 - char *buf = malloc(BUFSIZE); - if (!buf) { - LOG_OOM(); - return false; - } - - sc_pipe pout; - sc_pid pid = sc_adb_execute_p(argv, flags, &pout); - if (pid == SC_PROCESS_NONE) { - LOGE("Could not execute \"adb devices -l\""); - free(buf); - return false; - } - - ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, BUFSIZE - 1); - sc_pipe_close(pout); - - bool ok = process_check_success_intr(intr, pid, "adb devices -l", flags); - if (!ok) { - free(buf); - return false; - } - - if (r == -1) { - free(buf); - return false; - } - - assert((size_t) r < BUFSIZE); - if (r == BUFSIZE - 1) { - // The implementation assumes that the output of "adb devices -l" fits - // in the buffer in a single pass - LOGW("Result of \"adb devices -l\" does not fit in 64Kb. " - "Please report an issue."); - free(buf); - return false; - } - - // It is parsed as a NUL-terminated string - buf[r] = '\0'; - - // List all devices to the output list directly - ok = sc_adb_parse_devices(buf, out_vec); - free(buf); - return ok; -} - -static bool -sc_adb_accept_device(const struct sc_adb_device *device, - const struct sc_adb_device_selector *selector) { - switch (selector->type) { - case SC_ADB_DEVICE_SELECT_ALL: - return true; - case SC_ADB_DEVICE_SELECT_SERIAL: - assert(selector->serial); - char *device_serial_colon = strchr(device->serial, ':'); - if (device_serial_colon) { - // The device serial is an IP:port... - char *serial_colon = strchr(selector->serial, ':'); - if (!serial_colon) { - // But the requested serial has no ':', so only consider - // the IP part of the device serial. This allows to use - // "192.168.1.1" to match any "192.168.1.1:port". - size_t serial_len = strlen(selector->serial); - size_t device_ip_len = device_serial_colon - device->serial; - if (serial_len != device_ip_len) { - // They are not equal, they don't even have the same - // length - return false; - } - return !strncmp(selector->serial, device->serial, - device_ip_len); - } - } - return !strcmp(selector->serial, device->serial); - case SC_ADB_DEVICE_SELECT_USB: - return sc_adb_device_get_type(device->serial) == - SC_ADB_DEVICE_TYPE_USB; - case SC_ADB_DEVICE_SELECT_TCPIP: - // Both emulators and TCP/IP devices are selected via -e - return sc_adb_device_get_type(device->serial) != - SC_ADB_DEVICE_TYPE_USB; - default: - assert(!"Missing SC_ADB_DEVICE_SELECT_* handling"); - break; - } - - return false; -} - -static size_t -sc_adb_devices_select(struct sc_adb_device *devices, size_t len, - const struct sc_adb_device_selector *selector, - size_t *idx_out) { - size_t count = 0; - for (size_t i = 0; i < len; ++i) { - struct sc_adb_device *device = &devices[i]; - device->selected = sc_adb_accept_device(device, selector); - if (device->selected) { - if (idx_out && !count) { - *idx_out = i; - } - ++count; - } - } - - return count; -} - -static void -sc_adb_devices_log(enum sc_log_level level, struct sc_adb_device *devices, - size_t count) { - for (size_t i = 0; i < count; ++i) { - struct sc_adb_device *d = &devices[i]; - const char *selection = d->selected ? "-->" : " "; - bool is_usb = - sc_adb_device_get_type(d->serial) == SC_ADB_DEVICE_TYPE_USB; - const char *type = is_usb ? " (usb)" - : "(tcpip)"; - LOG(level, " %s %s %-20s %16s %s", - selection, type, d->serial, d->state, d->model ? d->model : ""); - } -} - -static bool -sc_adb_device_check_state(struct sc_adb_device *device, - struct sc_adb_device *devices, size_t count) { - const char *state = device->state; - - if (!strcmp("device", state)) { - return true; - } - - if (!strcmp("unauthorized", state)) { - LOGE("Device is unauthorized:"); - sc_adb_devices_log(SC_LOG_LEVEL_ERROR, devices, count); - LOGE("A popup should open on the device to request authorization."); - LOGE("Check the FAQ: " - ""); - } else { - LOGE("Device could not be connected (state=%s)", state); - } - - return false; -} - -bool -sc_adb_select_device(struct sc_intr *intr, - const struct sc_adb_device_selector *selector, - unsigned flags, struct sc_adb_device *out_device) { - struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER; - bool ok = sc_adb_list_devices(intr, flags, &vec); - if (!ok) { - LOGE("Could not list ADB devices"); - return false; - } - - if (vec.size == 0) { - LOGE("Could not find any ADB device"); - return false; - } - - size_t sel_idx; // index of the single matching device if sel_count == 1 - size_t sel_count = - sc_adb_devices_select(vec.data, vec.size, selector, &sel_idx); - - if (sel_count == 0) { - // if count > 0 && sel_count == 0, then necessarily a selection is - // requested - assert(selector->type != SC_ADB_DEVICE_SELECT_ALL); - - switch (selector->type) { - case SC_ADB_DEVICE_SELECT_SERIAL: - assert(selector->serial); - LOGE("Could not find ADB device %s:", selector->serial); - break; - case SC_ADB_DEVICE_SELECT_USB: - LOGE("Could not find any ADB device over USB:"); - break; - case SC_ADB_DEVICE_SELECT_TCPIP: - LOGE("Could not find any ADB device over TCP/IP:"); - break; - default: - assert(!"Unexpected selector type"); - break; - } - - sc_adb_devices_log(SC_LOG_LEVEL_ERROR, vec.data, vec.size); - sc_adb_devices_destroy(&vec); - return false; - } - - if (sel_count > 1) { - switch (selector->type) { - case SC_ADB_DEVICE_SELECT_ALL: - LOGE("Multiple (%" SC_PRIsizet ") ADB devices:", sel_count); - break; - case SC_ADB_DEVICE_SELECT_SERIAL: - assert(selector->serial); - LOGE("Multiple (%" SC_PRIsizet ") ADB devices with serial %s:", - sel_count, selector->serial); - break; - case SC_ADB_DEVICE_SELECT_USB: - LOGE("Multiple (%" SC_PRIsizet ") ADB devices over USB:", - sel_count); - break; - case SC_ADB_DEVICE_SELECT_TCPIP: - LOGE("Multiple (%" SC_PRIsizet ") ADB devices over TCP/IP:", - sel_count); - break; - default: - assert(!"Unexpected selector type"); - break; - } - sc_adb_devices_log(SC_LOG_LEVEL_ERROR, vec.data, vec.size); - LOGE("Select a device via -s (--serial), -d (--select-usb) or -e " - "(--select-tcpip)"); - sc_adb_devices_destroy(&vec); - return false; - } - - assert(sel_count == 1); // sel_idx is valid only if sel_count == 1 - struct sc_adb_device *device = &vec.data[sel_idx]; - - ok = sc_adb_device_check_state(device, vec.data, vec.size); - if (!ok) { - sc_adb_devices_destroy(&vec); - return false; - } - - LOGI("ADB device found:"); - sc_adb_devices_log(SC_LOG_LEVEL_INFO, vec.data, vec.size); - - // Move devics into out_device (do not destroy device) - sc_adb_device_move(out_device, device); - sc_adb_devices_destroy(&vec); - return true; -} - -char * -sc_adb_getprop(struct sc_intr *intr, const char *serial, const char *prop, - unsigned flags) { - assert(serial); - const char *const argv[] = - SC_ADB_COMMAND("-s", serial, "shell", "getprop", prop); - - sc_pipe pout; - sc_pid pid = sc_adb_execute_p(argv, flags, &pout); - if (pid == SC_PROCESS_NONE) { - LOGE("Could not execute \"adb getprop\""); - return NULL; - } - - char buf[128]; - ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf) - 1); - sc_pipe_close(pout); - - bool ok = process_check_success_intr(intr, pid, "adb getprop", flags); - if (!ok) { - return NULL; - } - - if (r == -1) { - return NULL; - } - - assert((size_t) r < sizeof(buf)); - buf[r] = '\0'; - size_t len = strcspn(buf, " \r\n"); - buf[len] = '\0'; - - return strdup(buf); -} - -char * -sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags) { - assert(serial); - const char *const argv[] = - SC_ADB_COMMAND("-s", serial, "shell", "ip", "route"); - - sc_pipe pout; - sc_pid pid = sc_adb_execute_p(argv, flags, &pout); - if (pid == SC_PROCESS_NONE) { - LOGD("Could not execute \"ip route\""); - return NULL; - } - - // "adb shell ip route" output should contain only a few lines - char buf[1024]; - ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf) - 1); - sc_pipe_close(pout); - - bool ok = process_check_success_intr(intr, pid, "ip route", flags); - if (!ok) { - return NULL; - } - - if (r == -1) { - return NULL; - } - - assert((size_t) r < sizeof(buf)); - if (r == sizeof(buf) - 1) { - // The implementation assumes that the output of "ip route" fits in the - // buffer in a single pass - LOGW("Result of \"ip route\" does not fit in 1Kb. " - "Please report an issue."); - return NULL; - } - - // It is parsed as a NUL-terminated string - buf[r] = '\0'; - - return sc_adb_parse_device_ip(buf); -} - -uint16_t -sc_adb_get_device_sdk_version(struct sc_intr *intr, const char *serial) { - char *sdk_version = - sc_adb_getprop(intr, serial, "ro.build.version.sdk", SC_ADB_SILENT); - if (!sdk_version) { - return 0; - } - - long value; - bool ok = sc_str_parse_integer(sdk_version, &value); - free(sdk_version); - if (!ok || value < 0 || value > 0xFFFF) { - return 0; - } - - return value; -} diff --git a/app/src/adb/adb.h b/app/src/adb/adb.h deleted file mode 100644 index e4903902..00000000 --- a/app/src/adb/adb.h +++ /dev/null @@ -1,129 +0,0 @@ -#ifndef SC_ADB_H -#define SC_ADB_H - -#include "common.h" - -#include -#include - -#include "adb/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) - -bool -sc_adb_init(void); - -void -sc_adb_destroy(void); - -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); - -/** - * Return the device SDK version. - */ -uint16_t -sc_adb_get_device_sdk_version(struct sc_intr *intr, const char *serial); - -#endif diff --git a/app/src/adb/adb_device.c b/app/src/adb/adb_device.c deleted file mode 100644 index 5ea8eb44..00000000 --- a/app/src/adb/adb_device.c +++ /dev/null @@ -1,43 +0,0 @@ -#include "adb_device.h" - -#include -#include - -void -sc_adb_device_destroy(struct sc_adb_device *device) { - free(device->serial); - free(device->state); - free(device->model); -} - -void -sc_adb_device_move(struct sc_adb_device *dst, struct sc_adb_device *src) { - *dst = *src; - src->serial = NULL; - src->state = NULL; - src->model = NULL; -} - -void -sc_adb_devices_destroy(struct sc_vec_adb_devices *devices) { - for (size_t i = 0; i < devices->size; ++i) { - sc_adb_device_destroy(&devices->data[i]); - } - sc_vector_destroy(devices); -} - -enum sc_adb_device_type -sc_adb_device_get_type(const char *serial) { - // Starts with "emulator-" - if (!strncmp(serial, "emulator-", sizeof("emulator-") - 1)) { - return SC_ADB_DEVICE_TYPE_EMULATOR; - } - - // If the serial contains a ':', then it is a TCP/IP device (it is - // sufficient to distinguish an ip:port from a real USB serial) - if (strchr(serial, ':')) { - return SC_ADB_DEVICE_TYPE_TCPIP; - } - - return SC_ADB_DEVICE_TYPE_USB; -} diff --git a/app/src/adb/adb_device.h b/app/src/adb/adb_device.h deleted file mode 100644 index 308663ef..00000000 --- a/app/src/adb/adb_device.h +++ /dev/null @@ -1,49 +0,0 @@ -#ifndef SC_ADB_DEVICE_H -#define SC_ADB_DEVICE_H - -#include "common.h" - -#include - -#include "util/vector.h" - -struct sc_adb_device { - char *serial; - char *state; - char *model; - bool selected; -}; - -enum sc_adb_device_type { - SC_ADB_DEVICE_TYPE_USB, - SC_ADB_DEVICE_TYPE_TCPIP, - SC_ADB_DEVICE_TYPE_EMULATOR, -}; - -struct sc_vec_adb_devices SC_VECTOR(struct sc_adb_device); - -void -sc_adb_device_destroy(struct sc_adb_device *device); - -/** - * Move src to dst - * - * After this call, the content of src is undefined, except that - * sc_adb_device_destroy() can be called. - * - * This is useful to take a device from a list that will be destroyed, without - * making unnecessary copies. - */ -void -sc_adb_device_move(struct sc_adb_device *dst, struct sc_adb_device *src); - -void -sc_adb_devices_destroy(struct sc_vec_adb_devices *devices); - -/** - * Deduce the device type from the serial - */ -enum sc_adb_device_type -sc_adb_device_get_type(const char *serial); - -#endif diff --git a/app/src/adb/adb_parser.c b/app/src/adb/adb_parser.c deleted file mode 100644 index 90a1b30b..00000000 --- a/app/src/adb/adb_parser.c +++ /dev/null @@ -1,229 +0,0 @@ -#include "adb_parser.h" - -#include -#include -#include -#include - -#include "util/log.h" -#include "util/str.h" - -static bool -sc_adb_parse_device(char *line, struct sc_adb_device *device) { - // One device line looks like: - // "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " - // "device:MyDevice transport_id:1" - - if (line[0] == '*') { - // Garbage lines printed by adb daemon while starting start with a '*' - return false; - } - - if (!strncmp("adb server", line, sizeof("adb server") - 1)) { - // Ignore lines starting with "adb server": - // adb server version (41) doesn't match this client (39); killing... - return false; - } - - char *s = line; // cursor in the line - - // After the serial: - // - "adb devices" writes a single '\t' - // - "adb devices -l" writes multiple spaces - // For flexibility, accept both. - size_t serial_len = strcspn(s, " \t"); - if (!serial_len) { - // empty serial - return false; - } - bool eol = s[serial_len] == '\0'; - if (eol) { - // serial alone is unexpected - return false; - } - s[serial_len] = '\0'; - char *serial = s; - s += serial_len + 1; - // After the serial, there might be several spaces - s += strspn(s, " \t"); // consume all separators - - size_t state_len = strcspn(s, " "); - if (!state_len) { - // empty state - return false; - } - eol = s[state_len] == '\0'; - s[state_len] = '\0'; - char *state = s; - - char *model = NULL; - if (!eol) { - s += state_len + 1; - - // Iterate over all properties "key:value key:value ..." - for (;;) { - size_t token_len = strcspn(s, " "); - if (!token_len) { - break; - } - eol = s[token_len] == '\0'; - s[token_len] = '\0'; - char *token = s; - - if (!strncmp("model:", token, sizeof("model:") - 1)) { - model = &token[sizeof("model:") - 1]; - // We only need the model - break; - } - - if (eol) { - break; - } else { - s+= token_len + 1; - } - } - } - - device->serial = strdup(serial); - if (!device->serial) { - return false; - } - - device->state = strdup(state); - if (!device->state) { - free(device->serial); - return false; - } - - if (model) { - device->model = strdup(model); - if (!device->model) { - LOG_OOM(); - // model is optional, do not fail - } - } else { - device->model = NULL; - } - - device->selected = false; - - return true; -} - -bool -sc_adb_parse_devices(char *str, struct sc_vec_adb_devices *out_vec) { -#define HEADER "List of devices attached" -#define HEADER_LEN (sizeof(HEADER) - 1) - bool header_found = false; - - size_t idx_line = 0; - while (str[idx_line] != '\0') { - char *line = &str[idx_line]; - size_t len = strcspn(line, "\n"); - - // The next line starts after the '\n' (replaced by `\0`) - idx_line += len; - - if (str[idx_line] != '\0') { - // The next line starts after the '\n' - ++idx_line; - } - - if (!header_found) { - if (!strncmp(line, HEADER, HEADER_LEN)) { - header_found = true; - } - // Skip everything until the header, there might be garbage lines - // related to daemon starting before - continue; - } - - // The line, but without any trailing '\r' - size_t line_len = sc_str_remove_trailing_cr(line, len); - line[line_len] = '\0'; - - struct sc_adb_device device; - bool ok = sc_adb_parse_device(line, &device); - if (!ok) { - continue; - } - - ok = sc_vector_push(out_vec, device); - if (!ok) { - LOG_OOM(); - LOGE("Could not push adb_device to vector"); - sc_adb_device_destroy(&device); - // continue anyway - continue; - } - } - - assert(header_found || out_vec->size == 0); - return header_found; -} - -static char * -sc_adb_parse_device_ip_from_line(char *line) { - // One line from "ip route" looks like: - // "192.168.1.0/24 dev wlan0 proto kernel scope link src 192.168.1.x" - - // Get the location of the device name (index of "wlan0" in the example) - ssize_t idx_dev_name = sc_str_index_of_column(line, 2, " "); - if (idx_dev_name == -1) { - return NULL; - } - - // Get the location of the ip address (column 8, but column 6 if we start - // from column 2). Must be computed before truncating individual columns. - ssize_t idx_ip = sc_str_index_of_column(&line[idx_dev_name], 6, " "); - if (idx_ip == -1) { - return NULL; - } - // idx_ip is searched from &line[idx_dev_name] - idx_ip += idx_dev_name; - - char *dev_name = &line[idx_dev_name]; - size_t dev_name_len = strcspn(dev_name, " \t"); - dev_name[dev_name_len] = '\0'; - - char *ip = &line[idx_ip]; - size_t ip_len = strcspn(ip, " \t"); - ip[ip_len] = '\0'; - - // Only consider lines where the device name starts with "wlan" - if (strncmp(dev_name, "wlan", sizeof("wlan") - 1)) { - LOGD("Device ip lookup: ignoring %s (%s)", ip, dev_name); - return NULL; - } - - return strdup(ip); -} - -char * -sc_adb_parse_device_ip(char *str) { - size_t idx_line = 0; - while (str[idx_line] != '\0') { - char *line = &str[idx_line]; - size_t len = strcspn(line, "\n"); - bool is_last_line = line[len] == '\0'; - - // The same, but without any trailing '\r' - size_t line_len = sc_str_remove_trailing_cr(line, len); - line[line_len] = '\0'; - - char *ip = sc_adb_parse_device_ip_from_line(line); - if (ip) { - // Found - return ip; - } - - if (is_last_line) { - break; - } - - // The next line starts after the '\n' - idx_line += len + 1; - } - - return NULL; -} diff --git a/app/src/adb/adb_parser.h b/app/src/adb/adb_parser.h deleted file mode 100644 index b8738a35..00000000 --- a/app/src/adb/adb_parser.h +++ /dev/null @@ -1,30 +0,0 @@ -#ifndef SC_ADB_PARSER_H -#define SC_ADB_PARSER_H - -#include "common.h" - -#include - -#include "adb/adb_device.h" - -/** - * Parse the available devices from the output of `adb devices` - * - * The parameter must be a NUL-terminated string. - * - * Warning: this function modifies the buffer for optimization purposes. - */ -bool -sc_adb_parse_devices(char *str, struct sc_vec_adb_devices *out_vec); - -/** - * Parse the ip from the output of `adb shell ip route` - * - * The parameter must be a NUL-terminated string. - * - * Warning: this function modifies the buffer for optimization purposes. - */ -char * -sc_adb_parse_device_ip(char *str); - -#endif diff --git a/app/src/adb/adb_tunnel.c b/app/src/adb/adb_tunnel.c deleted file mode 100644 index 43e80e13..00000000 --- a/app/src/adb/adb_tunnel.c +++ /dev/null @@ -1,172 +0,0 @@ -#include "adb_tunnel.h" - -#include -#include - -#include "adb/adb.h" -#include "util/log.h" -#include "util/net_intr.h" - -static bool -listen_on_port(struct sc_intr *intr, sc_socket socket, uint16_t port) { - return net_listen_intr(intr, socket, IPV4_LOCALHOST, port, 1); -} - -static bool -enable_tunnel_reverse_any_port(struct sc_adb_tunnel *tunnel, - struct sc_intr *intr, const char *serial, - const char *device_socket_name, - struct sc_port_range port_range) { - uint16_t port = port_range.first; - for (;;) { - if (!sc_adb_reverse(intr, serial, device_socket_name, port, - SC_ADB_NO_STDOUT)) { - // the command itself failed, it will fail on any port - return false; - } - - // At the application level, the device part is "the server" because it - // serves video stream and control. However, at the network level, the - // client listens and the server connects to the client. That way, the - // client can listen before starting the server app, so there is no - // need to try to connect until the server socket is listening on the - // device. - sc_socket server_socket = net_socket(); - if (server_socket != SC_SOCKET_NONE) { - bool ok = listen_on_port(intr, server_socket, port); - if (ok) { - // success - tunnel->server_socket = server_socket; - tunnel->local_port = port; - tunnel->enabled = true; - return true; - } - - net_close(server_socket); - } - - if (sc_intr_is_interrupted(intr)) { - // Stop immediately - return false; - } - - // failure, disable tunnel and try another port - if (!sc_adb_reverse_remove(intr, serial, device_socket_name, - SC_ADB_NO_STDOUT)) { - LOGW("Could not remove reverse tunnel on port %" PRIu16, port); - } - - // check before incrementing to avoid overflow on port 65535 - if (port < port_range.last) { - LOGW("Could not listen on port %" PRIu16", retrying on %" PRIu16, - port, (uint16_t) (port + 1)); - port++; - continue; - } - - if (port_range.first == port_range.last) { - LOGE("Could not listen on port %" PRIu16, port_range.first); - } else { - LOGE("Could not listen on any port in range %" PRIu16 ":%" PRIu16, - port_range.first, port_range.last); - } - return false; - } -} - -static bool -enable_tunnel_forward_any_port(struct sc_adb_tunnel *tunnel, - struct sc_intr *intr, const char *serial, - const char *device_socket_name, - struct sc_port_range port_range) { - tunnel->forward = true; - - uint16_t port = port_range.first; - for (;;) { - if (sc_adb_forward(intr, serial, port, device_socket_name, - SC_ADB_NO_STDOUT)) { - // success - tunnel->local_port = port; - tunnel->enabled = true; - return true; - } - - if (sc_intr_is_interrupted(intr)) { - // Stop immediately - return false; - } - - if (port < port_range.last) { - LOGW("Could not forward port %" PRIu16", retrying on %" PRIu16, - port, (uint16_t) (port + 1)); - port++; - continue; - } - - if (port_range.first == port_range.last) { - LOGE("Could not forward port %" PRIu16, port_range.first); - } else { - LOGE("Could not forward any port in range %" PRIu16 ":%" PRIu16, - port_range.first, port_range.last); - } - return false; - } -} - -void -sc_adb_tunnel_init(struct sc_adb_tunnel *tunnel) { - tunnel->enabled = false; - tunnel->forward = false; - tunnel->server_socket = SC_SOCKET_NONE; - tunnel->local_port = 0; -} - -bool -sc_adb_tunnel_open(struct sc_adb_tunnel *tunnel, struct sc_intr *intr, - const char *serial, const char *device_socket_name, - struct sc_port_range port_range, bool force_adb_forward) { - assert(!tunnel->enabled); - - if (!force_adb_forward) { - // Attempt to use "adb reverse" - if (enable_tunnel_reverse_any_port(tunnel, intr, serial, - device_socket_name, port_range)) { - return true; - } - - // if "adb reverse" does not work (e.g. over "adb connect"), it - // fallbacks to "adb forward", so the app socket is the client - - LOGW("'adb reverse' failed, fallback to 'adb forward'"); - } - - return enable_tunnel_forward_any_port(tunnel, intr, serial, - device_socket_name, port_range); -} - -bool -sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr, - const char *serial, const char *device_socket_name) { - assert(tunnel->enabled); - - bool ret; - if (tunnel->forward) { - ret = sc_adb_forward_remove(intr, serial, tunnel->local_port, - SC_ADB_NO_STDOUT); - } else { - ret = sc_adb_reverse_remove(intr, serial, device_socket_name, - SC_ADB_NO_STDOUT); - - assert(tunnel->server_socket != SC_SOCKET_NONE); - if (!net_close(tunnel->server_socket)) { - LOGW("Could not close server socket"); - } - - // server_socket is never used anymore - } - - // Consider tunnel disabled even if the command failed - tunnel->enabled = false; - - return ret; -} diff --git a/app/src/adb/adb_tunnel.h b/app/src/adb/adb_tunnel.h deleted file mode 100644 index 7ed5bf54..00000000 --- a/app/src/adb/adb_tunnel.h +++ /dev/null @@ -1,47 +0,0 @@ -#ifndef SC_ADB_TUNNEL_H -#define SC_ADB_TUNNEL_H - -#include "common.h" - -#include -#include - -#include "options.h" -#include "util/intr.h" -#include "util/net.h" - -struct sc_adb_tunnel { - bool enabled; - bool forward; // use "adb forward" instead of "adb reverse" - sc_socket server_socket; // only used if !forward - uint16_t local_port; -}; - -/** - * Initialize the adb tunnel struct to default values - */ -void -sc_adb_tunnel_init(struct sc_adb_tunnel *tunnel); - -/** - * Open a tunnel - * - * Blocking calls may be interrupted asynchronously via `intr`. - * - * If `force_adb_forward` is not set, then attempts to set up an "adb reverse" - * tunnel first. Only if it fails (typical on old Android version connected via - * TCP/IP), use "adb forward". - */ -bool -sc_adb_tunnel_open(struct sc_adb_tunnel *tunnel, struct sc_intr *intr, - const char *serial, const char *device_socket_name, - struct sc_port_range port_range, bool force_adb_forward); - -/** - * Close the tunnel - */ -bool -sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr, - const char *serial, const char *device_socket_name); - -#endif diff --git a/app/src/android/input.h b/app/src/android/input.h index 30c4bcb9..b51731b4 100644 --- a/app/src/android/input.h +++ b/app/src/android/input.h @@ -21,7 +21,7 @@ #define _ANDROID_INPUT_H /** - * Meta key / modifier state. + * Meta key / modifer state. */ enum android_metastate { /** No meta keys are pressed. */ diff --git a/app/src/android/keycodes.h b/app/src/android/keycodes.h index 03ebb9c8..60465a18 100644 --- a/app/src/android/keycodes.h +++ b/app/src/android/keycodes.h @@ -633,7 +633,7 @@ enum android_keycode { * Toggles between BS and CS digital satellite services. */ AKEYCODE_TV_SATELLITE_SERVICE = 240, /** Toggle Network key. - * Toggles selecting broadcast services. */ + * Toggles selecting broacast services. */ AKEYCODE_TV_NETWORK = 241, /** Antenna/Cable key. * Toggles broadcast input source between antenna and cable. */ diff --git a/app/src/audio_player.c b/app/src/audio_player.c deleted file mode 100644 index 9413c2ea..00000000 --- a/app/src/audio_player.c +++ /dev/null @@ -1,118 +0,0 @@ -#include "audio_player.h" - -#include "util/log.h" - -/** Downcast frame_sink to sc_audio_player */ -#define DOWNCAST(SINK) container_of(SINK, struct sc_audio_player, frame_sink) - -#define SC_SDL_SAMPLE_FMT AUDIO_F32 - -static void SDLCALL -sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) { - struct sc_audio_player *ap = userdata; - - assert(len_int > 0); - size_t len = len_int; - - assert(len % ap->audioreg.sample_size == 0); - uint32_t out_samples = len / ap->audioreg.sample_size; - - sc_audio_regulator_pull(&ap->audioreg, stream, out_samples); -} - -static bool -sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, - const AVFrame *frame) { - struct sc_audio_player *ap = DOWNCAST(sink); - - return sc_audio_regulator_push(&ap->audioreg, frame); -} - -static bool -sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, - const AVCodecContext *ctx) { - struct sc_audio_player *ap = DOWNCAST(sink); - -#ifdef SCRCPY_LAVU_HAS_CHLAYOUT - assert(ctx->ch_layout.nb_channels > 0 && ctx->ch_layout.nb_channels < 256); - uint8_t nb_channels = ctx->ch_layout.nb_channels; -#else - int tmp = av_get_channel_layout_nb_channels(ctx->channel_layout); - assert(tmp > 0 && tmp < 256); - uint8_t nb_channels = tmp; -#endif - - assert(ctx->sample_rate > 0); - assert(!av_sample_fmt_is_planar(SC_AV_SAMPLE_FMT)); - int out_bytes_per_sample = av_get_bytes_per_sample(SC_AV_SAMPLE_FMT); - assert(out_bytes_per_sample > 0); - - uint32_t target_buffering_samples = - ap->target_buffering_delay * ctx->sample_rate / SC_TICK_FREQ; - - size_t sample_size = nb_channels * out_bytes_per_sample; - bool ok = sc_audio_regulator_init(&ap->audioreg, sample_size, ctx, - target_buffering_samples); - if (!ok) { - return false; - } - - uint64_t aout_samples = ap->output_buffer_duration * ctx->sample_rate - / SC_TICK_FREQ; - assert(aout_samples <= 0xFFFF); - - SDL_AudioSpec desired = { - .freq = ctx->sample_rate, - .format = SC_SDL_SAMPLE_FMT, - .channels = nb_channels, - .samples = aout_samples, - .callback = sc_audio_player_sdl_callback, - .userdata = ap, - }; - SDL_AudioSpec obtained; - - ap->device = SDL_OpenAudioDevice(NULL, 0, &desired, &obtained, 0); - if (!ap->device) { - LOGE("Could not open audio device: %s", SDL_GetError()); - sc_audio_regulator_destroy(&ap->audioreg); - return false; - } - - // The thread calling open() is the thread calling push(), which fills the - // audio buffer consumed by the SDL audio thread. - ok = sc_thread_set_priority(SC_THREAD_PRIORITY_TIME_CRITICAL); - if (!ok) { - ok = sc_thread_set_priority(SC_THREAD_PRIORITY_HIGH); - (void) ok; // We don't care if it worked, at least we tried - } - - SDL_PauseAudioDevice(ap->device, 0); - - return true; -} - -static void -sc_audio_player_frame_sink_close(struct sc_frame_sink *sink) { - struct sc_audio_player *ap = DOWNCAST(sink); - - assert(ap->device); - SDL_PauseAudioDevice(ap->device, 1); - SDL_CloseAudioDevice(ap->device); - - sc_audio_regulator_destroy(&ap->audioreg); -} - -void -sc_audio_player_init(struct sc_audio_player *ap, sc_tick target_buffering, - sc_tick output_buffer_duration) { - ap->target_buffering_delay = target_buffering; - ap->output_buffer_duration = output_buffer_duration; - - static const struct sc_frame_sink_ops ops = { - .open = sc_audio_player_frame_sink_open, - .close = sc_audio_player_frame_sink_close, - .push = sc_audio_player_frame_sink_push, - }; - - ap->frame_sink.ops = &ops; -} diff --git a/app/src/audio_player.h b/app/src/audio_player.h deleted file mode 100644 index 5a66d43b..00000000 --- a/app/src/audio_player.h +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef SC_AUDIO_PLAYER_H -#define SC_AUDIO_PLAYER_H - -#include "common.h" - -#include - -#include "audio_regulator.h" -#include "trait/frame_sink.h" -#include "util/tick.h" - -struct sc_audio_player { - struct sc_frame_sink frame_sink; - - // The target buffering between the producer and the consumer. This value - // is directly use for compensation. - // Since audio capture and/or encoding on the device typically produce - // blocks of 960 samples (20ms) or 1024 samples (~21.3ms), this target - // value should be higher. - sc_tick target_buffering_delay; - - // SDL audio output buffer size - sc_tick output_buffer_duration; - - SDL_AudioDeviceID device; - struct sc_audio_regulator audioreg; -}; - -void -sc_audio_player_init(struct sc_audio_player *ap, sc_tick target_buffering, - sc_tick audio_output_buffer); - -#endif diff --git a/app/src/audio_regulator.c b/app/src/audio_regulator.c deleted file mode 100644 index 16fdd08b..00000000 --- a/app/src/audio_regulator.c +++ /dev/null @@ -1,456 +0,0 @@ -#include "audio_regulator.h" - -#include -#include -#include -#include -#include -#include - -#include "util/log.h" - -//#define SC_AUDIO_REGULATOR_DEBUG // uncomment to debug - -/** - * Real-time audio regulator with configurable latency - * - * As input, the regulator regularly receives AVFrames of decoded audio samples. - * As output, the audio player regularly requests audio samples to be played. - * In the middle, an audio buffer stores the samples produced but not consumed - * yet. - * - * The goal of the regulator is to feed the audio player with a latency as low - * as possible while avoiding buffer underrun (i.e. not being able to provide - * samples when requested). - * - * To achieve this, it attempts to maintain the average buffering (the number - * of samples present in the buffer) around a target value. If this target - * buffering is too low, then buffer underrun will occur frequently. If it is - * too high, then latency will become unacceptable. This target value is - * configured using the scrcpy option --audio-buffer. - * - * The regulator cannot adjust the sample input rate (it receives samples - * produced in real-time) or the sample output rate (it must provide samples as - * requested by the audio player). Therefore, it may only apply compensation by - * resampling (converting _m_ input samples to _n_ output samples). - * - * The compensation itself is applied by libswresample (FFmpeg). It is - * configured using swr_set_compensation(). An important work for the regulator - * is to estimate the compensation value regularly and apply it. - * - * The estimated buffering level is the result of averaging the "natural" - * buffering (samples are produced and consumed by blocks, so it must be - * smoothed), and making instant adjustments resulting of its own actions - * (explicit compensation and silence insertion on underflow), which are not - * smoothed. - * - * Buffer underflow events can occur when packets arrive too late. In that case, - * the regulator inserts silence. Once the packets finally arrive (late), one - * strategy could be to drop the samples that were replaced by silence, in - * order to keep a minimal latency. However, dropping samples in case of buffer - * underflow is inadvisable, as it would temporarily increase the underflow - * even more and cause very noticeable audio glitches. - * - * Therefore, the regulator doesn't drop any sample on underflow. The - * compensation mechanism will absorb the delay introduced by the inserted - * silence. - */ - -#define TO_BYTES(SAMPLES) sc_audiobuf_to_bytes(&ar->buf, (SAMPLES)) -#define TO_SAMPLES(BYTES) sc_audiobuf_to_samples(&ar->buf, (BYTES)) - -void -sc_audio_regulator_pull(struct sc_audio_regulator *ar, uint8_t *out, - uint32_t out_samples) { -#ifdef SC_AUDIO_REGULATOR_DEBUG - LOGD("[Audio] Audio regulator pulls %" PRIu32 " samples", out_samples); -#endif - - // A lock is necessary in the rare case where the producer needs to drop - // samples already pushed (when the buffer is full) - sc_mutex_lock(&ar->mutex); - - bool played = atomic_load_explicit(&ar->played, memory_order_relaxed); - if (!played) { - uint32_t buffered_samples = sc_audiobuf_can_read(&ar->buf); - // Wait until the buffer is filled up to at least target_buffering - // before playing - if (buffered_samples < ar->target_buffering) { -#ifdef SC_AUDIO_REGULATOR_DEBUG - LOGD("[Audio] Inserting initial buffering silence: %" PRIu32 - " samples", out_samples); -#endif - // Delay playback starting to reach the target buffering. Fill the - // whole buffer with silence (len is small compared to the - // arbitrary margin value). - memset(out, 0, out_samples * ar->sample_size); - sc_mutex_unlock(&ar->mutex); - return; - } - } - - uint32_t read = sc_audiobuf_read(&ar->buf, out, out_samples); - - sc_mutex_unlock(&ar->mutex); - - if (read < out_samples) { - uint32_t silence = out_samples - read; - // Insert silence. In theory, the inserted silent samples replace the - // missing real samples, which will arrive later, so they should be - // dropped to keep the latency minimal. However, this would cause very - // audible glitches, so let the clock compensation restore the target - // latency. -#ifdef SC_AUDIO_REGULATOR_DEBUG - LOGD("[Audio] Buffer underflow, inserting silence: %" PRIu32 " samples", - silence); -#endif - memset(out + TO_BYTES(read), 0, TO_BYTES(silence)); - - bool received = atomic_load_explicit(&ar->received, - memory_order_relaxed); - if (received) { - // Inserting additional samples immediately increases buffering - atomic_fetch_add_explicit(&ar->underflow, silence, - memory_order_relaxed); - } - } - - atomic_store_explicit(&ar->played, true, memory_order_relaxed); -} - -static uint8_t * -sc_audio_regulator_get_swr_buf(struct sc_audio_regulator *ar, - uint32_t min_samples) { - size_t min_buf_size = TO_BYTES(min_samples); - if (min_buf_size > ar->swr_buf_alloc_size) { - size_t new_size = min_buf_size + 4096; - uint8_t *buf = realloc(ar->swr_buf, new_size); - if (!buf) { - LOG_OOM(); - // Could not realloc to the requested size - return NULL; - } - ar->swr_buf = buf; - ar->swr_buf_alloc_size = new_size; - } - - return ar->swr_buf; -} - -bool -sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) { - SwrContext *swr_ctx = ar->swr_ctx; - - uint32_t input_samples = frame->nb_samples; - - assert(frame->pts >= 0); - int64_t pts = frame->pts; - if (ar->next_expected_pts && pts - ar->next_expected_pts > 100000) { - LOGV("[Audio] Discontinuity detected: %" PRIi64 "µs", - pts - ar->next_expected_pts); - // More than 100ms: consider it as a discontinuity - // (typically because silence packets were not captured) - uint32_t can_read = sc_audiobuf_can_read(&ar->buf); - if (input_samples + can_read < ar->target_buffering) { - // Adjust buffering to the target value directly - uint32_t silence = ar->target_buffering - can_read - input_samples; - sc_audiobuf_write_silence(&ar->buf, silence); - } - - // Reset state - ar->avg_buffering.avg = ar->target_buffering; - int ret = swr_set_compensation(swr_ctx, 0, 0); - (void) ret; - assert(!ret); // disabling compensation should never fail - ar->compensation_active = false; - ar->samples_since_resync = 0; - atomic_store_explicit(&ar->underflow, 0, memory_order_relaxed); - } - - int64_t packet_duration = input_samples * INT64_C(1000000) - / ar->sample_rate; - ar->next_expected_pts = pts + packet_duration; - - int64_t swr_delay = swr_get_delay(swr_ctx, ar->sample_rate); - // No need to av_rescale_rnd(), input and output sample rates are the same. - // Add more space (256) for clock compensation. - int dst_nb_samples = swr_delay + frame->nb_samples + 256; - - uint8_t *swr_buf = sc_audio_regulator_get_swr_buf(ar, dst_nb_samples); - if (!swr_buf) { - return false; - } - - int ret = swr_convert(swr_ctx, &swr_buf, dst_nb_samples, - (const uint8_t **) frame->data, frame->nb_samples); - if (ret < 0) { - LOGE("Resampling failed: %d", ret); - return false; - } - - // swr_convert() returns the number of samples which would have been - // written if the buffer was big enough. - uint32_t samples = MIN(ret, dst_nb_samples); -#ifdef SC_AUDIO_REGULATOR_DEBUG - LOGD("[Audio] %" PRIu32 " samples written to buffer", samples); -#endif - - uint32_t cap = sc_audiobuf_capacity(&ar->buf); - if (samples > cap) { - // Very very unlikely: a single resampled frame should never - // exceed the audio buffer size (or something is very wrong). - // Ignore the first bytes in swr_buf to avoid memory corruption anyway. - swr_buf += TO_BYTES(samples - cap); - samples = cap; - } - - uint32_t skipped_samples = 0; - - uint32_t written = sc_audiobuf_write(&ar->buf, swr_buf, samples); - if (written < samples) { - uint32_t remaining = samples - written; - - // All samples that could be written without locking have been written, - // now we need to lock to drop/consume old samples - sc_mutex_lock(&ar->mutex); - - // Retry with the lock - written += sc_audiobuf_write(&ar->buf, - swr_buf + TO_BYTES(written), - remaining); - if (written < samples) { - remaining = samples - written; - // Still insufficient, drop old samples to make space - skipped_samples = sc_audiobuf_read(&ar->buf, NULL, remaining); - assert(skipped_samples == remaining); - } - - sc_mutex_unlock(&ar->mutex); - - if (written < samples) { - // Now there is enough space - uint32_t w = sc_audiobuf_write(&ar->buf, - swr_buf + TO_BYTES(written), - remaining); - assert(w == remaining); - (void) w; - } - } - - uint32_t underflow = 0; - uint32_t max_buffered_samples; - bool played = atomic_load_explicit(&ar->played, memory_order_relaxed); - if (played) { - underflow = atomic_exchange_explicit(&ar->underflow, 0, - memory_order_relaxed); - ar->underflow_report += underflow; - - max_buffered_samples = ar->target_buffering * 11 / 10 - + 60 * ar->sample_rate / 1000 /* 60 ms */; - } else { - // Playback not started yet, do not accumulate more than - // max_initial_buffering samples, this would cause unnecessary delay - // (and glitches to compensate) on start. - max_buffered_samples = ar->target_buffering - + 10 * ar->sample_rate / 1000 /* 10 ms */; - } - - uint32_t can_read = sc_audiobuf_can_read(&ar->buf); - if (can_read > max_buffered_samples) { - uint32_t skip_samples = 0; - - sc_mutex_lock(&ar->mutex); - can_read = sc_audiobuf_can_read(&ar->buf); - if (can_read > max_buffered_samples) { - skip_samples = can_read - max_buffered_samples; - uint32_t r = sc_audiobuf_read(&ar->buf, NULL, skip_samples); - assert(r == skip_samples); - (void) r; - skipped_samples += skip_samples; - } - sc_mutex_unlock(&ar->mutex); - - if (skip_samples) { - if (played) { - LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32 - " samples", skip_samples); -#ifdef SC_AUDIO_REGULATOR_DEBUG - } else { - LOGD("[Audio] Playback not started, skipping %" PRIu32 - " samples", skip_samples); -#endif - } - } - } - - atomic_store_explicit(&ar->received, true, memory_order_relaxed); - if (!played) { - // Nothing more to do - return true; - } - - // Number of samples added (or removed, if negative) for compensation - int32_t instant_compensation = (int32_t) written - input_samples; - // Inserting silence instantly increases buffering - int32_t inserted_silence = (int32_t) underflow; - // Dropping input samples instantly decreases buffering - int32_t dropped = (int32_t) skipped_samples; - - // The compensation must apply instantly, it must not be smoothed - ar->avg_buffering.avg += instant_compensation + inserted_silence - dropped; - if (ar->avg_buffering.avg < 0) { - // Since dropping samples instantly reduces buffering, the difference - // is applied immediately to the average value, assuming that the delay - // between the producer and the consumer will be caught up. - // - // However, when this assumption is not valid, the average buffering - // may decrease indefinitely. Prevent it to become negative to limit - // the consequences. - ar->avg_buffering.avg = 0; - } - - // However, the buffering level must be smoothed - sc_average_push(&ar->avg_buffering, can_read); - -#ifdef SC_AUDIO_REGULATOR_DEBUG - LOGD("[Audio] can_read=%" PRIu32 " avg_buffering=%f", - can_read, sc_average_get(&ar->avg_buffering)); -#endif - - ar->samples_since_resync += written; - if (ar->samples_since_resync >= ar->sample_rate) { - // Recompute compensation every second - ar->samples_since_resync = 0; - - float avg = sc_average_get(&ar->avg_buffering); - int diff = ar->target_buffering - avg; - - // Enable compensation when the difference exceeds +/- 4ms. - // Disable compensation when the difference is lower than +/- 1ms. - int threshold = ar->compensation_active - ? ar->sample_rate / 1000 /* 1ms */ - : ar->sample_rate * 4 / 1000; /* 4ms */ - - if (abs(diff) < threshold) { - // Do not compensate for small values, the error is just noise - diff = 0; - } else if (diff < 0 && can_read < ar->target_buffering) { - // Do not accelerate if the instant buffering level is below the - // target, this would increase underflow - diff = 0; - } - // Compensate the diff over 4 seconds (but will be recomputed after 1 - // second) - int distance = 4 * ar->sample_rate; - // Limit compensation rate to 2% - int abs_max_diff = distance / 50; - diff = CLAMP(diff, -abs_max_diff, abs_max_diff); - LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32 - " compensation=%d (underflow=%" PRIu32 ")", - ar->target_buffering, avg, can_read, diff, ar->underflow_report); - ar->underflow_report = 0; - - int ret = swr_set_compensation(swr_ctx, diff, distance); - if (ret < 0) { - LOGW("Resampling compensation failed: %d", ret); - // not fatal - } else { - ar->compensation_active = diff != 0; - } - } - - return true; -} - -bool -sc_audio_regulator_init(struct sc_audio_regulator *ar, size_t sample_size, - const AVCodecContext *ctx, uint32_t target_buffering) { - SwrContext *swr_ctx = swr_alloc(); - if (!swr_ctx) { - LOG_OOM(); - return false; - } - ar->swr_ctx = swr_ctx; - -#ifdef SCRCPY_LAVU_HAS_CHLAYOUT - av_opt_set_chlayout(swr_ctx, "in_chlayout", &ctx->ch_layout, 0); - av_opt_set_chlayout(swr_ctx, "out_chlayout", &ctx->ch_layout, 0); -#else - av_opt_set_channel_layout(swr_ctx, "in_channel_layout", - ctx->channel_layout, 0); - av_opt_set_channel_layout(swr_ctx, "out_channel_layout", - ctx->channel_layout, 0); -#endif - - av_opt_set_int(swr_ctx, "in_sample_rate", ctx->sample_rate, 0); - av_opt_set_int(swr_ctx, "out_sample_rate", ctx->sample_rate, 0); - - av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt", ctx->sample_fmt, 0); - av_opt_set_sample_fmt(swr_ctx, "out_sample_fmt", SC_AV_SAMPLE_FMT, 0); - - int ret = swr_init(swr_ctx); - if (ret) { - LOGE("Failed to initialize the resampling context"); - goto error_free_swr_ctx; - } - - bool ok = sc_mutex_init(&ar->mutex); - if (!ok) { - goto error_free_swr_ctx; - } - - ar->target_buffering = target_buffering; - ar->sample_size = sample_size; - ar->sample_rate = ctx->sample_rate; - - // Use a ring-buffer of the target buffering size plus 1 second between the - // producer and the consumer. It's too big on purpose, to guarantee that - // the producer and the consumer will be able to access it in parallel - // without locking. - uint32_t audiobuf_samples = target_buffering + ar->sample_rate; - - ok = sc_audiobuf_init(&ar->buf, sample_size, audiobuf_samples); - if (!ok) { - goto error_destroy_mutex; - } - - size_t initial_swr_buf_size = TO_BYTES(4096); - ar->swr_buf = malloc(initial_swr_buf_size); - if (!ar->swr_buf) { - LOG_OOM(); - goto error_destroy_audiobuf; - } - ar->swr_buf_alloc_size = initial_swr_buf_size; - - // Samples are produced and consumed by blocks, so the buffering must be - // smoothed to get a relatively stable value. - sc_average_init(&ar->avg_buffering, 128); - ar->samples_since_resync = 0; - - ar->received = false; - atomic_init(&ar->played, false); - atomic_init(&ar->received, false); - atomic_init(&ar->underflow, 0); - ar->underflow_report = 0; - ar->compensation_active = false; - ar->next_expected_pts = 0; - - return true; - -error_destroy_audiobuf: - sc_audiobuf_destroy(&ar->buf); -error_destroy_mutex: - sc_mutex_destroy(&ar->mutex); -error_free_swr_ctx: - swr_free(&ar->swr_ctx); - - return false; -} - -void -sc_audio_regulator_destroy(struct sc_audio_regulator *ar) { - free(ar->swr_buf); - sc_audiobuf_destroy(&ar->buf); - sc_mutex_destroy(&ar->mutex); - swr_free(&ar->swr_ctx); -} diff --git a/app/src/audio_regulator.h b/app/src/audio_regulator.h deleted file mode 100644 index 4e18fe08..00000000 --- a/app/src/audio_regulator.h +++ /dev/null @@ -1,79 +0,0 @@ -#ifndef SC_AUDIO_REGULATOR_H -#define SC_AUDIO_REGULATOR_H - -#include "common.h" - -#include -#include -#include -#include -#include -#include -#include "util/audiobuf.h" -#include "util/average.h" -#include "util/thread.h" - -#define SC_AV_SAMPLE_FMT AV_SAMPLE_FMT_FLT - -struct sc_audio_regulator { - sc_mutex mutex; - - // Target buffering between the producer and the consumer (in samples) - uint32_t target_buffering; - - // Audio buffer to communicate between the receiver and the player - struct sc_audiobuf buf; - - // Resampler (only used from the receiver thread) - struct SwrContext *swr_ctx; - - // The sample rate is the same for input and output - uint32_t sample_rate; - // The number of bytes per sample (for all channels) - size_t sample_size; - - // Target buffer for resampling (only used by the receiver thread) - uint8_t *swr_buf; - size_t swr_buf_alloc_size; - - // Number of buffered samples (may be negative on underflow) (only used by - // the receiver thread) - struct sc_average avg_buffering; - // Count the number of samples to trigger a compensation update regularly - // (only used by the receiver thread) - uint32_t samples_since_resync; - - // Number of silence samples inserted since the last received packet - atomic_uint_least32_t underflow; - - // Number of silence samples inserted since the last log - uint32_t underflow_report; - - // Non-zero compensation applied (only used by the receiver thread) - bool compensation_active; - - // Set to true the first time a sample is received - atomic_bool received; - - // Set to true the first time samples are pulled by the player - atomic_bool played; - - // PTS of the next expected packet (useful to detect discontinuities) - int64_t next_expected_pts; -}; - -bool -sc_audio_regulator_init(struct sc_audio_regulator *ar, size_t sample_size, - const AVCodecContext *ctx, uint32_t target_buffering); - -void -sc_audio_regulator_destroy(struct sc_audio_regulator *ar); - -bool -sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame); - -void -sc_audio_regulator_pull(struct sc_audio_regulator *ar, uint8_t *out, - uint32_t samples); - -#endif diff --git a/app/src/buffer_util.h b/app/src/buffer_util.h new file mode 100644 index 00000000..a79014b1 --- /dev/null +++ b/app/src/buffer_util.h @@ -0,0 +1,38 @@ +#ifndef BUFFER_UTIL_H +#define BUFFER_UTIL_H + +#include +#include + +static inline void +buffer_write16be(uint8_t *buf, uint16_t value) { + buf[0] = value >> 8; + buf[1] = value; +} + +static inline void +buffer_write32be(uint8_t *buf, uint32_t value) { + buf[0] = value >> 24; + buf[1] = value >> 16; + buf[2] = value >> 8; + buf[3] = value; +} + +static inline uint16_t +buffer_read16be(const uint8_t *buf) { + return (buf[0] << 8) | buf[1]; +} + +static inline uint32_t +buffer_read32be(const uint8_t *buf) { + return (buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3]; +} + +static inline +uint64_t buffer_read64be(const uint8_t *buf) { + uint32_t msb = buffer_read32be(buf); + uint32_t lsb = buffer_read32be(&buf[4]); + return ((uint64_t) msb << 32) | lsb; +} + +#endif diff --git a/app/src/cbuf.h b/app/src/cbuf.h new file mode 100644 index 00000000..5d9fe4ae --- /dev/null +++ b/app/src/cbuf.h @@ -0,0 +1,50 @@ +// generic circular buffer (bounded queue) implementation +#ifndef CBUF_H +#define CBUF_H + +#include +#include + +// To define a circular buffer type of 20 ints: +// typedef CBUF(int, 20) my_cbuf_t; +// +// data has length CAP + 1 to distinguish empty vs full. +#define CBUF(TYPE, CAP) { \ + TYPE data[(CAP) + 1]; \ + size_t head; \ + size_t tail; \ +} + +#define cbuf_size_(PCBUF) \ + (sizeof((PCBUF)->data) / sizeof(*(PCBUF)->data)) + +#define cbuf_is_empty(PCBUF) \ + ((PCBUF)->head == (PCBUF)->tail) + +#define cbuf_is_full(PCBUF) \ + (((PCBUF)->head + 1) % cbuf_size_(PCBUF) == (PCBUF)->tail) + +#define cbuf_init(PCBUF) \ + (void) ((PCBUF)->head = (PCBUF)->tail = 0) + +#define cbuf_push(PCBUF, ITEM) \ + ({ \ + bool ok = !cbuf_is_full(PCBUF); \ + if (ok) { \ + (PCBUF)->data[(PCBUF)->head] = (ITEM); \ + (PCBUF)->head = ((PCBUF)->head + 1) % cbuf_size_(PCBUF); \ + } \ + ok; \ + }) \ + +#define cbuf_take(PCBUF, PITEM) \ + ({ \ + bool ok = !cbuf_is_empty(PCBUF); \ + if (ok) { \ + *(PITEM) = (PCBUF)->data[(PCBUF)->tail]; \ + (PCBUF)->tail = ((PCBUF)->tail + 1) % cbuf_size_(PCBUF); \ + } \ + ok; \ + }) + +#endif diff --git a/app/src/cli.c b/app/src/cli.c deleted file mode 100644 index b2e3e30a..00000000 --- a/app/src/cli.c +++ /dev/null @@ -1,3374 +0,0 @@ -#include "cli.h" - -#include -#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" -#include "util/tick.h" - -#define STR_IMPL_(x) #x -#define STR(x) STR_IMPL_(x) - -enum { - OPT_BIT_RATE = 1000, - OPT_WINDOW_TITLE, - OPT_PUSH_TARGET, - OPT_ALWAYS_ON_TOP, - OPT_CROP, - OPT_RECORD_FORMAT, - OPT_PREFER_TEXT, - OPT_WINDOW_X, - OPT_WINDOW_Y, - OPT_WINDOW_WIDTH, - OPT_WINDOW_HEIGHT, - OPT_WINDOW_BORDERLESS, - OPT_MAX_FPS, - OPT_LOCK_VIDEO_ORIENTATION, - OPT_DISPLAY, - OPT_DISPLAY_ID, - OPT_ROTATION, - OPT_RENDER_DRIVER, - OPT_NO_MIPMAPS, - OPT_CODEC_OPTIONS, - OPT_VIDEO_CODEC_OPTIONS, - OPT_FORCE_ADB_FORWARD, - OPT_DISABLE_SCREENSAVER, - OPT_SHORTCUT_MOD, - OPT_NO_KEY_REPEAT, - OPT_FORWARD_ALL_CLICKS, - OPT_LEGACY_PASTE, - OPT_ENCODER, - OPT_VIDEO_ENCODER, - OPT_POWER_OFF_ON_CLOSE, - OPT_V4L2_SINK, - OPT_DISPLAY_BUFFER, - OPT_VIDEO_BUFFER, - OPT_V4L2_BUFFER, - OPT_TUNNEL_HOST, - OPT_TUNNEL_PORT, - OPT_NO_CLIPBOARD_AUTOSYNC, - OPT_TCPIP, - OPT_RAW_KEY_EVENTS, - OPT_NO_DOWNSIZE_ON_ERROR, - OPT_OTG, - OPT_NO_CLEANUP, - OPT_PRINT_FPS, - OPT_NO_POWER_ON, - OPT_CODEC, - OPT_VIDEO_CODEC, - OPT_NO_AUDIO, - OPT_AUDIO_BIT_RATE, - OPT_AUDIO_CODEC, - OPT_AUDIO_CODEC_OPTIONS, - OPT_AUDIO_ENCODER, - OPT_LIST_ENCODERS, - OPT_LIST_DISPLAYS, - OPT_REQUIRE_AUDIO, - OPT_AUDIO_BUFFER, - OPT_AUDIO_OUTPUT_BUFFER, - OPT_NO_DISPLAY, - OPT_NO_VIDEO, - OPT_NO_AUDIO_PLAYBACK, - OPT_NO_VIDEO_PLAYBACK, - OPT_VIDEO_SOURCE, - OPT_AUDIO_SOURCE, - OPT_KILL_ADB_ON_CLOSE, - OPT_TIME_LIMIT, - OPT_PAUSE_ON_EXIT, - OPT_LIST_CAMERAS, - OPT_LIST_CAMERA_SIZES, - OPT_CAMERA_ID, - OPT_CAMERA_SIZE, - OPT_CAMERA_FACING, - OPT_CAMERA_AR, - OPT_CAMERA_FPS, - OPT_CAMERA_HIGH_SPEED, - OPT_DISPLAY_ORIENTATION, - OPT_RECORD_ORIENTATION, - OPT_ORIENTATION, - OPT_KEYBOARD, - OPT_MOUSE, - OPT_HID_KEYBOARD_DEPRECATED, - OPT_HID_MOUSE_DEPRECATED, - OPT_NO_WINDOW, - OPT_MOUSE_BIND, - OPT_NO_MOUSE_HOVER, - OPT_AUDIO_DUP, - OPT_GAMEPAD, - OPT_NEW_DISPLAY, - OPT_LIST_APPS, - OPT_START_APP, - OPT_SCREEN_OFF_TIMEOUT, - OPT_CAPTURE_ORIENTATION, - OPT_ANGLE, - OPT_NO_VD_SYSTEM_DECORATIONS, - OPT_NO_VD_DESTROY_CONTENT, - OPT_DISPLAY_IME_POLICY, -}; - -struct sc_option { - char shortopt; - int longopt_id; // either shortopt or longopt_id is non-zero - const char *longopt; - // no argument: argdesc == NULL && !optional_arg - // optional argument: argdesc != NULL && optional_arg - // required argument: argdesc != NULL && !optional_arg - const char *argdesc; - bool optional_arg; - const char *text; // if NULL, the option does not appear in the help -}; - -#define MAX_EQUIVALENT_SHORTCUTS 3 -struct sc_shortcut { - const char *shortcuts[MAX_EQUIVALENT_SHORTCUTS + 1]; - const char *text; -}; - -struct sc_envvar { - const char *name; - const char *text; -}; - -struct sc_exit_status { - unsigned value; - const char *text; -}; - -struct sc_getopt_adapter { - char *optstring; - struct option *longopts; -}; - -static const struct sc_option options[] = { - { - .longopt_id = OPT_ALWAYS_ON_TOP, - .longopt = "always-on-top", - .text = "Make scrcpy window always on top (above other windows).", - }, - { - .longopt_id = OPT_ANGLE, - .longopt = "angle", - .argdesc = "degrees", - .text = "Rotate the video content by a custom angle, in degrees " - "(clockwise).", - }, - { - .longopt_id = OPT_AUDIO_BIT_RATE, - .longopt = "audio-bit-rate", - .argdesc = "value", - .text = "Encode the audio at the given bit rate, expressed in bits/s. " - "Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n" - "Default is 128K (128000).", - }, - { - .longopt_id = OPT_AUDIO_BUFFER, - .longopt = "audio-buffer", - .argdesc = "ms", - .text = "Configure the audio buffering delay (in milliseconds).\n" - "Lower values decrease the latency, but increase the " - "likelihood of buffer underrun (causing audio glitches).\n" - "Default is 50.", - }, - { - .longopt_id = OPT_AUDIO_CODEC, - .longopt = "audio-codec", - .argdesc = "name", - .text = "Select an audio codec (opus, aac, flac or raw).\n" - "Default is opus.", - }, - { - .longopt_id = OPT_AUDIO_CODEC_OPTIONS, - .longopt = "audio-codec-options", - .argdesc = "key[:type]=value[,...]", - .text = "Set a list of comma-separated key:type=value options for the " - "device audio encoder.\n" - "The possible values for 'type' are 'int' (default), 'long', " - "'float' and 'string'.\n" - "The list of possible codec options is available in the " - "Android documentation: " - "", - }, - { - .longopt_id = OPT_AUDIO_DUP, - .longopt = "audio-dup", - .text = "Duplicate audio (capture and keep playing on the device).\n" - "This feature is only available with --audio-source=playback." - - }, - { - .longopt_id = OPT_AUDIO_ENCODER, - .longopt = "audio-encoder", - .argdesc = "name", - .text = "Use a specific MediaCodec audio encoder (depending on the " - "codec provided by --audio-codec).\n" - "The available encoders can be listed by --list-encoders.", - }, - { - .longopt_id = OPT_AUDIO_SOURCE, - .longopt = "audio-source", - .argdesc = "source", - .text = "Select the audio source. Possible values are:\n" - " - \"output\": forwards the whole audio output, and disables " - "playback on the device.\n" - " - \"playback\": captures the audio playback (Android apps " - "can opt-out, so the whole output is not necessarily " - "captured).\n" - " - \"mic\": captures the microphone.\n" - " - \"mic-unprocessed\": captures the microphone unprocessed " - "(raw) sound.\n" - " - \"mic-camcorder\": captures the microphone tuned for video " - "recording, with the same orientation as the camera if " - "available.\n" - " - \"mic-voice-recognition\": captures the microphone tuned " - "for voice recognition.\n" - " - \"mic-voice-communication\": captures the microphone tuned " - "for voice communications (it will for instance take advantage " - "of echo cancellation or automatic gain control if " - "available).\n" - " - \"voice-call\": captures voice call.\n" - " - \"voice-call-uplink\": captures voice call uplink only.\n" - " - \"voice-call-downlink\": captures voice call downlink " - "only.\n" - " - \"voice-performance\": captures audio meant to be " - "processed for live performance (karaoke), includes both the " - "microphone and the device playback.\n" - "Default is output.", - }, - { - .longopt_id = OPT_AUDIO_OUTPUT_BUFFER, - .longopt = "audio-output-buffer", - .argdesc = "ms", - .text = "Configure the size of the SDL audio output buffer (in " - "milliseconds).\n" - "If you get \"robotic\" audio playback, you should test with " - "a higher value (10). Do not change this setting otherwise.\n" - "Default is 5.", - }, - { - .shortopt = 'b', - .longopt = "video-bit-rate", - .argdesc = "value", - .text = "Encode the video at the given bit rate, expressed in bits/s. " - "Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n" - "Default is 8M (8000000).", - }, - { - // deprecated - .longopt_id = OPT_BIT_RATE, - .longopt = "bit-rate", - .argdesc = "value", - }, - { - .longopt_id = OPT_CAMERA_AR, - .longopt = "camera-ar", - .argdesc = "ar", - .text = "Select the camera size by its aspect ratio (+/- 10%).\n" - "Possible values are \"sensor\" (use the camera sensor aspect " - "ratio), \":\" (e.g. \"4:3\") or \"\" (e.g. " - "\"1.6\")." - }, - { - .longopt_id = OPT_CAMERA_FACING, - .longopt = "camera-facing", - .argdesc = "facing", - .text = "Select the device camera by its facing direction.\n" - "Possible values are \"front\", \"back\" and \"external\".", - }, - { - .longopt_id = OPT_CAMERA_FPS, - .longopt = "camera-fps", - .argdesc = "value", - .text = "Specify the camera capture frame rate.\n" - "If not specified, Android's default frame rate (30 fps) is " - "used.", - }, - { - .longopt_id = OPT_CAMERA_HIGH_SPEED, - .longopt = "camera-high-speed", - .text = "Enable high-speed camera capture mode.\n" - "This mode is restricted to specific resolutions and frame " - "rates, listed by --list-camera-sizes.", - }, - { - .longopt_id = OPT_CAMERA_ID, - .longopt = "camera-id", - .argdesc = "id", - .text = "Specify the device camera id to mirror.\n" - "The available camera ids can be listed by:\n" - " scrcpy --list-cameras", - }, - { - .longopt_id = OPT_CAMERA_SIZE, - .longopt = "camera-size", - .argdesc = "x", - .text = "Specify an explicit camera capture size.", - }, - { - .longopt_id = OPT_CAPTURE_ORIENTATION, - .longopt = "capture-orientation", - .argdesc = "value", - .text = "Set the capture video orientation.\n" - "Possible values are 0, 90, 180, 270, flip0, flip90, flip180 " - "and flip270, possibly prefixed by '@'.\n" - "The number represents the clockwise rotation in degrees; the " - "flip\" keyword applies a horizontal flip before the " - "rotation.\n" - "If a leading '@' is passed (@90) for display capture, then " - "the rotation is locked, and is relative to the natural device " - "orientation.\n" - "If '@' is passed alone, then the rotation is locked to the " - "initial device orientation.\n" - "Default is 0.", - }, - { - // Not really deprecated (--codec has never been released), but without - // declaring an explicit --codec option, getopt_long() partial matching - // behavior would consider --codec to be equivalent to --codec-options, - // which would be confusing. - .longopt_id = OPT_CODEC, - .longopt = "codec", - .argdesc = "value", - }, - { - // deprecated - .longopt_id = OPT_CODEC_OPTIONS, - .longopt = "codec-options", - .argdesc = "key[:type]=value[,...]", - }, - { - .longopt_id = OPT_CROP, - .longopt = "crop", - .argdesc = "width:height:x:y", - .text = "Crop the device screen on the server.\n" - "The values are expressed in the device natural orientation " - "(typically, portrait for a phone, landscape for a tablet).", - }, - { - .shortopt = 'd', - .longopt = "select-usb", - .text = "Use USB device (if there is exactly one, like adb -d).\n" - "Also see -e (--select-tcpip).", - }, - { - .longopt_id = OPT_DISABLE_SCREENSAVER, - .longopt = "disable-screensaver", - .text = "Disable screensaver while scrcpy is running.", - }, - { - // deprecated - .longopt_id = OPT_DISPLAY, - .longopt = "display", - .argdesc = "id", - }, - { - // deprecated - .longopt_id = OPT_DISPLAY_BUFFER, - .longopt = "display-buffer", - .argdesc = "ms", - }, - { - .longopt_id = OPT_DISPLAY_ID, - .longopt = "display-id", - .argdesc = "id", - .text = "Specify the device display id to mirror.\n" - "The available display ids can be listed by:\n" - " scrcpy --list-displays\n" - "Default is 0.", - }, - { - .longopt_id = OPT_DISPLAY_IME_POLICY, - .longopt = "display-ime-policy", - .argdesc = "value", - .text = "Set the policy for selecting where the IME should be " - "displayed.\n" - "Possible values are \"local\", \"fallback\" and \"hide\".\n" - "\"local\" means that the IME should appear on the local " - "display.\n" - "\"fallback\" means that the IME should appear on a fallback " - "display (the default display).\n" - "\"hide\" means that the IME should be hidden.", - }, - { - .longopt_id = OPT_DISPLAY_ORIENTATION, - .longopt = "display-orientation", - .argdesc = "value", - .text = "Set the initial display orientation.\n" - "Possible values are 0, 90, 180, 270, flip0, flip90, flip180 " - "and flip270. The number represents the clockwise rotation " - "in degrees; the \"flip\" keyword applies a horizontal flip " - "before the rotation.\n" - "Default is 0.", - }, - { - .shortopt = 'e', - .longopt = "select-tcpip", - .text = "Use TCP/IP device (if there is exactly one, like adb -e).\n" - "Also see -d (--select-usb).", - }, - { - // deprecated - .longopt_id = OPT_ENCODER, - .longopt = "encoder", - .argdesc = "name", - }, - { - .shortopt = 'f', - .longopt = "fullscreen", - .text = "Start in fullscreen.", - }, - { - .longopt_id = OPT_FORCE_ADB_FORWARD, - .longopt = "force-adb-forward", - .text = "Do not attempt to use \"adb reverse\" to connect to the " - "device.", - }, - { - // deprecated - .longopt_id = OPT_FORWARD_ALL_CLICKS, - .longopt = "forward-all-clicks", - }, - { - .shortopt = 'G', - .text = "Same as --gamepad=uhid, or --gamepad=aoa if --otg is set.", - }, - { - .longopt_id = OPT_GAMEPAD, - .longopt = "gamepad", - .argdesc = "mode", - .text = "Select how to send gamepad inputs to the device.\n" - "Possible values are \"disabled\", \"uhid\" and \"aoa\".\n" - "\"disabled\" does not send gamepad inputs to the device.\n" - "\"uhid\" simulates physical HID gamepads using the Linux UHID " - "kernel module on the device.\n" - "\"aoa\" simulates physical gamepads using the AOAv2 protocol." - "It may only work over USB.\n" - "Also see --keyboard and --mouse.", - }, - { - .shortopt = 'h', - .longopt = "help", - .text = "Print this help.", - }, - { - .shortopt = 'K', - .text = "Same as --keyboard=uhid, or --keyboard=aoa if --otg is set.", - }, - { - .longopt_id = OPT_KEYBOARD, - .longopt = "keyboard", - .argdesc = "mode", - .text = "Select how to send keyboard inputs to the device.\n" - "Possible values are \"disabled\", \"sdk\", \"uhid\" and " - "\"aoa\".\n" - "\"disabled\" does not send keyboard inputs to the device.\n" - "\"sdk\" uses the Android system API to deliver keyboard " - "events to applications.\n" - "\"uhid\" simulates a physical HID keyboard using the Linux " - "UHID kernel module on the device.\n" - "\"aoa\" simulates a physical keyboard using the AOAv2 " - "protocol. It may only work over USB.\n" - "For \"uhid\" and \"aoa\", the keyboard layout must be " - "configured (once and for all) on the device, via Settings -> " - "System -> Languages and input -> Physical keyboard. This " - "settings page can be started directly using the shortcut " - "MOD+k (except in OTG mode) or by executing: `adb shell am " - "start -a android.settings.HARD_KEYBOARD_SETTINGS`.\n" - "This option is only available when a HID keyboard is enabled " - "(or a physical keyboard is connected).\n" - "Also see --mouse and --gamepad.", - }, - { - .longopt_id = OPT_KILL_ADB_ON_CLOSE, - .longopt = "kill-adb-on-close", - .text = "Kill adb when scrcpy terminates.", - }, - { - // deprecated - //.shortopt = 'K', // old, reassigned - .longopt_id = OPT_HID_KEYBOARD_DEPRECATED, - .longopt = "hid-keyboard", - }, - { - .longopt_id = OPT_LEGACY_PASTE, - .longopt = "legacy-paste", - .text = "Inject computer clipboard text as a sequence of key events " - "on Ctrl+v (like MOD+Shift+v).\n" - "This is a workaround for some devices not behaving as " - "expected when setting the device clipboard programmatically.", - }, - { - .longopt_id = OPT_LIST_APPS, - .longopt = "list-apps", - .text = "List Android apps installed on the device.", - }, - { - .longopt_id = OPT_LIST_CAMERAS, - .longopt = "list-cameras", - .text = "List device cameras.", - }, - { - .longopt_id = OPT_LIST_CAMERA_SIZES, - .longopt = "list-camera-sizes", - .text = "List the valid camera capture sizes.", - }, - { - .longopt_id = OPT_LIST_DISPLAYS, - .longopt = "list-displays", - .text = "List device displays.", - }, - { - .longopt_id = OPT_LIST_ENCODERS, - .longopt = "list-encoders", - .text = "List video and audio encoders available on the device.", - }, - { - // deprecated - .longopt_id = OPT_LOCK_VIDEO_ORIENTATION, - .longopt = "lock-video-orientation", - .argdesc = "value", - }, - { - .shortopt = 'm', - .longopt = "max-size", - .argdesc = "value", - .text = "Limit both the width and height of the video to value. The " - "other dimension is computed so that the device aspect-ratio " - "is preserved.\n" - "Default is 0 (unlimited).", - }, - { - // deprecated - //.shortopt = 'M', // old, reassigned - .longopt_id = OPT_HID_MOUSE_DEPRECATED, - .longopt = "hid-mouse", - }, - { - .shortopt = 'M', - .text = "Same as --mouse=uhid, or --mouse=aoa if --otg is set.", - }, - { - .longopt_id = OPT_MAX_FPS, - .longopt = "max-fps", - .argdesc = "value", - .text = "Limit the frame rate of screen capture (officially supported " - "since Android 10, but may work on earlier versions).", - }, - { - .longopt_id = OPT_MOUSE, - .longopt = "mouse", - .argdesc = "mode", - .text = "Select how to send mouse inputs to the device.\n" - "Possible values are \"disabled\", \"sdk\", \"uhid\" and " - "\"aoa\".\n" - "\"disabled\" does not send mouse inputs to the device.\n" - "\"sdk\" uses the Android system API to deliver mouse events" - "to applications.\n" - "\"uhid\" simulates a physical HID mouse using the Linux UHID " - "kernel module on the device.\n" - "\"aoa\" simulates a physical mouse using the AOAv2 protocol. " - "It may only work over USB.\n" - "In \"uhid\" and \"aoa\" modes, the computer mouse is captured " - "to control the device directly (relative mouse mode).\n" - "LAlt, LSuper or RSuper toggle the capture mode, to give " - "control of the mouse back to the computer.\n" - "Also see --keyboard and --gamepad.", - }, - { - .longopt_id = OPT_MOUSE_BIND, - .longopt = "mouse-bind", - .argdesc = "xxxx[:xxxx]", - .text = "Configure bindings of secondary clicks.\n" - "The argument must be one or two sequences (separated by ':') " - "of exactly 4 characters, one for each secondary click (in " - "order: right click, middle click, 4th click, 5th click).\n" - "The first sequence defines the primary bindings, used when a " - "mouse button is pressed alone. The second sequence defines " - "the secondary bindings, used when a mouse button is pressed " - "while the Shift key is held.\n" - "If the second sequence of bindings is omitted, then it is the " - "same as the first one.\n" - "Each character must be one of the following:\n" - " '+': forward the click to the device\n" - " '-': ignore the click\n" - " 'b': trigger shortcut BACK (or turn screen on if off)\n" - " 'h': trigger shortcut HOME\n" - " 's': trigger shortcut APP_SWITCH\n" - " 'n': trigger shortcut \"expand notification panel\"\n" - "Default is 'bhsn:++++' for SDK mouse, and '++++:bhsn' for AOA " - "and UHID.", - }, - { - .shortopt = 'n', - .longopt = "no-control", - .text = "Disable device control (mirror the device in read-only).", - }, - { - .shortopt = 'N', - .longopt = "no-playback", - .text = "Disable video and audio playback on the computer (equivalent " - "to --no-video-playback --no-audio-playback).", - }, - { - .longopt_id = OPT_NEW_DISPLAY, - .longopt = "new-display", - .argdesc = "[x][/]", - .optional_arg = true, - .text = "Create a new display with the specified resolution and " - "density. If not provided, they default to the main display " - "dimensions and DPI.\n" - "Examples:\n" - " --new-display=1920x1080\n" - " --new-display=1920x1080/420 # force 420 dpi\n" - " --new-display # main display size and density\n" - " --new-display=/240 # main display size and 240 dpi", - }, - { - .longopt_id = OPT_NO_AUDIO, - .longopt = "no-audio", - .text = "Disable audio forwarding.", - }, - { - .longopt_id = OPT_NO_AUDIO_PLAYBACK, - .longopt = "no-audio-playback", - .text = "Disable audio playback on the computer.", - }, - { - .longopt_id = OPT_NO_CLEANUP, - .longopt = "no-cleanup", - .text = "By default, scrcpy removes the server binary from the device " - "and restores the device state (show touches, stay awake and " - "power mode) on exit.\n" - "This option disables this cleanup." - }, - { - .longopt_id = OPT_NO_CLIPBOARD_AUTOSYNC, - .longopt = "no-clipboard-autosync", - .text = "By default, scrcpy automatically synchronizes the computer " - "clipboard to the device clipboard before injecting Ctrl+v, " - "and the device clipboard to the computer clipboard whenever " - "it changes.\n" - "This option disables this automatic synchronization." - }, - { - .longopt_id = OPT_NO_DOWNSIZE_ON_ERROR, - .longopt = "no-downsize-on-error", - .text = "By default, on MediaCodec error, scrcpy automatically tries " - "again with a lower definition.\n" - "This option disables this behavior.", - }, - { - // deprecated - .longopt_id = OPT_NO_DISPLAY, - .longopt = "no-display", - }, - { - .longopt_id = OPT_NO_KEY_REPEAT, - .longopt = "no-key-repeat", - .text = "Do not forward repeated key events when a key is held down.", - }, - { - .longopt_id = OPT_NO_MIPMAPS, - .longopt = "no-mipmaps", - .text = "If the renderer is OpenGL 3.0+ or OpenGL ES 2.0+, then " - "mipmaps are automatically generated to improve downscaling " - "quality. This option disables the generation of mipmaps.", - }, - { - .longopt_id = OPT_NO_MOUSE_HOVER, - .longopt = "no-mouse-hover", - .text = "Do not forward mouse hover (mouse motion without any clicks) " - "events.", - }, - { - .longopt_id = OPT_NO_POWER_ON, - .longopt = "no-power-on", - .text = "Do not power on the device on start.", - }, - { - .longopt_id = OPT_NO_VD_DESTROY_CONTENT, - .longopt = "no-vd-destroy-content", - .text = "Disable virtual display \"destroy content on removal\" " - "flag.\n" - "With this option, when the virtual display is closed, the " - "running apps are moved to the main display rather than being " - "destroyed.", - }, - { - .longopt_id = OPT_NO_VD_SYSTEM_DECORATIONS, - .longopt = "no-vd-system-decorations", - .text = "Disable virtual display system decorations flag.", - }, - { - .longopt_id = OPT_NO_VIDEO, - .longopt = "no-video", - .text = "Disable video forwarding.", - }, - { - .longopt_id = OPT_NO_VIDEO_PLAYBACK, - .longopt = "no-video-playback", - .text = "Disable video playback on the computer.", - }, - { - .longopt_id = OPT_NO_WINDOW, - .longopt = "no-window", - .text = "Disable scrcpy window. Implies --no-video-playback.", - }, - { - .longopt_id = OPT_ORIENTATION, - .longopt = "orientation", - .argdesc = "value", - .text = "Same as --display-orientation=value " - "--record-orientation=value.", - }, - { - .longopt_id = OPT_OTG, - .longopt = "otg", - .text = "Run in OTG mode: simulate physical keyboard and mouse, " - "as if the computer keyboard and mouse were plugged directly " - "to the device via an OTG cable.\n" - "In this mode, adb (USB debugging) is not necessary, and " - "mirroring is disabled.\n" - "LAlt, LSuper or RSuper toggle the mouse capture mode, to give " - "control of the mouse back to the computer.\n" - "Keyboard and mouse may be disabled separately using" - "--keyboard=disabled and --mouse=disabled.\n" - "It may only work over USB.\n" - "See --keyboard, --mouse and --gamepad.", - }, - { - .shortopt = 'p', - .longopt = "port", - .argdesc = "port[:port]", - .text = "Set the TCP port (range) used by the client to listen.\n" - "Default is " STR(DEFAULT_LOCAL_PORT_RANGE_FIRST) ":" - STR(DEFAULT_LOCAL_PORT_RANGE_LAST) ".", - }, - { - .longopt_id = OPT_PAUSE_ON_EXIT, - .longopt = "pause-on-exit", - .argdesc = "mode", - .optional_arg = true, - .text = "Configure pause on exit. Possible values are \"true\" (always " - "pause on exit), \"false\" (never pause on exit) and " - "\"if-error\" (pause only if an error occurred).\n" - "This is useful to prevent the terminal window from " - "automatically closing, so that error messages can be read.\n" - "Default is \"false\".\n" - "Passing the option without argument is equivalent to passing " - "\"true\".", - }, - { - .longopt_id = OPT_POWER_OFF_ON_CLOSE, - .longopt = "power-off-on-close", - .text = "Turn the device screen off when closing scrcpy.", - }, - { - .longopt_id = OPT_PREFER_TEXT, - .longopt = "prefer-text", - .text = "Inject alpha characters and space as text events instead of " - "key events.\n" - "This avoids issues when combining multiple keys to enter a " - "special character, but breaks the expected behavior of alpha " - "keys in games (typically WASD).", - }, - { - .longopt_id = OPT_PRINT_FPS, - .longopt = "print-fps", - .text = "Start FPS counter, to print framerate logs to the console. " - "It can be started or stopped at any time with MOD+i.", - }, - { - .longopt_id = OPT_PUSH_TARGET, - .longopt = "push-target", - .argdesc = "path", - .text = "Set the target directory for pushing files to the device by " - "drag & drop. It is passed as is to \"adb push\".\n" - "Default is \"/sdcard/Download/\".", - }, - { - .shortopt = 'r', - .longopt = "record", - .argdesc = "file.mp4", - .text = "Record screen to file.\n" - "The format is determined by the --record-format option if " - "set, or by the file extension.", - }, - { - .longopt_id = OPT_RAW_KEY_EVENTS, - .longopt = "raw-key-events", - .text = "Inject key events for all input keys, and ignore text events." - }, - { - .longopt_id = OPT_RECORD_FORMAT, - .longopt = "record-format", - .argdesc = "format", - .text = "Force recording format (mp4, mkv, m4a, mka, opus, aac, flac " - "or wav).", - }, - { - .longopt_id = OPT_RECORD_ORIENTATION, - .longopt = "record-orientation", - .argdesc = "value", - .text = "Set the record orientation.\n" - "Possible values are 0, 90, 180 and 270. The number represents " - "the clockwise rotation in degrees.\n" - "Default is 0.", - }, - { - .longopt_id = OPT_RENDER_DRIVER, - .longopt = "render-driver", - .argdesc = "name", - .text = "Request SDL to use the given render driver (this is just a " - "hint).\n" - "Supported names are currently \"direct3d\", \"opengl\", " - "\"opengles2\", \"opengles\", \"metal\" and \"software\".\n" - "", - }, - { - .longopt_id = OPT_REQUIRE_AUDIO, - .longopt = "require-audio", - .text = "By default, scrcpy mirrors only the video when audio capture " - "fails on the device. This option makes scrcpy fail if audio " - "is enabled but does not work." - }, - { - // deprecated - .longopt_id = OPT_ROTATION, - .longopt = "rotation", - .argdesc = "value", - }, - { - .shortopt = 's', - .longopt = "serial", - .argdesc = "serial", - .text = "The device serial number. Mandatory only if several devices " - "are connected to adb.", - }, - { - .shortopt = 'S', - .longopt = "turn-screen-off", - .text = "Turn the device screen off immediately.", - }, - { - .longopt_id = OPT_SCREEN_OFF_TIMEOUT, - .longopt = "screen-off-timeout", - .argdesc = "seconds", - .text = "Set the screen off timeout while scrcpy is running (restore " - "the initial value on exit).", - }, - { - .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" - "Several shortcut modifiers can be specified, separated by " - "','.\n" - "For example, to use either LCtrl or LSuper for scrcpy " - "shortcuts, pass \"lctrl,lsuper\".\n" - "Default is \"lalt,lsuper\" (left-Alt or left-Super).", - }, - { - .longopt_id = OPT_START_APP, - .longopt = "start-app", - .argdesc = "name", - .text = "Start an Android app, by its exact package name.\n" - "Add a '?' prefix to select an app whose name starts with the " - "given name, case-insensitive (retrieving app names on the " - "device may take some time):\n" - " scrcpy --start-app=?firefox\n" - "Add a '+' prefix to force-stop before starting the app:\n" - " scrcpy --new-display --start-app=+org.mozilla.firefox\n" - "Both prefixes can be used, in that order:\n" - " scrcpy --start-app=+?firefox", - }, - { - .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 connect 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.\n" - "Prefix the address with a '+' to force a reconnection.", - }, - { - .longopt_id = OPT_TIME_LIMIT, - .longopt = "time-limit", - .argdesc = "seconds", - .text = "Set the maximum mirroring time, in seconds.", - }, - { - .longopt_id = OPT_TUNNEL_HOST, - .longopt = "tunnel-host", - .argdesc = "ip", - .text = "Set the IP address of the adb tunnel to reach the scrcpy " - "server. This option automatically enables " - "--force-adb-forward.\n" - "Default is localhost.", - }, - { - .longopt_id = OPT_TUNNEL_PORT, - .longopt = "tunnel-port", - .argdesc = "port", - .text = "Set the TCP port of the adb tunnel to reach the scrcpy " - "server. This option automatically enables " - "--force-adb-forward.\n" - "Default is 0 (not forced): the local port used for " - "establishing the tunnel will be used.", - }, - { - .shortopt = 'v', - .longopt = "version", - .text = "Print the version of scrcpy.", - }, - { - .shortopt = 'V', - .longopt = "verbosity", - .argdesc = "value", - .text = "Set the log level (verbose, debug, info, warn or error).\n" -#ifndef NDEBUG - "Default is debug.", -#else - "Default is info.", -#endif - }, - { - .longopt_id = OPT_V4L2_SINK, - .longopt = "v4l2-sink", - .argdesc = "/dev/videoN", - .text = "Output to v4l2loopback device.\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 --video-buffer, but specific to " - "V4L2 sink.\n" - "Default is 0 (no buffering).\n" - "This option is only available on Linux.", - }, - { - .longopt_id = OPT_VIDEO_BUFFER, - .longopt = "video-buffer", - .argdesc = "ms", - .text = "Add a buffering delay (in milliseconds) before displaying " - "video frames.\n" - "This increases latency to compensate for jitter.\n" - "Default is 0 (no buffering).", - }, - { - .longopt_id = OPT_VIDEO_CODEC, - .longopt = "video-codec", - .argdesc = "name", - .text = "Select a video codec (h264, h265 or av1).\n" - "Default is h264.", - }, - { - .longopt_id = OPT_VIDEO_CODEC_OPTIONS, - .longopt = "video-codec-options", - .argdesc = "key[:type]=value[,...]", - .text = "Set a list of comma-separated key:type=value options for the " - "device video encoder.\n" - "The possible values for 'type' are 'int' (default), 'long', " - "'float' and 'string'.\n" - "The list of possible codec options is available in the " - "Android documentation: " - "", - }, - { - .longopt_id = OPT_VIDEO_ENCODER, - .longopt = "video-encoder", - .argdesc = "name", - .text = "Use a specific MediaCodec video encoder (depending on the " - "codec provided by --video-codec).\n" - "The available encoders can be listed by --list-encoders.", - }, - { - .longopt_id = OPT_VIDEO_SOURCE, - .longopt = "video-source", - .argdesc = "source", - .text = "Select the video source (display or camera).\n" - "Camera mirroring requires Android 12+.\n" - "Default is display.", - }, - { - .shortopt = 'w', - .longopt = "stay-awake", - .text = "Keep the device on while scrcpy is running, when the device " - "is plugged in.", - }, - { - .longopt_id = OPT_WINDOW_BORDERLESS, - .longopt = "window-borderless", - .text = "Disable window decorations (display borderless window)." - }, - { - .longopt_id = OPT_WINDOW_TITLE, - .longopt = "window-title", - .argdesc = "text", - .text = "Set a custom window title.", - }, - { - .longopt_id = OPT_WINDOW_X, - .longopt = "window-x", - .argdesc = "value", - .text = "Set the initial window horizontal position.\n" - "Default is \"auto\".", - }, - { - .longopt_id = OPT_WINDOW_Y, - .longopt = "window-y", - .argdesc = "value", - .text = "Set the initial window vertical position.\n" - "Default is \"auto\".", - }, - { - .longopt_id = OPT_WINDOW_WIDTH, - .longopt = "window-width", - .argdesc = "value", - .text = "Set the initial window width.\n" - "Default is 0 (automatic).", - }, - { - .longopt_id = OPT_WINDOW_HEIGHT, - .longopt = "window-height", - .argdesc = "value", - .text = "Set the initial window height.\n" - "Default is 0 (automatic).", - }, -}; - -static const struct sc_shortcut shortcuts[] = { - { - .shortcuts = { "MOD+f" }, - .text = "Switch fullscreen mode", - }, - { - .shortcuts = { "MOD+Left" }, - .text = "Rotate display left", - }, - { - .shortcuts = { "MOD+Right" }, - .text = "Rotate display right", - }, - { - .shortcuts = { "MOD+Shift+Left", "MOD+Shift+Right" }, - .text = "Flip display horizontally", - }, - { - .shortcuts = { "MOD+Shift+Up", "MOD+Shift+Down" }, - .text = "Flip display vertically", - }, - { - .shortcuts = { "MOD+z" }, - .text = "Pause or re-pause display", - }, - { - .shortcuts = { "MOD+Shift+z" }, - .text = "Unpause display", - }, - { - .shortcuts = { "MOD+Shift+r" }, - .text = "Reset video capture/encoding", - }, - { - .shortcuts = { "MOD+g" }, - .text = "Resize window to 1:1 (pixel-perfect)", - }, - { - .shortcuts = { "MOD+w", "Double-click on black borders" }, - .text = "Resize window to remove black borders", - }, - { - .shortcuts = { "MOD+h", "Middle-click" }, - .text = "Click on HOME", - }, - { - .shortcuts = { - "MOD+b", - "MOD+Backspace", - "Right-click (when screen is on)", - }, - .text = "Click on BACK", - }, - { - .shortcuts = { "MOD+s", "4th-click" }, - .text = "Click on APP_SWITCH", - }, - { - .shortcuts = { "MOD+m" }, - .text = "Click on MENU", - }, - { - .shortcuts = { "MOD+Up" }, - .text = "Click on VOLUME_UP", - }, - { - .shortcuts = { "MOD+Down" }, - .text = "Click on VOLUME_DOWN", - }, - { - .shortcuts = { "MOD+p" }, - .text = "Click on POWER (turn screen on/off)", - }, - { - .shortcuts = { "Right-click (when screen is off)" }, - .text = "Power on", - }, - { - .shortcuts = { "MOD+o" }, - .text = "Turn device screen off (keep mirroring)", - }, - { - .shortcuts = { "MOD+Shift+o" }, - .text = "Turn device screen on", - }, - { - .shortcuts = { "MOD+r" }, - .text = "Rotate device screen", - }, - { - .shortcuts = { "MOD+n", "5th-click" }, - .text = "Expand notification panel", - }, - { - .shortcuts = { "MOD+Shift+n" }, - .text = "Collapse notification panel", - }, - { - .shortcuts = { "MOD+c" }, - .text = "Copy to clipboard (inject COPY keycode, Android >= 7 only)", - }, - { - .shortcuts = { "MOD+x" }, - .text = "Cut to clipboard (inject CUT keycode, Android >= 7 only)", - }, - { - .shortcuts = { "MOD+v" }, - .text = "Copy computer clipboard to device, then paste (inject PASTE " - "keycode, Android >= 7 only)", - }, - { - .shortcuts = { "MOD+Shift+v" }, - .text = "Inject computer clipboard text as a sequence of key events", - }, - { - .shortcuts = { "MOD+k" }, - .text = "Open keyboard settings on the device (for HID keyboard only)", - }, - { - .shortcuts = { "MOD+i" }, - .text = "Enable/disable FPS counter (print frames/second in logs)", - }, - { - .shortcuts = { "Ctrl+click-and-move" }, - .text = "Pinch-to-zoom and rotate from the center of the screen", - }, - { - .shortcuts = { "Shift+click-and-move" }, - .text = "Tilt vertically (slide with 2 fingers)", - }, - { - .shortcuts = { "Ctrl+Shift+click-and-move" }, - .text = "Tilt horizontally (slide with 2 fingers)", - }, - { - .shortcuts = { "Drag & drop APK file" }, - .text = "Install APK from computer", - }, - { - .shortcuts = { "Drag & drop non-APK file" }, - .text = "Push file to device (see --push-target)", - }, -}; - -static const struct sc_envvar envvars[] = { - { - .name = "ADB", - .text = "Path to adb executable", - }, - { - .name = "ANDROID_SERIAL", - .text = "Device serial to use if no selector (-s, -d, -e or " - "--tcpip=) is specified", - }, - { - .name = "SCRCPY_ICON_PATH", - .text = "Path to the program icon", - }, - { - .name = "SCRCPY_SERVER_PATH", - .text = "Path to the server binary", - }, -}; - -static const struct sc_exit_status exit_statuses[] = { - { - .value = 0, - .text = "Normal program termination", - }, - { - .value = 1, - .text = "Start failure", - }, - { - .value = 2, - .text = "Device disconnected while running", - }, -}; - -static char * -sc_getopt_adapter_create_optstring(void) { - struct sc_strbuf buf; - if (!sc_strbuf_init(&buf, 64)) { - return false; - } - - for (size_t i = 0; i < ARRAY_LEN(options); ++i) { - const struct sc_option *opt = &options[i]; - if (opt->shortopt) { - if (!sc_strbuf_append_char(&buf, opt->shortopt)) { - goto error; - } - // If there is an argument, add ':' - if (opt->argdesc) { - if (!sc_strbuf_append_char(&buf, ':')) { - goto error; - } - // If the argument is optional, add another ':' - if (opt->optional_arg && !sc_strbuf_append_char(&buf, ':')) { - goto error; - } - } - } - } - - return buf.s; - -error: - free(buf.s); - return NULL; -} - -static struct option * -sc_getopt_adapter_create_longopts(void) { - struct option *longopts = - malloc((ARRAY_LEN(options) + 1) * sizeof(*longopts)); - if (!longopts) { - LOG_OOM(); - return NULL; - } - - size_t out_idx = 0; - for (size_t i = 0; i < ARRAY_LEN(options); ++i) { - const struct sc_option *in = &options[i]; - - // If longopt_id is set, then longopt must be set - assert(!in->longopt_id || in->longopt); - - if (!in->longopt) { - // The longopts array must only contain long options - continue; - } - struct option *out = &longopts[out_idx++]; - - out->name = in->longopt; - - if (!in->argdesc) { - assert(!in->optional_arg); - out->has_arg = no_argument; - } else if (in->optional_arg) { - out->has_arg = optional_argument; - } else { - out->has_arg = required_argument; - } - - out->flag = NULL; - - // Either shortopt or longopt_id is set, but not both - assert(!!in->shortopt ^ !!in->longopt_id); - out->val = in->shortopt ? in->shortopt : in->longopt_id; - } - - // The array must be terminated by a NULL item - longopts[out_idx] = (struct option) {0}; - - return longopts; -} - -static bool -sc_getopt_adapter_init(struct sc_getopt_adapter *adapter) { - adapter->optstring = sc_getopt_adapter_create_optstring(); - if (!adapter->optstring) { - return false; - } - - adapter->longopts = sc_getopt_adapter_create_longopts(); - if (!adapter->longopts) { - free(adapter->optstring); - return false; - } - - return true; -} - -static void -sc_getopt_adapter_destroy(struct sc_getopt_adapter *adapter) { - free(adapter->optstring); - free(adapter->longopts); -} - -static void -print_option_usage_header(const struct sc_option *opt) { - struct sc_strbuf buf; - if (!sc_strbuf_init(&buf, 64)) { - goto error; - } - - bool ok = true; - (void) ok; // only used for assertions - - if (opt->shortopt) { - ok = sc_strbuf_append_char(&buf, '-'); - assert(ok); - - ok = sc_strbuf_append_char(&buf, opt->shortopt); - assert(ok); - - if (opt->longopt) { - ok = sc_strbuf_append_staticstr(&buf, ", "); - assert(ok); - } - } - - if (opt->longopt) { - ok = sc_strbuf_append_staticstr(&buf, "--"); - assert(ok); - - if (!sc_strbuf_append_str(&buf, opt->longopt)) { - goto error; - } - } - - if (opt->argdesc) { - if (opt->optional_arg && !sc_strbuf_append_char(&buf, '[')) { - goto error; - } - - if (!sc_strbuf_append_char(&buf, '=')) { - goto error; - } - - if (!sc_strbuf_append_str(&buf, opt->argdesc)) { - goto error; - } - - if (opt->optional_arg && !sc_strbuf_append_char(&buf, ']')) { - goto error; - } - } - - printf("\n %s\n", buf.s); - free(buf.s); - return; - -error: - printf("\n"); -} - -static void -print_option_usage(const struct sc_option *opt, unsigned cols) { - assert(cols > 8); // sc_str_wrap_lines() requires indent < columns - - if (!opt->text) { - // Option not documented in help (for example because it is deprecated) - return; - } - - print_option_usage_header(opt); - - char *text = sc_str_wrap_lines(opt->text, cols, 8); - if (!text) { - printf("\n"); - return; - } - - printf("%s\n", text); - free(text); -} - -static void -print_shortcuts_intro(unsigned cols) { - char *intro = sc_str_wrap_lines( - "In the following list, MOD is the shortcut modifier. By default, it's " - "(left) Alt or (left) Super, but it can be configured by " - "--shortcut-mod (see above).", cols, 4); - if (!intro) { - printf("\n"); - return; - } - - printf("\n%s\n", intro); - free(intro); -} - -static void -print_shortcut(const struct sc_shortcut *shortcut, unsigned cols) { - assert(cols > 8); // sc_str_wrap_lines() requires indent < columns - assert(shortcut->shortcuts[0]); // At least one shortcut - assert(shortcut->text); - - printf("\n"); - - unsigned i = 0; - while (shortcut->shortcuts[i]) { - printf(" %s\n", shortcut->shortcuts[i]); - ++i; - } - - char *text = sc_str_wrap_lines(shortcut->text, cols, 8); - if (!text) { - printf("\n"); - return; - } - - printf("%s\n", text); - free(text); -} - -static void -print_envvar(const struct sc_envvar *envvar, unsigned cols) { - assert(cols > 8); // sc_str_wrap_lines() requires indent < columns - assert(envvar->name); - assert(envvar->text); - - printf("\n %s\n", envvar->name); - char *text = sc_str_wrap_lines(envvar->text, cols, 8); - if (!text) { - printf("\n"); - return; - } - - printf("%s\n", text); - free(text); -} - -static void -print_exit_status(const struct sc_exit_status *status, unsigned cols) { - assert(cols > 8); // sc_str_wrap_lines() requires indent < columns - assert(status->text); - - // The text starts at 9: 4 ident spaces, 3 chars for numeric value, 2 spaces - char *text = sc_str_wrap_lines(status->text, cols, 9); - if (!text) { - printf("\n"); - return; - } - - assert(strlen(text) >= 9); // Contains at least the initial indentation - - // 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, const char sep, size_t max_items, long *out, - long min, long max, const char *name) { - size_t count = sc_str_parse_integers(s, sep, max_items, out); - if (!count) { - LOGE("Could not parse %s: %s", name, s); - return 0; - } - - 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_buffering_time(const char *s, sc_tick *tick) { - long value; - // In practice, buffering time should not exceed a few seconds. - // Limit it to some arbitrary value (1 hour) to prevent 32-bit overflow - // when multiplied by the audio sample size and the number of samples per - // millisecond. - bool ok = parse_integer_arg(s, &value, false, 0, 60 * 60 * 1000, - "buffering time"); - if (!ok) { - return false; - } - - *tick = SC_TICK_FROM_MS(value); - return true; -} - -static bool -parse_audio_output_buffer(const char *s, sc_tick *tick) { - long value; - bool ok = parse_integer_arg(s, &value, false, 0, 1000, - "audio output buffer"); - if (!ok) { - return false; - } - - *tick = SC_TICK_FROM_MS(value); - return true; -} - -static bool -parse_display_ime_policy(const char *s, enum sc_display_ime_policy *policy) { - if (!strcmp(s, "local")) { - *policy = SC_DISPLAY_IME_POLICY_LOCAL; - return true; - } - if (!strcmp(s, "fallback")) { - *policy = SC_DISPLAY_IME_POLICY_FALLBACK; - return true; - } - if (!strcmp(s, "hide")) { - *policy = SC_DISPLAY_IME_POLICY_HIDE; - return true; - } - LOGE("Unsupported display IME policy: %s (expected local, fallback or " - "hide)", s); - return false; -} - -static bool -parse_orientation(const char *s, enum sc_orientation *orientation) { - if (!strcmp(s, "0")) { - *orientation = SC_ORIENTATION_0; - return true; - } - if (!strcmp(s, "90")) { - *orientation = SC_ORIENTATION_90; - return true; - } - if (!strcmp(s, "180")) { - *orientation = SC_ORIENTATION_180; - return true; - } - if (!strcmp(s, "270")) { - *orientation = SC_ORIENTATION_270; - return true; - } - if (!strcmp(s, "flip0")) { - *orientation = SC_ORIENTATION_FLIP_0; - return true; - } - if (!strcmp(s, "flip90")) { - *orientation = SC_ORIENTATION_FLIP_90; - return true; - } - if (!strcmp(s, "flip180")) { - *orientation = SC_ORIENTATION_FLIP_180; - return true; - } - if (!strcmp(s, "flip270")) { - *orientation = SC_ORIENTATION_FLIP_270; - return true; - } - LOGE("Unsupported orientation: %s (expected 0, 90, 180, 270, flip0, " - "flip90, flip180 or flip270)", optarg); - return false; -} - -static bool -parse_capture_orientation(const char *s, enum sc_orientation *orientation, - enum sc_orientation_lock *lock) { - if (*s == '\0') { - LOGE("Capture orientation may not be empty (expected 0, 90, 180, 270, " - "flip0, flip90, flip180 or flip270, possibly prefixed by '@')"); - return false; - } - - // Lock the orientation by a leading '@' - if (s[0] == '@') { - // Consume '@' - ++s; - if (*s == '\0') { - // Only '@': lock to the initial orientation (orientation is unused) - *lock = SC_ORIENTATION_LOCKED_INITIAL; - return true; - } - *lock = SC_ORIENTATION_LOCKED_VALUE; - } else { - *lock = SC_ORIENTATION_UNLOCKED; - } - - return parse_orientation(s, orientation); -} - -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; -} - -static enum sc_shortcut_mod -parse_shortcut_mods_item(const char *item, size_t len) { -#define STREQ(literal, s, len) \ - ((sizeof(literal)-1 == len) && !memcmp(literal, s, len)) - - if (STREQ("lctrl", item, len)) { - return SC_SHORTCUT_MOD_LCTRL; - } - if (STREQ("rctrl", item, len)) { - return SC_SHORTCUT_MOD_RCTRL; - } - if (STREQ("lalt", item, len)) { - return SC_SHORTCUT_MOD_LALT; - } - if (STREQ("ralt", item, len)) { - return SC_SHORTCUT_MOD_RALT; - } - if (STREQ("lsuper", item, len)) { - return SC_SHORTCUT_MOD_LSUPER; - } - if (STREQ("rsuper", item, len)) { - return SC_SHORTCUT_MOD_RSUPER; - } -#undef STREQ - - bool has_plus = strchr(item, '+'); - if (has_plus) { - LOGE("Shortcut mod combination with '+' is not supported anymore: " - "'%.*s' (see #4741)", (int) len, item); - return 0; - } - - LOGE("Unknown modifier key: %.*s " - "(must be one of: lctrl, rctrl, lalt, ralt, lsuper, rsuper)", - (int) len, item); - - return 0; -} - -static bool -parse_shortcut_mods(const char *s, uint8_t *shortcut_mods) { - uint8_t mods = 0; - - // A list of shortcut modifiers, for example "lctrl,rctrl,rsuper" - - for (;;) { - char *comma = strchr(s, ','); - assert(!comma || comma > s); - size_t limit = comma ? (size_t) (comma - s) : strlen(s); - - enum sc_shortcut_mod mod = parse_shortcut_mods_item(s, limit); - if (!mod) { - return false; - } - - mods |= mod; - - if (!comma) { - break; - } - - s = comma + 1; - } - - *shortcut_mods = mods; - - return true; -} - -#ifdef SC_TEST -// expose the function to unit-tests -bool -sc_parse_shortcut_mods(const char *s, uint8_t *mods) { - return parse_shortcut_mods(s, mods); -} -#endif - -static enum sc_record_format -get_record_format(const char *name) { - if (!strcmp(name, "mp4")) { - return SC_RECORD_FORMAT_MP4; - } - if (!strcmp(name, "mkv")) { - return SC_RECORD_FORMAT_MKV; - } - if (!strcmp(name, "m4a")) { - return SC_RECORD_FORMAT_M4A; - } - if (!strcmp(name, "mka")) { - return SC_RECORD_FORMAT_MKA; - } - if (!strcmp(name, "opus")) { - return SC_RECORD_FORMAT_OPUS; - } - if (!strcmp(name, "aac")) { - return SC_RECORD_FORMAT_AAC; - } - if (!strcmp(name, "flac")) { - return SC_RECORD_FORMAT_FLAC; - } - if (!strcmp(name, "wav")) { - return SC_RECORD_FORMAT_WAV; - } - return 0; -} - -static bool -parse_record_format(const char *optarg, enum sc_record_format *format) { - enum sc_record_format fmt = get_record_format(optarg); - if (!fmt) { - LOGE("Unsupported record format: %s (expected mp4, mkv, m4a, mka, " - "opus, aac, flac or wav)", optarg); - return false; - } - - *format = fmt; - return true; -} - -static bool -parse_ip(const char *optarg, uint32_t *ipv4) { - return net_parse_ipv4(optarg, ipv4); -} - -static bool -parse_port(const char *optarg, uint16_t *port) { - long value; - if (!parse_integer_arg(optarg, &value, false, 0, 0xFFFF, "port")) { - return false; - } - *port = (uint16_t) value; - return true; -} - -static enum sc_record_format -guess_record_format(const char *filename) { - const char *dot = strrchr(filename, '.'); - if (!dot) { - return 0; - } - - const char *ext = dot + 1; - return get_record_format(ext); -} - -static bool -parse_video_codec(const char *optarg, enum sc_codec *codec) { - if (!strcmp(optarg, "h264")) { - *codec = SC_CODEC_H264; - return true; - } - if (!strcmp(optarg, "h265")) { - *codec = SC_CODEC_H265; - return true; - } - if (!strcmp(optarg, "av1")) { - *codec = SC_CODEC_AV1; - return true; - } - LOGE("Unsupported video codec: %s (expected h264, h265 or av1)", optarg); - return false; -} - -static bool -parse_audio_codec(const char *optarg, enum sc_codec *codec) { - if (!strcmp(optarg, "opus")) { - *codec = SC_CODEC_OPUS; - return true; - } - if (!strcmp(optarg, "aac")) { - *codec = SC_CODEC_AAC; - return true; - } - if (!strcmp(optarg, "flac")) { - *codec = SC_CODEC_FLAC; - return true; - } - if (!strcmp(optarg, "raw")) { - *codec = SC_CODEC_RAW; - return true; - } - LOGE("Unsupported audio codec: %s (expected opus, aac, flac or raw)", - optarg); - return false; -} - -static bool -parse_video_source(const char *optarg, enum sc_video_source *source) { - if (!strcmp(optarg, "display")) { - *source = SC_VIDEO_SOURCE_DISPLAY; - return true; - } - - if (!strcmp(optarg, "camera")) { - *source = SC_VIDEO_SOURCE_CAMERA; - return true; - } - - LOGE("Unsupported video source: %s (expected display or camera)", optarg); - return false; -} - -static bool -parse_audio_source(const char *optarg, enum sc_audio_source *source) { - if (!strcmp(optarg, "mic")) { - *source = SC_AUDIO_SOURCE_MIC; - return true; - } - - if (!strcmp(optarg, "output")) { - *source = SC_AUDIO_SOURCE_OUTPUT; - return true; - } - - if (!strcmp(optarg, "playback")) { - *source = SC_AUDIO_SOURCE_PLAYBACK; - return true; - } - - if (!strcmp(optarg, "mic-unprocessed")) { - *source = SC_AUDIO_SOURCE_MIC_UNPROCESSED; - return true; - } - - if (!strcmp(optarg, "mic-camcorder")) { - *source = SC_AUDIO_SOURCE_MIC_CAMCORDER; - return true; - } - - if (!strcmp(optarg, "mic-voice-recognition")) { - *source = SC_AUDIO_SOURCE_MIC_VOICE_RECOGNITION; - return true; - } - - if (!strcmp(optarg, "mic-voice-communication")) { - *source = SC_AUDIO_SOURCE_MIC_VOICE_COMMUNICATION; - return true; - } - - if (!strcmp(optarg, "voice-call")) { - *source = SC_AUDIO_SOURCE_VOICE_CALL; - return true; - } - - if (!strcmp(optarg, "voice-call-uplink")) { - *source = SC_AUDIO_SOURCE_VOICE_CALL_UPLINK; - return true; - } - - if (!strcmp(optarg, "voice-call-downlink")) { - *source = SC_AUDIO_SOURCE_VOICE_CALL_DOWNLINK; - return true; - } - - if (!strcmp(optarg, "voice-performance")) { - *source = SC_AUDIO_SOURCE_VOICE_PERFORMANCE; - return true; - } - - LOGE("Unsupported audio source: %s (expected output, mic, playback, " - "mic-unprocessed, mic-camcorder, mic-voice-recognition, " - "mic-voice-communication, voice-call, voice-call-uplink, " - "voice-call-downlink, voice-performance)", optarg); - return false; -} - -static bool -parse_camera_facing(const char *optarg, enum sc_camera_facing *facing) { - if (!strcmp(optarg, "front")) { - *facing = SC_CAMERA_FACING_FRONT; - return true; - } - - if (!strcmp(optarg, "back")) { - *facing = SC_CAMERA_FACING_BACK; - return true; - } - - if (!strcmp(optarg, "external")) { - *facing = SC_CAMERA_FACING_EXTERNAL; - return true; - } - - if (*optarg == '\0') { - // Empty string is a valid value (equivalent to not passing the option) - *facing = SC_CAMERA_FACING_ANY; - return true; - } - - LOGE("Unsupported camera facing: %s (expected front, back or external)", - optarg); - return false; -} - -static bool -parse_camera_fps(const char *s, uint16_t *camera_fps) { - long value; - bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "camera fps"); - if (!ok) { - return false; - } - - *camera_fps = (uint16_t) value; - return true; -} - -static bool -parse_keyboard(const char *optarg, enum sc_keyboard_input_mode *mode) { - if (!strcmp(optarg, "disabled")) { - *mode = SC_KEYBOARD_INPUT_MODE_DISABLED; - return true; - } - - if (!strcmp(optarg, "sdk")) { - *mode = SC_KEYBOARD_INPUT_MODE_SDK; - return true; - } - - if (!strcmp(optarg, "uhid")) { - *mode = SC_KEYBOARD_INPUT_MODE_UHID; - return true; - } - - if (!strcmp(optarg, "aoa")) { -#ifdef HAVE_USB - *mode = SC_KEYBOARD_INPUT_MODE_AOA; - return true; -#else - LOGE("--keyboard=aoa is disabled."); - return false; -#endif - } - - LOGE("Unsupported keyboard: %s (expected disabled, sdk, uhid and aoa)", - optarg); - return false; -} - -static bool -parse_mouse(const char *optarg, enum sc_mouse_input_mode *mode) { - if (!strcmp(optarg, "disabled")) { - *mode = SC_MOUSE_INPUT_MODE_DISABLED; - return true; - } - - if (!strcmp(optarg, "sdk")) { - *mode = SC_MOUSE_INPUT_MODE_SDK; - return true; - } - - if (!strcmp(optarg, "uhid")) { - *mode = SC_MOUSE_INPUT_MODE_UHID; - return true; - } - - if (!strcmp(optarg, "aoa")) { -#ifdef HAVE_USB - *mode = SC_MOUSE_INPUT_MODE_AOA; - return true; -#else - LOGE("--mouse=aoa is disabled."); - return false; -#endif - } - - LOGE("Unsupported mouse: %s (expected disabled, sdk, uhid or aoa)", optarg); - return false; -} - -static bool -parse_gamepad(const char *optarg, enum sc_gamepad_input_mode *mode) { - if (!strcmp(optarg, "disabled")) { - *mode = SC_GAMEPAD_INPUT_MODE_DISABLED; - return true; - } - - if (!strcmp(optarg, "uhid")) { - *mode = SC_GAMEPAD_INPUT_MODE_UHID; - return true; - } - - if (!strcmp(optarg, "aoa")) { -#ifdef HAVE_USB - *mode = SC_GAMEPAD_INPUT_MODE_AOA; - return true; -#else - LOGE("--gamepad=aoa is disabled."); - return false; -#endif - } - - LOGE("Unsupported gamepad: %s (expected disabled or aoa)", optarg); - return false; -} - -static bool -parse_time_limit(const char *s, sc_tick *tick) { - long value; - bool ok = parse_integer_arg(s, &value, false, 0, 0x7FFFFFFF, "time limit"); - if (!ok) { - return false; - } - - *tick = SC_TICK_FROM_SEC(value); - return true; -} - -static bool -parse_screen_off_timeout(const char *s, sc_tick *tick) { - long value; - // value in seconds, but must fit in 31 bits in milliseconds - bool ok = parse_integer_arg(s, &value, false, 0, 0x7FFFFFFF / 1000, - "screen off timeout"); - if (!ok) { - return false; - } - - *tick = SC_TICK_FROM_SEC(value); - return true; -} - -static bool -parse_pause_on_exit(const char *s, enum sc_pause_on_exit *pause_on_exit) { - if (!s || !strcmp(s, "true")) { - *pause_on_exit = SC_PAUSE_ON_EXIT_TRUE; - return true; - } - - if (!strcmp(s, "false")) { - *pause_on_exit = SC_PAUSE_ON_EXIT_FALSE; - return true; - } - - if (!strcmp(s, "if-error")) { - *pause_on_exit = SC_PAUSE_ON_EXIT_IF_ERROR; - return true; - } - - LOGE("Unsupported pause on exit mode: %s " - "(expected true, false or if-error)", s); - return false; - -} - -static bool -parse_mouse_binding(char c, enum sc_mouse_binding *b) { - switch (c) { - case '+': - *b = SC_MOUSE_BINDING_CLICK; - return true; - case '-': - *b = SC_MOUSE_BINDING_DISABLED; - return true; - case 'b': - *b = SC_MOUSE_BINDING_BACK; - return true; - case 'h': - *b = SC_MOUSE_BINDING_HOME; - return true; - case 's': - *b = SC_MOUSE_BINDING_APP_SWITCH; - return true; - case 'n': - *b = SC_MOUSE_BINDING_EXPAND_NOTIFICATION_PANEL; - return true; - default: - LOGE("Invalid mouse binding: '%c' " - "(expected '+', '-', 'b', 'h', 's' or 'n')", c); - return false; - } -} - -static bool -parse_mouse_binding_set(const char *s, struct sc_mouse_binding_set *mbs) { - assert(strlen(s) >= 4); - - if (!parse_mouse_binding(s[0], &mbs->right_click)) { - return false; - } - if (!parse_mouse_binding(s[1], &mbs->middle_click)) { - return false; - } - if (!parse_mouse_binding(s[2], &mbs->click4)) { - return false; - } - if (!parse_mouse_binding(s[3], &mbs->click5)) { - return false; - } - - return true; -} - -static bool -parse_mouse_bindings(const char *s, struct sc_mouse_bindings *mb) { - size_t len = strlen(s); - // either "xxxx" or "xxxx:xxxx" - if (len != 4 && (len != 9 || s[4] != ':')) { - LOGE("Invalid mouse bindings: '%s' (expected 'xxxx' or 'xxxx:xxxx', " - "with each 'x' being in {'+', '-', 'b', 'h', 's', 'n'})", s); - return false; - } - - if (!parse_mouse_binding_set(s, &mb->pri)) { - return false; - } - - if (len == 9) { - if (!parse_mouse_binding_set(s + 5, &mb->sec)) { - return false; - } - } else { - // use the same bindings for Shift+click - mb->sec = mb->pri; - } - - return true; -} - -static bool -parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], - const char *optstring, const struct option *longopts) { - struct scrcpy_options *opts = &args->opts; - - optind = 0; // reset to start from the first argument in tests - - int c; - while ((c = getopt_long(argc, argv, optstring, longopts, NULL)) != -1) { - switch (c) { - case OPT_BIT_RATE: - LOGE("--bit-rate has been removed, " - "use --video-bit-rate or --audio-bit-rate."); - return false; - case 'b': - if (!parse_bit_rate(optarg, &opts->video_bit_rate)) { - return false; - } - break; - case OPT_AUDIO_BIT_RATE: - if (!parse_bit_rate(optarg, &opts->audio_bit_rate)) { - return false; - } - break; - case OPT_CROP: - opts->crop = optarg; - break; - case OPT_DISPLAY: - LOGE("--display has been removed, use --display-id instead."); - return false; - case OPT_DISPLAY_ID: - if (!parse_display_id(optarg, &opts->display_id)) { - return false; - } - break; - case 'd': - opts->select_usb = true; - break; - case 'e': - opts->select_tcpip = true; - break; - case 'f': - opts->fullscreen = true; - break; - case OPT_RECORD_FORMAT: - if (!parse_record_format(optarg, &opts->record_format)) { - return false; - } - break; - case 'h': - args->help = true; - break; - case 'K': - opts->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_UHID_OR_AOA; - break; - case OPT_KEYBOARD: - if (!parse_keyboard(optarg, &opts->keyboard_input_mode)) { - return false; - } - break; - case OPT_HID_KEYBOARD_DEPRECATED: - LOGE("--hid-keyboard has been removed, use --keyboard=aoa or " - "--keyboard=uhid instead."); - return false; - case OPT_MAX_FPS: - opts->max_fps = optarg; - break; - case 'm': - if (!parse_max_size(optarg, &opts->max_size)) { - return false; - } - break; - case 'M': - opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_UHID_OR_AOA; - break; - case OPT_MOUSE: - if (!parse_mouse(optarg, &opts->mouse_input_mode)) { - return false; - } - break; - case OPT_MOUSE_BIND: - if (!parse_mouse_bindings(optarg, &opts->mouse_bindings)) { - return false; - } - break; - case OPT_NO_MOUSE_HOVER: - opts->mouse_hover = false; - break; - case OPT_HID_MOUSE_DEPRECATED: - LOGE("--hid-mouse has been removed, use --mouse=aoa or " - "--mouse=uhid instead."); - return false; - case OPT_LOCK_VIDEO_ORIENTATION: - LOGE("--lock-video-orientation has been removed, use " - "--capture-orientation instead."); - return false; - case OPT_CAPTURE_ORIENTATION: - if (!parse_capture_orientation(optarg, - &opts->capture_orientation, - &opts->capture_orientation_lock)) { - return false; - } - break; - case OPT_TUNNEL_HOST: - if (!parse_ip(optarg, &opts->tunnel_host)) { - return false; - } - break; - case OPT_TUNNEL_PORT: - if (!parse_port(optarg, &opts->tunnel_port)) { - return false; - } - break; - case 'n': - opts->control = false; - break; - case OPT_NO_DISPLAY: - LOGE("--no-display has been removed, use --no-playback " - "instead."); - return false; - case 'N': - opts->video_playback = false; - opts->audio_playback = false; - break; - case OPT_NO_VIDEO_PLAYBACK: - opts->video_playback = false; - break; - case OPT_NO_AUDIO_PLAYBACK: - opts->audio_playback = false; - break; - case 'p': - if (!parse_port_range(optarg, &opts->port_range)) { - 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_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: - LOGE("--rotation has been removed, use --orientation or " - "--capture-orientation instead."); - return false; - case OPT_DISPLAY_ORIENTATION: - if (!parse_orientation(optarg, &opts->display_orientation)) { - return false; - } - break; - case OPT_RECORD_ORIENTATION: - if (!parse_orientation(optarg, &opts->record_orientation)) { - return false; - } - break; - case OPT_ORIENTATION: { - enum sc_orientation orientation; - if (!parse_orientation(optarg, &orientation)) { - return false; - } - opts->display_orientation = orientation; - opts->record_orientation = orientation; - break; - } - case OPT_RENDER_DRIVER: - opts->render_driver = optarg; - break; - case OPT_NO_MIPMAPS: - opts->mipmaps = false; - break; - case OPT_NO_KEY_REPEAT: - opts->forward_key_repeat = false; - break; - case OPT_CODEC_OPTIONS: - LOGE("--codec-options has been removed, " - "use --video-codec-options or --audio-codec-options."); - return false; - case OPT_VIDEO_CODEC_OPTIONS: - opts->video_codec_options = optarg; - break; - case OPT_AUDIO_CODEC_OPTIONS: - opts->audio_codec_options = optarg; - break; - case OPT_ENCODER: - LOGE("--encoder has been removed, " - "use --video-encoder or --audio-encoder."); - return false; - case OPT_VIDEO_ENCODER: - opts->video_encoder = optarg; - break; - case OPT_AUDIO_ENCODER: - opts->audio_encoder = 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: - LOGE("--forward-all-clicks has been removed, " - "use --mouse-bind=++++ instead."); - return false; - 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: - LOGE("--display-buffer has been removed, use --video-buffer " - "instead."); - return false; - case OPT_VIDEO_BUFFER: - if (!parse_buffering_time(optarg, &opts->video_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_VIDEO: - opts->video = false; - break; - case OPT_NO_AUDIO: - opts->audio = false; - break; - case OPT_NO_CLEANUP: - opts->cleanup = false; - break; - case OPT_NO_POWER_ON: - opts->power_on = false; - break; - case OPT_PRINT_FPS: - opts->start_fps_counter = true; - break; - case OPT_CODEC: - LOGE("--codec has been removed, " - "use --video-codec or --audio-codec."); - return false; - case OPT_VIDEO_CODEC: - if (!parse_video_codec(optarg, &opts->video_codec)) { - return false; - } - break; - case OPT_AUDIO_CODEC: - if (!parse_audio_codec(optarg, &opts->audio_codec)) { - return false; - } - break; - case OPT_OTG: -#ifdef HAVE_USB - opts->otg = true; - break; -#else - LOGE("OTG mode (--otg) is disabled."); - return false; -#endif - case OPT_V4L2_SINK: -#ifdef HAVE_V4L2 - opts->v4l2_device = optarg; - break; -#else - LOGE("V4L2 (--v4l2-sink) is disabled (or unsupported on this " - "platform)."); - return false; -#endif - case OPT_V4L2_BUFFER: -#ifdef HAVE_V4L2 - if (!parse_buffering_time(optarg, &opts->v4l2_buffer)) { - return false; - } - break; -#else - LOGE("V4L2 (--v4l2-buffer) is disabled (or unsupported on this " - "platform)."); - return false; -#endif - case OPT_LIST_ENCODERS: - opts->list |= SC_OPTION_LIST_ENCODERS; - break; - case OPT_LIST_DISPLAYS: - opts->list |= SC_OPTION_LIST_DISPLAYS; - break; - case OPT_LIST_CAMERAS: - opts->list |= SC_OPTION_LIST_CAMERAS; - break; - case OPT_LIST_CAMERA_SIZES: - opts->list |= SC_OPTION_LIST_CAMERA_SIZES; - break; - case OPT_LIST_APPS: - opts->list |= SC_OPTION_LIST_APPS; - break; - case OPT_REQUIRE_AUDIO: - opts->require_audio = true; - break; - case OPT_AUDIO_BUFFER: - if (!parse_buffering_time(optarg, &opts->audio_buffer)) { - return false; - } - break; - case OPT_AUDIO_OUTPUT_BUFFER: - if (!parse_audio_output_buffer(optarg, - &opts->audio_output_buffer)) { - return false; - } - break; - case OPT_VIDEO_SOURCE: - if (!parse_video_source(optarg, &opts->video_source)) { - return false; - } - break; - case OPT_AUDIO_SOURCE: - if (!parse_audio_source(optarg, &opts->audio_source)) { - return false; - } - break; - case OPT_KILL_ADB_ON_CLOSE: - opts->kill_adb_on_close = true; - break; - case OPT_TIME_LIMIT: - if (!parse_time_limit(optarg, &opts->time_limit)) { - return false; - } - break; - case OPT_PAUSE_ON_EXIT: - if (!parse_pause_on_exit(optarg, &args->pause_on_exit)) { - return false; - } - break; - case OPT_CAMERA_AR: - opts->camera_ar = optarg; - break; - case OPT_CAMERA_ID: - opts->camera_id = optarg; - break; - case OPT_CAMERA_SIZE: - opts->camera_size = optarg; - break; - case OPT_CAMERA_FACING: - if (!parse_camera_facing(optarg, &opts->camera_facing)) { - return false; - } - break; - case OPT_CAMERA_FPS: - if (!parse_camera_fps(optarg, &opts->camera_fps)) { - return false; - } - break; - case OPT_CAMERA_HIGH_SPEED: - opts->camera_high_speed = true; - break; - case OPT_NO_WINDOW: - opts->window = false; - break; - case OPT_AUDIO_DUP: - opts->audio_dup = true; - break; - case 'G': - opts->gamepad_input_mode = SC_GAMEPAD_INPUT_MODE_UHID_OR_AOA; - break; - case OPT_GAMEPAD: - if (!parse_gamepad(optarg, &opts->gamepad_input_mode)) { - return false; - } - break; - case OPT_NEW_DISPLAY: - opts->new_display = optarg ? optarg : ""; - break; - case OPT_START_APP: - opts->start_app = optarg; - break; - case OPT_SCREEN_OFF_TIMEOUT: - if (!parse_screen_off_timeout(optarg, - &opts->screen_off_timeout)) { - return false; - } - break; - case OPT_ANGLE: - opts->angle = optarg; - break; - case OPT_NO_VD_DESTROY_CONTENT: - opts->vd_destroy_content = false; - break; - case OPT_NO_VD_SYSTEM_DECORATIONS: - opts->vd_system_decorations = false; - break; - case OPT_DISPLAY_IME_POLICY: - if (!parse_display_ime_policy(optarg, - &opts->display_ime_policy)) { - return false; - } - break; - default: - // getopt prints the error message on stderr - return false; - } - } - - int index = optind; - if (index < argc) { - LOGE("Unexpected additional argument: %s", argv[index]); - return false; - } - - // If a TCP/IP address is provided, then tcpip must be enabled - assert(opts->tcpip || !opts->tcpip_dst); - - unsigned selectors = !!opts->serial - + !!opts->tcpip_dst - + opts->select_tcpip - + opts->select_usb; - if (selectors > 1) { - LOGE("At most one device selector option may be passed, among:\n" - " --serial (-s)\n" - " --select-usb (-d)\n" - " --select-tcpip (-e)\n" - " --tcpip= (with an argument)"); - return false; - } - - bool otg = false; - bool v4l2 = false; -#ifdef HAVE_USB - otg = opts->otg; -#endif -#ifdef HAVE_V4L2 - v4l2 = !!opts->v4l2_device; -#endif - - if (!opts->window) { - // Without window, there cannot be any video playback - opts->video_playback = false; - // Controls are still possible, allowing for options like - // --turn-screen-off - } - - if (!opts->video) { - opts->video_playback = false; - // Do not power on the device on start if video capture is disabled - opts->power_on = false; - } - - if (!opts->audio) { - opts->audio_playback = false; - } - - if (opts->video && !opts->video_playback && !opts->record_filename - && !v4l2) { - LOGI("No video playback, no recording, no V4L2 sink: video disabled"); - opts->video = false; - } - - if (opts->audio && !opts->audio_playback && !opts->record_filename) { - LOGI("No audio playback, no recording: audio disabled"); - opts->audio = false; - } - - if (!opts->video && !opts->audio && !opts->control && !otg) { - LOGE("No video, no audio, no control, no OTG: nothing to do"); - return false; - } - - if (!opts->video && !otg) { - // If video is disabled, then scrcpy must exit on audio failure. - opts->require_audio = true; - } - - if (opts->audio_playback && opts->audio_buffer == -1) { - if (opts->audio_codec == SC_CODEC_FLAC) { - // Use 50 ms audio buffer by default, but use a higher value for - // FLAC, which is not low latency (the default encoder produces - // blocks of 4096 samples, which represent ~85.333ms). - LOGI("FLAC audio: audio buffer increased to 120 ms (use " - "--audio-buffer to set a custom value)"); - opts->audio_buffer = SC_TICK_FROM_MS(120); - } else { - opts->audio_buffer = SC_TICK_FROM_MS(50); - } - } - -#ifdef HAVE_V4L2 - if (v4l2) { - if (!opts->video) { - LOGE("V4L2 sink requires video capture, but --no-video was set."); - return false; - } - - // 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"); - return false; - } -#endif - - if (opts->control) { - if (opts->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AUTO) { - opts->keyboard_input_mode = otg ? SC_KEYBOARD_INPUT_MODE_AOA - : SC_KEYBOARD_INPUT_MODE_SDK; - } else if (opts->keyboard_input_mode - == SC_KEYBOARD_INPUT_MODE_UHID_OR_AOA) { - opts->keyboard_input_mode = otg ? SC_KEYBOARD_INPUT_MODE_AOA - : SC_KEYBOARD_INPUT_MODE_UHID; - } - - if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_AUTO) { - if (otg) { - opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_AOA; - } else if (!opts->video_playback) { - LOGI("No video mirroring, SDK mouse disabled"); - opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_DISABLED; - } else { - opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_SDK; - } - } else if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_UHID_OR_AOA) { - opts->mouse_input_mode = otg ? SC_MOUSE_INPUT_MODE_AOA - : SC_MOUSE_INPUT_MODE_UHID; - } else if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_SDK - && !opts->video_playback) { - LOGE("SDK mouse mode requires video playback. Try --mouse=uhid."); - return false; - } - if (opts->gamepad_input_mode == SC_GAMEPAD_INPUT_MODE_UHID_OR_AOA) { - opts->gamepad_input_mode = otg ? SC_GAMEPAD_INPUT_MODE_AOA - : SC_GAMEPAD_INPUT_MODE_UHID; - } - } - - // If mouse bindings are not explicitly set, configure default bindings - if (opts->mouse_bindings.pri.right_click == SC_MOUSE_BINDING_AUTO) { - assert(opts->mouse_bindings.pri.middle_click == SC_MOUSE_BINDING_AUTO); - assert(opts->mouse_bindings.pri.click4 == SC_MOUSE_BINDING_AUTO); - assert(opts->mouse_bindings.pri.click5 == SC_MOUSE_BINDING_AUTO); - assert(opts->mouse_bindings.sec.right_click == SC_MOUSE_BINDING_AUTO); - assert(opts->mouse_bindings.sec.middle_click == SC_MOUSE_BINDING_AUTO); - assert(opts->mouse_bindings.sec.click4 == SC_MOUSE_BINDING_AUTO); - assert(opts->mouse_bindings.sec.click5 == SC_MOUSE_BINDING_AUTO); - - static struct sc_mouse_binding_set default_shortcuts = { - .right_click = SC_MOUSE_BINDING_BACK, - .middle_click = SC_MOUSE_BINDING_HOME, - .click4 = SC_MOUSE_BINDING_APP_SWITCH, - .click5 = SC_MOUSE_BINDING_EXPAND_NOTIFICATION_PANEL, - }; - - static struct sc_mouse_binding_set forward = { - .right_click = SC_MOUSE_BINDING_CLICK, - .middle_click = SC_MOUSE_BINDING_CLICK, - .click4 = SC_MOUSE_BINDING_CLICK, - .click5 = SC_MOUSE_BINDING_CLICK, - }; - - // By default, forward all clicks only for UHID and AOA - if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_SDK) { - opts->mouse_bindings.pri = default_shortcuts; - opts->mouse_bindings.sec = forward; - } else { - opts->mouse_bindings.pri = forward; - opts->mouse_bindings.sec = default_shortcuts; - } - } - - if (opts->new_display) { - if (opts->video_source != SC_VIDEO_SOURCE_DISPLAY) { - LOGE("--new-display is only available with --video-source=display"); - return false; - } - - if (!opts->video) { - LOGE("--new-display is incompatible with --no-video"); - return false; - } - } - - if (otg) { - if (!opts->control) { - LOGE("--no-control is not allowed in OTG mode"); - return false; - } - - enum sc_keyboard_input_mode kmode = opts->keyboard_input_mode; - if (kmode != SC_KEYBOARD_INPUT_MODE_AOA - && kmode != SC_KEYBOARD_INPUT_MODE_DISABLED) { - LOGE("In OTG mode, --keyboard only supports aoa or disabled."); - return false; - } - - enum sc_mouse_input_mode mmode = opts->mouse_input_mode; - if (mmode != SC_MOUSE_INPUT_MODE_AOA - && mmode != SC_MOUSE_INPUT_MODE_DISABLED) { - LOGE("In OTG mode, --mouse only supports aoa or disabled."); - return false; - } - - enum sc_gamepad_input_mode gmode = opts->gamepad_input_mode; - if (gmode != SC_GAMEPAD_INPUT_MODE_AOA - && gmode != SC_GAMEPAD_INPUT_MODE_DISABLED) { - LOGE("In OTG mode, --gamepad only supports aoa or disabled."); - return false; - } - - if (kmode == SC_KEYBOARD_INPUT_MODE_DISABLED - && mmode == SC_MOUSE_INPUT_MODE_DISABLED - && gmode == SC_GAMEPAD_INPUT_MODE_DISABLED) { - LOGE("Cannot not disable all inputs in OTG mode."); - return false; - } - } - - if (opts->keyboard_input_mode != SC_KEYBOARD_INPUT_MODE_SDK) { - if (opts->key_inject_mode == SC_KEY_INJECT_MODE_TEXT) { - LOGE("--prefer-text is specific to --keyboard=sdk"); - return false; - } - - if (opts->key_inject_mode == SC_KEY_INJECT_MODE_RAW) { - LOGE("--raw-key-events is specific to --keyboard=sdk"); - return false; - } - - if (!opts->forward_key_repeat) { - LOGE("--no-key-repeat is specific to --keyboard=sdk"); - return false; - } - } - - if (opts->mouse_input_mode != SC_MOUSE_INPUT_MODE_SDK - && !opts->mouse_hover) { - LOGE("--no-mouse-over is specific to --mouse=sdk"); - return false; - } - - if ((opts->tunnel_host || opts->tunnel_port) && !opts->force_adb_forward) { - LOGI("Tunnel host/port is set, " - "--force-adb-forward automatically enabled."); - opts->force_adb_forward = true; - } - - if (opts->video_source == SC_VIDEO_SOURCE_CAMERA) { - if (opts->display_id) { - LOGE("--display-id is only available with --video-source=display"); - return false; - } - - if (opts->display_ime_policy != SC_DISPLAY_IME_POLICY_UNDEFINED) { - LOGE("--display-ime-policy is only available with " - "--video-source=display"); - return false; - } - - if (opts->camera_id && opts->camera_facing != SC_CAMERA_FACING_ANY) { - LOGE("Cannot specify both --camera-id and --camera-facing"); - return false; - } - - if (opts->camera_size) { - if (opts->max_size) { - LOGE("Cannot specify both --camera-size and -m/--max-size"); - return false; - } - - if (opts->camera_ar) { - LOGE("Cannot specify both --camera-size and --camera-ar"); - return false; - } - } - - if (opts->camera_high_speed && !opts->camera_fps) { - LOGE("--camera-high-speed requires an explicit --camera-fps value"); - return false; - } - - if (opts->control) { - LOGI("Camera video source: control disabled"); - opts->control = false; - } - } else if (opts->camera_id - || opts->camera_ar - || opts->camera_facing != SC_CAMERA_FACING_ANY - || opts->camera_fps - || opts->camera_high_speed - || opts->camera_size) { - LOGE("Camera options are only available with --video-source=camera"); - return false; - } - - if (opts->display_id != 0 && opts->new_display) { - LOGE("Cannot specify both --display-id and --new-display"); - return false; - } - - if (opts->display_ime_policy != SC_DISPLAY_IME_POLICY_UNDEFINED - && opts->display_id == 0 && !opts->new_display) { - LOGE("--display-ime-policy is only supported on a secondary display"); - return false; - } - - if (opts->audio && opts->audio_source == SC_AUDIO_SOURCE_AUTO) { - // Select the audio source according to the video source - if (opts->video_source == SC_VIDEO_SOURCE_DISPLAY) { - if (opts->audio_dup) { - LOGI("Audio duplication enabled: audio source switched to " - "\"playback\""); - opts->audio_source = SC_AUDIO_SOURCE_PLAYBACK; - } else { - opts->audio_source = SC_AUDIO_SOURCE_OUTPUT; - } - } else { - opts->audio_source = SC_AUDIO_SOURCE_MIC; - LOGI("Camera video source: microphone audio source selected"); - } - } - - if (opts->audio_dup) { - if (!opts->audio) { - LOGE("--audio-dup not supported if audio is disabled"); - return false; - } - - if (opts->audio_source != SC_AUDIO_SOURCE_PLAYBACK) { - LOGE("--audio-dup is specific to --audio-source=playback"); - return false; - } - } - - if (opts->record_format && !opts->record_filename) { - LOGE("Record format specified without recording"); - return false; - } - - if (opts->record_filename) { - if (!opts->video && !opts->audio) { - LOGE("Video and audio disabled, nothing to record"); - return false; - } - - if (!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->record_orientation != SC_ORIENTATION_0) { - if (sc_orientation_is_mirror(opts->record_orientation)) { - LOGE("Record orientation only supports rotation, not " - "flipping: %s", - sc_orientation_get_name(opts->record_orientation)); - return false; - } - } - - if (opts->video - && sc_record_format_is_audio_only(opts->record_format)) { - LOGE("Audio container does not support video stream"); - return false; - } - - if (opts->record_format == SC_RECORD_FORMAT_OPUS - && opts->audio_codec != SC_CODEC_OPUS) { - LOGE("Recording to OPUS file requires an OPUS audio stream " - "(try with --audio-codec=opus)"); - return false; - } - - if (opts->record_format == SC_RECORD_FORMAT_AAC - && opts->audio_codec != SC_CODEC_AAC) { - LOGE("Recording to AAC file requires an AAC audio stream " - "(try with --audio-codec=aac)"); - return false; - } - if (opts->record_format == SC_RECORD_FORMAT_FLAC - && opts->audio_codec != SC_CODEC_FLAC) { - LOGE("Recording to FLAC file requires a FLAC audio stream " - "(try with --audio-codec=flac)"); - return false; - } - - if (opts->record_format == SC_RECORD_FORMAT_WAV - && opts->audio_codec != SC_CODEC_RAW) { - LOGE("Recording to WAV file requires a RAW audio stream " - "(try with --audio-codec=raw)"); - return false; - } - - if ((opts->record_format == SC_RECORD_FORMAT_MP4 || - opts->record_format == SC_RECORD_FORMAT_M4A) - && opts->audio_codec == SC_CODEC_RAW) { - LOGE("Recording to MP4 container does not support RAW audio"); - return false; - } - } - - if (opts->audio_codec == SC_CODEC_FLAC && opts->audio_bit_rate) { - LOGW("--audio-bit-rate is ignored for FLAC audio codec"); - } - - if (opts->audio_codec == SC_CODEC_RAW) { - if (opts->audio_bit_rate) { - LOGW("--audio-bit-rate is ignored for raw audio codec"); - } - if (opts->audio_codec_options) { - LOGW("--audio-codec-options is ignored for raw audio codec"); - } - if (opts->audio_encoder) { - LOGW("--audio-encoder is ignored for raw audio codec"); - } - } - - if (!opts->control) { - if (opts->turn_screen_off) { - LOGE("Cannot request to turn screen off if control is disabled"); - return false; - } - if (opts->stay_awake) { - LOGE("Cannot request to stay awake if control is disabled"); - return false; - } - if (opts->show_touches) { - LOGE("Cannot request to show touches if control is disabled"); - return false; - } - if (opts->power_off_on_close) { - LOGE("Cannot request power off on close if control is disabled"); - return false; - } - if (opts->start_app) { - LOGE("Cannot start an Android app if control is disabled"); - return false; - } - } - -# ifdef _WIN32 - if (!otg && (opts->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AOA - || opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_AOA)) { - LOGE("On Windows, it is not possible to open a USB device already open " - "by another process (like adb)."); - LOGE("Therefore, --keyboard=aoa and --mouse=aoa may only work in OTG" - "mode (--otg)."); - return false; - } -# endif - - if (opts->start_fps_counter && !opts->video_playback) { - LOGW("--print-fps has no effect without video playback"); - opts->start_fps_counter = false; - } - - if (otg) { - // OTG mode is compatible with only very few options. - // Only report obvious errors. - if (opts->record_filename) { - LOGE("OTG mode: cannot record"); - return false; - } - if (opts->turn_screen_off) { - LOGE("OTG mode: could not turn screen off"); - return false; - } - if (opts->stay_awake) { - LOGE("OTG mode: could not stay awake"); - return false; - } - if (opts->show_touches) { - LOGE("OTG mode: could not request to show touches"); - return false; - } - if (opts->power_off_on_close) { - LOGE("OTG mode: could not request power off on close"); - return false; - } - if (opts->display_id) { - LOGE("OTG mode: could not select display"); - return false; - } - if (v4l2) { - LOGE("OTG mode: could not sink to V4L2 device"); - return false; - } - } - - return true; -} - -static enum sc_pause_on_exit -sc_get_pause_on_exit(int argc, char *argv[]) { - // Read arguments backwards so that the last --pause-on-exit is considered - // (same behavior as getopt()) - for (int i = argc - 1; i >= 1; --i) { - const char *arg = argv[i]; - // Starts with "--pause-on-exit" - if (!strncmp("--pause-on-exit", arg, 15)) { - if (arg[15] == '\0') { - // No argument - return SC_PAUSE_ON_EXIT_TRUE; - } - if (arg[15] != '=') { - // Invalid parameter, ignore - return SC_PAUSE_ON_EXIT_FALSE; - } - const char *value = &arg[16]; - if (!strcmp(value, "true")) { - return SC_PAUSE_ON_EXIT_TRUE; - } - if (!strcmp(value, "if-error")) { - return SC_PAUSE_ON_EXIT_IF_ERROR; - } - // Set to false, including when the value is invalid - return SC_PAUSE_ON_EXIT_FALSE; - } - } - - return SC_PAUSE_ON_EXIT_FALSE; -} - -bool -scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { - struct sc_getopt_adapter adapter; - if (!sc_getopt_adapter_init(&adapter)) { - LOGW("Could not create getopt adapter"); - return false; - } - - bool ret = parse_args_with_getopt(args, argc, argv, adapter.optstring, - adapter.longopts); - - sc_getopt_adapter_destroy(&adapter); - - if (!ret && args->pause_on_exit == SC_PAUSE_ON_EXIT_FALSE) { - // Check if "--pause-on-exit" is present in the arguments list, because - // it must be taken into account even if command line parsing failed - args->pause_on_exit = sc_get_pause_on_exit(argc, argv); - } - - return ret; -} diff --git a/app/src/cli.h b/app/src/cli.h deleted file mode 100644 index 6fd579a4..00000000 --- a/app/src/cli.h +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef SCRCPY_CLI_H -#define SCRCPY_CLI_H - -#include "common.h" - -#include - -#include "options.h" - -enum sc_pause_on_exit { - SC_PAUSE_ON_EXIT_TRUE, - SC_PAUSE_ON_EXIT_FALSE, - SC_PAUSE_ON_EXIT_IF_ERROR, -}; - -struct scrcpy_cli_args { - struct scrcpy_options opts; - bool help; - bool version; - enum sc_pause_on_exit pause_on_exit; -}; - -void -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, uint8_t *shortcut_mods); -#endif - -#endif diff --git a/app/src/clock.c b/app/src/clock.c deleted file mode 100644 index 8a77e514..00000000 --- a/app/src/clock.c +++ /dev/null @@ -1,38 +0,0 @@ -#include "clock.h" - -#include - -#include "util/log.h" - -//#define SC_CLOCK_DEBUG // uncomment to debug - -#define SC_CLOCK_RANGE 32 - -void -sc_clock_init(struct sc_clock *clock) { - clock->range = 0; - clock->offset = 0; -} - -void -sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream) { - if (clock->range < SC_CLOCK_RANGE) { - ++clock->range; - } - - sc_tick offset = system - stream; - unsigned clock_weight = clock->range - 1; - unsigned value_weight = SC_CLOCK_RANGE - clock->range + 1; - clock->offset = (clock->offset * clock_weight + offset * value_weight) - / SC_CLOCK_RANGE; - -#ifdef SC_CLOCK_DEBUG - LOGD("Clock estimation: pts + %" PRItick, clock->offset); -#endif -} - -sc_tick -sc_clock_to_system_time(struct sc_clock *clock, sc_tick stream) { - assert(clock->range); // sc_clock_update() must have been called - return stream + clock->offset; -} diff --git a/app/src/clock.h b/app/src/clock.h deleted file mode 100644 index 0d34ab99..00000000 --- a/app/src/clock.h +++ /dev/null @@ -1,43 +0,0 @@ -#ifndef SC_CLOCK_H -#define SC_CLOCK_H - -#include "common.h" - -#include "util/tick.h" - -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 - * - * Theoretically, the slope encodes the drift between the device clock and the - * computer clock. It is expected to be very close to 1. - * - * Since the clock is used to estimate very close points in the future (which - * are reestimated on every clock update, see delay_buffer), the error caused - * by clock drift is totally negligible, so it is better to assume that the - * slope is 1 than to estimate it (the estimation error would be larger). - * - * Therefore, only the offset is estimated. - */ -struct sc_clock { - unsigned range; - sc_tick offset; -}; - -void -sc_clock_init(struct sc_clock *clock); - -void -sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream); - -sc_tick -sc_clock_to_system_time(struct sc_clock *clock, sc_tick stream); - -#endif diff --git a/app/src/command.c b/app/src/command.c new file mode 100644 index 00000000..4cb2e408 --- /dev/null +++ b/app/src/command.c @@ -0,0 +1,203 @@ +#include "command.h" + +#include +#include +#include +#include + +#include "common.h" +#include "log.h" +#include "str_util.h" + +static const char *adb_command; + +static inline const char * +get_adb_command(void) { + if (!adb_command) { + adb_command = getenv("ADB"); + if (!adb_command) + adb_command = "adb"; + } + return adb_command; +} + +// serialize argv to string "[arg1], [arg2], [arg3]" +static size_t +argv_to_string(const char *const *argv, char *buf, size_t bufsize) { + size_t idx = 0; + bool first = true; + while (*argv) { + const char *arg = *argv; + size_t len = strlen(arg); + // count space for "[], ...\0" + if (idx + len + 8 >= bufsize) { + // not enough space, truncate + assert(idx < bufsize - 4); + memcpy(&buf[idx], "...", 3); + idx += 3; + break; + } + if (first) { + first = false; + } else { + buf[idx++] = ','; + buf[idx++] = ' '; + } + buf[idx++] = '['; + memcpy(&buf[idx], arg, len); + idx += len; + buf[idx++] = ']'; + argv++; + } + assert(idx < bufsize); + buf[idx] = '\0'; + return idx; +} + +static void +show_adb_err_msg(enum process_result err, const char *const argv[]) { + char buf[512]; + switch (err) { + case PROCESS_ERROR_GENERIC: + argv_to_string(argv, buf, sizeof(buf)); + LOGE("Failed to execute: %s", buf); + break; + case PROCESS_ERROR_MISSING_BINARY: + argv_to_string(argv, buf, sizeof(buf)); + LOGE("Command not found: %s", buf); + LOGE("(make 'adb' accessible from your PATH or define its full" + "path in the ADB environment variable)"); + break; + case PROCESS_SUCCESS: + // do nothing + break; + } +} + +process_t +adb_execute(const char *serial, const char *const adb_cmd[], size_t len) { + const char *cmd[len + 4]; + int i; + process_t process; + cmd[0] = get_adb_command(); + if (serial) { + cmd[1] = "-s"; + cmd[2] = serial; + i = 3; + } else { + i = 1; + } + + memcpy(&cmd[i], adb_cmd, len * sizeof(const char *)); + cmd[len + i] = NULL; + enum process_result r = cmd_execute(cmd[0], cmd, &process); + if (r != PROCESS_SUCCESS) { + show_adb_err_msg(r, cmd); + return PROCESS_NONE; + } + return process; +} + +process_t +adb_forward(const char *serial, uint16_t local_port, + const char *device_socket_name) { + char local[4 + 5 + 1]; // tcp:PORT + char remote[108 + 14 + 1]; // localabstract:NAME + sprintf(local, "tcp:%" PRIu16, local_port); + snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); + const char *const adb_cmd[] = {"forward", local, remote}; + return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); +} + +process_t +adb_forward_remove(const char *serial, uint16_t local_port) { + char local[4 + 5 + 1]; // tcp:PORT + sprintf(local, "tcp:%" PRIu16, local_port); + const char *const adb_cmd[] = {"forward", "--remove", local}; + return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); +} + +process_t +adb_reverse(const char *serial, const char *device_socket_name, + uint16_t local_port) { + char local[4 + 5 + 1]; // tcp:PORT + char remote[108 + 14 + 1]; // localabstract:NAME + sprintf(local, "tcp:%" PRIu16, local_port); + snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); + const char *const adb_cmd[] = {"reverse", remote, local}; + return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); +} + +process_t +adb_reverse_remove(const char *serial, const char *device_socket_name) { + char remote[108 + 14 + 1]; // localabstract:NAME + snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); + const char *const adb_cmd[] = {"reverse", "--remove", remote}; + return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); +} + +process_t +adb_push(const char *serial, const char *local, const char *remote) { +#ifdef __WINDOWS__ + // Windows will parse the string, so the paths must be quoted + // (see sys/win/command.c) + local = strquote(local); + if (!local) { + return PROCESS_NONE; + } + remote = strquote(remote); + if (!remote) { + SDL_free((void *) local); + return PROCESS_NONE; + } +#endif + + const char *const adb_cmd[] = {"push", local, remote}; + process_t proc = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); + +#ifdef __WINDOWS__ + SDL_free((void *) remote); + SDL_free((void *) local); +#endif + + return proc; +} + +process_t +adb_install(const char *serial, const char *local) { +#ifdef __WINDOWS__ + // Windows will parse the string, so the local name must be quoted + // (see sys/win/command.c) + local = strquote(local); + if (!local) { + return PROCESS_NONE; + } +#endif + + const char *const adb_cmd[] = {"install", "-r", local}; + process_t proc = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); + +#ifdef __WINDOWS__ + SDL_free((void *) local); +#endif + + return proc; +} + +bool +process_check_success(process_t proc, const char *name) { + if (proc == PROCESS_NONE) { + LOGE("Could not execute \"%s\"", name); + return false; + } + exit_code_t exit_code; + if (!cmd_simple_wait(proc, &exit_code)) { + if (exit_code != NO_EXIT_CODE) { + LOGE("\"%s\" returned with value %" PRIexitcode, name, exit_code); + } else { + LOGE("\"%s\" exited unexpectedly", name); + } + return false; + } + return true; +} diff --git a/app/src/command.h b/app/src/command.h new file mode 100644 index 00000000..db6358da --- /dev/null +++ b/app/src/command.h @@ -0,0 +1,86 @@ +#ifndef COMMAND_H +#define COMMAND_H + +#include +#include + +#ifdef _WIN32 + + // not needed here, but winsock2.h must never be included AFTER windows.h +# include +# include +# define PATH_SEPARATOR '\\' +# define PRIexitcode "lu" +// +# ifdef _WIN64 +# define PRIsizet PRIu64 +# else +# define PRIsizet PRIu32 +# endif +# define PROCESS_NONE NULL + typedef HANDLE process_t; + typedef DWORD exit_code_t; + +#else + +# include +# define PATH_SEPARATOR '/' +# define PRIsizet "zu" +# define PRIexitcode "d" +# define PROCESS_NONE -1 + typedef pid_t process_t; + typedef int exit_code_t; + +#endif + +# define NO_EXIT_CODE -1 + +enum process_result { + PROCESS_SUCCESS, + PROCESS_ERROR_GENERIC, + PROCESS_ERROR_MISSING_BINARY, +}; + +enum process_result +cmd_execute(const char *path, const char *const argv[], process_t *process); + +bool +cmd_terminate(process_t pid); + +bool +cmd_simple_wait(process_t pid, exit_code_t *exit_code); + +process_t +adb_execute(const char *serial, const char *const adb_cmd[], size_t len); + +process_t +adb_forward(const char *serial, uint16_t local_port, + const char *device_socket_name); + +process_t +adb_forward_remove(const char *serial, uint16_t local_port); + +process_t +adb_reverse(const char *serial, const char *device_socket_name, + uint16_t local_port); + +process_t +adb_reverse_remove(const char *serial, const char *device_socket_name); + +process_t +adb_push(const char *serial, const char *local, const char *remote); + +process_t +adb_install(const char *serial, const char *local); + +// convenience function to wait for a successful process execution +// automatically log process errors with the provided process name +bool +process_check_success(process_t proc, const char *name); + +// return the absolute path of the executable (the scrcpy binary) +// may be NULL on error; to be freed by SDL_free +char * +get_executable_path(void); + +#endif diff --git a/app/src/common.h b/app/src/common.h index 0382d094..8963f058 100644 --- a/app/src/common.h +++ b/app/src/common.h @@ -1,15 +1,28 @@ -#ifndef SC_COMMON_H -#define SC_COMMON_H +#ifndef COMMON_H +#define COMMON_H -#include "config.h" -#include "compat.h" +#include #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 MIN(X,Y) (X) < (Y) ? (X) : (Y) +#define MAX(X,Y) (X) > (Y) ? (X) : (Y) -#define container_of(ptr, type, member) \ - ((type *) (((char *) (ptr)) - offsetof(type, member))) +struct size { + uint16_t width; + uint16_t height; +}; + +struct point { + int32_t x; + int32_t y; +}; + +struct position { + // The video screen size may be different from the real device screen size, + // so store to which size the absolute position apply, to scale it + // accordingly. + struct size screen_size; + struct point point; +}; #endif diff --git a/app/src/compat.c b/app/src/compat.c deleted file mode 100644 index 785f843c..00000000 --- a/app/src/compat.c +++ /dev/null @@ -1,110 +0,0 @@ -#include "compat.h" - -#include "config.h" - -#include -#ifndef HAVE_REALLOCARRAY -# include -#endif -#include -#include -#include -#include - -#ifndef HAVE_STRDUP -char *strdup(const char *s) { - size_t size = strlen(s) + 1; - 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 - -#if !defined(HAVE_NRAND48) || !defined(HAVE_JRAND48) -#define SC_RAND48_MASK UINT64_C(0xFFFFFFFFFFFF) // 48 bits -#define SC_RAND48_A UINT64_C(0x5DEECE66D) -#define SC_RAND48_C 0xB -static inline uint64_t rand_iter48(uint64_t x) { - assert((x & ~SC_RAND48_MASK) == 0); - return (x * SC_RAND48_A + SC_RAND48_C) & SC_RAND48_MASK; -} - -static uint64_t rand_iter48_xsubi(unsigned short xsubi[3]) { - uint64_t x = ((uint64_t) xsubi[0] << 32) - | ((uint64_t) xsubi[1] << 16) - | xsubi[2]; - - x = rand_iter48(x); - - xsubi[0] = (x >> 32) & 0XFFFF; - xsubi[1] = (x >> 16) & 0XFFFF; - xsubi[2] = x & 0XFFFF; - - return x; -} - -#ifndef HAVE_NRAND48 -long nrand48(unsigned short xsubi[3]) { - // range [0, 2^31) - return rand_iter48_xsubi(xsubi) >> 17; -} -#endif - -#ifndef HAVE_JRAND48 -long jrand48(unsigned short xsubi[3]) { - // range [-2^31, 2^31) - union { - uint32_t u; - int32_t i; - } v; - v.u = rand_iter48_xsubi(xsubi) >> 16; - return v.i; -} -#endif -#endif - -#ifndef HAVE_REALLOCARRAY -void *reallocarray(void *ptr, size_t nmemb, size_t size) { - size_t bytes; - if (__builtin_mul_overflow(nmemb, size, &bytes)) { - errno = ENOMEM; - return NULL; - } - return realloc(ptr, bytes); -} -#endif diff --git a/app/src/compat.h b/app/src/compat.h index 296d1a9f..de667bbf 100644 --- a/app/src/compat.h +++ b/app/src/compat.h @@ -1,19 +1,17 @@ -#ifndef SC_COMPAT_H -#define SC_COMPAT_H +#ifndef COMPAT_H +#define COMPAT_H -#include "config.h" - -#include #include -#include #include -#ifndef _WIN32 -# define PRIu64_ PRIu64 -# define SC_PRIsizet "zu" -#else -# define PRIu64_ "I64u" // Windows... -# define SC_PRIsizet "Iu" +// In ffmpeg/doc/APIchanges: +// 2016-04-11 - 6f69f7a / 9200514 - lavf 57.33.100 / 57.5.0 - avformat.h +// Add AVStream.codecpar, deprecate AVStream.codec. +#if (LIBAVFORMAT_VERSION_MICRO >= 100 /* FFmpeg */ && \ + LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(57, 33, 100)) \ + || (LIBAVFORMAT_VERSION_MICRO < 100 && /* Libav */ \ + LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(57, 5, 0)) +# define SCRCPY_LAVF_HAS_NEW_CODEC_PARAMS_API #endif // In ffmpeg/doc/APIchanges: @@ -27,43 +25,22 @@ # define SCRCPY_LAVF_REQUIRES_REGISTER_ALL #endif -// Not documented in ffmpeg/doc/APIchanges, but AV_CODEC_ID_AV1 has been added -// by FFmpeg commit d42809f9835a4e9e5c7c63210abb09ad0ef19cfb (included in tag -// n3.3). -#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(57, 89, 100) -# define SCRCPY_LAVC_HAS_AV1 -#endif - // In ffmpeg/doc/APIchanges: -// 2018-01-28 - ea3672b7d6 - lavf 58.7.100 - avformat.h -// 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 +// 2016-04-21 - 7fc329e - lavc 57.37.100 - avcodec.h +// Add a new audio/video encoding and decoding API with decoupled input +// and output -- avcodec_send_packet(), avcodec_receive_frame(), +// avcodec_send_frame() and avcodec_receive_packet(). +#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 37, 100) +# define SCRCPY_LAVF_HAS_NEW_ENCODING_DECODING_API #endif -// Not documented in ffmpeg/doc/APIchanges, but the channel_layout API -// has been replaced by chlayout in FFmpeg commit -// f423497b455da06c1337846902c770028760e094. -#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(57, 23, 100) -# define SCRCPY_LAVU_HAS_CHLAYOUT -#endif - -// In ffmpeg/doc/APIchanges: -// 2023-10-06 - 5432d2aacad - lavc 60.15.100 - avformat.h -// Deprecate AVFormatContext.{nb_,}side_data, av_stream_add_side_data(), -// av_stream_new_side_data(), and av_stream_get_side_data(). Side data fields -// from AVFormatContext.codecpar should be used from now on. -#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(60, 15, 100) -# define SCRCPY_LAVC_HAS_CODECPAR_CODEC_SIDEDATA -#endif - -#if SDL_VERSION_ATLEAST(2, 0, 6) -// -# define SCRCPY_SDL_HAS_HINT_TOUCH_MOUSE_EVENTS +#if SDL_VERSION_ATLEAST(2, 0, 5) +// +# define SCRCPY_SDL_HAS_HINT_MOUSE_FOCUS_CLICKTHROUGH +// +# define SCRCPY_SDL_HAS_GET_DISPLAY_USABLE_BOUNDS +// +# define SCRCPY_SDL_HAS_WINDOW_ALWAYS_ON_TOP #endif #if SDL_VERSION_ATLEAST(2, 0, 8) @@ -71,40 +48,4 @@ # define SCRCPY_SDL_HAS_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR #endif -#if SDL_VERSION_ATLEAST(2, 0, 16) -# define SCRCPY_SDL_HAS_THREAD_PRIORITY_TIME_CRITICAL -#endif - -#if SDL_VERSION_ATLEAST(2, 0, 18) -# define SCRCPY_SDL_HAS_HINT_APP_NAME -#endif - -#if SDL_VERSION_ATLEAST(2, 0, 14) -# define SCRCPY_SDL_HAS_HINT_AUDIO_DEVICE_APP_NAME -#endif - -#ifndef HAVE_STRDUP -char *strdup(const char *s); -#endif - -#ifndef HAVE_ASPRINTF -int asprintf(char **strp, const char *fmt, ...); -#endif - -#ifndef HAVE_VASPRINTF -int vasprintf(char **strp, const char *fmt, va_list ap); -#endif - -#ifndef HAVE_NRAND48 -long nrand48(unsigned short xsubi[3]); -#endif - -#ifndef HAVE_JRAND48 -long jrand48(unsigned short xsubi[3]); -#endif - -#ifndef HAVE_REALLOCARRAY -void *reallocarray(void *ptr, size_t nmemb, size_t size); -#endif - #endif diff --git a/app/src/control_msg.c b/app/src/control_msg.c index e46c6165..9c3d9849 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -1,193 +1,67 @@ #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) - -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 copy_key_labels[] = { - "none", - "copy", - "cut", -}; - -static inline const char * -get_well_known_pointer_id_name(uint64_t pointer_id) { - switch (pointer_id) { - case SC_POINTER_ID_MOUSE: - return "mouse"; - case SC_POINTER_ID_GENERIC_FINGER: - return "finger"; - case SC_POINTER_ID_VIRTUAL_FINGER: - return "vfinger"; - default: - return NULL; - } -} +#include "buffer_util.h" +#include "log.h" +#include "str_util.h" 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_position(uint8_t *buf, const struct position *position) { + buffer_write32be(&buf[0], position->point.x); + buffer_write32be(&buf[4], position->point.y); + buffer_write16be(&buf[8], position->screen_size.width); + buffer_write16be(&buf[10], position->screen_size.height); } -// Write truncated string, and return the size +// write length (2 bytes) + string (non nul-terminated) static size_t -write_string_payload(uint8_t *payload, const char *utf8, size_t max_len) { - if (!utf8) { - return 0; - } - size_t len = sc_str_utf8_truncation_index(utf8, max_len); - memcpy(payload, utf8, len); - return len; -} - -// Write length (4 bytes) + string (non null-terminated) -static size_t -write_string(uint8_t *buf, const char *utf8, size_t max_len) { - size_t len = write_string_payload(buf + 4, utf8, max_len); - sc_write32be(buf, len); - return 4 + len; -} - -// Write length (1 byte) + string (non null-terminated) -static size_t -write_string_tiny(uint8_t *buf, const char *utf8, size_t max_len) { - assert(max_len <= 0xFF); - size_t len = write_string_payload(buf + 1, utf8, max_len); - buf[0] = len; - return 1 + len; +write_string(const char *utf8, size_t max_len, unsigned char *buf) { + size_t len = utf8_truncation_index(utf8, max_len); + buffer_write16be(buf, (uint16_t) len); + memcpy(&buf[2], utf8, len); + return 2 + len; } size_t -sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { +control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { buf[0] = msg->type; switch (msg->type) { - case SC_CONTROL_MSG_TYPE_INJECT_KEYCODE: + case 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(&buf[1], msg->inject_text.text, - SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); + buffer_write32be(&buf[2], msg->inject_keycode.keycode); + buffer_write32be(&buf[6], msg->inject_keycode.metastate); + return 10; + case CONTROL_MSG_TYPE_INJECT_TEXT: { + size_t len = write_string(msg->inject_text.text, + CONTROL_MSG_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.action_button); - sc_write32be(&buf[28], msg->inject_touch_event.buttons); - return 32; - case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT: + case CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT: + buf[1] = msg->inject_mouse_event.action; + buffer_write32be(&buf[2], msg->inject_mouse_event.buttons); + write_position(&buf[6], &msg->inject_mouse_event.position); + return 18; + case CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT: write_position(&buf[1], &msg->inject_scroll_event.position); - // Accept values in the range [-16, 16]. - // Normalize to [-1, 1] in order to use sc_float_to_i16fp(). - float hscroll_norm = msg->inject_scroll_event.hscroll / 16; - hscroll_norm = CLAMP(hscroll_norm, -1, 1); - float vscroll_norm = msg->inject_scroll_event.vscroll / 16; - vscroll_norm = CLAMP(vscroll_norm, -1, 1); - int16_t hscroll = sc_float_to_i16fp(hscroll_norm); - int16_t vscroll = sc_float_to_i16fp(vscroll_norm); - sc_write16be(&buf[13], (uint16_t) hscroll); - sc_write16be(&buf[15], (uint16_t) vscroll); - sc_write32be(&buf[17], msg->inject_scroll_event.buttons); + buffer_write32be(&buf[13], + (uint32_t) msg->inject_scroll_event.hscroll); + buffer_write32be(&buf[17], + (uint32_t) msg->inject_scroll_event.vscroll); return 21; - case 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(&buf[10], msg->set_clipboard.text, - SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH); - return 10 + len; - case SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER: - buf[1] = msg->set_display_power.on; - return 2; - case SC_CONTROL_MSG_TYPE_UHID_CREATE: - sc_write16be(&buf[1], msg->uhid_create.id); - sc_write16be(&buf[3], msg->uhid_create.vendor_id); - sc_write16be(&buf[5], msg->uhid_create.product_id); - - size_t index = 7; - index += write_string_tiny(&buf[index], msg->uhid_create.name, 127); - - sc_write16be(&buf[index], msg->uhid_create.report_desc_size); - index += 2; - - memcpy(&buf[index], msg->uhid_create.report_desc, - msg->uhid_create.report_desc_size); - index += msg->uhid_create.report_desc_size; - - return index; - case SC_CONTROL_MSG_TYPE_UHID_INPUT: - sc_write16be(&buf[1], msg->uhid_input.id); - sc_write16be(&buf[3], msg->uhid_input.size); - memcpy(&buf[5], msg->uhid_input.data, msg->uhid_input.size); - return 5 + msg->uhid_input.size; - case SC_CONTROL_MSG_TYPE_UHID_DESTROY: - sc_write16be(&buf[1], msg->uhid_destroy.id); - return 3; - case SC_CONTROL_MSG_TYPE_START_APP: { - size_t len = write_string_tiny(&buf[1], msg->start_app.name, 255); + case CONTROL_MSG_TYPE_SET_CLIPBOARD: { + size_t len = write_string(msg->inject_text.text, + CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH, + &buf[1]); return 1 + len; } - case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: - case SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL: - case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS: - case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE: - case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS: - case SC_CONTROL_MSG_TYPE_RESET_VIDEO: + case CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: + buf[1] = msg->set_screen_power_mode.mode; + return 2; + case CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON: + case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: + case CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL: + case CONTROL_MSG_TYPE_GET_CLIPBOARD: // no additional data return 1; default: @@ -197,154 +71,13 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { } void -sc_control_msg_log(const struct sc_control_msg *msg) { -#define LOG_CMSG(fmt, ...) LOGV("input: " fmt, ## __VA_ARGS__) +control_msg_destroy(struct control_msg *msg) { 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); + case CONTROL_MSG_TYPE_INJECT_TEXT: + SDL_free(msg->inject_text.text); 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 action_button=%06lx buttons=%06lx", - pointer_name, - MOTIONEVENT_ACTION_LABEL(action), - msg->inject_touch_event.position.point.x, - msg->inject_touch_event.position.point.y, - msg->inject_touch_event.pressure, - (long) msg->inject_touch_event.action_button, - (long) msg->inject_touch_event.buttons); - } else { - // numeric pointer id - LOG_CMSG("touch [id=%" PRIu64_ "] %-4s position=%" PRIi32 ",%" - PRIi32 " pressure=%f action_button=%06lx" - " buttons=%06lx", - id, - MOTIONEVENT_ACTION_LABEL(action), - msg->inject_touch_event.position.point.x, - msg->inject_touch_event.position.point.y, - msg->inject_touch_event.pressure, - (long) msg->inject_touch_event.action_button, - (long) msg->inject_touch_event.buttons); - } - break; - } - case 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_DISPLAY_POWER: - LOG_CMSG("display power %s", - msg->set_display_power.on ? "on" : "off"); - 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; - case SC_CONTROL_MSG_TYPE_UHID_CREATE: { - // Quote only if name is not null - const char *name = msg->uhid_create.name; - const char *quote = name ? "\"" : ""; - LOG_CMSG("UHID create [%" PRIu16 "] %04" PRIx16 ":%04" PRIx16 - " name=%s%s%s report_desc_size=%" PRIu16, - msg->uhid_create.id, - msg->uhid_create.vendor_id, - msg->uhid_create.product_id, - quote, name, quote, - msg->uhid_create.report_desc_size); - break; - } - case SC_CONTROL_MSG_TYPE_UHID_INPUT: { - char *hex = sc_str_to_hex_string(msg->uhid_input.data, - msg->uhid_input.size); - if (hex) { - LOG_CMSG("UHID input [%" PRIu16 "] %s", - msg->uhid_input.id, hex); - free(hex); - } else { - LOG_CMSG("UHID input [%" PRIu16 "] size=%" PRIu16, - msg->uhid_input.id, msg->uhid_input.size); - } - break; - } - case SC_CONTROL_MSG_TYPE_UHID_DESTROY: - LOG_CMSG("UHID destroy [%" PRIu16 "]", msg->uhid_destroy.id); - break; - case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS: - LOG_CMSG("open hard keyboard settings"); - break; - case SC_CONTROL_MSG_TYPE_START_APP: - LOG_CMSG("start app \"%s\"", msg->start_app.name); - break; - case SC_CONTROL_MSG_TYPE_RESET_VIDEO: - LOG_CMSG("reset video"); - break; - default: - LOG_CMSG("unknown type: %u", (unsigned) msg->type); - break; - } -} - -bool -sc_control_msg_is_droppable(const struct sc_control_msg *msg) { - // Cannot drop UHID_CREATE messages, because it would cause all further - // UHID_INPUT messages for this device to be invalid. - // Cannot drop UHID_DESTROY messages either, because a further UHID_CREATE - // with the same id may fail. - return msg->type != SC_CONTROL_MSG_TYPE_UHID_CREATE - && msg->type != SC_CONTROL_MSG_TYPE_UHID_DESTROY; -} - -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; - case SC_CONTROL_MSG_TYPE_START_APP: - free(msg->start_app.name); + case CONTROL_MSG_TYPE_SET_CLIPBOARD: + SDL_free(msg->set_clipboard.text); break; default: // do nothing diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 74dbcba8..e7fdfc4c 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -1,7 +1,5 @@ -#ifndef SC_CONTROLMSG_H -#define SC_CONTROLMSG_H - -#include "common.h" +#ifndef CONTROLMSG_H +#define CONTROLMSG_H #include #include @@ -9,125 +7,68 @@ #include "android/input.h" #include "android/keycodes.h" -#include "coords.h" -#include "hid/hid_event.h" +#include "common.h" -#define SC_CONTROL_MSG_MAX_SIZE (1 << 18) // 256k +#define CONTROL_MSG_TEXT_MAX_LENGTH 300 +#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH 4093 +#define CONTROL_MSG_SERIALIZED_MAX_SIZE \ + (3 + CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH) -#define 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 SC_POINTER_ID_MOUSE UINT64_C(-1) -#define SC_POINTER_ID_GENERIC_FINGER UINT64_C(-2) - -// Used for injecting an additional virtual pointer for pinch-to-zoom -#define SC_POINTER_ID_VIRTUAL_FINGER UINT64_C(-3) - -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_DISPLAY_POWER, - SC_CONTROL_MSG_TYPE_ROTATE_DEVICE, - SC_CONTROL_MSG_TYPE_UHID_CREATE, - SC_CONTROL_MSG_TYPE_UHID_INPUT, - SC_CONTROL_MSG_TYPE_UHID_DESTROY, - SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS, - SC_CONTROL_MSG_TYPE_START_APP, - SC_CONTROL_MSG_TYPE_RESET_VIDEO, +enum control_msg_type { + CONTROL_MSG_TYPE_INJECT_KEYCODE, + CONTROL_MSG_TYPE_INJECT_TEXT, + CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT, + CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, + CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, + CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, + CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL, + CONTROL_MSG_TYPE_GET_CLIPBOARD, + CONTROL_MSG_TYPE_SET_CLIPBOARD, + CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, }; -enum sc_copy_key { - SC_COPY_KEY_NONE, - SC_COPY_KEY_COPY, - SC_COPY_KEY_CUT, +enum screen_power_mode { + // see + SCREEN_POWER_MODE_OFF = 0, + SCREEN_POWER_MODE_NORMAL = 2, }; -struct sc_control_msg { - enum sc_control_msg_type type; +struct control_msg { + enum control_msg_type type; union { struct { enum android_keyevent_action action; enum android_keycode keycode; - uint32_t repeat; enum android_metastate metastate; } inject_keycode; struct { - char *text; // owned, to be freed by free() + char *text; // owned, to be freed by SDL_free() } inject_text; struct { enum android_motionevent_action action; - enum android_motionevent_buttons action_button; enum android_motionevent_buttons buttons; - uint64_t pointer_id; - struct sc_position position; - float pressure; - } inject_touch_event; + struct position position; + } inject_mouse_event; struct { - struct sc_position position; - float hscroll; - float vscroll; - enum android_motionevent_buttons buttons; + struct position position; + int32_t hscroll; + int32_t vscroll; } inject_scroll_event; struct { - enum android_keyevent_action action; // action for the BACK key - // screen may only be turned on on ACTION_DOWN - } back_or_screen_on; - struct { - enum sc_copy_key copy_key; - } get_clipboard; - struct { - uint64_t sequence; - char *text; // owned, to be freed by free() - bool paste; + char *text; // owned, to be freed by SDL_free() } set_clipboard; struct { - bool on; - } set_display_power; - struct { - uint16_t id; - uint16_t vendor_id; - uint16_t product_id; - const char *name; // pointer to static data - uint16_t report_desc_size; - const uint8_t *report_desc; // pointer to static data - } uhid_create; - struct { - uint16_t id; - uint16_t size; - uint8_t data[SC_HID_MAX_SIZE]; - } uhid_input; - struct { - uint16_t id; - } uhid_destroy; - struct { - char *name; - } start_app; + enum screen_power_mode mode; + } set_screen_power_mode; }; }; -// buf size must be at least CONTROL_MSG_MAX_SIZE +// buf size must be at least CONTROL_MSG_SERIALIZED_MAX_SIZE // return the number of bytes written size_t -sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf); +control_msg_serialize(const struct control_msg *msg, unsigned char *buf); void -sc_control_msg_log(const struct sc_control_msg *msg); - -// Even when the buffer is "full", some messages must absolutely not be dropped -// to avoid inconsistencies. -bool -sc_control_msg_is_droppable(const struct sc_control_msg *msg); - -void -sc_control_msg_destroy(struct sc_control_msg *msg); +control_msg_destroy(struct control_msg *msg); #endif diff --git a/app/src/controller.c b/app/src/controller.c index 749de0a5..4b1f4c8b 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -1,202 +1,117 @@ #include "controller.h" -#include +#include -#include "util/log.h" - -// Drop droppable events above this limit -#define SC_CONTROL_MSG_QUEUE_LIMIT 60 - -static void -sc_controller_receiver_on_ended(struct sc_receiver *receiver, bool error, - void *userdata) { - (void) receiver; - - struct sc_controller *controller = userdata; - // Forward the event to the controller listener - controller->cbs->on_ended(controller, error, controller->cbs_userdata); -} +#include "config.h" +#include "lock_util.h" +#include "log.h" bool -sc_controller_init(struct sc_controller *controller, sc_socket control_socket, - const struct sc_controller_callbacks *cbs, - void *cbs_userdata) { - sc_vecdeque_init(&controller->queue); +controller_init(struct controller *controller, socket_t control_socket) { + cbuf_init(&controller->queue); - // Add 4 to support 4 non-droppable events without re-allocation - bool ok = sc_vecdeque_reserve(&controller->queue, - SC_CONTROL_MSG_QUEUE_LIMIT + 4); - if (!ok) { + if (!receiver_init(&controller->receiver, control_socket)) { return false; } - static const struct sc_receiver_callbacks receiver_cbs = { - .on_ended = sc_controller_receiver_on_ended, - }; - - ok = sc_receiver_init(&controller->receiver, control_socket, &receiver_cbs, - controller); - if (!ok) { - sc_vecdeque_destroy(&controller->queue); + if (!(controller->mutex = SDL_CreateMutex())) { + receiver_destroy(&controller->receiver); return false; } - ok = sc_mutex_init(&controller->mutex); - if (!ok) { - sc_receiver_destroy(&controller->receiver); - sc_vecdeque_destroy(&controller->queue); - return false; - } - - ok = sc_cond_init(&controller->msg_cond); - if (!ok) { - sc_receiver_destroy(&controller->receiver); - sc_mutex_destroy(&controller->mutex); - sc_vecdeque_destroy(&controller->queue); + if (!(controller->msg_cond = SDL_CreateCond())) { + receiver_destroy(&controller->receiver); + SDL_DestroyMutex(controller->mutex); return false; } controller->control_socket = control_socket; controller->stopped = false; - assert(cbs && cbs->on_ended); - controller->cbs = cbs; - controller->cbs_userdata = cbs_userdata; - return true; } void -sc_controller_configure(struct sc_controller *controller, - struct sc_acksync *acksync, - struct sc_uhid_devices *uhid_devices) { - controller->receiver.acksync = acksync; - controller->receiver.uhid_devices = uhid_devices; -} +controller_destroy(struct controller *controller) { + SDL_DestroyCond(controller->msg_cond); + SDL_DestroyMutex(controller->mutex); -void -sc_controller_destroy(struct sc_controller *controller) { - sc_cond_destroy(&controller->msg_cond); - sc_mutex_destroy(&controller->mutex); - - while (!sc_vecdeque_is_empty(&controller->queue)) { - struct sc_control_msg *msg = sc_vecdeque_popref(&controller->queue); - assert(msg); - sc_control_msg_destroy(msg); + struct control_msg msg; + while (cbuf_take(&controller->queue, &msg)) { + control_msg_destroy(&msg); } - sc_vecdeque_destroy(&controller->queue); - sc_receiver_destroy(&controller->receiver); + 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); +controller_push_msg(struct controller *controller, + const struct control_msg *msg) { + mutex_lock(controller->mutex); + bool was_empty = cbuf_is_empty(&controller->queue); + bool res = cbuf_push(&controller->queue, *msg); + if (was_empty) { + cond_signal(controller->msg_cond); } - - bool pushed = false; - - sc_mutex_lock(&controller->mutex); - size_t size = sc_vecdeque_size(&controller->queue); - if (size < SC_CONTROL_MSG_QUEUE_LIMIT) { - bool was_empty = sc_vecdeque_is_empty(&controller->queue); - sc_vecdeque_push_noresize(&controller->queue, *msg); - pushed = true; - if (was_empty) { - sc_cond_signal(&controller->msg_cond); - } - } else if (!sc_control_msg_is_droppable(msg)) { - bool ok = sc_vecdeque_push(&controller->queue, *msg); - if (ok) { - pushed = true; - } else { - // A non-droppable event must be dropped anyway - LOG_OOM(); - } - } - // Otherwise, the msg is discarded - - sc_mutex_unlock(&controller->mutex); - - return pushed; + mutex_unlock(controller->mutex); + return res; } static bool -process_msg(struct sc_controller *controller, - const struct sc_control_msg *msg, bool *eos) { - static uint8_t serialized_msg[SC_CONTROL_MSG_MAX_SIZE]; - size_t length = sc_control_msg_serialize(msg, serialized_msg); +process_msg(struct controller *controller, + const struct control_msg *msg) { + unsigned char serialized_msg[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int length = control_msg_serialize(msg, serialized_msg); if (!length) { - *eos = false; return false; } - - ssize_t w = - net_send_all(controller->control_socket, serialized_msg, length); - if ((size_t) w != length) { - *eos = true; - return false; - } - - return true; + int w = net_send_all(controller->control_socket, serialized_msg, length); + return w == length; } static int run_controller(void *data) { - struct sc_controller *controller = data; - - bool error = false; + struct controller *controller = data; for (;;) { - sc_mutex_lock(&controller->mutex); - while (!controller->stopped - && sc_vecdeque_is_empty(&controller->queue)) { - sc_cond_wait(&controller->msg_cond, &controller->mutex); + mutex_lock(controller->mutex); + while (!controller->stopped && cbuf_is_empty(&controller->queue)) { + cond_wait(controller->msg_cond, controller->mutex); } if (controller->stopped) { // stop immediately, do not process further msgs - sc_mutex_unlock(&controller->mutex); - LOGD("Controller stopped"); + mutex_unlock(controller->mutex); break; } + struct control_msg msg; + bool non_empty = cbuf_take(&controller->queue, &msg); + SDL_assert(non_empty); + mutex_unlock(controller->mutex); - assert(!sc_vecdeque_is_empty(&controller->queue)); - struct sc_control_msg msg = sc_vecdeque_pop(&controller->queue); - sc_mutex_unlock(&controller->mutex); - - bool eos; - bool ok = process_msg(controller, &msg, &eos); - sc_control_msg_destroy(&msg); + bool ok = process_msg(controller, &msg); + control_msg_destroy(&msg); if (!ok) { - if (eos) { - LOGD("Controller stopped (socket closed)"); - } // else error already logged - error = !eos; + LOGD("Cannot write msg to socket"); break; } } - - controller->cbs->on_ended(controller, error, controller->cbs_userdata); - return 0; } bool -sc_controller_start(struct sc_controller *controller) { +controller_start(struct 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"); + controller->thread = SDL_CreateThread(run_controller, "controller", + controller); + if (!controller->thread) { + LOGC("Could not start controller thread"); return false; } - if (!sc_receiver_start(&controller->receiver)) { - sc_controller_stop(controller); - sc_thread_join(&controller->thread, NULL); + if (!receiver_start(&controller->receiver)) { + controller_stop(controller); + SDL_WaitThread(controller->thread, NULL); return false; } @@ -204,15 +119,15 @@ sc_controller_start(struct sc_controller *controller) { } void -sc_controller_stop(struct sc_controller *controller) { - sc_mutex_lock(&controller->mutex); +controller_stop(struct controller *controller) { + mutex_lock(controller->mutex); controller->stopped = true; - sc_cond_signal(&controller->msg_cond); - sc_mutex_unlock(&controller->mutex); + cond_signal(controller->msg_cond); + mutex_unlock(controller->mutex); } void -sc_controller_join(struct sc_controller *controller) { - sc_thread_join(&controller->thread, NULL); - sc_receiver_join(&controller->receiver); +controller_join(struct controller *controller) { + SDL_WaitThread(controller->thread, NULL); + receiver_join(&controller->receiver); } diff --git a/app/src/controller.h b/app/src/controller.h index 57ad79b3..ae13e39f 100644 --- a/app/src/controller.h +++ b/app/src/controller.h @@ -1,61 +1,44 @@ -#ifndef SC_CONTROLLER_H -#define SC_CONTROLLER_H - -#include "common.h" +#ifndef CONTROLLER_H +#define CONTROLLER_H #include +#include +#include +#include "cbuf.h" #include "control_msg.h" +#include "net.h" #include "receiver.h" -#include "util/acksync.h" -#include "util/net.h" -#include "util/thread.h" -#include "util/vecdeque.h" -struct sc_control_msg_queue SC_VECDEQUE(struct sc_control_msg); +struct control_msg_queue CBUF(struct control_msg, 64); -struct sc_controller { - sc_socket control_socket; - sc_thread thread; - sc_mutex mutex; - sc_cond msg_cond; +struct controller { + socket_t control_socket; + SDL_Thread *thread; + SDL_mutex *mutex; + SDL_cond *msg_cond; bool stopped; - struct sc_control_msg_queue queue; - struct sc_receiver receiver; - - const struct sc_controller_callbacks *cbs; - void *cbs_userdata; -}; - -struct sc_controller_callbacks { - void (*on_ended)(struct sc_controller *controller, bool error, - void *userdata); + struct control_msg_queue queue; + struct receiver receiver; }; bool -sc_controller_init(struct sc_controller *controller, sc_socket control_socket, - const struct sc_controller_callbacks *cbs, - void *cbs_userdata); +controller_init(struct controller *controller, socket_t control_socket); void -sc_controller_configure(struct sc_controller *controller, - struct sc_acksync *acksync, - struct sc_uhid_devices *uhid_devices); - -void -sc_controller_destroy(struct sc_controller *controller); +controller_destroy(struct controller *controller); bool -sc_controller_start(struct sc_controller *controller); +controller_start(struct controller *controller); void -sc_controller_stop(struct sc_controller *controller); +controller_stop(struct controller *controller); void -sc_controller_join(struct sc_controller *controller); +controller_join(struct controller *controller); bool -sc_controller_push_msg(struct sc_controller *controller, - const struct sc_control_msg *msg); +controller_push_msg(struct controller *controller, + const struct control_msg *msg); #endif diff --git a/app/src/convert.c b/app/src/convert.c new file mode 100644 index 00000000..adf6d400 --- /dev/null +++ b/app/src/convert.c @@ -0,0 +1,228 @@ +#include "convert.h" + +#define MAP(FROM, TO) case FROM: *to = TO; return true +#define FAIL default: return false + +static bool +convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to) { + switch (from) { + MAP(SDL_KEYDOWN, AKEY_EVENT_ACTION_DOWN); + MAP(SDL_KEYUP, AKEY_EVENT_ACTION_UP); + FAIL; + } +} + +static enum android_metastate +autocomplete_metastate(enum android_metastate metastate) { + // fill dependant flags + if (metastate & (AMETA_SHIFT_LEFT_ON | AMETA_SHIFT_RIGHT_ON)) { + metastate |= AMETA_SHIFT_ON; + } + if (metastate & (AMETA_CTRL_LEFT_ON | AMETA_CTRL_RIGHT_ON)) { + metastate |= AMETA_CTRL_ON; + } + if (metastate & (AMETA_ALT_LEFT_ON | AMETA_ALT_RIGHT_ON)) { + metastate |= AMETA_ALT_ON; + } + if (metastate & (AMETA_META_LEFT_ON | AMETA_META_RIGHT_ON)) { + metastate |= AMETA_META_ON; + } + + return metastate; +} + + +static enum android_metastate +convert_meta_state(SDL_Keymod mod) { + enum android_metastate metastate = 0; + if (mod & KMOD_LSHIFT) { + metastate |= AMETA_SHIFT_LEFT_ON; + } + if (mod & KMOD_RSHIFT) { + metastate |= AMETA_SHIFT_RIGHT_ON; + } + if (mod & KMOD_LCTRL) { + metastate |= AMETA_CTRL_LEFT_ON; + } + if (mod & KMOD_RCTRL) { + metastate |= AMETA_CTRL_RIGHT_ON; + } + if (mod & KMOD_LALT) { + metastate |= AMETA_ALT_LEFT_ON; + } + if (mod & KMOD_RALT) { + metastate |= AMETA_ALT_RIGHT_ON; + } + if (mod & KMOD_LGUI) { // Windows key + metastate |= AMETA_META_LEFT_ON; + } + if (mod & KMOD_RGUI) { // Windows key + metastate |= AMETA_META_RIGHT_ON; + } + if (mod & KMOD_NUM) { + metastate |= AMETA_NUM_LOCK_ON; + } + if (mod & KMOD_CAPS) { + metastate |= AMETA_CAPS_LOCK_ON; + } + if (mod & KMOD_MODE) { // Alt Gr + // no mapping? + } + + // fill the dependent fields + return autocomplete_metastate(metastate); +} + +static bool +convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod) { + switch (from) { + MAP(SDLK_RETURN, AKEYCODE_ENTER); + MAP(SDLK_KP_ENTER, AKEYCODE_NUMPAD_ENTER); + MAP(SDLK_ESCAPE, AKEYCODE_ESCAPE); + MAP(SDLK_BACKSPACE, AKEYCODE_DEL); + MAP(SDLK_TAB, AKEYCODE_TAB); + MAP(SDLK_PAGEUP, AKEYCODE_PAGE_UP); + MAP(SDLK_DELETE, AKEYCODE_FORWARD_DEL); + MAP(SDLK_HOME, AKEYCODE_MOVE_HOME); + MAP(SDLK_END, AKEYCODE_MOVE_END); + MAP(SDLK_PAGEDOWN, AKEYCODE_PAGE_DOWN); + MAP(SDLK_RIGHT, AKEYCODE_DPAD_RIGHT); + MAP(SDLK_LEFT, AKEYCODE_DPAD_LEFT); + MAP(SDLK_DOWN, AKEYCODE_DPAD_DOWN); + MAP(SDLK_UP, AKEYCODE_DPAD_UP); + } + if (mod & (KMOD_LALT | KMOD_RALT | KMOD_LGUI | KMOD_RGUI)) { + return false; + } + // if ALT and META are not pressed, also handle letters and space + switch (from) { + MAP(SDLK_a, AKEYCODE_A); + MAP(SDLK_b, AKEYCODE_B); + MAP(SDLK_c, AKEYCODE_C); + MAP(SDLK_d, AKEYCODE_D); + MAP(SDLK_e, AKEYCODE_E); + MAP(SDLK_f, AKEYCODE_F); + MAP(SDLK_g, AKEYCODE_G); + MAP(SDLK_h, AKEYCODE_H); + MAP(SDLK_i, AKEYCODE_I); + MAP(SDLK_j, AKEYCODE_J); + MAP(SDLK_k, AKEYCODE_K); + MAP(SDLK_l, AKEYCODE_L); + MAP(SDLK_m, AKEYCODE_M); + MAP(SDLK_n, AKEYCODE_N); + MAP(SDLK_o, AKEYCODE_O); + MAP(SDLK_p, AKEYCODE_P); + MAP(SDLK_q, AKEYCODE_Q); + MAP(SDLK_r, AKEYCODE_R); + MAP(SDLK_s, AKEYCODE_S); + MAP(SDLK_t, AKEYCODE_T); + MAP(SDLK_u, AKEYCODE_U); + MAP(SDLK_v, AKEYCODE_V); + MAP(SDLK_w, AKEYCODE_W); + MAP(SDLK_x, AKEYCODE_X); + MAP(SDLK_y, AKEYCODE_Y); + MAP(SDLK_z, AKEYCODE_Z); + MAP(SDLK_SPACE, AKEYCODE_SPACE); + FAIL; + } +} + +static bool +convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) { + switch (from) { + MAP(SDL_MOUSEBUTTONDOWN, AMOTION_EVENT_ACTION_DOWN); + MAP(SDL_MOUSEBUTTONUP, AMOTION_EVENT_ACTION_UP); + FAIL; + } +} + +static enum android_motionevent_buttons +convert_mouse_buttons(uint32_t state) { + enum android_motionevent_buttons buttons = 0; + if (state & SDL_BUTTON_LMASK) { + buttons |= AMOTION_EVENT_BUTTON_PRIMARY; + } + if (state & SDL_BUTTON_RMASK) { + buttons |= AMOTION_EVENT_BUTTON_SECONDARY; + } + if (state & SDL_BUTTON_MMASK) { + buttons |= AMOTION_EVENT_BUTTON_TERTIARY; + } + if (state & SDL_BUTTON_X1) { + buttons |= AMOTION_EVENT_BUTTON_BACK; + } + if (state & SDL_BUTTON_X2) { + buttons |= AMOTION_EVENT_BUTTON_FORWARD; + } + return buttons; +} + +bool +input_key_from_sdl_to_android(const SDL_KeyboardEvent *from, + struct control_msg *to) { + to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE; + + if (!convert_keycode_action(from->type, &to->inject_keycode.action)) { + return false; + } + + uint16_t mod = from->keysym.mod; + if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod)) { + return false; + } + + to->inject_keycode.metastate = convert_meta_state(mod); + + return true; +} + +bool +mouse_button_from_sdl_to_android(const SDL_MouseButtonEvent *from, + struct size screen_size, + struct control_msg *to) { + to->type = CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT; + + if (!convert_mouse_action(from->type, &to->inject_mouse_event.action)) { + return false; + } + + to->inject_mouse_event.buttons = + convert_mouse_buttons(SDL_BUTTON(from->button)); + to->inject_mouse_event.position.screen_size = screen_size; + to->inject_mouse_event.position.point.x = from->x; + to->inject_mouse_event.position.point.y = from->y; + + return true; +} + +bool +mouse_motion_from_sdl_to_android(const SDL_MouseMotionEvent *from, + struct size screen_size, + struct control_msg *to) { + to->type = CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT; + to->inject_mouse_event.action = AMOTION_EVENT_ACTION_MOVE; + to->inject_mouse_event.buttons = convert_mouse_buttons(from->state); + to->inject_mouse_event.position.screen_size = screen_size; + to->inject_mouse_event.position.point.x = from->x; + to->inject_mouse_event.position.point.y = from->y; + + return true; +} + +bool +mouse_wheel_from_sdl_to_android(const SDL_MouseWheelEvent *from, + struct position position, + struct control_msg *to) { + to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT; + + to->inject_scroll_event.position = position; + + int mul = from->direction == SDL_MOUSEWHEEL_NORMAL ? 1 : -1; + // SDL behavior seems inconsistent between horizontal and vertical scrolling + // so reverse the horizontal + // + to->inject_scroll_event.hscroll = -mul * from->x; + to->inject_scroll_event.vscroll = mul * from->y; + + return true; +} diff --git a/app/src/convert.h b/app/src/convert.h new file mode 100644 index 00000000..5989e163 --- /dev/null +++ b/app/src/convert.h @@ -0,0 +1,41 @@ +#ifndef CONVERT_H +#define CONVERT_H + +#include +#include + +#include "control_msg.h" + +struct complete_mouse_motion_event { + SDL_MouseMotionEvent *mouse_motion_event; + struct size screen_size; +}; + +struct complete_mouse_wheel_event { + SDL_MouseWheelEvent *mouse_wheel_event; + struct point position; +}; + +bool +input_key_from_sdl_to_android(const SDL_KeyboardEvent *from, + struct control_msg *to); + +bool +mouse_button_from_sdl_to_android(const SDL_MouseButtonEvent *from, + struct size screen_size, + struct control_msg *to); + +// the video size may be different from the real device size, so we need the +// size to which the absolute position apply, to scale it accordingly +bool +mouse_motion_from_sdl_to_android(const SDL_MouseMotionEvent *from, + struct size screen_size, + struct control_msg *to); + +// on Android, a scroll event requires the current mouse position +bool +mouse_wheel_from_sdl_to_android(const SDL_MouseWheelEvent *from, + struct position position, + struct control_msg *to); + +#endif diff --git a/app/src/coords.h b/app/src/coords.h deleted file mode 100644 index cdabb782..00000000 --- a/app/src/coords.h +++ /dev/null @@ -1,24 +0,0 @@ -#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 diff --git a/app/src/decoder.c b/app/src/decoder.c index 4d0a1daf..8fa218f4 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -1,107 +1,103 @@ #include "decoder.h" -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include -#include "util/log.h" - -/** Downcast packet_sink to decoder */ -#define DOWNCAST(SINK) container_of(SINK, struct sc_decoder, packet_sink) - -static bool -sc_decoder_open(struct sc_decoder *decoder, AVCodecContext *ctx) { - decoder->frame = av_frame_alloc(); - if (!decoder->frame) { - LOG_OOM(); - return false; - } - - if (!sc_frame_source_sinks_open(&decoder->frame_source, ctx)) { - av_frame_free(&decoder->frame); - return false; - } - - decoder->ctx = ctx; - - return true; -} +#include "compat.h" +#include "config.h" +#include "buffer_util.h" +#include "events.h" +#include "lock_util.h" +#include "log.h" +#include "recorder.h" +#include "video_buffer.h" +// set the decoded frame as ready for rendering, and notify static void -sc_decoder_close(struct sc_decoder *decoder) { - sc_frame_source_sinks_close(&decoder->frame_source); - av_frame_free(&decoder->frame); -} - -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; +push_frame(struct decoder *decoder) { + bool previous_frame_skipped; + video_buffer_offer_decoded_frame(decoder->video_buffer, + &previous_frame_skipped); + if (previous_frame_skipped) { + // the previous EVENT_NEW_FRAME will consume this frame + return; } - - int ret = avcodec_send_packet(decoder->ctx, packet); - if (ret < 0 && ret != AVERROR(EAGAIN)) { - LOGE("Decoder '%s': could not send video packet: %d", - decoder->name, ret); - return false; - } - - for (;;) { - ret = avcodec_receive_frame(decoder->ctx, decoder->frame); - if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { - break; - } - - if (ret) { - LOGE("Decoder '%s', could not receive video frame: %d", - decoder->name, ret); - return false; - } - - // a frame was received - bool ok = sc_frame_source_sinks_push(&decoder->frame_source, - decoder->frame); - av_frame_unref(decoder->frame); - if (!ok) { - // Error already logged - return false; - } - } - - return true; -} - -static bool -sc_decoder_packet_sink_open(struct sc_packet_sink *sink, AVCodecContext *ctx) { - struct sc_decoder *decoder = DOWNCAST(sink); - return sc_decoder_open(decoder, ctx); -} - -static void -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); + static SDL_Event new_frame_event = { + .type = EVENT_NEW_FRAME, + }; + SDL_PushEvent(&new_frame_event); } void -sc_decoder_init(struct sc_decoder *decoder, const char *name) { - decoder->name = name; // statically allocated - sc_frame_source_init(&decoder->frame_source); - - static const struct sc_packet_sink_ops ops = { - .open = sc_decoder_packet_sink_open, - .close = sc_decoder_packet_sink_close, - .push = sc_decoder_packet_sink_push, - }; - - decoder->packet_sink.ops = &ops; +decoder_init(struct decoder *decoder, struct video_buffer *vb) { + decoder->video_buffer = vb; +} + +bool +decoder_open(struct decoder *decoder, const AVCodec *codec) { + decoder->codec_ctx = avcodec_alloc_context3(codec); + if (!decoder->codec_ctx) { + LOGC("Could not allocate decoder context"); + return false; + } + + if (avcodec_open2(decoder->codec_ctx, codec, NULL) < 0) { + LOGE("Could not open codec"); + avcodec_free_context(&decoder->codec_ctx); + return false; + } + + return true; +} + +void +decoder_close(struct decoder *decoder) { + avcodec_close(decoder->codec_ctx); + avcodec_free_context(&decoder->codec_ctx); +} + +bool +decoder_push(struct decoder *decoder, const AVPacket *packet) { +// the new decoding/encoding API has been introduced by: +// +#ifdef SCRCPY_LAVF_HAS_NEW_ENCODING_DECODING_API + int ret; + if ((ret = avcodec_send_packet(decoder->codec_ctx, packet)) < 0) { + LOGE("Could not send video packet: %d", ret); + return false; + } + ret = avcodec_receive_frame(decoder->codec_ctx, + decoder->video_buffer->decoding_frame); + if (!ret) { + // a frame was received + push_frame(decoder); + } else if (ret != AVERROR(EAGAIN)) { + LOGE("Could not receive video frame: %d", ret); + return false; + } +#else + int got_picture; + int len = avcodec_decode_video2(decoder->codec_ctx, + decoder->video_buffer->decoding_frame, + &got_picture, + packet); + if (len < 0) { + LOGE("Could not decode video packet: %d", len); + return false; + } + if (got_picture) { + push_frame(decoder); + } +#endif + return true; +} + +void +decoder_interrupt(struct decoder *decoder) { + video_buffer_interrupt(decoder->video_buffer); } diff --git a/app/src/decoder.h b/app/src/decoder.h index 1f525fae..76fee80e 100644 --- a/app/src/decoder.h +++ b/app/src/decoder.h @@ -1,25 +1,29 @@ -#ifndef SC_DECODER_H -#define SC_DECODER_H +#ifndef DECODER_H +#define DECODER_H -#include "common.h" +#include +#include -#include +struct video_buffer; -#include "trait/frame_source.h" -#include "trait/packet_sink.h" - -struct sc_decoder { - struct sc_packet_sink packet_sink; // packet sink trait - struct sc_frame_source frame_source; // frame source trait - - const char *name; // must be statically allocated (e.g. a string literal) - - AVCodecContext *ctx; - AVFrame *frame; +struct decoder { + struct video_buffer *video_buffer; + AVCodecContext *codec_ctx; }; -// The name must be statically allocated (e.g. a string literal) void -sc_decoder_init(struct sc_decoder *decoder, const char *name); +decoder_init(struct decoder *decoder, struct video_buffer *vb); + +bool +decoder_open(struct decoder *decoder, const AVCodec *codec); + +void +decoder_close(struct decoder *decoder); + +bool +decoder_push(struct decoder *decoder, const AVPacket *packet); + +void +decoder_interrupt(struct decoder *decoder); #endif diff --git a/app/src/delay_buffer.c b/app/src/delay_buffer.c deleted file mode 100644 index f75c6f72..00000000 --- a/app/src/delay_buffer.c +++ /dev/null @@ -1,241 +0,0 @@ -#include "delay_buffer.h" - -#include -#include -#include - -#include "util/log.h" - -/** Downcast frame_sink to sc_delay_buffer */ -#define DOWNCAST(SINK) container_of(SINK, struct sc_delay_buffer, frame_sink) - -static bool -sc_delayed_frame_init(struct sc_delayed_frame *dframe, const AVFrame *frame) { - dframe->frame = av_frame_alloc(); - if (!dframe->frame) { - LOG_OOM(); - return false; - } - - if (av_frame_ref(dframe->frame, frame)) { - LOG_OOM(); - av_frame_free(&dframe->frame); - return false; - } - - return true; -} - -static void -sc_delayed_frame_destroy(struct sc_delayed_frame *dframe) { - av_frame_unref(dframe->frame); - av_frame_free(&dframe->frame); -} - -static int -run_buffering(void *data) { - struct sc_delay_buffer *db = data; - - assert(db->delay > 0); - - for (;;) { - sc_mutex_lock(&db->mutex); - - while (!db->stopped && sc_vecdeque_is_empty(&db->queue)) { - sc_cond_wait(&db->queue_cond, &db->mutex); - } - - if (db->stopped) { - sc_mutex_unlock(&db->mutex); - goto stopped; - } - - struct sc_delayed_frame dframe = sc_vecdeque_pop(&db->queue); - - sc_tick max_deadline = sc_tick_now() + db->delay; - // PTS (written by the server) are expressed in microseconds - sc_tick pts = SC_TICK_FROM_US(dframe.frame->pts); - - bool timed_out = false; - while (!db->stopped && !timed_out) { - sc_tick deadline = sc_clock_to_system_time(&db->clock, pts) - + db->delay; - if (deadline > max_deadline) { - deadline = max_deadline; - } - - timed_out = - !sc_cond_timedwait(&db->wait_cond, &db->mutex, deadline); - } - - bool stopped = db->stopped; - sc_mutex_unlock(&db->mutex); - - if (stopped) { - sc_delayed_frame_destroy(&dframe); - goto stopped; - } - -#ifdef SC_BUFFERING_DEBUG - LOGD("Buffering: %" PRItick ";%" PRItick ";%" PRItick, - pts, dframe.push_date, sc_tick_now()); -#endif - - bool ok = sc_frame_source_sinks_push(&db->frame_source, dframe.frame); - sc_delayed_frame_destroy(&dframe); - if (!ok) { - LOGE("Delayed frame could not be pushed, stopping"); - sc_mutex_lock(&db->mutex); - // Prevent to push any new frame - db->stopped = true; - sc_mutex_unlock(&db->mutex); - goto stopped; - } - } - -stopped: - assert(db->stopped); - - // Flush queue - while (!sc_vecdeque_is_empty(&db->queue)) { - struct sc_delayed_frame *dframe = sc_vecdeque_popref(&db->queue); - sc_delayed_frame_destroy(dframe); - } - - LOGD("Buffering thread ended"); - - return 0; -} - -static bool -sc_delay_buffer_frame_sink_open(struct sc_frame_sink *sink, - const AVCodecContext *ctx) { - struct sc_delay_buffer *db = DOWNCAST(sink); - (void) ctx; - - bool ok = sc_mutex_init(&db->mutex); - if (!ok) { - return false; - } - - ok = sc_cond_init(&db->queue_cond); - if (!ok) { - goto error_destroy_mutex; - } - - ok = sc_cond_init(&db->wait_cond); - if (!ok) { - goto error_destroy_queue_cond; - } - - sc_clock_init(&db->clock); - sc_vecdeque_init(&db->queue); - db->stopped = false; - - if (!sc_frame_source_sinks_open(&db->frame_source, ctx)) { - goto error_destroy_wait_cond; - } - - ok = sc_thread_create(&db->thread, run_buffering, "scrcpy-dbuf", db); - if (!ok) { - LOGE("Could not start buffering thread"); - goto error_close_sinks; - } - - return true; - -error_close_sinks: - sc_frame_source_sinks_close(&db->frame_source); -error_destroy_wait_cond: - sc_cond_destroy(&db->wait_cond); -error_destroy_queue_cond: - sc_cond_destroy(&db->queue_cond); -error_destroy_mutex: - sc_mutex_destroy(&db->mutex); - - return false; -} - -static void -sc_delay_buffer_frame_sink_close(struct sc_frame_sink *sink) { - struct sc_delay_buffer *db = DOWNCAST(sink); - - sc_mutex_lock(&db->mutex); - db->stopped = true; - sc_cond_signal(&db->queue_cond); - sc_cond_signal(&db->wait_cond); - sc_mutex_unlock(&db->mutex); - - sc_thread_join(&db->thread, NULL); - - sc_frame_source_sinks_close(&db->frame_source); - - sc_cond_destroy(&db->wait_cond); - sc_cond_destroy(&db->queue_cond); - sc_mutex_destroy(&db->mutex); -} - -static bool -sc_delay_buffer_frame_sink_push(struct sc_frame_sink *sink, - const AVFrame *frame) { - struct sc_delay_buffer *db = DOWNCAST(sink); - - sc_mutex_lock(&db->mutex); - - if (db->stopped) { - sc_mutex_unlock(&db->mutex); - return false; - } - - sc_tick pts = SC_TICK_FROM_US(frame->pts); - sc_clock_update(&db->clock, sc_tick_now(), pts); - sc_cond_signal(&db->wait_cond); - - if (db->first_frame_asap && db->clock.range == 1) { - sc_mutex_unlock(&db->mutex); - return sc_frame_source_sinks_push(&db->frame_source, frame); - } - - struct sc_delayed_frame dframe; - bool ok = sc_delayed_frame_init(&dframe, frame); - if (!ok) { - sc_mutex_unlock(&db->mutex); - return false; - } - -#ifdef SC_BUFFERING_DEBUG - dframe.push_date = sc_tick_now(); -#endif - - ok = sc_vecdeque_push(&db->queue, dframe); - if (!ok) { - sc_mutex_unlock(&db->mutex); - LOG_OOM(); - return false; - } - - sc_cond_signal(&db->queue_cond); - - sc_mutex_unlock(&db->mutex); - - return true; -} - -void -sc_delay_buffer_init(struct sc_delay_buffer *db, sc_tick delay, - bool first_frame_asap) { - assert(delay > 0); - - db->delay = delay; - db->first_frame_asap = first_frame_asap; - - sc_frame_source_init(&db->frame_source); - - static const struct sc_frame_sink_ops ops = { - .open = sc_delay_buffer_frame_sink_open, - .close = sc_delay_buffer_frame_sink_close, - .push = sc_delay_buffer_frame_sink_push, - }; - - db->frame_sink.ops = &ops; -} diff --git a/app/src/delay_buffer.h b/app/src/delay_buffer.h deleted file mode 100644 index 61cd77e4..00000000 --- a/app/src/delay_buffer.h +++ /dev/null @@ -1,63 +0,0 @@ -#ifndef SC_DELAY_BUFFER_H -#define SC_DELAY_BUFFER_H - -#include "common.h" - -#include -#include - -#include "clock.h" -#include "trait/frame_source.h" -#include "trait/frame_sink.h" -#include "util/thread.h" -#include "util/tick.h" -#include "util/vecdeque.h" - -//#define SC_BUFFERING_DEBUG // uncomment to debug - -// forward declarations -typedef struct AVFrame AVFrame; - -struct sc_delayed_frame { - AVFrame *frame; -#ifdef SC_BUFFERING_DEBUG - sc_tick push_date; -#endif -}; - -struct sc_delayed_frame_queue SC_VECDEQUE(struct sc_delayed_frame); - -struct sc_delay_buffer { - struct sc_frame_source frame_source; // frame source trait - struct sc_frame_sink frame_sink; // frame sink trait - - sc_tick delay; - bool first_frame_asap; - - sc_thread thread; - sc_mutex mutex; - sc_cond queue_cond; - sc_cond wait_cond; - - struct sc_clock clock; - struct sc_delayed_frame_queue queue; - bool stopped; -}; - -struct sc_delay_buffer_callbacks { - bool (*on_new_frame)(struct sc_delay_buffer *db, const AVFrame *frame, - void *userdata); -}; - -/** - * Initialize a delay buffer. - * - * \param delay a (strictly) positive delay - * \param first_frame_asap if true, do not delay the first frame (useful for - a video stream). - */ -void -sc_delay_buffer_init(struct sc_delay_buffer *db, sc_tick delay, - bool first_frame_asap); - -#endif diff --git a/app/src/demuxer.c b/app/src/demuxer.c deleted file mode 100644 index 885cd6ee..00000000 --- a/app/src/demuxer.c +++ /dev/null @@ -1,316 +0,0 @@ -#include "demuxer.h" - -#include -#include -#include -#include - -#include "packet_merger.h" -#include "util/binary.h" -#include "util/log.h" - -#define SC_PACKET_HEADER_SIZE 12 - -#define SC_PACKET_FLAG_CONFIG (UINT64_C(1) << 63) -#define SC_PACKET_FLAG_KEY_FRAME (UINT64_C(1) << 62) - -#define SC_PACKET_PTS_MASK (SC_PACKET_FLAG_KEY_FRAME - 1) - -static enum AVCodecID -sc_demuxer_to_avcodec_id(uint32_t codec_id) { -#define SC_CODEC_ID_H264 UINT32_C(0x68323634) // "h264" in ASCII -#define SC_CODEC_ID_H265 UINT32_C(0x68323635) // "h265" in ASCII -#define SC_CODEC_ID_AV1 UINT32_C(0x00617631) // "av1" in ASCII -#define SC_CODEC_ID_OPUS UINT32_C(0x6f707573) // "opus" in ASCII -#define SC_CODEC_ID_AAC UINT32_C(0x00616163) // "aac" in ASCII -#define SC_CODEC_ID_FLAC UINT32_C(0x666c6163) // "flac" in ASCII -#define SC_CODEC_ID_RAW UINT32_C(0x00726177) // "raw" in ASCII - switch (codec_id) { - case SC_CODEC_ID_H264: - return AV_CODEC_ID_H264; - case SC_CODEC_ID_H265: - return AV_CODEC_ID_HEVC; - case SC_CODEC_ID_AV1: -#ifdef SCRCPY_LAVC_HAS_AV1 - return AV_CODEC_ID_AV1; -#else - LOGE("AV1 not supported by this FFmpeg version"); - return AV_CODEC_ID_NONE; -#endif - case SC_CODEC_ID_OPUS: - return AV_CODEC_ID_OPUS; - case SC_CODEC_ID_AAC: - return AV_CODEC_ID_AAC; - case SC_CODEC_ID_FLAC: - return AV_CODEC_ID_FLAC; - case SC_CODEC_ID_RAW: - return AV_CODEC_ID_PCM_S16LE; - default: - LOGE("Unknown codec id 0x%08" PRIx32, codec_id); - return AV_CODEC_ID_NONE; - } -} - -static bool -sc_demuxer_recv_codec_id(struct sc_demuxer *demuxer, uint32_t *codec_id) { - uint8_t data[4]; - ssize_t r = net_recv_all(demuxer->socket, data, 4); - if (r < 4) { - return false; - } - - *codec_id = sc_read32be(data); - return true; -} - -static bool -sc_demuxer_recv_video_size(struct sc_demuxer *demuxer, uint32_t *width, - uint32_t *height) { - uint8_t data[8]; - ssize_t r = net_recv_all(demuxer->socket, data, 8); - if (r < 8) { - return false; - } - - *width = sc_read32be(data); - *height = sc_read32be(data + 4); - return true; -} - -static bool -sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) { - // The video and audio streams contain a sequence of raw packets (as - // provided by MediaCodec), each prefixed with a "meta" header. - // - // The "meta" header length is 12 bytes: - // [. . . . . . . .|. . . .]. . . . . . . . . . . . . . . ... - // <-------------> <-----> <-----------------------------... - // PTS packet raw packet - // size - // - // It is followed by bytes containing the packet/frame. - // - // The most significant bits of the PTS are used for packet flags: - // - // byte 7 byte 6 byte 5 byte 4 byte 3 byte 2 byte 1 byte 0 - // CK...... ........ ........ ........ ........ ........ ........ ........ - // ^^<-------------------------------------------------------------------> - // || PTS - // | `- key frame - // `-- config packet - - uint8_t header[SC_PACKET_HEADER_SIZE]; - ssize_t r = net_recv_all(demuxer->socket, header, SC_PACKET_HEADER_SIZE); - if (r < SC_PACKET_HEADER_SIZE) { - return false; - } - - uint64_t pts_flags = sc_read64be(header); - uint32_t len = sc_read32be(&header[8]); - assert(len); - - if (av_new_packet(packet, len)) { - LOG_OOM(); - return false; - } - - r = net_recv_all(demuxer->socket, packet->data, len); - if (r < 0 || ((uint32_t) r) < len) { - av_packet_unref(packet); - return false; - } - - if (pts_flags & SC_PACKET_FLAG_CONFIG) { - packet->pts = AV_NOPTS_VALUE; - } else { - packet->pts = pts_flags & SC_PACKET_PTS_MASK; - } - - if (pts_flags & SC_PACKET_FLAG_KEY_FRAME) { - packet->flags |= AV_PKT_FLAG_KEY; - } - - packet->dts = packet->pts; - return true; -} - -static int -run_demuxer(void *data) { - struct sc_demuxer *demuxer = data; - - // Flag to report end-of-stream (i.e. device disconnected) - enum sc_demuxer_status status = SC_DEMUXER_STATUS_ERROR; - - uint32_t raw_codec_id; - bool ok = sc_demuxer_recv_codec_id(demuxer, &raw_codec_id); - if (!ok) { - LOGE("Demuxer '%s': stream disabled due to connection error", - demuxer->name); - goto end; - } - - if (raw_codec_id == 0) { - LOGW("Demuxer '%s': stream explicitly disabled by the device", - demuxer->name); - sc_packet_source_sinks_disable(&demuxer->packet_source); - status = SC_DEMUXER_STATUS_DISABLED; - goto end; - } - - if (raw_codec_id == 1) { - LOGE("Demuxer '%s': stream configuration error on the device", - demuxer->name); - goto end; - } - - enum AVCodecID codec_id = sc_demuxer_to_avcodec_id(raw_codec_id); - if (codec_id == AV_CODEC_ID_NONE) { - LOGE("Demuxer '%s': stream disabled due to unsupported codec", - demuxer->name); - sc_packet_source_sinks_disable(&demuxer->packet_source); - goto end; - } - - const AVCodec *codec = avcodec_find_decoder(codec_id); - if (!codec) { - LOGE("Demuxer '%s': stream disabled due to missing decoder", - demuxer->name); - sc_packet_source_sinks_disable(&demuxer->packet_source); - goto end; - } - - AVCodecContext *codec_ctx = avcodec_alloc_context3(codec); - if (!codec_ctx) { - LOG_OOM(); - goto end; - } - - codec_ctx->flags |= AV_CODEC_FLAG_LOW_DELAY; - - if (codec->type == AVMEDIA_TYPE_VIDEO) { - uint32_t width; - uint32_t height; - ok = sc_demuxer_recv_video_size(demuxer, &width, &height); - if (!ok) { - goto finally_free_context; - } - - codec_ctx->width = width; - codec_ctx->height = height; - codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P; - } else { - // Hardcoded audio properties -#ifdef SCRCPY_LAVU_HAS_CHLAYOUT - codec_ctx->ch_layout = (AVChannelLayout) AV_CHANNEL_LAYOUT_STEREO; -#else - codec_ctx->channel_layout = AV_CH_LAYOUT_STEREO; - codec_ctx->channels = 2; -#endif - codec_ctx->sample_rate = 48000; - - if (raw_codec_id == SC_CODEC_ID_FLAC) { - // The sample_fmt is not set by the FLAC decoder - codec_ctx->sample_fmt = AV_SAMPLE_FMT_S16; - } - } - - if (avcodec_open2(codec_ctx, codec, NULL) < 0) { - LOGE("Demuxer '%s': could not open codec", demuxer->name); - goto finally_free_context; - } - - if (!sc_packet_source_sinks_open(&demuxer->packet_source, codec_ctx)) { - goto finally_free_context; - } - - // Config packets must be merged with the next non-config packet only for - // H.26x - bool must_merge_config_packet = raw_codec_id == SC_CODEC_ID_H264 - || raw_codec_id == SC_CODEC_ID_H265; - - struct sc_packet_merger merger; - - if (must_merge_config_packet) { - sc_packet_merger_init(&merger); - } - - AVPacket *packet = av_packet_alloc(); - if (!packet) { - LOG_OOM(); - goto finally_close_sinks; - } - - for (;;) { - bool ok = sc_demuxer_recv_packet(demuxer, packet); - if (!ok) { - // end of stream - status = SC_DEMUXER_STATUS_EOS; - break; - } - - if (must_merge_config_packet) { - // Prepend any config packet to the next media packet - ok = sc_packet_merger_merge(&merger, packet); - if (!ok) { - av_packet_unref(packet); - break; - } - } - - ok = sc_packet_source_sinks_push(&demuxer->packet_source, packet); - av_packet_unref(packet); - if (!ok) { - // The sink already logged its concrete error - break; - } - } - - LOGD("Demuxer '%s': end of frames", demuxer->name); - - if (must_merge_config_packet) { - sc_packet_merger_destroy(&merger); - } - - av_packet_free(&packet); -finally_close_sinks: - sc_packet_source_sinks_close(&demuxer->packet_source); -finally_free_context: - avcodec_free_context(&codec_ctx); -end: - demuxer->cbs->on_ended(demuxer, status, demuxer->cbs_userdata); - - return 0; -} - -void -sc_demuxer_init(struct sc_demuxer *demuxer, const char *name, sc_socket socket, - const struct sc_demuxer_callbacks *cbs, void *cbs_userdata) { - assert(socket != SC_SOCKET_NONE); - - demuxer->name = name; // statically allocated - demuxer->socket = socket; - sc_packet_source_init(&demuxer->packet_source); - - assert(cbs && cbs->on_ended); - - demuxer->cbs = cbs; - demuxer->cbs_userdata = cbs_userdata; -} - -bool -sc_demuxer_start(struct sc_demuxer *demuxer) { - LOGD("Demuxer '%s': starting thread", demuxer->name); - - bool ok = sc_thread_create(&demuxer->thread, run_demuxer, "scrcpy-demuxer", - demuxer); - if (!ok) { - LOGE("Demuxer '%s': could not start thread", demuxer->name); - return false; - } - return true; -} - -void -sc_demuxer_join(struct sc_demuxer *demuxer) { - sc_thread_join(&demuxer->thread, NULL); -} diff --git a/app/src/demuxer.h b/app/src/demuxer.h deleted file mode 100644 index 2b7cb703..00000000 --- a/app/src/demuxer.h +++ /dev/null @@ -1,46 +0,0 @@ -#ifndef SC_DEMUXER_H -#define SC_DEMUXER_H - -#include "common.h" - -#include - -#include "trait/packet_source.h" -#include "util/net.h" -#include "util/thread.h" - -struct sc_demuxer { - struct sc_packet_source packet_source; // packet source trait - - const char *name; // must be statically allocated (e.g. a string literal) - - sc_socket socket; - sc_thread thread; - - const struct sc_demuxer_callbacks *cbs; - void *cbs_userdata; -}; - -enum sc_demuxer_status { - SC_DEMUXER_STATUS_EOS, - SC_DEMUXER_STATUS_DISABLED, - SC_DEMUXER_STATUS_ERROR, -}; - -struct sc_demuxer_callbacks { - void (*on_ended)(struct sc_demuxer *demuxer, enum sc_demuxer_status, - void *userdata); -}; - -// The name must be statically allocated (e.g. a string literal) -void -sc_demuxer_init(struct sc_demuxer *demuxer, const char *name, sc_socket socket, - const struct sc_demuxer_callbacks *cbs, void *cbs_userdata); - -bool -sc_demuxer_start(struct sc_demuxer *demuxer); - -void -sc_demuxer_join(struct sc_demuxer *demuxer); - -#endif diff --git a/app/src/device.c b/app/src/device.c new file mode 100644 index 00000000..8027ccbb --- /dev/null +++ b/app/src/device.c @@ -0,0 +1,22 @@ +#include "device.h" +#include "log.h" + +bool +device_read_info(socket_t device_socket, char *device_name, struct size *size) { + unsigned char buf[DEVICE_NAME_FIELD_LENGTH + 4]; + int r = net_recv_all(device_socket, buf, sizeof(buf)); + if (r < DEVICE_NAME_FIELD_LENGTH + 4) { + LOGE("Could not retrieve device information"); + return false; + } + // in case the client sends garbage + buf[DEVICE_NAME_FIELD_LENGTH - 1] = '\0'; + // strcpy is safe here, since name contains at least + // DEVICE_NAME_FIELD_LENGTH bytes and strlen(buf) < DEVICE_NAME_FIELD_LENGTH + strcpy(device_name, (char *) buf); + size->width = (buf[DEVICE_NAME_FIELD_LENGTH] << 8) + | buf[DEVICE_NAME_FIELD_LENGTH + 1]; + size->height = (buf[DEVICE_NAME_FIELD_LENGTH + 2] << 8) + | buf[DEVICE_NAME_FIELD_LENGTH + 3]; + return true; +} diff --git a/app/src/device.h b/app/src/device.h new file mode 100644 index 00000000..f3449e5e --- /dev/null +++ b/app/src/device.h @@ -0,0 +1,16 @@ +#ifndef DEVICE_H +#define DEVICE_H + +#include + +#include "common.h" +#include "net.h" + +#define DEVICE_NAME_FIELD_LENGTH 64 +#define DEVICE_SDCARD_PATH "/sdcard/" + +// name must be at least DEVICE_NAME_FIELD_LENGTH bytes +bool +device_read_info(socket_t device_socket, char *device_name, struct size *size); + +#endif diff --git a/app/src/device_msg.c b/app/src/device_msg.c index 7621c040..a90d78dd 100644 --- a/app/src/device_msg.c +++ b/app/src/device_msg.c @@ -1,75 +1,38 @@ #include "device_msg.h" -#include -#include #include +#include -#include "util/binary.h" -#include "util/log.h" +#include "buffer_util.h" +#include "log.h" ssize_t -sc_device_msg_deserialize(const uint8_t *buf, size_t len, - struct sc_device_msg *msg) { - if (!len) { - return 0; // no message +device_msg_deserialize(const unsigned char *buf, size_t len, + struct device_msg *msg) { + if (len < 3) { + // at least type + empty string length + return 0; // not available } msg->type = buf[0]; switch (msg->type) { case DEVICE_MSG_TYPE_CLIPBOARD: { - if (len < 5) { - // at least type + empty string length - return 0; // no complete message + uint16_t clipboard_len = buffer_read16be(&buf[1]); + if (clipboard_len > len - 3) { + return 0; // not available } - size_t clipboard_len = sc_read32be(&buf[1]); - if (clipboard_len > len - 5) { - return 0; // no complete message - } - char *text = malloc(clipboard_len + 1); + char *text = SDL_malloc(clipboard_len + 1); if (!text) { - LOG_OOM(); + LOGW("Could not allocate text for clipboard"); return -1; } if (clipboard_len) { - memcpy(text, &buf[5], clipboard_len); + memcpy(text, &buf[3], clipboard_len); } text[clipboard_len] = '\0'; msg->clipboard.text = text; - return 5 + clipboard_len; - } - case DEVICE_MSG_TYPE_ACK_CLIPBOARD: { - if (len < 9) { - return 0; // no complete message - } - uint64_t sequence = sc_read64be(&buf[1]); - msg->ack_clipboard.sequence = sequence; - return 9; - } - case DEVICE_MSG_TYPE_UHID_OUTPUT: { - if (len < 5) { - // at least id + size - return 0; // not available - } - uint16_t id = sc_read16be(&buf[1]); - size_t size = sc_read16be(&buf[3]); - if (size < len - 5) { - return 0; // not available - } - uint8_t *data = malloc(size); - if (!data) { - LOG_OOM(); - return -1; - } - if (size) { - memcpy(data, &buf[5], size); - } - - msg->uhid_output.id = id; - msg->uhid_output.size = size; - msg->uhid_output.data = data; - - return 5 + size; + return 3 + clipboard_len; } default: LOGW("Unknown device message type: %d", (int) msg->type); @@ -78,16 +41,8 @@ sc_device_msg_deserialize(const uint8_t *buf, size_t len, } void -sc_device_msg_destroy(struct sc_device_msg *msg) { - switch (msg->type) { - case DEVICE_MSG_TYPE_CLIPBOARD: - free(msg->clipboard.text); - break; - case DEVICE_MSG_TYPE_UHID_OUTPUT: - free(msg->uhid_output.data); - break; - default: - // nothing to do - break; +device_msg_destroy(struct device_msg *msg) { + if (msg->type == DEVICE_MSG_TYPE_CLIPBOARD) { + SDL_free(msg->clipboard.text); } } diff --git a/app/src/device_msg.h b/app/src/device_msg.h index d6c701bb..fd4a7eb1 100644 --- a/app/src/device_msg.h +++ b/app/src/device_msg.h @@ -1,45 +1,32 @@ -#ifndef SC_DEVICEMSG_H -#define SC_DEVICEMSG_H +#ifndef DEVICEMSG_H +#define DEVICEMSG_H -#include "common.h" - -#include +#include #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) +#define DEVICE_MSG_TEXT_MAX_LENGTH 4093 +#define DEVICE_MSG_SERIALIZED_MAX_SIZE (3 + DEVICE_MSG_TEXT_MAX_LENGTH) -enum sc_device_msg_type { +enum device_msg_type { DEVICE_MSG_TYPE_CLIPBOARD, - DEVICE_MSG_TYPE_ACK_CLIPBOARD, - DEVICE_MSG_TYPE_UHID_OUTPUT, }; -struct sc_device_msg { - enum sc_device_msg_type type; +struct device_msg { + enum device_msg_type type; union { struct { - char *text; // owned, to be freed by free() + char *text; // owned, to be freed by SDL_free() } clipboard; - struct { - uint64_t sequence; - } ack_clipboard; - struct { - uint16_t id; - uint16_t size; - uint8_t *data; // owned, to be freed by free() - } uhid_output; }; }; // return the number of bytes consumed (0 for no msg available, -1 on error) ssize_t -sc_device_msg_deserialize(const uint8_t *buf, size_t len, - struct sc_device_msg *msg); +device_msg_deserialize(const unsigned char *buf, size_t len, + struct device_msg *msg); void -sc_device_msg_destroy(struct sc_device_msg *msg); +device_msg_destroy(struct device_msg *msg); #endif diff --git a/app/src/display.c b/app/src/display.c deleted file mode 100644 index aee8ef80..00000000 --- a/app/src/display.c +++ /dev/null @@ -1,344 +0,0 @@ -#include "display.h" - -#include -#include -#include -#include - -#include "util/log.h" - -static bool -sc_display_init_novideo_icon(struct sc_display *display, - SDL_Surface *icon_novideo) { - assert(icon_novideo); - - if (SDL_RenderSetLogicalSize(display->renderer, - icon_novideo->w, icon_novideo->h)) { - LOGW("Could not set renderer logical size: %s", SDL_GetError()); - // don't fail - } - - display->texture = SDL_CreateTextureFromSurface(display->renderer, - icon_novideo); - if (!display->texture) { - LOGE("Could not create texture: %s", SDL_GetError()); - return false; - } - - return true; -} - -bool -sc_display_init(struct sc_display *display, SDL_Window *window, - SDL_Surface *icon_novideo, bool mipmaps) { - display->renderer = - SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED); - if (!display->renderer) { - LOGE("Could not create renderer: %s", SDL_GetError()); - return false; - } - - SDL_RendererInfo renderer_info; - int r = SDL_GetRendererInfo(display->renderer, &renderer_info); - const char *renderer_name = r ? NULL : renderer_info.name; - LOGI("Renderer: %s", renderer_name ? renderer_name : "(unknown)"); - - display->mipmaps = false; - -#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE - display->gl_context = NULL; -#endif - - // starts with "opengl" - bool use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6); - if (use_opengl) { - -#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE - // Persuade macOS to give us something better than OpenGL 2.1. - // If we create a Core Profile context, we get the best OpenGL version. - SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, - SDL_GL_CONTEXT_PROFILE_CORE); - - LOGD("Creating OpenGL Core Profile context"); - display->gl_context = SDL_GL_CreateContext(window); - if (!display->gl_context) { - LOGE("Could not create OpenGL context: %s", SDL_GetError()); - SDL_DestroyRenderer(display->renderer); - return false; - } -#endif - - struct sc_opengl *gl = &display->gl; - sc_opengl_init(gl); - - LOGI("OpenGL version: %s", gl->version); - - if (mipmaps) { - bool supports_mipmaps = - sc_opengl_version_at_least(gl, 3, 0, /* OpenGL 3.0+ */ - 2, 0 /* OpenGL ES 2.0+ */); - if (supports_mipmaps) { - LOGI("Trilinear filtering enabled"); - display->mipmaps = true; - } else { - LOGW("Trilinear filtering disabled " - "(OpenGL 3.0+ or ES 2.0+ required)"); - } - } else { - LOGI("Trilinear filtering disabled"); - } - } else if (mipmaps) { - LOGD("Trilinear filtering disabled (not an OpenGL renderer)"); - } - - display->texture = NULL; - display->pending.flags = 0; - display->pending.frame = NULL; - display->has_frame = false; - - if (icon_novideo) { - // Without video, set a static scrcpy icon as window content - bool ok = sc_display_init_novideo_icon(display, icon_novideo); - if (!ok) { -#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE - SDL_GL_DeleteContext(display->gl_context); -#endif - SDL_DestroyRenderer(display->renderer); - return false; - } - } - - return true; -} - -void -sc_display_destroy(struct sc_display *display) { - if (display->pending.frame) { - av_frame_free(&display->pending.frame); - } -#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE - SDL_GL_DeleteContext(display->gl_context); -#endif - if (display->texture) { - SDL_DestroyTexture(display->texture); - } - SDL_DestroyRenderer(display->renderer); -} - -static SDL_Texture * -sc_display_create_texture(struct sc_display *display, - struct sc_size size) { - SDL_Renderer *renderer = display->renderer; - SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12, - SDL_TEXTUREACCESS_STREAMING, - size.width, size.height); - if (!texture) { - LOGD("Could not create texture: %s", SDL_GetError()); - return NULL; - } - - if (display->mipmaps) { - struct sc_opengl *gl = &display->gl; - - SDL_GL_BindTexture(texture, NULL, NULL); - - // Enable trilinear filtering for downscaling - gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, - GL_LINEAR_MIPMAP_LINEAR); - gl->TexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -1.f); - - SDL_GL_UnbindTexture(texture); - } - - return texture; -} - -static inline void -sc_display_set_pending_size(struct sc_display *display, struct sc_size size) { - assert(!display->texture); - display->pending.size = size; - display->pending.flags |= SC_DISPLAY_PENDING_FLAG_SIZE; -} - -static bool -sc_display_set_pending_frame(struct sc_display *display, const AVFrame *frame) { - if (!display->pending.frame) { - display->pending.frame = av_frame_alloc(); - if (!display->pending.frame) { - LOG_OOM(); - return false; - } - } - - int r = av_frame_ref(display->pending.frame, frame); - if (r) { - LOGE("Could not ref frame: %d", r); - return false; - } - - display->pending.flags |= SC_DISPLAY_PENDING_FLAG_FRAME; - - return true; -} - -static bool -sc_display_apply_pending(struct sc_display *display) { - if (display->pending.flags & SC_DISPLAY_PENDING_FLAG_SIZE) { - assert(!display->texture); - display->texture = - sc_display_create_texture(display, display->pending.size); - if (!display->texture) { - return false; - } - - display->pending.flags &= ~SC_DISPLAY_PENDING_FLAG_SIZE; - } - - if (display->pending.flags & SC_DISPLAY_PENDING_FLAG_FRAME) { - assert(display->pending.frame); - bool ok = sc_display_update_texture(display, display->pending.frame); - if (!ok) { - return false; - } - - av_frame_unref(display->pending.frame); - display->pending.flags &= ~SC_DISPLAY_PENDING_FLAG_FRAME; - } - - return true; -} - -static bool -sc_display_set_texture_size_internal(struct sc_display *display, - struct sc_size size) { - assert(size.width && size.height); - - if (display->texture) { - SDL_DestroyTexture(display->texture); - } - - display->texture = sc_display_create_texture(display, size); - if (!display->texture) { - return false; - } - - LOGI("Texture: %" PRIu16 "x%" PRIu16, size.width, size.height); - return true; -} - -enum sc_display_result -sc_display_set_texture_size(struct sc_display *display, struct sc_size size) { - bool ok = sc_display_set_texture_size_internal(display, size); - if (!ok) { - sc_display_set_pending_size(display, size); - return SC_DISPLAY_RESULT_PENDING; - - } - - return SC_DISPLAY_RESULT_OK; -} - -static SDL_YUV_CONVERSION_MODE -sc_display_to_sdl_color_range(enum AVColorRange color_range) { - return color_range == AVCOL_RANGE_JPEG ? SDL_YUV_CONVERSION_JPEG - : SDL_YUV_CONVERSION_AUTOMATIC; -} - -static bool -sc_display_update_texture_internal(struct sc_display *display, - const AVFrame *frame) { - if (!display->has_frame) { - // First frame - display->has_frame = true; - - // Configure YUV color range conversion - SDL_YUV_CONVERSION_MODE sdl_color_range = - sc_display_to_sdl_color_range(frame->color_range); - SDL_SetYUVConversionMode(sdl_color_range); - } - - int ret = SDL_UpdateYUVTexture(display->texture, NULL, - frame->data[0], frame->linesize[0], - frame->data[1], frame->linesize[1], - frame->data[2], frame->linesize[2]); - if (ret) { - LOGD("Could not update texture: %s", SDL_GetError()); - return false; - } - - if (display->mipmaps) { - SDL_GL_BindTexture(display->texture, NULL, NULL); - display->gl.GenerateMipmap(GL_TEXTURE_2D); - SDL_GL_UnbindTexture(display->texture); - } - - return true; -} - -enum sc_display_result -sc_display_update_texture(struct sc_display *display, const AVFrame *frame) { - bool ok = sc_display_update_texture_internal(display, frame); - if (!ok) { - ok = sc_display_set_pending_frame(display, frame); - if (!ok) { - LOGE("Could not set pending frame"); - return SC_DISPLAY_RESULT_ERROR; - } - - return SC_DISPLAY_RESULT_PENDING; - } - - return SC_DISPLAY_RESULT_OK; -} - -enum sc_display_result -sc_display_render(struct sc_display *display, const SDL_Rect *geometry, - enum sc_orientation orientation) { - SDL_RenderClear(display->renderer); - - if (display->pending.flags) { - bool ok = sc_display_apply_pending(display); - if (!ok) { - return SC_DISPLAY_RESULT_PENDING; - } - } - - SDL_Renderer *renderer = display->renderer; - SDL_Texture *texture = display->texture; - - if (orientation == SC_ORIENTATION_0) { - int ret = SDL_RenderCopy(renderer, texture, NULL, geometry); - if (ret) { - LOGE("Could not render texture: %s", SDL_GetError()); - return SC_DISPLAY_RESULT_ERROR; - } - } else { - unsigned cw_rotation = sc_orientation_get_rotation(orientation); - double angle = 90 * cw_rotation; - - const SDL_Rect *dstrect = NULL; - SDL_Rect rect; - if (sc_orientation_is_swap(orientation)) { - rect.x = geometry->x + (geometry->w - geometry->h) / 2; - rect.y = geometry->y + (geometry->h - geometry->w) / 2; - rect.w = geometry->h; - rect.h = geometry->w; - dstrect = ▭ - } else { - dstrect = geometry; - } - - SDL_RendererFlip flip = sc_orientation_is_mirror(orientation) - ? SDL_FLIP_HORIZONTAL : 0; - - int ret = SDL_RenderCopyEx(renderer, texture, NULL, dstrect, angle, - NULL, flip); - if (ret) { - LOGE("Could not render texture: %s", SDL_GetError()); - return SC_DISPLAY_RESULT_ERROR; - } - } - - SDL_RenderPresent(display->renderer); - return SC_DISPLAY_RESULT_OK; -} diff --git a/app/src/display.h b/app/src/display.h deleted file mode 100644 index 4de9b0a9..00000000 --- a/app/src/display.h +++ /dev/null @@ -1,64 +0,0 @@ -#ifndef SC_DISPLAY_H -#define SC_DISPLAY_H - -#include "common.h" - -#include -#include -#include -#include - -#include "coords.h" -#include "opengl.h" -#include "options.h" - -#ifdef __APPLE__ -# define SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE -#endif - -struct sc_display { - SDL_Renderer *renderer; - SDL_Texture *texture; - - struct sc_opengl gl; -#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE - SDL_GLContext *gl_context; -#endif - - bool mipmaps; - - struct { -#define SC_DISPLAY_PENDING_FLAG_SIZE 1 -#define SC_DISPLAY_PENDING_FLAG_FRAME 2 - int8_t flags; - struct sc_size size; - AVFrame *frame; - } pending; - - bool has_frame; -}; - -enum sc_display_result { - SC_DISPLAY_RESULT_OK, - SC_DISPLAY_RESULT_PENDING, - SC_DISPLAY_RESULT_ERROR, -}; - -bool -sc_display_init(struct sc_display *display, SDL_Window *window, - SDL_Surface *icon_novideo, bool mipmaps); - -void -sc_display_destroy(struct sc_display *display); - -enum sc_display_result -sc_display_set_texture_size(struct sc_display *display, struct sc_size size); - -enum sc_display_result -sc_display_update_texture(struct sc_display *display, const AVFrame *frame); - -enum sc_display_result -sc_display_render(struct sc_display *display, const SDL_Rect *geometry, - enum sc_orientation orientation); - -#endif diff --git a/app/src/events.c b/app/src/events.c deleted file mode 100644 index b4322d1b..00000000 --- a/app/src/events.c +++ /dev/null @@ -1,68 +0,0 @@ -#include "events.h" - -#include - -#include "util/log.h" -#include "util/thread.h" - -bool -sc_push_event_impl(uint32_t type, const char *name) { - SDL_Event event; - event.type = type; - int ret = SDL_PushEvent(&event); - // ret < 0: error (queue full) - // ret == 0: event was filtered - // ret == 1: success - if (ret != 1) { - LOGE("Could not post %s event: %s", name, SDL_GetError()); - return false; - } - - return true; -} - -bool -sc_post_to_main_thread(sc_runnable_fn run, void *userdata) { - SDL_Event event = { - .user = { - .type = SC_EVENT_RUN_ON_MAIN_THREAD, - .data1 = run, - .data2 = userdata, - }, - }; - int ret = SDL_PushEvent(&event); - // ret < 0: error (queue full) - // ret == 0: event was filtered - // ret == 1: success - if (ret != 1) { - if (ret == 0) { - // if ret == 0, this is expected on exit, log in debug mode - LOGD("Could not post runnable to main thread (filtered)"); - } else { - assert(ret < 0); - LOGW("Could not post runnable to main thread: %s", SDL_GetError()); - } - return false; - } - - return true; -} - -static int SDLCALL -task_event_filter(void *userdata, SDL_Event *event) { - (void) userdata; - - if (event->type == SC_EVENT_RUN_ON_MAIN_THREAD) { - // Reject this event type from now on - return 0; - } - - return 1; -} - -void -sc_reject_new_runnables(void) { - assert(sc_thread_get_id() == SC_MAIN_THREAD_ID); - - SDL_SetEventFilter(task_event_filter, NULL); -} diff --git a/app/src/events.h b/app/src/events.h index 2fe4d3a7..e9512048 100644 --- a/app/src/events.h +++ b/app/src/events.h @@ -1,38 +1,3 @@ -#ifndef SC_EVENTS_H -#define SC_EVENTS_H - -#include "common.h" - -#include -#include -#include - -enum { - SC_EVENT_NEW_FRAME = SDL_USEREVENT, - SC_EVENT_RUN_ON_MAIN_THREAD, - SC_EVENT_DEVICE_DISCONNECTED, - SC_EVENT_SERVER_CONNECTION_FAILED, - SC_EVENT_SERVER_CONNECTED, - SC_EVENT_USB_DEVICE_DISCONNECTED, - SC_EVENT_DEMUXER_ERROR, - SC_EVENT_RECORDER_ERROR, - SC_EVENT_SCREEN_INIT_SIZE, - SC_EVENT_TIME_LIMIT_REACHED, - SC_EVENT_CONTROLLER_ERROR, - SC_EVENT_AOA_OPEN_ERROR, -}; - -bool -sc_push_event_impl(uint32_t type, const char *name); - -#define sc_push_event(TYPE) sc_push_event_impl(TYPE, # TYPE) - -typedef void (*sc_runnable_fn)(void *userdata); - -bool -sc_post_to_main_thread(sc_runnable_fn run, void *userdata); - -void -sc_reject_new_runnables(void); - -#endif +#define EVENT_NEW_SESSION SDL_USEREVENT +#define EVENT_NEW_FRAME (SDL_USEREVENT + 1) +#define EVENT_STREAM_STOPPED (SDL_USEREVENT + 2) diff --git a/app/src/file_handler.c b/app/src/file_handler.c new file mode 100644 index 00000000..051db897 --- /dev/null +++ b/app/src/file_handler.c @@ -0,0 +1,183 @@ +#include "file_handler.h" + +#include +#include + +#include "config.h" +#include "command.h" +#include "device.h" +#include "lock_util.h" +#include "log.h" + +static void +file_handler_request_destroy(struct file_handler_request *req) { + SDL_free(req->file); +} + +bool +file_handler_init(struct file_handler *file_handler, const char *serial) { + + cbuf_init(&file_handler->queue); + + if (!(file_handler->mutex = SDL_CreateMutex())) { + return false; + } + + if (!(file_handler->event_cond = SDL_CreateCond())) { + SDL_DestroyMutex(file_handler->mutex); + return false; + } + + if (serial) { + file_handler->serial = SDL_strdup(serial); + if (!file_handler->serial) { + LOGW("Cannot strdup serial"); + SDL_DestroyCond(file_handler->event_cond); + SDL_DestroyMutex(file_handler->mutex); + return false; + } + } else { + file_handler->serial = NULL; + } + + // lazy initialization + file_handler->initialized = false; + + file_handler->stopped = false; + file_handler->current_process = PROCESS_NONE; + + return true; +} + +void +file_handler_destroy(struct file_handler *file_handler) { + SDL_DestroyCond(file_handler->event_cond); + SDL_DestroyMutex(file_handler->mutex); + SDL_free(file_handler->serial); + + struct file_handler_request req; + while (cbuf_take(&file_handler->queue, &req)) { + file_handler_request_destroy(&req); + } +} + +static process_t +install_apk(const char *serial, const char *file) { + return adb_install(serial, file); +} + +static process_t +push_file(const char *serial, const char *file) { + return adb_push(serial, file, DEVICE_SDCARD_PATH); +} + +bool +file_handler_request(struct file_handler *file_handler, + file_handler_action_t action, char *file) { + // start file_handler if it's used for the first time + if (!file_handler->initialized) { + if (!file_handler_start(file_handler)) { + return false; + } + file_handler->initialized = true; + } + + LOGI("Request to %s %s", action == ACTION_INSTALL_APK ? "install" : "push", + file); + struct file_handler_request req = { + .action = action, + .file = file, + }; + + mutex_lock(file_handler->mutex); + bool was_empty = cbuf_is_empty(&file_handler->queue); + bool res = cbuf_push(&file_handler->queue, req); + if (was_empty) { + cond_signal(file_handler->event_cond); + } + mutex_unlock(file_handler->mutex); + return res; +} + +static int +run_file_handler(void *data) { + struct file_handler *file_handler = data; + + for (;;) { + mutex_lock(file_handler->mutex); + file_handler->current_process = PROCESS_NONE; + while (!file_handler->stopped && cbuf_is_empty(&file_handler->queue)) { + cond_wait(file_handler->event_cond, file_handler->mutex); + } + if (file_handler->stopped) { + // stop immediately, do not process further events + mutex_unlock(file_handler->mutex); + break; + } + struct file_handler_request req; + bool non_empty = cbuf_take(&file_handler->queue, &req); + SDL_assert(non_empty); + + process_t process; + if (req.action == ACTION_INSTALL_APK) { + LOGI("Installing %s...", req.file); + process = install_apk(file_handler->serial, req.file); + } else { + LOGI("Pushing %s...", req.file); + process = push_file(file_handler->serial, req.file); + } + file_handler->current_process = process; + mutex_unlock(file_handler->mutex); + + if (req.action == ACTION_INSTALL_APK) { + if (process_check_success(process, "adb install")) { + LOGI("%s successfully installed", req.file); + } else { + LOGE("Failed to install %s", req.file); + } + } else { + if (process_check_success(process, "adb push")) { + LOGI("%s successfully pushed to /sdcard/", req.file); + } else { + LOGE("Failed to push %s to /sdcard/", req.file); + } + } + + file_handler_request_destroy(&req); + } + return 0; +} + +bool +file_handler_start(struct file_handler *file_handler) { + LOGD("Starting file_handler thread"); + + file_handler->thread = SDL_CreateThread(run_file_handler, "file_handler", + file_handler); + if (!file_handler->thread) { + LOGC("Could not start file_handler thread"); + return false; + } + + return true; +} + +void +file_handler_stop(struct file_handler *file_handler) { + mutex_lock(file_handler->mutex); + file_handler->stopped = true; + cond_signal(file_handler->event_cond); + if (file_handler->current_process != PROCESS_NONE) { + if (!cmd_terminate(file_handler->current_process)) { + LOGW("Cannot terminate install process"); + } + cmd_simple_wait(file_handler->current_process, NULL); + file_handler->current_process = PROCESS_NONE; + } + mutex_unlock(file_handler->mutex); +} + +void +file_handler_join(struct file_handler *file_handler) { + SDL_WaitThread(file_handler->thread, NULL); +} diff --git a/app/src/file_handler.h b/app/src/file_handler.h new file mode 100644 index 00000000..22245105 --- /dev/null +++ b/app/src/file_handler.h @@ -0,0 +1,55 @@ +#ifndef FILE_HANDLER_H +#define FILE_HANDLER_H + +#include +#include +#include + +#include "cbuf.h" +#include "command.h" + +typedef enum { + ACTION_INSTALL_APK, + ACTION_PUSH_FILE, +} file_handler_action_t; + +struct file_handler_request { + file_handler_action_t action; + char *file; +}; + +struct file_handler_request_queue CBUF(struct file_handler_request, 16); + +struct file_handler { + char *serial; + SDL_Thread *thread; + SDL_mutex *mutex; + SDL_cond *event_cond; + bool stopped; + bool initialized; + process_t current_process; + struct file_handler_request_queue queue; +}; + +bool +file_handler_init(struct file_handler *file_handler, const char *serial); + +void +file_handler_destroy(struct file_handler *file_handler); + +bool +file_handler_start(struct file_handler *file_handler); + +void +file_handler_stop(struct file_handler *file_handler); + +void +file_handler_join(struct file_handler *file_handler); + +// take ownership of file, and will SDL_free() it +bool +file_handler_request(struct file_handler *file_handler, + file_handler_action_t action, + char *file); + +#endif diff --git a/app/src/file_pusher.c b/app/src/file_pusher.c deleted file mode 100644 index 681fb5d6..00000000 --- a/app/src/file_pusher.c +++ /dev/null @@ -1,189 +0,0 @@ -#include "file_pusher.h" - -#include -#include -#include - -#include "adb/adb.h" -#include "util/log.h" - -#define DEFAULT_PUSH_TARGET "/sdcard/Download/" - -static void -sc_file_pusher_request_destroy(struct sc_file_pusher_request *req) { - free(req->file); -} - -bool -sc_file_pusher_init(struct sc_file_pusher *fp, const char *serial, - const char *push_target) { - assert(serial); - - sc_vecdeque_init(&fp->queue); - - bool ok = sc_mutex_init(&fp->mutex); - if (!ok) { - return false; - } - - ok = sc_cond_init(&fp->event_cond); - if (!ok) { - sc_mutex_destroy(&fp->mutex); - return false; - } - - ok = sc_intr_init(&fp->intr); - if (!ok) { - sc_cond_destroy(&fp->event_cond); - sc_mutex_destroy(&fp->mutex); - return false; - } - - fp->serial = strdup(serial); - if (!fp->serial) { - LOG_OOM(); - sc_intr_destroy(&fp->intr); - sc_cond_destroy(&fp->event_cond); - sc_mutex_destroy(&fp->mutex); - return false; - } - - // lazy initialization - fp->initialized = false; - - fp->stopped = false; - - fp->push_target = push_target ? push_target : DEFAULT_PUSH_TARGET; - - return true; -} - -void -sc_file_pusher_destroy(struct sc_file_pusher *fp) { - sc_cond_destroy(&fp->event_cond); - sc_mutex_destroy(&fp->mutex); - sc_intr_destroy(&fp->intr); - free(fp->serial); - - while (!sc_vecdeque_is_empty(&fp->queue)) { - struct sc_file_pusher_request *req = sc_vecdeque_popref(&fp->queue); - assert(req); - sc_file_pusher_request_destroy(req); - } -} - -bool -sc_file_pusher_request(struct sc_file_pusher *fp, - enum sc_file_pusher_action action, char *file) { - // start file_pusher if it's used for the first time - if (!fp->initialized) { - if (!sc_file_pusher_start(fp)) { - return false; - } - fp->initialized = true; - } - - LOGI("Request to %s %s", action == SC_FILE_PUSHER_ACTION_INSTALL_APK - ? "install" : "push", - file); - struct sc_file_pusher_request req = { - .action = action, - .file = file, - }; - - sc_mutex_lock(&fp->mutex); - bool was_empty = sc_vecdeque_is_empty(&fp->queue); - bool res = sc_vecdeque_push(&fp->queue, req); - if (!res) { - LOG_OOM(); - sc_mutex_unlock(&fp->mutex); - return false; - } - - if (was_empty) { - sc_cond_signal(&fp->event_cond); - } - sc_mutex_unlock(&fp->mutex); - - return true; -} - -static int -run_file_pusher(void *data) { - struct sc_file_pusher *fp = data; - struct sc_intr *intr = &fp->intr; - - const char *serial = fp->serial; - assert(serial); - - const char *push_target = fp->push_target; - assert(push_target); - - for (;;) { - sc_mutex_lock(&fp->mutex); - while (!fp->stopped && sc_vecdeque_is_empty(&fp->queue)) { - sc_cond_wait(&fp->event_cond, &fp->mutex); - } - if (fp->stopped) { - // stop immediately, do not process further events - sc_mutex_unlock(&fp->mutex); - break; - } - - assert(!sc_vecdeque_is_empty(&fp->queue)); - struct sc_file_pusher_request req = sc_vecdeque_pop(&fp->queue); - sc_mutex_unlock(&fp->mutex); - - if (req.action == SC_FILE_PUSHER_ACTION_INSTALL_APK) { - LOGI("Installing %s...", req.file); - bool ok = sc_adb_install(intr, serial, req.file, 0); - if (ok) { - LOGI("%s successfully installed", req.file); - } else { - LOGE("Failed to install %s", req.file); - } - } else { - LOGI("Pushing %s...", req.file); - bool ok = sc_adb_push(intr, serial, req.file, push_target, 0); - if (ok) { - LOGI("%s successfully pushed to %s", req.file, push_target); - } else { - LOGE("Failed to push %s to %s", req.file, push_target); - } - } - - sc_file_pusher_request_destroy(&req); - } - return 0; -} - -bool -sc_file_pusher_start(struct sc_file_pusher *fp) { - LOGD("Starting file_pusher thread"); - - bool ok = sc_thread_create(&fp->thread, run_file_pusher, "scrcpy-file", fp); - if (!ok) { - LOGE("Could not start file_pusher thread"); - return false; - } - - return true; -} - -void -sc_file_pusher_stop(struct sc_file_pusher *fp) { - if (fp->initialized) { - sc_mutex_lock(&fp->mutex); - fp->stopped = true; - sc_cond_signal(&fp->event_cond); - sc_intr_interrupt(&fp->intr); - sc_mutex_unlock(&fp->mutex); - } -} - -void -sc_file_pusher_join(struct sc_file_pusher *fp) { - if (fp->initialized) { - sc_thread_join(&fp->thread, NULL); - } -} diff --git a/app/src/file_pusher.h b/app/src/file_pusher.h deleted file mode 100644 index 0ffb3721..00000000 --- a/app/src/file_pusher.h +++ /dev/null @@ -1,58 +0,0 @@ -#ifndef SC_FILE_PUSHER_H -#define SC_FILE_PUSHER_H - -#include "common.h" - -#include - -#include "util/intr.h" -#include "util/thread.h" -#include "util/vecdeque.h" - -enum sc_file_pusher_action { - SC_FILE_PUSHER_ACTION_INSTALL_APK, - SC_FILE_PUSHER_ACTION_PUSH_FILE, -}; - -struct sc_file_pusher_request { - enum sc_file_pusher_action action; - char *file; -}; - -struct sc_file_pusher_request_queue SC_VECDEQUE(struct sc_file_pusher_request); - -struct sc_file_pusher { - char *serial; - const char *push_target; - sc_thread thread; - sc_mutex mutex; - sc_cond event_cond; - bool stopped; - bool initialized; - struct sc_file_pusher_request_queue queue; - - struct sc_intr intr; -}; - -bool -sc_file_pusher_init(struct sc_file_pusher *fp, const char *serial, - const char *push_target); - -void -sc_file_pusher_destroy(struct sc_file_pusher *fp); - -bool -sc_file_pusher_start(struct sc_file_pusher *fp); - -void -sc_file_pusher_stop(struct sc_file_pusher *fp); - -void -sc_file_pusher_join(struct sc_file_pusher *fp); - -// take ownership of file, and will free() it -bool -sc_file_pusher_request(struct sc_file_pusher *fp, - enum sc_file_pusher_action action, char *file); - -#endif diff --git a/app/src/fps_counter.c b/app/src/fps_counter.c index 1daa42ba..daece470 100644 --- a/app/src/fps_counter.c +++ b/app/src/fps_counter.c @@ -1,53 +1,44 @@ #include "fps_counter.h" -#include -#include +#include +#include -#include "util/log.h" +#include "lock_util.h" +#include "log.h" -#define SC_FPS_COUNTER_INTERVAL SC_TICK_FROM_SEC(1) +#define FPS_COUNTER_INTERVAL_MS 1000 bool -sc_fps_counter_init(struct sc_fps_counter *counter) { - bool ok = sc_mutex_init(&counter->mutex); - if (!ok) { +fps_counter_init(struct fps_counter *counter) { + counter->mutex = SDL_CreateMutex(); + if (!counter->mutex) { return false; } - ok = sc_cond_init(&counter->state_cond); - if (!ok) { - sc_mutex_destroy(&counter->mutex); + counter->state_cond = SDL_CreateCond(); + if (!counter->state_cond) { + SDL_DestroyMutex(counter->mutex); return false; } - counter->thread_started = false; - atomic_init(&counter->started, 0); + counter->thread = NULL; + SDL_AtomicSet(&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); +fps_counter_destroy(struct fps_counter *counter) { + SDL_DestroyCond(counter->state_cond); + SDL_DestroyMutex(counter->mutex); } // must be called with mutex locked static void -display_fps(struct sc_fps_counter *counter) { +display_fps(struct fps_counter *counter) { unsigned rendered_per_second = - counter->nr_rendered * SC_TICK_FREQ / SC_FPS_COUNTER_INTERVAL; + counter->nr_rendered * 1000 / FPS_COUNTER_INTERVAL_MS; if (counter->nr_skipped) { LOGI("%u fps (+%u frames skipped)", rendered_per_second, counter->nr_skipped); @@ -58,7 +49,7 @@ display_fps(struct sc_fps_counter *counter) { // must be called with mutex locked static void -check_interval_expired(struct sc_fps_counter *counter, sc_tick now) { +check_interval_expired(struct fps_counter *counter, uint32_t now) { if (now < counter->next_timestamp) { return; } @@ -68,119 +59,111 @@ check_interval_expired(struct sc_fps_counter *counter, sc_tick now) { 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; + (now - counter->next_timestamp) / FPS_COUNTER_INTERVAL_MS + 1; + counter->next_timestamp += FPS_COUNTER_INTERVAL_MS * elapsed_slices; } static int run_fps_counter(void *data) { - struct sc_fps_counter *counter = data; + struct fps_counter *counter = data; - sc_mutex_lock(&counter->mutex); + mutex_lock(counter->mutex); while (!counter->interrupted) { - while (!counter->interrupted && !is_started(counter)) { - sc_cond_wait(&counter->state_cond, &counter->mutex); + while (!counter->interrupted && !SDL_AtomicGet(&counter->started)) { + cond_wait(counter->state_cond, counter->mutex); } - while (!counter->interrupted && is_started(counter)) { - sc_tick now = sc_tick_now(); + while (!counter->interrupted && SDL_AtomicGet(&counter->started)) { + uint32_t now = SDL_GetTicks(); check_interval_expired(counter, now); + SDL_assert(counter->next_timestamp > now); + uint32_t remaining = counter->next_timestamp - now; + // ignore the reason (timeout or signaled), we just loop anyway - sc_cond_timedwait(&counter->state_cond, &counter->mutex, - counter->next_timestamp); + cond_wait_timeout(counter->state_cond, counter->mutex, remaining); } } - sc_mutex_unlock(&counter->mutex); + mutex_unlock(counter->mutex); return 0; } bool -sc_fps_counter_start(struct sc_fps_counter *counter) { - sc_mutex_lock(&counter->mutex); - counter->interrupted = false; - counter->next_timestamp = sc_tick_now() + SC_FPS_COUNTER_INTERVAL; +fps_counter_start(struct fps_counter *counter) { + mutex_lock(counter->mutex); + counter->next_timestamp = SDL_GetTicks() + FPS_COUNTER_INTERVAL_MS; counter->nr_rendered = 0; counter->nr_skipped = 0; - sc_mutex_unlock(&counter->mutex); + mutex_unlock(counter->mutex); - set_started(counter, true); - sc_cond_signal(&counter->state_cond); + SDL_AtomicSet(&counter->started, 1); + 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) { + // counter->thread is always accessed from the same thread, no need to lock + if (!counter->thread) { + counter->thread = + SDL_CreateThread(run_fps_counter, "fps counter", counter); + if (!counter->thread) { 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"); +fps_counter_stop(struct fps_counter *counter) { + SDL_AtomicSet(&counter->started, 0); + cond_signal(counter->state_cond); } bool -sc_fps_counter_is_started(struct sc_fps_counter *counter) { - return is_started(counter); +fps_counter_is_started(struct fps_counter *counter) { + return SDL_AtomicGet(&counter->started); } void -sc_fps_counter_interrupt(struct sc_fps_counter *counter) { - if (!counter->thread_started) { +fps_counter_interrupt(struct fps_counter *counter) { + if (!counter->thread) { return; } - sc_mutex_lock(&counter->mutex); + mutex_lock(counter->mutex); counter->interrupted = true; - sc_mutex_unlock(&counter->mutex); + mutex_unlock(counter->mutex); // wake up blocking wait - sc_cond_signal(&counter->state_cond); + 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); +fps_counter_join(struct fps_counter *counter) { + if (counter->thread) { + SDL_WaitThread(counter->thread, NULL); } } void -sc_fps_counter_add_rendered_frame(struct sc_fps_counter *counter) { - if (!is_started(counter)) { +fps_counter_add_rendered_frame(struct fps_counter *counter) { + if (!SDL_AtomicGet(&counter->started)) { return; } - sc_mutex_lock(&counter->mutex); - sc_tick now = sc_tick_now(); + mutex_lock(counter->mutex); + uint32_t now = SDL_GetTicks(); check_interval_expired(counter, now); ++counter->nr_rendered; - sc_mutex_unlock(&counter->mutex); + mutex_unlock(counter->mutex); } void -sc_fps_counter_add_skipped_frame(struct sc_fps_counter *counter) { - if (!is_started(counter)) { +fps_counter_add_skipped_frame(struct fps_counter *counter) { + if (!SDL_AtomicGet(&counter->started)) { return; } - sc_mutex_lock(&counter->mutex); - sc_tick now = sc_tick_now(); + mutex_lock(counter->mutex); + uint32_t now = SDL_GetTicks(); check_interval_expired(counter, now); ++counter->nr_skipped; - sc_mutex_unlock(&counter->mutex); + mutex_unlock(counter->mutex); } diff --git a/app/src/fps_counter.h b/app/src/fps_counter.h index 3eab461c..6b560a35 100644 --- a/app/src/fps_counter.h +++ b/app/src/fps_counter.h @@ -1,59 +1,55 @@ -#ifndef SC_FPSCOUNTER_H -#define SC_FPSCOUNTER_H +#ifndef FPSCOUNTER_H +#define FPSCOUNTER_H -#include "common.h" - -#include #include +#include +#include +#include +#include -#include "util/thread.h" -#include "util/tick.h" - -struct sc_fps_counter { - sc_thread thread; - sc_mutex mutex; - sc_cond state_cond; - - bool thread_started; +struct fps_counter { + SDL_Thread *thread; + SDL_mutex *mutex; + SDL_cond *state_cond; // 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; + SDL_atomic_t started; // the following fields are protected by the mutex bool interrupted; unsigned nr_rendered; unsigned nr_skipped; - sc_tick next_timestamp; + uint32_t next_timestamp; }; bool -sc_fps_counter_init(struct sc_fps_counter *counter); +fps_counter_init(struct fps_counter *counter); void -sc_fps_counter_destroy(struct sc_fps_counter *counter); +fps_counter_destroy(struct fps_counter *counter); bool -sc_fps_counter_start(struct sc_fps_counter *counter); +fps_counter_start(struct fps_counter *counter); void -sc_fps_counter_stop(struct sc_fps_counter *counter); +fps_counter_stop(struct fps_counter *counter); bool -sc_fps_counter_is_started(struct sc_fps_counter *counter); +fps_counter_is_started(struct fps_counter *counter); // request to stop the thread (on quit) -// must be called before sc_fps_counter_join() +// must be called before fps_counter_join() void -sc_fps_counter_interrupt(struct sc_fps_counter *counter); +fps_counter_interrupt(struct fps_counter *counter); void -sc_fps_counter_join(struct sc_fps_counter *counter); +fps_counter_join(struct fps_counter *counter); void -sc_fps_counter_add_rendered_frame(struct sc_fps_counter *counter); +fps_counter_add_rendered_frame(struct fps_counter *counter); void -sc_fps_counter_add_skipped_frame(struct sc_fps_counter *counter); +fps_counter_add_skipped_frame(struct fps_counter *counter); #endif diff --git a/app/src/frame_buffer.c b/app/src/frame_buffer.c deleted file mode 100644 index 9fd4cf6f..00000000 --- a/app/src/frame_buffer.c +++ /dev/null @@ -1,88 +0,0 @@ -#include "frame_buffer.h" - -#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); -} diff --git a/app/src/frame_buffer.h b/app/src/frame_buffer.h deleted file mode 100644 index e748adfb..00000000 --- a/app/src/frame_buffer.h +++ /dev/null @@ -1,45 +0,0 @@ -#ifndef SC_FRAME_BUFFER_H -#define SC_FRAME_BUFFER_H - -#include "common.h" - -#include -#include - -#include "util/thread.h" - -// forward declarations -typedef struct AVFrame AVFrame; - -/** - * A frame buffer holds 1 pending frame, which is the last frame received from - * the producer (typically, the decoder). - * - * If a pending frame has not been consumed when the producer pushes a new - * frame, then it is lost. The intent is to always provide access to the very - * last frame to minimize latency. - */ - -struct sc_frame_buffer { - AVFrame *pending_frame; - AVFrame *tmp_frame; // To preserve the pending frame on error - - sc_mutex mutex; - - bool pending_frame_consumed; -}; - -bool -sc_frame_buffer_init(struct sc_frame_buffer *fb); - -void -sc_frame_buffer_destroy(struct sc_frame_buffer *fb); - -bool -sc_frame_buffer_push(struct sc_frame_buffer *fb, const AVFrame *frame, - bool *skipped); - -void -sc_frame_buffer_consume(struct sc_frame_buffer *fb, AVFrame *dst); - -#endif diff --git a/app/src/hid/hid_event.h b/app/src/hid/hid_event.h deleted file mode 100644 index b0d45ce8..00000000 --- a/app/src/hid/hid_event.h +++ /dev/null @@ -1,27 +0,0 @@ -#ifndef SC_HID_EVENT_H -#define SC_HID_EVENT_H - -#include "common.h" - -#include -#include - -#define SC_HID_MAX_SIZE 15 - -struct sc_hid_input { - uint16_t hid_id; - uint8_t data[SC_HID_MAX_SIZE]; - uint8_t size; -}; - -struct sc_hid_open { - uint16_t hid_id; - const uint8_t *report_desc; // pointer to static memory - size_t report_desc_size; -}; - -struct sc_hid_close { - uint16_t hid_id; -}; - -#endif diff --git a/app/src/hid/hid_gamepad.c b/app/src/hid/hid_gamepad.c deleted file mode 100644 index 842eae9e..00000000 --- a/app/src/hid/hid_gamepad.c +++ /dev/null @@ -1,454 +0,0 @@ -#include "hid_gamepad.h" - -#include -#include -#include -#include - -#include "util/binary.h" -#include "util/log.h" - -// 2x2 bytes for left stick (X, Y) -// 2x2 bytes for right stick (Z, Rz) -// 2x2 bytes for L2/R2 triggers -// 2 bytes for buttons + padding, -// 1 byte for hat switch (dpad) + padding -#define SC_HID_GAMEPAD_EVENT_SIZE 15 - -// The ->buttons field stores the state for all buttons, but only some of them -// (the 16 LSB) must be transmitted "as is". The DPAD (hat switch) buttons are -// stored locally in the MSB of this field, but not transmitted as is: they are -// transformed to generate another specific byte. -#define SC_HID_BUTTONS_MASK 0xFFFF - -// outside SC_HID_BUTTONS_MASK -#define SC_GAMEPAD_BUTTONS_BIT_DPAD_UP UINT32_C(0x10000) -#define SC_GAMEPAD_BUTTONS_BIT_DPAD_DOWN UINT32_C(0x20000) -#define SC_GAMEPAD_BUTTONS_BIT_DPAD_LEFT UINT32_C(0x40000) -#define SC_GAMEPAD_BUTTONS_BIT_DPAD_RIGHT UINT32_C(0x80000) - -/** - * Gamepad descriptor manually crafted to transmit the input reports. - * - * The HID specification is available here: - * - * - * The HID Usage Tables is also useful: - * - */ -static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = { - // Usage Page (Generic Desktop) - 0x05, 0x01, - // Usage (Gamepad) - 0x09, 0x05, - - // Collection (Application) - 0xA1, 0x01, - - // Collection (Physical) - 0xA1, 0x00, - - // Usage Page (Generic Desktop) - 0x05, 0x01, - // Usage (X) Left stick x - 0x09, 0x30, - // Usage (Y) Left stick y - 0x09, 0x31, - // Usage (Rx) Right stick x - 0x09, 0x33, - // Usage (Ry) Right stick y - 0x09, 0x34, - // Logical Minimum (0) - 0x15, 0x00, - // Logical Maximum (65535) - // Cannot use 26 FF FF because 0xFFFF is interpreted as signed 16-bit - 0x27, 0xFF, 0xFF, 0x00, 0x00, // little-endian - // Report Size (16) - 0x75, 0x10, - // Report Count (4) - 0x95, 0x04, - // Input (Data, Variable, Absolute): 4x2 bytes (X, Y, Z, Rz) - 0x81, 0x02, - - // Usage Page (Generic Desktop) - 0x05, 0x01, - // Usage (Z) - 0x09, 0x32, - // Usage (Rz) - 0x09, 0x35, - // Logical Minimum (0) - 0x15, 0x00, - // Logical Maximum (32767) - 0x26, 0xFF, 0x7F, - // Report Size (16) - 0x75, 0x10, - // Report Count (2) - 0x95, 0x02, - // Input (Data, Variable, Absolute): 2x2 bytes (L2, R2) - 0x81, 0x02, - - // Usage Page (Buttons) - 0x05, 0x09, - // Usage Minimum (1) - 0x19, 0x01, - // Usage Maximum (16) - 0x29, 0x10, - // Logical Minimum (0) - 0x15, 0x00, - // Logical Maximum (1) - 0x25, 0x01, - // Report Count (16) - 0x95, 0x10, - // Report Size (1) - 0x75, 0x01, - // Input (Data, Variable, Absolute): 16 buttons bits - 0x81, 0x02, - - // Usage Page (Generic Desktop) - 0x05, 0x01, - // Usage (Hat switch) - 0x09, 0x39, - // Logical Minimum (1) - 0x15, 0x01, - // Logical Maximum (8) - 0x25, 0x08, - // Report Size (4) - 0x75, 0x04, - // Report Count (1) - 0x95, 0x01, - // Input (Data, Variable, Null State): 4-bit value - 0x81, 0x42, - - // End Collection - 0xC0, - - // End Collection - 0xC0, -}; - -/** - * A gamepad HID input report is 15 bytes long: - * - bytes 0-3: left stick state - * - bytes 4-7: right stick state - * - bytes 8-11: L2/R2 triggers state - * - bytes 12-13: buttons state - * - bytes 14: hat switch position (dpad) - * - * +---------------+ - * byte 0: |. . . . . . . .| - * | | left stick x (0-65535, little-endian) - * byte 1: |. . . . . . . .| - * +---------------+ - * byte 2: |. . . . . . . .| - * | | left stick y (0-65535, little-endian) - * byte 3: |. . . . . . . .| - * +---------------+ - * byte 4: |. . . . . . . .| - * | | right stick x (0-65535, little-endian) - * byte 5: |. . . . . . . .| - * +---------------+ - * byte 6: |. . . . . . . .| - * | | right stick y (0-65535, little-endian) - * byte 7: |. . . . . . . .| - * +---------------+ - * byte 8: |. . . . . . . .| - * | | L2 trigger (0-32767, little-endian) - * byte 9: |0 . . . . . . .| - * +---------------+ - * byte 10: |. . . . . . . .| - * | | R2 trigger (0-32767, little-endian) - * byte 11: |0 . . . . . . .| - * +---------------+ - * - * ,--------------- SC_GAMEPAD_BUTTON_RIGHT_SHOULDER - * | ,------------- SC_GAMEPAD_BUTTON_LEFT_SHOULDER - * | | - * | | ,--------- SC_GAMEPAD_BUTTON_NORTH - * | | | ,------- SC_GAMEPAD_BUTTON_WEST - * | | | | - * | | | | ,--- SC_GAMEPAD_BUTTON_EAST - * | | | | | ,- SC_GAMEPAD_BUTTON_SOUTH - * v v v v v v - * +---------------+ - * byte 12: |. . 0 . . 0 . .| - * | | Buttons (16-bit little-endian) - * byte 13: |0 . . . . . 0 0| - * +---------------+ - * ^ ^ ^ ^ ^ - * | | | | | - * | | | | | - * | | | | `----- SC_GAMEPAD_BUTTON_BACK - * | | | `------- SC_GAMEPAD_BUTTON_START - * | | `--------- SC_GAMEPAD_BUTTON_GUIDE - * | `----------- SC_GAMEPAD_BUTTON_LEFT_STICK - * `------------- SC_GAMEPAD_BUTTON_RIGHT_STICK - * - * +---------------+ - * byte 14: |0 0 0 0 . . . .| hat switch (dpad) position (0-8) - * +---------------+ - * 9 possible positions and their values: - * 8 1 2 - * 7 0 3 - * 6 5 4 - * (8 is top-left, 1 is top, 2 is top-right, etc.) - */ - -// [-32768 to 32767] -> [0 to 65535] -#define AXIS_RESCALE(V) (uint16_t) (((int32_t) V) + 0x8000) - -static void -sc_hid_gamepad_slot_init(struct sc_hid_gamepad_slot *slot, - uint32_t gamepad_id) { - assert(gamepad_id != SC_GAMEPAD_ID_INVALID); - slot->gamepad_id = gamepad_id; - slot->buttons = 0; - slot->axis_left_x = AXIS_RESCALE(0); - slot->axis_left_y = AXIS_RESCALE(0); - slot->axis_right_x = AXIS_RESCALE(0); - slot->axis_right_y = AXIS_RESCALE(0); - slot->axis_left_trigger = 0; - slot->axis_right_trigger = 0; -} - -static ssize_t -sc_hid_gamepad_slot_find(struct sc_hid_gamepad *hid, uint32_t gamepad_id) { - for (size_t i = 0; i < SC_MAX_GAMEPADS; ++i) { - if (gamepad_id == hid->slots[i].gamepad_id) { - // found - return i; - } - } - - return -1; -} - -void -sc_hid_gamepad_init(struct sc_hid_gamepad *hid) { - for (size_t i = 0; i < SC_MAX_GAMEPADS; ++i) { - hid->slots[i].gamepad_id = SC_GAMEPAD_ID_INVALID; - } -} - -static inline uint16_t -sc_hid_gamepad_slot_get_id(size_t slot_idx) { - assert(slot_idx < SC_MAX_GAMEPADS); - return SC_HID_ID_GAMEPAD_FIRST + slot_idx; -} - -bool -sc_hid_gamepad_generate_open(struct sc_hid_gamepad *hid, - struct sc_hid_open *hid_open, - uint32_t gamepad_id) { - assert(gamepad_id != SC_GAMEPAD_ID_INVALID); - ssize_t slot_idx = sc_hid_gamepad_slot_find(hid, SC_GAMEPAD_ID_INVALID); - if (slot_idx == -1) { - LOGW("No gamepad slot available for new gamepad %" PRIu32, gamepad_id); - return false; - } - - sc_hid_gamepad_slot_init(&hid->slots[slot_idx], gamepad_id); - - uint16_t hid_id = sc_hid_gamepad_slot_get_id(slot_idx); - hid_open->hid_id = hid_id; - hid_open->report_desc = SC_HID_GAMEPAD_REPORT_DESC; - hid_open->report_desc_size = sizeof(SC_HID_GAMEPAD_REPORT_DESC); - - return true; -} - -bool -sc_hid_gamepad_generate_close(struct sc_hid_gamepad *hid, - struct sc_hid_close *hid_close, - uint32_t gamepad_id) { - assert(gamepad_id != SC_GAMEPAD_ID_INVALID); - ssize_t slot_idx = sc_hid_gamepad_slot_find(hid, gamepad_id); - if (slot_idx == -1) { - LOGW("Unknown gamepad removed %" PRIu32, gamepad_id); - return false; - } - - hid->slots[slot_idx].gamepad_id = SC_GAMEPAD_ID_INVALID; - - uint16_t hid_id = sc_hid_gamepad_slot_get_id(slot_idx); - hid_close->hid_id = hid_id; - - return true; -} - -static uint8_t -sc_hid_gamepad_get_dpad_value(uint32_t buttons) { - // Value depending on direction: - // 8 1 2 - // 7 0 3 - // 6 5 4 - if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_UP) { - if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_LEFT) { - return 8; - } - if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_RIGHT) { - return 2; - } - return 1; - } - if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_DOWN) { - if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_LEFT) { - return 6; - } - if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_RIGHT) { - return 4; - } - return 5; - } - if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_LEFT) { - return 7; - } - if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_RIGHT) { - return 3; - } - - return 0; -} - -static void -sc_hid_gamepad_event_from_slot(uint16_t hid_id, - const struct sc_hid_gamepad_slot *slot, - struct sc_hid_input *hid_input) { - hid_input->hid_id = hid_id; - hid_input->size = SC_HID_GAMEPAD_EVENT_SIZE; - - uint8_t *data = hid_input->data; - // Values must be written in little-endian - sc_write16le(data, slot->axis_left_x); - sc_write16le(data + 2, slot->axis_left_y); - sc_write16le(data + 4, slot->axis_right_x); - sc_write16le(data + 6, slot->axis_right_y); - sc_write16le(data + 8, slot->axis_left_trigger); - sc_write16le(data + 10, slot->axis_right_trigger); - sc_write16le(data + 12, slot->buttons & SC_HID_BUTTONS_MASK); - data[14] = sc_hid_gamepad_get_dpad_value(slot->buttons); -} - -static uint32_t -sc_hid_gamepad_get_button_id(enum sc_gamepad_button button) { - switch (button) { - case SC_GAMEPAD_BUTTON_SOUTH: - return 0x0001; - case SC_GAMEPAD_BUTTON_EAST: - return 0x0002; - case SC_GAMEPAD_BUTTON_WEST: - return 0x0008; - case SC_GAMEPAD_BUTTON_NORTH: - return 0x0010; - case SC_GAMEPAD_BUTTON_BACK: - return 0x0400; - case SC_GAMEPAD_BUTTON_GUIDE: - return 0x1000; - case SC_GAMEPAD_BUTTON_START: - return 0x0800; - case SC_GAMEPAD_BUTTON_LEFT_STICK: - return 0x2000; - case SC_GAMEPAD_BUTTON_RIGHT_STICK: - return 0x4000; - case SC_GAMEPAD_BUTTON_LEFT_SHOULDER: - return 0x0040; - case SC_GAMEPAD_BUTTON_RIGHT_SHOULDER: - return 0x0080; - case SC_GAMEPAD_BUTTON_DPAD_UP: - return SC_GAMEPAD_BUTTONS_BIT_DPAD_UP; - case SC_GAMEPAD_BUTTON_DPAD_DOWN: - return SC_GAMEPAD_BUTTONS_BIT_DPAD_DOWN; - case SC_GAMEPAD_BUTTON_DPAD_LEFT: - return SC_GAMEPAD_BUTTONS_BIT_DPAD_LEFT; - case SC_GAMEPAD_BUTTON_DPAD_RIGHT: - return SC_GAMEPAD_BUTTONS_BIT_DPAD_RIGHT; - default: - // unknown button, ignore - return 0; - } -} - -bool -sc_hid_gamepad_generate_input_from_button(struct sc_hid_gamepad *hid, - struct sc_hid_input *hid_input, - const struct sc_gamepad_button_event *event) { - if ((event->button < 0) || (event->button > 15)) { - return false; - } - - uint32_t gamepad_id = event->gamepad_id; - - ssize_t slot_idx = sc_hid_gamepad_slot_find(hid, gamepad_id); - if (slot_idx == -1) { - LOGW("Axis event for unknown gamepad %" PRIu32, gamepad_id); - return false; - } - - assert(slot_idx < SC_MAX_GAMEPADS); - - struct sc_hid_gamepad_slot *slot = &hid->slots[slot_idx]; - - uint32_t button = sc_hid_gamepad_get_button_id(event->button); - if (!button) { - // unknown button, ignore - return false; - } - - if (event->action == SC_ACTION_DOWN) { - slot->buttons |= button; - } else { - assert(event->action == SC_ACTION_UP); - slot->buttons &= ~button; - } - - uint16_t hid_id = sc_hid_gamepad_slot_get_id(slot_idx); - sc_hid_gamepad_event_from_slot(hid_id, slot, hid_input); - - return true; -} - -bool -sc_hid_gamepad_generate_input_from_axis(struct sc_hid_gamepad *hid, - struct sc_hid_input *hid_input, - const struct sc_gamepad_axis_event *event) { - uint32_t gamepad_id = event->gamepad_id; - - ssize_t slot_idx = sc_hid_gamepad_slot_find(hid, gamepad_id); - if (slot_idx == -1) { - LOGW("Button event for unknown gamepad %" PRIu32, gamepad_id); - return false; - } - - assert(slot_idx < SC_MAX_GAMEPADS); - - struct sc_hid_gamepad_slot *slot = &hid->slots[slot_idx]; - - switch (event->axis) { - case SC_GAMEPAD_AXIS_LEFTX: - slot->axis_left_x = AXIS_RESCALE(event->value); - break; - case SC_GAMEPAD_AXIS_LEFTY: - slot->axis_left_y = AXIS_RESCALE(event->value); - break; - case SC_GAMEPAD_AXIS_RIGHTX: - slot->axis_right_x = AXIS_RESCALE(event->value); - break; - case SC_GAMEPAD_AXIS_RIGHTY: - slot->axis_right_y = AXIS_RESCALE(event->value); - break; - case SC_GAMEPAD_AXIS_LEFT_TRIGGER: - // Trigger is always positive between 0 and 32767 - slot->axis_left_trigger = MAX(0, event->value); - break; - case SC_GAMEPAD_AXIS_RIGHT_TRIGGER: - // Trigger is always positive between 0 and 32767 - slot->axis_right_trigger = MAX(0, event->value); - break; - default: - return false; - } - - uint16_t hid_id = sc_hid_gamepad_slot_get_id(slot_idx); - sc_hid_gamepad_event_from_slot(hid_id, slot, hid_input); - - return true; -} diff --git a/app/src/hid/hid_gamepad.h b/app/src/hid/hid_gamepad.h deleted file mode 100644 index 8d939ac7..00000000 --- a/app/src/hid/hid_gamepad.h +++ /dev/null @@ -1,54 +0,0 @@ -#ifndef SC_HID_GAMEPAD_H -#define SC_HID_GAMEPAD_H - -#include "common.h" - -#include -#include - -#include "hid/hid_event.h" -#include "input_events.h" - -#define SC_MAX_GAMEPADS 8 -#define SC_HID_ID_GAMEPAD_FIRST 3 -#define SC_HID_ID_GAMEPAD_LAST (SC_HID_ID_GAMEPAD_FIRST + SC_MAX_GAMEPADS - 1) - -struct sc_hid_gamepad_slot { - uint32_t gamepad_id; - uint32_t buttons; - uint16_t axis_left_x; - uint16_t axis_left_y; - uint16_t axis_right_x; - uint16_t axis_right_y; - uint16_t axis_left_trigger; - uint16_t axis_right_trigger; -}; - -struct sc_hid_gamepad { - struct sc_hid_gamepad_slot slots[SC_MAX_GAMEPADS]; -}; - -void -sc_hid_gamepad_init(struct sc_hid_gamepad *hid); - -bool -sc_hid_gamepad_generate_open(struct sc_hid_gamepad *hid, - struct sc_hid_open *hid_open, - uint32_t gamepad_id); - -bool -sc_hid_gamepad_generate_close(struct sc_hid_gamepad *hid, - struct sc_hid_close *hid_close, - uint32_t gamepad_id); - -bool -sc_hid_gamepad_generate_input_from_button(struct sc_hid_gamepad *hid, - struct sc_hid_input *hid_input, - const struct sc_gamepad_button_event *event); - -bool -sc_hid_gamepad_generate_input_from_axis(struct sc_hid_gamepad *hid, - struct sc_hid_input *hid_input, - const struct sc_gamepad_axis_event *event); - -#endif diff --git a/app/src/hid/hid_keyboard.c b/app/src/hid/hid_keyboard.c deleted file mode 100644 index 6477396a..00000000 --- a/app/src/hid/hid_keyboard.c +++ /dev/null @@ -1,345 +0,0 @@ -#include "hid_keyboard.h" - -#include -#include - -#include "util/log.h" - -#define SC_HID_MOD_NONE 0x00 -#define SC_HID_MOD_LEFT_CONTROL (1 << 0) -#define SC_HID_MOD_LEFT_SHIFT (1 << 1) -#define SC_HID_MOD_LEFT_ALT (1 << 2) -#define SC_HID_MOD_LEFT_GUI (1 << 3) -#define SC_HID_MOD_RIGHT_CONTROL (1 << 4) -#define SC_HID_MOD_RIGHT_SHIFT (1 << 5) -#define SC_HID_MOD_RIGHT_ALT (1 << 6) -#define SC_HID_MOD_RIGHT_GUI (1 << 7) - -#define SC_HID_KEYBOARD_INDEX_MODS 0 -#define SC_HID_KEYBOARD_INDEX_KEYS 2 - -// USB HID protocol says 6 keys in an event is the requirement for BIOS -// keyboard support, though OS could support more keys via modifying the report -// desc. 6 should be enough for scrcpy. -#define SC_HID_KEYBOARD_MAX_KEYS 6 -#define SC_HID_KEYBOARD_INPUT_SIZE \ - (SC_HID_KEYBOARD_INDEX_KEYS + SC_HID_KEYBOARD_MAX_KEYS) - -#define SC_HID_RESERVED 0x00 -#define SC_HID_ERROR_ROLL_OVER 0x01 - -/** - * For HID, only report descriptor is needed. - * - * The specification is available here: - * - * - * In particular, read: - * - §6.2.2 Report Descriptor - * - Appendix B.1 Protocol 1 (Keyboard) - * - Appendix C: Keyboard Implementation - * - * The HID Usage Tables is also useful: - * - * - * 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 uint8_t SC_HID_KEYBOARD_REPORT_DESC[] = { - // Usage Page (Generic Desktop) - 0x05, 0x01, - // Usage (Keyboard) - 0x09, 0x06, - - // Collection (Application) - 0xA1, 0x01, - - // Usage Page (Key Codes) - 0x05, 0x07, - // Usage Minimum (224) - 0x19, 0xE0, - // Usage Maximum (231) - 0x29, 0xE7, - // Logical Minimum (0) - 0x15, 0x00, - // Logical Maximum (1) - 0x25, 0x01, - // Report Size (1) - 0x75, 0x01, - // Report Count (8) - 0x95, 0x08, - // Input (Data, Variable, Absolute): Modifier byte - 0x81, 0x02, - - // Report Size (8) - 0x75, 0x08, - // Report Count (1) - 0x95, 0x01, - // Input (Constant): Reserved byte - 0x81, 0x01, - - // Usage Page (LEDs) - 0x05, 0x08, - // Usage Minimum (1) - 0x19, 0x01, - // Usage Maximum (5) - 0x29, 0x05, - // Report Size (1) - 0x75, 0x01, - // Report Count (5) - 0x95, 0x05, - // Output (Data, Variable, Absolute): LED report - 0x91, 0x02, - - // Report Size (3) - 0x75, 0x03, - // Report Count (1) - 0x95, 0x01, - // Output (Constant): LED report padding - 0x91, 0x01, - - // Usage Page (Key Codes) - 0x05, 0x07, - // Usage Minimum (0) - 0x19, 0x00, - // Usage Maximum (101) - 0x29, SC_HID_KEYBOARD_KEYS - 1, - // Logical Minimum (0) - 0x15, 0x00, - // Logical Maximum(101) - 0x25, SC_HID_KEYBOARD_KEYS - 1, - // Report Size (8) - 0x75, 0x08, - // Report Count (6) - 0x95, SC_HID_KEYBOARD_MAX_KEYS, - // Input (Data, Array): Keys - 0x81, 0x00, - - // End Collection - 0xC0 -}; - -/** - * A keyboard HID input report is 8 bytes long: - * - * - byte 0: modifiers (1 flag per modifier key, 8 possible modifier keys) - * - byte 1: reserved (always 0) - * - bytes 2 to 7: pressed keys (6 at most) - * - * 7 6 5 4 3 2 1 0 - * +---------------+ - * byte 0: |. . . . . . . .| modifiers - * +---------------+ - * ^ ^ ^ ^ ^ ^ ^ ^ - * | | | | | | | `- left Ctrl - * | | | | | | `--- left Shift - * | | | | | `----- left Alt - * | | | | `------- left Gui - * | | | `--------- right Ctrl - * | | `----------- right Shift - * | `------------- right Alt - * `--------------- right Gui - * - * +---------------+ - * byte 1: |0 0 0 0 0 0 0 0| reserved - * +---------------+ - * - * +---------------+ - * bytes 2 to 7: |. . . . . . . .| scancode of 1st key pressed - * +---------------+ - * |. . . . . . . .| scancode of 2nd key pressed - * +---------------+ - * |. . . . . . . .| scancode of 3rd key pressed - * +---------------+ - * |. . . . . . . .| scancode of 4th key pressed - * +---------------+ - * |. . . . . . . .| scancode of 5th key pressed - * +---------------+ - * |. . . . . . . .| scancode of 6th key pressed - * +---------------+ - * - * If there are less than 6 keys pressed, the last items are set to 0. - * For example, if A and W are pressed: - * - * +---------------+ - * bytes 2 to 7: |0 0 0 0 0 1 0 0| A is pressed (scancode = 4) - * +---------------+ - * |0 0 0 1 1 0 1 0| W is pressed (scancode = 26) - * +---------------+ - * |0 0 0 0 0 0 0 0| ^ - * +---------------+ | only 2 keys are pressed, the - * |0 0 0 0 0 0 0 0| | remaining items are set to 0 - * +---------------+ | - * |0 0 0 0 0 0 0 0| | - * +---------------+ | - * |0 0 0 0 0 0 0 0| v - * +---------------+ - * - * Pressing more than 6 keys is not supported. If this happens (typically, - * never in practice), report a "phantom state": - * - * +---------------+ - * bytes 2 to 7: |0 0 0 0 0 0 0 1| ^ - * +---------------+ | - * |0 0 0 0 0 0 0 1| | more than 6 keys pressed: - * +---------------+ | the list is filled with a special - * |0 0 0 0 0 0 0 1| | rollover error code (0x01) - * +---------------+ | - * |0 0 0 0 0 0 0 1| | - * +---------------+ | - * |0 0 0 0 0 0 0 1| | - * +---------------+ | - * |0 0 0 0 0 0 0 1| v - * +---------------+ - */ - -static void -sc_hid_keyboard_input_init(struct sc_hid_input *hid_input) { - hid_input->hid_id = SC_HID_ID_KEYBOARD; - hid_input->size = SC_HID_KEYBOARD_INPUT_SIZE; - - uint8_t *data = hid_input->data; - - data[SC_HID_KEYBOARD_INDEX_MODS] = SC_HID_MOD_NONE; - data[1] = SC_HID_RESERVED; - memset(&data[SC_HID_KEYBOARD_INDEX_KEYS], 0, SC_HID_KEYBOARD_MAX_KEYS); -} - -static uint16_t -sc_hid_mod_from_sdl_keymod(uint16_t mod) { - uint16_t mods = SC_HID_MOD_NONE; - if (mod & SC_MOD_LCTRL) { - mods |= SC_HID_MOD_LEFT_CONTROL; - } - if (mod & SC_MOD_LSHIFT) { - mods |= SC_HID_MOD_LEFT_SHIFT; - } - if (mod & SC_MOD_LALT) { - mods |= SC_HID_MOD_LEFT_ALT; - } - if (mod & SC_MOD_LGUI) { - mods |= SC_HID_MOD_LEFT_GUI; - } - if (mod & SC_MOD_RCTRL) { - mods |= SC_HID_MOD_RIGHT_CONTROL; - } - if (mod & SC_MOD_RSHIFT) { - mods |= SC_HID_MOD_RIGHT_SHIFT; - } - if (mod & SC_MOD_RALT) { - mods |= SC_HID_MOD_RIGHT_ALT; - } - if (mod & SC_MOD_RGUI) { - mods |= SC_HID_MOD_RIGHT_GUI; - } - return mods; -} - -void -sc_hid_keyboard_init(struct sc_hid_keyboard *hid) { - memset(hid->keys, false, SC_HID_KEYBOARD_KEYS); -} - -static inline bool -scancode_is_modifier(enum sc_scancode scancode) { - return scancode >= SC_SCANCODE_LCTRL && scancode <= SC_SCANCODE_RGUI; -} - -bool -sc_hid_keyboard_generate_input_from_key(struct sc_hid_keyboard *hid, - struct sc_hid_input *hid_input, - const struct sc_key_event *event) { - enum sc_scancode scancode = event->scancode; - assert(scancode >= 0); - - // SDL also generates events when only modifiers are pressed, we cannot - // ignore them totally, for example press 'a' first then press 'Control', - // if we ignore 'Control' event, only 'a' is sent. - if (scancode >= SC_HID_KEYBOARD_KEYS && !scancode_is_modifier(scancode)) { - // Scancode to ignore - return false; - } - - sc_hid_keyboard_input_init(hid_input); - - uint16_t mods = sc_hid_mod_from_sdl_keymod(event->mods_state); - - if (scancode < SC_HID_KEYBOARD_KEYS) { - // Pressed is true and released is false - hid->keys[scancode] = (event->action == SC_ACTION_DOWN); - LOGV("keys[%02x] = %s", scancode, - hid->keys[scancode] ? "true" : "false"); - } - - hid_input->data[SC_HID_KEYBOARD_INDEX_MODS] = mods; - - uint8_t *keys_data = &hid_input->data[SC_HID_KEYBOARD_INDEX_KEYS]; - // Re-calculate pressed keys every time - int keys_pressed_count = 0; - for (int i = 0; i < SC_HID_KEYBOARD_KEYS; ++i) { - if (hid->keys[i]) { - // USB HID protocol says that if keys exceeds report count, a - // phantom state should be reported - if (keys_pressed_count >= SC_HID_KEYBOARD_MAX_KEYS) { - // Phantom state: - // - Modifiers - // - Reserved - // - ErrorRollOver * HID_MAX_KEYS - memset(keys_data, SC_HID_ERROR_ROLL_OVER, - SC_HID_KEYBOARD_MAX_KEYS); - goto end; - } - - keys_data[keys_pressed_count] = i; - ++keys_pressed_count; - } - } - -end: - LOGV("hid keyboard: key %-4s scancode=%02x (%u) mod=%02x", - event->action == SC_ACTION_DOWN ? "down" : "up", event->scancode, - event->scancode, mods); - - return true; -} - -bool -sc_hid_keyboard_generate_input_from_mods(struct sc_hid_input *hid_input, - uint16_t mods_state) { - bool capslock = mods_state & SC_MOD_CAPS; - bool numlock = mods_state & SC_MOD_NUM; - if (!capslock && !numlock) { - // Nothing to do - return false; - } - - sc_hid_keyboard_input_init(hid_input); - - unsigned i = 0; - if (capslock) { - hid_input->data[SC_HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_CAPSLOCK; - ++i; - } - if (numlock) { - hid_input->data[SC_HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_NUMLOCK; - ++i; - } - - return true; -} - -void sc_hid_keyboard_generate_open(struct sc_hid_open *hid_open) { - hid_open->hid_id = SC_HID_ID_KEYBOARD; - hid_open->report_desc = SC_HID_KEYBOARD_REPORT_DESC; - hid_open->report_desc_size = sizeof(SC_HID_KEYBOARD_REPORT_DESC); -} - -void sc_hid_keyboard_generate_close(struct sc_hid_close *hid_close) { - hid_close->hid_id = SC_HID_ID_KEYBOARD; -} diff --git a/app/src/hid/hid_keyboard.h b/app/src/hid/hid_keyboard.h deleted file mode 100644 index 5ecfd8cf..00000000 --- a/app/src/hid/hid_keyboard.h +++ /dev/null @@ -1,54 +0,0 @@ -#ifndef SC_HID_KEYBOARD_H -#define SC_HID_KEYBOARD_H - -#include "common.h" - -#include -#include - -#include "hid/hid_event.h" -#include "input_events.h" - -// See "SDL2/SDL_scancode.h". -// Maybe SDL_Keycode is used by most people, but SDL_Scancode is taken from USB -// HID protocol. -// 0x65 is Application, typically AT-101 Keyboard ends here. -#define SC_HID_KEYBOARD_KEYS 0x66 - -#define SC_HID_ID_KEYBOARD 1 - -/** - * HID keyboard events are sequence-based, every time keyboard state changes - * it sends an array of currently pressed keys, the host is responsible for - * compare events and determine which key becomes pressed and which key becomes - * released. In order to convert SDL_KeyboardEvent to HID events, we first use - * an array of keys to save each keys' state. And when a SDL_KeyboardEvent was - * emitted, we updated our state, and then we use a loop to generate HID - * events. The sequence of array elements is unimportant and when too much keys - * pressed at the same time (more than report count), we should generate - * phantom state. Don't forget that modifiers should be updated too, even for - * phantom state. - */ -struct sc_hid_keyboard { - bool keys[SC_HID_KEYBOARD_KEYS]; -}; - -void -sc_hid_keyboard_init(struct sc_hid_keyboard *hid); - -void -sc_hid_keyboard_generate_open(struct sc_hid_open *hid_open); - -void -sc_hid_keyboard_generate_close(struct sc_hid_close *hid_close); - -bool -sc_hid_keyboard_generate_input_from_key(struct sc_hid_keyboard *hid, - struct sc_hid_input *hid_input, - const struct sc_key_event *event); - -bool -sc_hid_keyboard_generate_input_from_mods(struct sc_hid_input *hid_input, - uint16_t mods_state); - -#endif diff --git a/app/src/hid/hid_mouse.c b/app/src/hid/hid_mouse.c deleted file mode 100644 index 33f0807e..00000000 --- a/app/src/hid/hid_mouse.c +++ /dev/null @@ -1,222 +0,0 @@ -#include "hid_mouse.h" - -#include - -// 1 byte for buttons + padding, 1 byte for X position, 1 byte for Y position, -// 1 byte for wheel motion, 1 byte for hozizontal scrolling -#define SC_HID_MOUSE_INPUT_SIZE 5 - -/** - * 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) (p32) - */ -static const uint8_t SC_HID_MOUSE_REPORT_DESC[] = { - // Usage Page (Generic Desktop) - 0x05, 0x01, - // Usage (Mouse) - 0x09, 0x02, - - // Collection (Application) - 0xA1, 0x01, - - // Usage (Pointer) - 0x09, 0x01, - - // Collection (Physical) - 0xA1, 0x00, - - // Usage Page (Buttons) - 0x05, 0x09, - - // Usage Minimum (1) - 0x19, 0x01, - // Usage Maximum (5) - 0x29, 0x05, - // Logical Minimum (0) - 0x15, 0x00, - // Logical Maximum (1) - 0x25, 0x01, - // Report Count (5) - 0x95, 0x05, - // Report Size (1) - 0x75, 0x01, - // Input (Data, Variable, Absolute): 5 buttons bits - 0x81, 0x02, - - // Report Count (1) - 0x95, 0x01, - // Report Size (3) - 0x75, 0x03, - // Input (Constant): 3 bits padding - 0x81, 0x01, - - // Usage Page (Generic Desktop) - 0x05, 0x01, - // Usage (X) - 0x09, 0x30, - // Usage (Y) - 0x09, 0x31, - // Usage (Wheel) - 0x09, 0x38, - // Logical Minimum (-127) - 0x15, 0x81, - // Logical 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, - - // Usage Page (Consumer Page) - 0x05, 0x0C, - // Usage(AC Pan) - 0x0A, 0x38, 0x02, - // Logical Minimum (-127) - 0x15, 0x81, - // Logical Maximum (127) - 0x25, 0x7F, - // Report Size (8) - 0x75, 0x08, - // Report Count (1) - 0x95, 0x01, - // Input (Data, Variable, Relative): 1 byte (AC Pan) - 0x81, 0x06, - - // End Collection - 0xC0, - - // End Collection - 0xC0, -}; - -/** - * A mouse HID input report is 4 bytes long: - * - * - byte 0: buttons state - * - byte 1: relative x motion (signed byte from -127 to 127) - * - byte 2: relative y motion (signed byte from -127 to 127) - * - byte 3: wheel motion (-1, 0 or 1) - * - * 7 6 5 4 3 2 1 0 - * +---------------+ - * byte 0: |0 0 0 . . . . .| buttons state - * +---------------+ - * ^ ^ ^ ^ ^ - * | | | | `- left button - * | | | `--- right button - * | | `----- middle button - * | `------- button 4 - * `--------- button 5 - * - * +---------------+ - * byte 1: |. . . . . . . .| relative x motion - * +---------------+ - * byte 2: |. . . . . . . .| relative y motion - * +---------------+ - * byte 3: |. . . . . . . .| wheel motion - * +---------------+ - * - * As an example, here is the report for a motion of (x=5, y=-4) with left - * button pressed: - * - * +---------------+ - * |0 0 0 0 0 0 0 1| left button pressed - * +---------------+ - * |0 0 0 0 0 1 0 1| horizontal motion (x = 5) - * +---------------+ - * |1 1 1 1 1 1 0 0| relative y motion (y = -4) - * +---------------+ - * |0 0 0 0 0 0 0 0| wheel motion - * +---------------+ - */ - -static void -sc_hid_mouse_input_init(struct sc_hid_input *hid_input) { - hid_input->hid_id = SC_HID_ID_MOUSE; - hid_input->size = SC_HID_MOUSE_INPUT_SIZE; - // Leave ->data uninitialized, it will be fully initialized by callers -} - -static uint8_t -sc_hid_buttons_from_buttons_state(uint8_t buttons_state) { - uint8_t c = 0; - if (buttons_state & SC_MOUSE_BUTTON_LEFT) { - c |= 1 << 0; - } - if (buttons_state & SC_MOUSE_BUTTON_RIGHT) { - c |= 1 << 1; - } - if (buttons_state & SC_MOUSE_BUTTON_MIDDLE) { - c |= 1 << 2; - } - if (buttons_state & SC_MOUSE_BUTTON_X1) { - c |= 1 << 3; - } - if (buttons_state & SC_MOUSE_BUTTON_X2) { - c |= 1 << 4; - } - return c; -} - -void -sc_hid_mouse_generate_input_from_motion(struct sc_hid_input *hid_input, - const struct sc_mouse_motion_event *event) { - sc_hid_mouse_input_init(hid_input); - - uint8_t *data = hid_input->data; - data[0] = sc_hid_buttons_from_buttons_state(event->buttons_state); - data[1] = CLAMP(event->xrel, -127, 127); - data[2] = CLAMP(event->yrel, -127, 127); - data[3] = 0; // no vertical scrolling - data[4] = 0; // no horizontal scrolling -} - -void -sc_hid_mouse_generate_input_from_click(struct sc_hid_input *hid_input, - const struct sc_mouse_click_event *event) { - sc_hid_mouse_input_init(hid_input); - - uint8_t *data = hid_input->data; - data[0] = sc_hid_buttons_from_buttons_state(event->buttons_state); - data[1] = 0; // no x motion - data[2] = 0; // no y motion - data[3] = 0; // no vertical scrolling - data[4] = 0; // no horizontal scrolling -} - -bool -sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input, - const struct sc_mouse_scroll_event *event) { - if (!event->vscroll_int && !event->hscroll_int) { - // Need a full integral value for HID - return false; - } - - sc_hid_mouse_input_init(hid_input); - - uint8_t *data = hid_input->data; - data[0] = 0; // buttons state irrelevant (and unknown) - data[1] = 0; // no x motion - data[2] = 0; // no y motion - data[3] = CLAMP(event->vscroll_int, -127, 127); - data[4] = CLAMP(event->hscroll_int, -127, 127); - return true; -} - -void sc_hid_mouse_generate_open(struct sc_hid_open *hid_open) { - hid_open->hid_id = SC_HID_ID_MOUSE; - hid_open->report_desc = SC_HID_MOUSE_REPORT_DESC; - hid_open->report_desc_size = sizeof(SC_HID_MOUSE_REPORT_DESC); -} - -void sc_hid_mouse_generate_close(struct sc_hid_close *hid_close) { - hid_close->hid_id = SC_HID_ID_MOUSE; -} diff --git a/app/src/hid/hid_mouse.h b/app/src/hid/hid_mouse.h deleted file mode 100644 index 4ae4bfd4..00000000 --- a/app/src/hid/hid_mouse.h +++ /dev/null @@ -1,29 +0,0 @@ -#ifndef SC_HID_MOUSE_H -#define SC_HID_MOUSE_H - -#include "common.h" - -#include "hid/hid_event.h" -#include "input_events.h" - -#define SC_HID_ID_MOUSE 2 - -void -sc_hid_mouse_generate_open(struct sc_hid_open *hid_open); - -void -sc_hid_mouse_generate_close(struct sc_hid_close *hid_close); - -void -sc_hid_mouse_generate_input_from_motion(struct sc_hid_input *hid_input, - const struct sc_mouse_motion_event *event); - -void -sc_hid_mouse_generate_input_from_click(struct sc_hid_input *hid_input, - const struct sc_mouse_click_event *event); - -bool -sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input, - const struct sc_mouse_scroll_event *event); - -#endif diff --git a/app/src/icon.c b/app/src/icon.c deleted file mode 100644 index 797afc75..00000000 --- a/app/src/icon.c +++ /dev/null @@ -1,288 +0,0 @@ -#include "icon.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "config.h" -#include "util/env.h" -#ifdef PORTABLE -# include "util/file.h" -#endif -#include "util/log.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) { - char *icon_path = sc_get_env("SCRCPY_ICON_PATH"); - if (icon_path) { - // if the envvar is set, use it - LOGD("Using SCRCPY_ICON_PATH: %s", icon_path); - return icon_path; - } - -#ifndef PORTABLE - LOGD("Using icon: " SCRCPY_DEFAULT_ICON_PATH); - icon_path = strdup(SCRCPY_DEFAULT_ICON_PATH); - if (!icon_path) { - LOG_OOM(); - return NULL; - } -#else - icon_path = sc_file_get_local_path(SCRCPY_PORTABLE_ICON_FILENAME); - if (!icon_path) { - LOGE("Could not get icon path"); - return NULL; - } - LOGD("Using icon (portable): %s", icon_path); -#endif - - return icon_path; -} - -static AVFrame * -decode_image(const char *path) { - AVFrame *result = NULL; - - AVFormatContext *ctx = avformat_alloc_context(); - if (!ctx) { - LOG_OOM(); - return NULL; - } - - if (avformat_open_input(&ctx, path, NULL, NULL) < 0) { - LOGE("Could not open icon image: %s", path); - goto free_ctx; - } - - if (avformat_find_stream_info(ctx, NULL) < 0) { - LOGE("Could not find image stream info"); - goto close_input; - } - - -// In ffmpeg/doc/APIchanges: -// 2021-04-27 - 46dac8cf3d - lavf 59.0.100 - avformat.h -// av_find_best_stream now uses a const AVCodec ** parameter -// for the returned decoder. -#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(59, 0, 100) - const AVCodec *codec; -#else - AVCodec *codec; -#endif - - int stream = - av_find_best_stream(ctx, AVMEDIA_TYPE_VIDEO, -1, -1, &codec, 0); - if (stream < 0 ) { - LOGE("Could not find best image stream"); - goto close_input; - } - - AVCodecParameters *params = ctx->streams[stream]->codecpar; - - 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 free_codec_ctx; - } - - AVPacket *packet = av_packet_alloc(); - if (!packet) { - LOG_OOM(); - av_frame_free(&frame); - goto free_codec_ctx; - } - - if (av_read_frame(ctx, packet) < 0) { - LOGE("Could not read frame"); - av_packet_free(&packet); - av_frame_free(&frame); - goto free_codec_ctx; - } - - 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 free_codec_ctx; - } - - 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 free_codec_ctx; - } - - av_packet_free(&packet); - - result = frame; - -free_codec_ctx: - avcodec_free_context(&codec_ctx); -close_input: - avformat_close_input(&ctx); -free_ctx: - avformat_free_context(ctx); - - return result; -} - -#if !SDL_VERSION_ATLEAST(2, 0, 10) -// SDL_PixelFormatEnum has been introduced in SDL 2.0.10. Use int for older SDL -// versions. -typedef int SDL_PixelFormatEnum; -#endif - -static SDL_PixelFormatEnum -to_sdl_pixel_format(enum AVPixelFormat fmt) { - switch (fmt) { - case AV_PIX_FMT_RGB24: return SDL_PIXELFORMAT_RGB24; - case AV_PIX_FMT_BGR24: return SDL_PIXELFORMAT_BGR24; - case AV_PIX_FMT_ARGB: return SDL_PIXELFORMAT_ARGB32; - case AV_PIX_FMT_RGBA: return SDL_PIXELFORMAT_RGBA32; - case AV_PIX_FMT_ABGR: return SDL_PIXELFORMAT_ABGR32; - case AV_PIX_FMT_BGRA: return SDL_PIXELFORMAT_BGRA32; - case AV_PIX_FMT_RGB565BE: return SDL_PIXELFORMAT_RGB565; - case AV_PIX_FMT_RGB555BE: return SDL_PIXELFORMAT_RGB555; - case AV_PIX_FMT_BGR565BE: return SDL_PIXELFORMAT_BGR565; - case AV_PIX_FMT_BGR555BE: return SDL_PIXELFORMAT_BGR555; - case AV_PIX_FMT_RGB444BE: return SDL_PIXELFORMAT_RGB444; -#if SDL_VERSION_ATLEAST(2, 0, 12) - case AV_PIX_FMT_BGR444BE: return SDL_PIXELFORMAT_BGR444; -#endif - case AV_PIX_FMT_PAL8: return SDL_PIXELFORMAT_INDEX8; - default: return SDL_PIXELFORMAT_UNKNOWN; - } -} - -static SDL_Surface * -load_from_path(const char *path) { - AVFrame *frame = decode_image(path); - if (!frame) { - return NULL; - } - - const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(frame->format); - if (!desc) { - LOGE("Could not get icon format descriptor"); - goto error; - } - - bool is_packed = !(desc->flags & AV_PIX_FMT_FLAG_PLANAR); - if (!is_packed) { - LOGE("Could not load non-packed icon"); - goto error; - } - - SDL_PixelFormatEnum format = to_sdl_pixel_format(frame->format); - if (format == SDL_PIXELFORMAT_UNKNOWN) { - LOGE("Unsupported icon pixel format: %s (%d)", desc->name, - frame->format); - goto error; - } - - int bits_per_pixel = av_get_bits_per_pixel(desc); - SDL_Surface *surface = - SDL_CreateRGBSurfaceWithFormatFrom(frame->data[0], - frame->width, frame->height, - bits_per_pixel, - frame->linesize[0], - format); - - if (!surface) { - LOGE("Could not create icon surface"); - goto error; - } - - if (frame->format == AV_PIX_FMT_PAL8) { - // Initialize the SDL palette - uint8_t *data = frame->data[1]; - SDL_Color colors[256]; - for (int i = 0; i < 256; ++i) { - SDL_Color *color = &colors[i]; - - // The palette is transported in AVFrame.data[1], is 1024 bytes - // long (256 4-byte entries) and is formatted the same as in - // AV_PIX_FMT_RGB32 described above (i.e., it is also - // endian-specific). - // -#if SDL_BYTEORDER == SDL_BIG_ENDIAN - color->a = data[i * 4]; - color->r = data[i * 4 + 1]; - color->g = data[i * 4 + 2]; - color->b = data[i * 4 + 3]; -#else - color->a = data[i * 4 + 3]; - color->r = data[i * 4 + 2]; - color->g = data[i * 4 + 1]; - color->b = data[i * 4]; -#endif - } - - SDL_Palette *palette = surface->format->palette; - assert(palette); - int ret = SDL_SetPaletteColors(palette, colors, 0, 256); - if (ret) { - LOGE("Could not set palette colors"); - SDL_FreeSurface(surface); - goto error; - } - } - - surface->userdata = frame; // frame owns the data - - return surface; - -error: - av_frame_free(&frame); - return NULL; -} - -SDL_Surface * -scrcpy_icon_load(void) { - char *icon_path = get_icon_path(); - if (!icon_path) { - return NULL; - } - - SDL_Surface *icon = load_from_path(icon_path); - free(icon_path); - return icon; -} - -void -scrcpy_icon_destroy(SDL_Surface *icon) { - AVFrame *frame = icon->userdata; - assert(frame); - av_frame_free(&frame); - SDL_FreeSurface(icon); -} diff --git a/app/src/icon.h b/app/src/icon.h deleted file mode 100644 index 6bcf46d2..00000000 --- a/app/src/icon.h +++ /dev/null @@ -1,14 +0,0 @@ -#ifndef SC_ICON_H -#define SC_ICON_H - -#include "common.h" - -#include - -SDL_Surface * -scrcpy_icon_load(void); - -void -scrcpy_icon_destroy(SDL_Surface *icon); - -#endif diff --git a/app/src/icon.xpm b/app/src/icon.xpm new file mode 100644 index 00000000..73b29da9 --- /dev/null +++ b/app/src/icon.xpm @@ -0,0 +1,53 @@ +/* XPM */ +static char * icon_xpm[] = { +"48 48 2 1", +" c None", +". c #96C13E", +" .. .. ", +" ... ... ", +" ... ...... ... ", +" ................ ", +" .............. ", +" ................ ", +" .................. ", +" .................... ", +" ..... ........ ..... ", +" ..... ........ ..... ", +" ...................... ", +" ........................ ", +" ........................ ", +" ........................ ", +" ", +" ", +" .... ........................ .... ", +" ...... ........................ ...... ", +" ...... ........................ ...... ", +" ...... ........................ ...... ", +" ...... ........................ ...... ", +" ...... ........................ ...... ", +" ...... ........................ ...... ", +" ...... ........................ ...... ", +" ...... ........................ ...... ", +" ...... ........................ ...... ", +" ...... ........................ ...... ", +" ...... ........................ ...... ", +" ...... ........................ ...... ", +" ...... ........................ ...... ", +" ...... ........................ ...... ", +" ...... ........................ ...... ", +" ...... ........................ ...... ", +" ...... ........................ ...... ", +" ...... ........................ ...... ", +" .... ........................ .... ", +" ........................ ", +" ...................... ", +" ...... ...... ", +" ...... ...... ", +" ...... ...... ", +" ...... ...... ", +" ...... ...... ", +" ...... ...... ", +" ...... ...... ", +" ...... ...... ", +" ...... ...... ", +" .... .... "}; diff --git a/app/src/input_events.h b/app/src/input_events.h deleted file mode 100644 index 1e34b50e..00000000 --- a/app/src/input_events.h +++ /dev/null @@ -1,530 +0,0 @@ -#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), -}; - -// Use the naming from SDL3 for gamepad axis and buttons: -// - -enum sc_gamepad_axis { - SC_GAMEPAD_AXIS_UNKNOWN = -1, - SC_GAMEPAD_AXIS_LEFTX = SDL_CONTROLLER_AXIS_LEFTX, - SC_GAMEPAD_AXIS_LEFTY = SDL_CONTROLLER_AXIS_LEFTY, - SC_GAMEPAD_AXIS_RIGHTX = SDL_CONTROLLER_AXIS_RIGHTX, - SC_GAMEPAD_AXIS_RIGHTY = SDL_CONTROLLER_AXIS_RIGHTY, - SC_GAMEPAD_AXIS_LEFT_TRIGGER = SDL_CONTROLLER_AXIS_TRIGGERLEFT, - SC_GAMEPAD_AXIS_RIGHT_TRIGGER = SDL_CONTROLLER_AXIS_TRIGGERRIGHT, -}; - -enum sc_gamepad_button { - SC_GAMEPAD_BUTTON_UNKNOWN = -1, - SC_GAMEPAD_BUTTON_SOUTH = SDL_CONTROLLER_BUTTON_A, - SC_GAMEPAD_BUTTON_EAST = SDL_CONTROLLER_BUTTON_B, - SC_GAMEPAD_BUTTON_WEST = SDL_CONTROLLER_BUTTON_X, - SC_GAMEPAD_BUTTON_NORTH = SDL_CONTROLLER_BUTTON_Y, - SC_GAMEPAD_BUTTON_BACK = SDL_CONTROLLER_BUTTON_BACK, - SC_GAMEPAD_BUTTON_GUIDE = SDL_CONTROLLER_BUTTON_GUIDE, - SC_GAMEPAD_BUTTON_START = SDL_CONTROLLER_BUTTON_START, - SC_GAMEPAD_BUTTON_LEFT_STICK = SDL_CONTROLLER_BUTTON_LEFTSTICK, - SC_GAMEPAD_BUTTON_RIGHT_STICK = SDL_CONTROLLER_BUTTON_RIGHTSTICK, - SC_GAMEPAD_BUTTON_LEFT_SHOULDER = SDL_CONTROLLER_BUTTON_LEFTSHOULDER, - SC_GAMEPAD_BUTTON_RIGHT_SHOULDER = SDL_CONTROLLER_BUTTON_RIGHTSHOULDER, - SC_GAMEPAD_BUTTON_DPAD_UP = SDL_CONTROLLER_BUTTON_DPAD_UP, - SC_GAMEPAD_BUTTON_DPAD_DOWN = SDL_CONTROLLER_BUTTON_DPAD_DOWN, - SC_GAMEPAD_BUTTON_DPAD_LEFT = SDL_CONTROLLER_BUTTON_DPAD_LEFT, - SC_GAMEPAD_BUTTON_DPAD_RIGHT = SDL_CONTROLLER_BUTTON_DPAD_RIGHT, -}; - -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; - int32_t hscroll_int; - int32_t vscroll_int; - 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; -}; - -// As documented in : -// The ID value starts at 0 and increments from there. The value -1 is an -// invalid ID. -#define SC_GAMEPAD_ID_INVALID UINT32_C(-1) - -struct sc_gamepad_device_event { - uint32_t gamepad_id; -}; - -struct sc_gamepad_button_event { - uint32_t gamepad_id; - enum sc_action action; - enum sc_gamepad_button button; -}; - -struct sc_gamepad_axis_event { - uint32_t gamepad_id; - enum sc_gamepad_axis axis; - int16_t value; -}; - -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) { - assert(buttons_state < 0x100); // fits in uint8_t - - // SC_MOUSE_BUTTON_* constants are initialized from SDL_BUTTON(index) - return buttons_state; -} - -static inline enum sc_gamepad_axis -sc_gamepad_axis_from_sdl(uint8_t axis) { - if (axis <= SDL_CONTROLLER_AXIS_TRIGGERRIGHT) { - // SC_GAMEPAD_AXIS_* constants are initialized from - // SDL_CONTROLLER_AXIS_* - return axis; - } - return SC_GAMEPAD_AXIS_UNKNOWN; -} - -static inline enum sc_gamepad_button -sc_gamepad_button_from_sdl(uint8_t button) { - if (button <= SDL_CONTROLLER_BUTTON_DPAD_RIGHT) { - // SC_GAMEPAD_BUTTON_* constants are initialized from - // SDL_CONTROLLER_BUTTON_* - return button; - } - return SC_GAMEPAD_BUTTON_UNKNOWN; -} - -static inline enum sc_action -sc_action_from_sdl_controllerbutton_type(uint32_t type) { - assert(type == SDL_CONTROLLERBUTTONDOWN || type == SDL_CONTROLLERBUTTONUP); - if (type == SDL_CONTROLLERBUTTONDOWN) { - return SC_ACTION_DOWN; - } - return SC_ACTION_UP; -} - -#endif diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 3e4dd0f3..fb8ef8f0 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -1,245 +1,146 @@ #include "input_manager.h" -#include -#include -#include -#include +#include +#include "convert.h" +#include "lock_util.h" +#include "log.h" -#include "android/input.h" -#include "android/keycodes.h" -#include "input_events.h" -#include "screen.h" -#include "shortcut_mod.h" -#include "util/log.h" - -void -sc_input_manager_init(struct sc_input_manager *im, - const struct sc_input_manager_params *params) { - // A key/mouse processor may not be present if there is no controller - assert((!params->kp && !params->mp && !params->gp) || params->controller); - // A processor must have ops initialized - assert(!params->kp || params->kp->ops); - assert(!params->mp || params->mp->ops); - assert(!params->gp || params->gp->ops); - - im->controller = params->controller; - im->fp = params->fp; - im->screen = params->screen; - im->kp = params->kp; - im->mp = params->mp; - im->gp = params->gp; - - im->mouse_bindings = params->mouse_bindings; - im->legacy_paste = params->legacy_paste; - im->clipboard_autosync = params->clipboard_autosync; - - im->sdl_shortcut_mods = sc_shortcut_mods_to_sdl(params->shortcut_mods); - - im->vfinger_down = false; - im->vfinger_invert_x = false; - im->vfinger_invert_y = false; - - im->mouse_buttons_state = 0; - - im->last_keycode = SDLK_UNKNOWN; - im->last_mod = 0; - im->key_repeat = 0; - - im->next_sequence = 1; // 0 is reserved for SC_SEQUENCE_INVALID +// Convert window coordinates (as provided by SDL_GetMouseState() to renderer +// coordinates (as provided in SDL mouse events) +// +// See my question: +// +static void +convert_to_renderer_coordinates(SDL_Renderer *renderer, int *x, int *y) { + SDL_Rect viewport; + float scale_x, scale_y; + SDL_RenderGetViewport(renderer, &viewport); + SDL_RenderGetScale(renderer, &scale_x, &scale_y); + *x = (int) (*x / scale_x) - viewport.x; + *y = (int) (*y / scale_y) - viewport.y; } -static void -send_keycode(struct sc_input_manager *im, enum android_keycode keycode, - enum sc_action action, const char *name) { - assert(im->controller && im->kp); +static struct point +get_mouse_point(struct screen *screen) { + int x; + int y; + SDL_GetMouseState(&x, &y); + convert_to_renderer_coordinates(screen->renderer, &x, &y); + return (struct point) { + .x = x, + .y = y, + }; +} +static const int ACTION_DOWN = 1; +static const int ACTION_UP = 1 << 1; + +static void +send_keycode(struct controller *controller, enum android_keycode keycode, + int actions, 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; + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_INJECT_KEYCODE; msg.inject_keycode.keycode = keycode; msg.inject_keycode.metastate = 0; - msg.inject_keycode.repeat = 0; - if (!sc_controller_push_msg(im->controller, &msg)) { - LOGW("Could not request 'inject %s'", name); + if (actions & ACTION_DOWN) { + msg.inject_keycode.action = AKEY_EVENT_ACTION_DOWN; + if (!controller_push_msg(controller, &msg)) { + LOGW("Cannot request 'inject %s (DOWN)'", name); + return; + } + } + + if (actions & ACTION_UP) { + msg.inject_keycode.action = AKEY_EVENT_ACTION_UP; + if (!controller_push_msg(controller, &msg)) { + LOGW("Cannot request 'inject %s (UP)'", name); + } } } static inline void -action_home(struct sc_input_manager *im, enum sc_action action) { - send_keycode(im, AKEYCODE_HOME, action, "HOME"); +action_home(struct controller *controller, int actions) { + send_keycode(controller, AKEYCODE_HOME, actions, "HOME"); } static inline void -action_back(struct sc_input_manager *im, enum sc_action action) { - send_keycode(im, AKEYCODE_BACK, action, "BACK"); +action_back(struct controller *controller, int actions) { + send_keycode(controller, AKEYCODE_BACK, actions, "BACK"); } static inline void -action_app_switch(struct sc_input_manager *im, enum sc_action action) { - send_keycode(im, AKEYCODE_APP_SWITCH, action, "APP_SWITCH"); +action_app_switch(struct controller *controller, int actions) { + send_keycode(controller, AKEYCODE_APP_SWITCH, actions, "APP_SWITCH"); } static inline void -action_power(struct sc_input_manager *im, enum sc_action action) { - send_keycode(im, AKEYCODE_POWER, action, "POWER"); +action_power(struct controller *controller, int actions) { + send_keycode(controller, AKEYCODE_POWER, actions, "POWER"); } static inline void -action_volume_up(struct sc_input_manager *im, enum sc_action action) { - send_keycode(im, AKEYCODE_VOLUME_UP, action, "VOLUME_UP"); +action_volume_up(struct controller *controller, int actions) { + send_keycode(controller, AKEYCODE_VOLUME_UP, actions, "VOLUME_UP"); } static inline void -action_volume_down(struct sc_input_manager *im, enum sc_action action) { - send_keycode(im, AKEYCODE_VOLUME_DOWN, action, "VOLUME_DOWN"); +action_volume_down(struct controller *controller, int actions) { + send_keycode(controller, AKEYCODE_VOLUME_DOWN, actions, "VOLUME_DOWN"); } static inline void -action_menu(struct sc_input_manager *im, enum sc_action action) { - send_keycode(im, AKEYCODE_MENU, action, "MENU"); +action_menu(struct controller *controller, int actions) { + send_keycode(controller, AKEYCODE_MENU, actions, "MENU"); } // turn the screen on if it was off, press BACK otherwise -// If the screen is off, it is turned on only on ACTION_DOWN static void -press_back_or_turn_screen_on(struct sc_input_manager *im, - enum sc_action action) { - assert(im->controller && im->kp); +press_back_or_turn_screen_on(struct controller *controller) { + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON; - struct sc_control_msg msg; - msg.type = SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON; - msg.back_or_screen_on.action = action == SC_ACTION_DOWN - ? AKEY_EVENT_ACTION_DOWN - : AKEY_EVENT_ACTION_UP; - - if (!sc_controller_push_msg(im->controller, &msg)) { - LOGW("Could not request 'press back or turn screen on'"); + if (!controller_push_msg(controller, &msg)) { + LOGW("Cannot request 'turn screen on'"); } } static void -expand_notification_panel(struct sc_input_manager *im) { - assert(im->controller); +expand_notification_panel(struct controller *controller) { + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL; - struct sc_control_msg msg; - msg.type = SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL; - - if (!sc_controller_push_msg(im->controller, &msg)) { - LOGW("Could not request 'expand notification panel'"); + if (!controller_push_msg(controller, &msg)) { + LOGW("Cannot request 'expand notification panel'"); } } static void -expand_settings_panel(struct sc_input_manager *im) { - assert(im->controller); +collapse_notification_panel(struct controller *controller) { + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL; - struct sc_control_msg msg; - msg.type = SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL; - - if (!sc_controller_push_msg(im->controller, &msg)) { - LOGW("Could not request 'expand settings panel'"); + if (!controller_push_msg(controller, &msg)) { + LOGW("Cannot request 'collapse notification panel'"); } } static void -collapse_panels(struct sc_input_manager *im) { - assert(im->controller); +request_device_clipboard(struct controller *controller) { + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_GET_CLIPBOARD; - struct sc_control_msg msg; - msg.type = SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS; - - if (!sc_controller_push_msg(im->controller, &msg)) { - LOGW("Could not request 'collapse notification panel'"); + if (!controller_push_msg(controller, &msg)) { + LOGW("Cannot request device clipboard"); } } -static bool -get_device_clipboard(struct sc_input_manager *im, enum sc_copy_key copy_key) { - assert(im->controller && im->kp); - - struct sc_control_msg msg; - msg.type = SC_CONTROL_MSG_TYPE_GET_CLIPBOARD; - msg.get_clipboard.copy_key = copy_key; - - if (!sc_controller_push_msg(im->controller, &msg)) { - LOGW("Could not request 'get device clipboard'"); - return false; - } - - return true; -} - -static bool -set_device_clipboard(struct sc_input_manager *im, bool paste, - uint64_t sequence) { - assert(im->controller && im->kp); - +static void +set_device_clipboard(struct controller *controller) { 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(im->controller, &msg)) { - free(text_dup); - LOGW("Could not request 'set device clipboard'"); - return false; - } - - return true; -} - -static void -set_display_power(struct sc_input_manager *im, bool on) { - assert(im->controller); - - struct sc_control_msg msg; - msg.type = SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER; - msg.set_display_power.on = on; - - if (!sc_controller_push_msg(im->controller, &msg)) { - LOGW("Could not request 'set screen power mode'"); - } -} - -static void -switch_fps_counter_state(struct sc_input_manager *im) { - struct sc_fps_counter *fps_counter = &im->screen->fps_counter; - - // the started state can only be written from the current thread, so there - // is no ToCToU issue - if (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_input_manager *im) { - assert(im->controller && im->kp); - - char *text = SDL_GetClipboardText(); - if (!text) { - LOGW("Could not get clipboard text: %s", SDL_GetError()); + LOGW("Cannot get clipboard text: %s", SDL_GetError()); return; } if (!*text) { @@ -248,840 +149,304 @@ clipboard_paste(struct sc_input_manager *im) { return; } - char *text_dup = strdup(text); - SDL_free(text); - if (!text_dup) { - LOGW("Could not strdup input text"); - return; - } + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_SET_CLIPBOARD; + msg.set_clipboard.text = text; - struct sc_control_msg msg; - msg.type = SC_CONTROL_MSG_TYPE_INJECT_TEXT; - msg.inject_text.text = text_dup; - if (!sc_controller_push_msg(im->controller, &msg)) { - free(text_dup); - LOGW("Could not request 'paste clipboard'"); + if (!controller_push_msg(controller, &msg)) { + SDL_free(text); + LOGW("Cannot request 'set device clipboard'"); } } static void -rotate_device(struct sc_input_manager *im) { - assert(im->controller); +set_screen_power_mode(struct controller *controller, + enum screen_power_mode mode) { + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; + msg.set_screen_power_mode.mode = mode; - struct sc_control_msg msg; - msg.type = SC_CONTROL_MSG_TYPE_ROTATE_DEVICE; - - if (!sc_controller_push_msg(im->controller, &msg)) { - LOGW("Could not request device rotation"); + if (!controller_push_msg(controller, &msg)) { + LOGW("Cannot request 'set screen power mode'"); } } static void -open_hard_keyboard_settings(struct sc_input_manager *im) { - assert(im->controller); - - struct sc_control_msg msg; - msg.type = SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS; - - if (!sc_controller_push_msg(im->controller, &msg)) { - LOGW("Could not request opening hard keyboard settings"); - } -} - -static void -reset_video(struct sc_input_manager *im) { - assert(im->controller); - - struct sc_control_msg msg; - msg.type = SC_CONTROL_MSG_TYPE_RESET_VIDEO; - - if (!sc_controller_push_msg(im->controller, &msg)) { - LOGW("Could not request reset video"); - } -} - -static void -apply_orientation_transform(struct sc_input_manager *im, - enum sc_orientation transform) { - struct sc_screen *screen = im->screen; - enum sc_orientation new_orientation = - sc_orientation_apply(screen->orientation, transform); - sc_screen_set_orientation(screen, new_orientation); -} - -static void -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 (sc_shortcut_mods_is_shortcut_mod(im->sdl_shortcut_mods, - 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 = SC_POINTER_ID_VIRTUAL_FINGER; - msg.inject_touch_event.pressure = up ? 0.0f : 1.0f; - msg.inject_touch_event.action_button = 0; - msg.inject_touch_event.buttons = 0; - - if (!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, - bool invert_x, bool invert_y) { - if (invert_x) { - point.x = size.width - point.x; - } - if (invert_y) { - 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 - bool control = im->controller; - bool paused = im->screen->paused; - bool video = im->screen->video; - - SDL_Keycode sdl_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; - - // Either the modifier includes a shortcut modifier, or the key - // press/release is a modifier key. - // The second condition is necessary to ignore the release of the modifier - // key (because in this case mod is 0). - uint16_t mods = im->sdl_shortcut_mods; - bool is_shortcut = sc_shortcut_mods_is_shortcut_mod(mods, mod) - || sc_shortcut_mods_is_shortcut_key(mods, sdl_keycode); - - if (down && !repeat) { - if (sdl_keycode == im->last_keycode && mod == im->last_mod) { - ++im->key_repeat; - } else { - im->key_repeat = 0; - im->last_keycode = sdl_keycode; - im->last_mod = mod; - } - } - - if (is_shortcut) { - enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP; - switch (sdl_keycode) { - case SDLK_h: - if (im->kp && !shift && !repeat && !paused) { - action_home(im, action); - } - return; - case SDLK_b: // fall-through - case SDLK_BACKSPACE: - if (im->kp && !shift && !repeat && !paused) { - action_back(im, action); - } - return; - case SDLK_s: - if (im->kp && !shift && !repeat && !paused) { - action_app_switch(im, action); - } - return; - case SDLK_m: - if (im->kp && !shift && !repeat && !paused) { - action_menu(im, action); - } - return; - case SDLK_p: - if (im->kp && !shift && !repeat && !paused) { - action_power(im, action); - } - return; - case SDLK_o: - if (control && !repeat && down && !paused) { - bool on = shift; - set_display_power(im, on); - } - return; - case SDLK_z: - if (video && down && !repeat) { - sc_screen_set_paused(im->screen, !shift); - } - return; - case SDLK_DOWN: - if (shift) { - if (video && !repeat && down) { - apply_orientation_transform(im, - SC_ORIENTATION_FLIP_180); - } - } else if (im->kp && !paused) { - // forward repeated events - action_volume_down(im, action); - } - return; - case SDLK_UP: - if (shift) { - if (video && !repeat && down) { - apply_orientation_transform(im, - SC_ORIENTATION_FLIP_180); - } - } else if (im->kp && !paused) { - // forward repeated events - action_volume_up(im, action); - } - return; - case SDLK_LEFT: - if (video && !repeat && down) { - if (shift) { - apply_orientation_transform(im, - SC_ORIENTATION_FLIP_0); - } else { - apply_orientation_transform(im, - SC_ORIENTATION_270); - } - } - return; - case SDLK_RIGHT: - if (video && !repeat && down) { - if (shift) { - apply_orientation_transform(im, - SC_ORIENTATION_FLIP_0); - } else { - apply_orientation_transform(im, - SC_ORIENTATION_90); - } - } - return; - case SDLK_c: - if (im->kp && !shift && !repeat && down && !paused) { - get_device_clipboard(im, SC_COPY_KEY_COPY); - } - return; - case SDLK_x: - if (im->kp && !shift && !repeat && down && !paused) { - get_device_clipboard(im, SC_COPY_KEY_CUT); - } - return; - case SDLK_v: - if (im->kp && !repeat && down && !paused) { - if (shift || im->legacy_paste) { - // inject the text as input events - clipboard_paste(im); - } else { - // store the text in the device clipboard and paste, - // without requesting an acknowledgment - set_device_clipboard(im, true, SC_SEQUENCE_INVALID); - } - } - return; - case SDLK_f: - if (video && !shift && !repeat && down) { - sc_screen_toggle_fullscreen(im->screen); - } - return; - case SDLK_w: - if (video && !shift && !repeat && down) { - sc_screen_resize_to_fit(im->screen); - } - return; - case SDLK_g: - if (video && !shift && !repeat && down) { - sc_screen_resize_to_pixel_perfect(im->screen); - } - return; - case SDLK_i: - if (video && !shift && !repeat && down) { - switch_fps_counter_state(im); - } - return; - case SDLK_n: - if (control && !repeat && down && !paused) { - if (shift) { - collapse_panels(im); - } else if (im->key_repeat == 0) { - expand_notification_panel(im); - } else { - expand_settings_panel(im); - } - } - return; - case SDLK_r: - if (control && !repeat && down && !paused) { - if (shift) { - reset_video(im); - } else { - rotate_device(im); - } - } - return; - case SDLK_k: - if (control && !shift && !repeat && down && !paused - && im->kp && im->kp->hid) { - // Only if the current keyboard is hid - open_hard_keyboard_settings(im); - } - return; - } - - return; - } - - if (!im->kp || paused) { - return; - } - - uint64_t ack_to_wait = SC_SEQUENCE_INVALID; - bool is_ctrl_v = ctrl && !shift && sdl_keycode == SDLK_v && down && !repeat; - if (im->clipboard_autosync && is_ctrl_v) { - if (im->legacy_paste) { - // inject the text as input events - clipboard_paste(im); - return; - } - - // Request an acknowledgement only if necessary - uint64_t sequence = im->kp->async_paste ? im->next_sequence - : SC_SEQUENCE_INVALID; - - // Synchronize the computer clipboard to the device clipboard before - // sending Ctrl+v, to allow seamless copy-paste. - bool ok = set_device_clipboard(im, 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; - } - } - - enum sc_keycode keycode = sc_keycode_from_sdl(sdl_keycode); - if (keycode == SC_KEYCODE_UNKNOWN) { - return; - } - - enum sc_scancode scancode = sc_scancode_from_sdl(event->keysym.scancode); - if (scancode == SC_SCANCODE_UNKNOWN) { - return; - } - - struct sc_key_event evt = { - .action = sc_action_from_sdl_keyboard_type(event->type), - .keycode = keycode, - .scancode = 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 struct sc_position -sc_input_manager_get_position(struct sc_input_manager *im, int32_t x, - int32_t y) { - if (im->mp->relative_mode) { - // No absolute position - return (struct sc_position) { - .screen_size = {0, 0}, - .point = {0, 0}, - }; - } - - return (struct sc_position) { - .screen_size = im->screen->frame_size, - .point = sc_screen_convert_window_to_frame_coords(im->screen, x, y), - }; -} - -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 = sc_input_manager_get_position(im, event->x, event->y), - .pointer_id = im->vfinger_down ? SC_POINTER_ID_GENERIC_FINGER - : SC_POINTER_ID_MOUSE, - .xrel = event->xrel, - .yrel = event->yrel, - .buttons_state = im->mouse_buttons_state, - }; - - 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, - im->vfinger_invert_x, - im->vfinger_invert_y); - 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 enum sc_mouse_binding -sc_input_manager_get_binding(const struct sc_mouse_binding_set *bindings, - uint8_t sdl_button) { - switch (sdl_button) { - case SDL_BUTTON_LEFT: - return SC_MOUSE_BINDING_CLICK; - case SDL_BUTTON_RIGHT: - return bindings->right_click; - case SDL_BUTTON_MIDDLE: - return bindings->middle_click; - case SDL_BUTTON_X1: - return bindings->click4; - case SDL_BUTTON_X2: - return bindings->click5; - default: - return SC_MOUSE_BINDING_DISABLED; - } -} - -static void -sc_input_manager_process_mouse_button(struct sc_input_manager *im, - const SDL_MouseButtonEvent *event) { - if (event->which == SDL_TOUCH_MOUSEID) { - // simulated from touch events, so it's a duplicate - return; - } - - bool control = im->controller; - bool paused = im->screen->paused; - bool down = event->type == SDL_MOUSEBUTTONDOWN; - - enum sc_mouse_button button = sc_mouse_button_from_sdl(event->button); - if (button == SC_MOUSE_BUTTON_UNKNOWN) { - return; - } - - if (!down) { - // Mark the button as released - im->mouse_buttons_state &= ~button; - } - - SDL_Keymod keymod = SDL_GetModState(); - bool ctrl_pressed = keymod & KMOD_CTRL; - bool shift_pressed = keymod & KMOD_SHIFT; - - if (control && !paused) { - enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP; - - struct sc_mouse_binding_set *bindings = !shift_pressed - ? &im->mouse_bindings.pri - : &im->mouse_bindings.sec; - enum sc_mouse_binding binding = - sc_input_manager_get_binding(bindings, event->button); - assert(binding != SC_MOUSE_BINDING_AUTO); - switch (binding) { - case SC_MOUSE_BINDING_DISABLED: - // ignore click - return; - case SC_MOUSE_BINDING_BACK: - if (im->kp) { - press_back_or_turn_screen_on(im, action); - } - return; - case SC_MOUSE_BINDING_HOME: - if (im->kp) { - action_home(im, action); - } - return; - case SC_MOUSE_BINDING_APP_SWITCH: - if (im->kp) { - action_app_switch(im, action); - } - return; - case SC_MOUSE_BINDING_EXPAND_NOTIFICATION_PANEL: - if (down) { - if (event->clicks < 2) { - expand_notification_panel(im); - } else { - expand_settings_panel(im); - } - } - return; - default: - assert(binding == SC_MOUSE_BINDING_CLICK); - break; - } - } - - // double-click on black borders resizes to fit the device screen - bool video = im->screen->video; - bool mouse_relative_mode = im->mp && im->mp->relative_mode; - if (video && !mouse_relative_mode && 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; - } - } - - if (!im->mp || paused) { - return; - } - - if (down) { - // Mark the button as pressed - im->mouse_buttons_state |= button; - } - - bool change_vfinger = event->button == SDL_BUTTON_LEFT && - ((down && !im->vfinger_down && (ctrl_pressed || shift_pressed)) || - (!down && im->vfinger_down)); - bool use_finger = im->vfinger_down || change_vfinger; - - struct sc_mouse_click_event evt = { - .position = sc_input_manager_get_position(im, event->x, event->y), - .action = sc_action_from_sdl_mousebutton_type(event->type), - .button = button, - .pointer_id = use_finger ? SC_POINTER_ID_GENERIC_FINGER - : SC_POINTER_ID_MOUSE, - .buttons_state = im->mouse_buttons_state, - }; - - 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, rotate and tilt simulation. - // - // If Ctrl is hold when the left-click button is pressed, then - // pinch-to-zoom mode is enabled: on every mouse event until the left-click - // 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. - // - // To simulate a vertical tilt gesture (a vertical slide with two fingers), - // Shift can be used instead of Ctrl. The "virtual finger" has a position - // inverted with respect to the vertical axis of symmetry in the middle of - // the screen. - // - // To simulate a horizontal tilt gesture (a horizontal slide with two - // fingers), Ctrl+Shift can be used. The "virtual finger" has a position - // inverted with respect to the horizontal axis of symmetry in the middle - // of the screen. It is expected to be less frequently used, that's why the - // one-mod shortcuts are assigned to rotation and vertical tilt. - if (change_vfinger) { - struct sc_point mouse = - sc_screen_convert_window_to_frame_coords(im->screen, event->x, - event->y); - if (down) { - // Ctrl Shift invert_x invert_y - // ---- ----- ==> -------- -------- - // 0 0 0 0 - - // 0 1 1 0 vertical tilt - // 1 0 1 1 rotate - // 1 1 0 1 horizontal tilt - im->vfinger_invert_x = ctrl_pressed ^ shift_pressed; - im->vfinger_invert_y = ctrl_pressed; - } - struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size, - im->vfinger_invert_x, - im->vfinger_invert_y); - enum android_motionevent_action action = down - ? AMOTION_EVENT_ACTION_DOWN - : AMOTION_EVENT_ACTION_UP; - 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); - (void) buttons; // Actual buttons are tracked manually to ignore shortcuts - - struct sc_mouse_scroll_event evt = { - .position = sc_input_manager_get_position(im, mouse_x, mouse_y), -#if SDL_VERSION_ATLEAST(2, 0, 18) - .hscroll = event->preciseX, - .vscroll = event->preciseY, -#else - .hscroll = event->x, - .vscroll = event->y, -#endif - .hscroll_int = event->x, - .vscroll_int = event->y, - .buttons_state = im->mouse_buttons_state, - }; - - im->mp->ops->process_mouse_scroll(im->mp, &evt); -} - -static void -sc_input_manager_process_gamepad_device(struct sc_input_manager *im, - const SDL_ControllerDeviceEvent *event) { - if (event->type == SDL_CONTROLLERDEVICEADDED) { - SDL_GameController *gc = SDL_GameControllerOpen(event->which); - if (!gc) { - LOGW("Could not open game controller"); - return; - } - - SDL_Joystick *joystick = SDL_GameControllerGetJoystick(gc); - if (!joystick) { - LOGW("Could not get controller joystick"); - SDL_GameControllerClose(gc); - return; - } - - struct sc_gamepad_device_event evt = { - .gamepad_id = SDL_JoystickInstanceID(joystick), - }; - im->gp->ops->process_gamepad_added(im->gp, &evt); - } else if (event->type == SDL_CONTROLLERDEVICEREMOVED) { - SDL_JoystickID id = event->which; - - SDL_GameController *gc = SDL_GameControllerFromInstanceID(id); - if (gc) { - SDL_GameControllerClose(gc); - } else { - LOGW("Unknown gamepad device removed"); - } - - struct sc_gamepad_device_event evt = { - .gamepad_id = id, - }; - im->gp->ops->process_gamepad_removed(im->gp, &evt); +switch_fps_counter_state(struct fps_counter *fps_counter) { + // the started state can only be written from the current thread, so there + // is no ToCToU issue + if (fps_counter_is_started(fps_counter)) { + fps_counter_stop(fps_counter); + LOGI("FPS counter stopped"); } else { - // Nothing to do - return; + if (fps_counter_start(fps_counter)) { + LOGI("FPS counter started"); + } else { + LOGE("FPS counter starting failed"); + } } } static void -sc_input_manager_process_gamepad_axis(struct sc_input_manager *im, - const SDL_ControllerAxisEvent *event) { - enum sc_gamepad_axis axis = sc_gamepad_axis_from_sdl(event->axis); - if (axis == SC_GAMEPAD_AXIS_UNKNOWN) { +clipboard_paste(struct controller *controller) { + char *text = SDL_GetClipboardText(); + if (!text) { + LOGW("Cannot get clipboard text: %s", SDL_GetError()); + return; + } + if (!*text) { + // empty text + SDL_free(text); return; } - struct sc_gamepad_axis_event evt = { - .gamepad_id = event->which, - .axis = axis, - .value = event->value, - }; - im->gp->ops->process_gamepad_axis(im->gp, &evt); -} - -static void -sc_input_manager_process_gamepad_button(struct sc_input_manager *im, - const SDL_ControllerButtonEvent *event) { - enum sc_gamepad_button button = sc_gamepad_button_from_sdl(event->button); - if (button == SC_GAMEPAD_BUTTON_UNKNOWN) { - return; - } - - struct sc_gamepad_button_event evt = { - .gamepad_id = event->which, - .action = sc_action_from_sdl_controllerbutton_type(event->type), - .button = button, - }; - im->gp->ops->process_gamepad_button(im->gp, &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); + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; + msg.inject_text.text = text; + if (!controller_push_msg(controller, &msg)) { + SDL_free(text); + LOGW("Cannot request 'paste clipboard'"); } } void -sc_input_manager_handle_event(struct sc_input_manager *im, - const SDL_Event *event) { - bool control = im->controller; - bool paused = im->screen->paused; - switch (event->type) { - case SDL_TEXTINPUT: - if (!im->kp || paused) { - 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 (!im->mp || paused) { - break; - } - sc_input_manager_process_mouse_motion(im, &event->motion); - break; - case SDL_MOUSEWHEEL: - if (!im->mp || paused) { - 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 (!im->mp || paused) { - break; - } - sc_input_manager_process_touch(im, &event->tfinger); - break; - case SDL_CONTROLLERDEVICEADDED: - case SDL_CONTROLLERDEVICEREMOVED: - // Handle device added or removed even if paused - if (!im->gp) { - break; - } - sc_input_manager_process_gamepad_device(im, &event->cdevice); - break; - case SDL_CONTROLLERAXISMOTION: - if (!im->gp || paused) { - break; - } - sc_input_manager_process_gamepad_axis(im, &event->caxis); - break; - case SDL_CONTROLLERBUTTONDOWN: - case SDL_CONTROLLERBUTTONUP: - if (!im->gp || paused) { - break; - } - sc_input_manager_process_gamepad_button(im, &event->cbutton); - break; - case SDL_DROPFILE: { - if (!control) { - break; - } - sc_input_manager_process_file(im, &event->drop); +input_manager_process_text_input(struct input_manager *input_manager, + const SDL_TextInputEvent *event) { + char c = event->text[0]; + if (isalpha(c) || c == ' ') { + SDL_assert(event->text[1] == '\0'); + // letters and space are handled as raw key event + return; + } + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; + msg.inject_text.text = SDL_strdup(event->text); + if (!msg.inject_text.text) { + LOGW("Cannot strdup input text"); + return; + } + if (!controller_push_msg(input_manager->controller, &msg)) { + SDL_free(msg.inject_text.text); + LOGW("Cannot request 'inject text'"); + } +} + +void +input_manager_process_key(struct input_manager *input_manager, + const SDL_KeyboardEvent *event, + bool control) { + // control: indicates the state of the command-line option --no-control + // ctrl: the Ctrl key + + bool ctrl = event->keysym.mod & (KMOD_LCTRL | KMOD_RCTRL); + bool alt = event->keysym.mod & (KMOD_LALT | KMOD_RALT); + bool meta = event->keysym.mod & (KMOD_LGUI | KMOD_RGUI); + + if (alt) { + // no shortcut involves Alt or Meta, and they should not be forwarded + // to the device + return; + } + + struct controller *controller = input_manager->controller; + + // capture all Ctrl events + if (ctrl | meta) { + SDL_Keycode keycode = event->keysym.sym; + bool down = event->type == SDL_KEYDOWN; + int action = down ? ACTION_DOWN : ACTION_UP; + bool repeat = event->repeat; + bool shift = event->keysym.mod & (KMOD_LSHIFT | KMOD_RSHIFT); + switch (keycode) { + case SDLK_h: + if (control && ctrl && !meta && !shift && !repeat) { + action_home(controller, action); + } + return; + case SDLK_b: // fall-through + case SDLK_BACKSPACE: + if (control && ctrl && !meta && !shift && !repeat) { + action_back(controller, action); + } + return; + case SDLK_s: + if (control && ctrl && !meta && !shift && !repeat) { + action_app_switch(controller, action); + } + return; + case SDLK_m: + if (control && ctrl && !meta && !shift && !repeat) { + action_menu(controller, action); + } + return; + case SDLK_p: + if (control && ctrl && !meta && !shift && !repeat) { + action_power(controller, action); + } + return; + case SDLK_o: + if (control && ctrl && !shift && !meta && down) { + set_screen_power_mode(controller, SCREEN_POWER_MODE_OFF); + } + return; + case SDLK_DOWN: +#ifdef __APPLE__ + if (control && !ctrl && meta && !shift) { +#else + if (control && ctrl && !meta && !shift) { +#endif + // forward repeated events + action_volume_down(controller, action); + } + return; + case SDLK_UP: +#ifdef __APPLE__ + if (control && !ctrl && meta && !shift) { +#else + if (control && ctrl && !meta && !shift) { +#endif + // forward repeated events + action_volume_up(controller, action); + } + return; + case SDLK_c: + if (control && ctrl && !meta && !shift && !repeat && down) { + request_device_clipboard(controller); + } + return; + case SDLK_v: + if (control && ctrl && !meta && !repeat && down) { + if (shift) { + // store the text in the device clipboard + set_device_clipboard(controller); + } else { + // inject the text as input events + clipboard_paste(controller); + } + } + return; + case SDLK_f: + if (ctrl && !meta && !shift && !repeat && down) { + screen_switch_fullscreen(input_manager->screen); + } + return; + case SDLK_x: + if (ctrl && !meta && !shift && !repeat && down) { + screen_resize_to_fit(input_manager->screen); + } + return; + case SDLK_g: + if (ctrl && !meta && !shift && !repeat && down) { + screen_resize_to_pixel_perfect(input_manager->screen); + } + return; + case SDLK_i: + if (ctrl && !meta && !shift && !repeat && down) { + struct fps_counter *fps_counter = + input_manager->video_buffer->fps_counter; + switch_fps_counter_state(fps_counter); + } + return; + case SDLK_n: + if (control && ctrl && !meta && !repeat && down) { + if (shift) { + collapse_notification_panel(controller); + } else { + expand_notification_panel(controller); + } + } + return; + } + + return; + } + + if (!control) { + return; + } + + struct control_msg msg; + if (input_key_from_sdl_to_android(event, &msg)) { + if (!controller_push_msg(controller, &msg)) { + LOGW("Cannot request 'inject keycode'"); + } + } +} + +void +input_manager_process_mouse_motion(struct input_manager *input_manager, + const SDL_MouseMotionEvent *event) { + if (!event->state) { + // do not send motion events when no button is pressed + return; + } + struct control_msg msg; + if (mouse_motion_from_sdl_to_android(event, + input_manager->screen->frame_size, + &msg)) { + if (!controller_push_msg(input_manager->controller, &msg)) { + LOGW("Cannot request 'inject mouse motion event'"); + } + } +} + +static bool +is_outside_device_screen(struct input_manager *input_manager, int x, int y) +{ + return x < 0 || x >= input_manager->screen->frame_size.width || + y < 0 || y >= input_manager->screen->frame_size.height; +} + +void +input_manager_process_mouse_button(struct input_manager *input_manager, + const SDL_MouseButtonEvent *event, + bool control) { + if (event->type == SDL_MOUSEBUTTONDOWN) { + if (control && event->button == SDL_BUTTON_RIGHT) { + press_back_or_turn_screen_on(input_manager->controller); + return; + } + if (control && event->button == SDL_BUTTON_MIDDLE) { + action_home(input_manager->controller, ACTION_DOWN | ACTION_UP); + return; + } + // double-click on black borders resize to fit the device screen + if (event->button == SDL_BUTTON_LEFT && event->clicks == 2) { + bool outside = + is_outside_device_screen(input_manager, event->x, event->y); + if (outside) { + screen_resize_to_fit(input_manager->screen); + return; + } + } + // otherwise, send the click event to the device + } + + if (!control) { + return; + } + + struct control_msg msg; + if (mouse_button_from_sdl_to_android(event, + input_manager->screen->frame_size, + &msg)) { + if (!controller_push_msg(input_manager->controller, &msg)) { + LOGW("Cannot request 'inject mouse button event'"); + } + } +} + +void +input_manager_process_mouse_wheel(struct input_manager *input_manager, + const SDL_MouseWheelEvent *event) { + struct position position = { + .screen_size = input_manager->screen->frame_size, + .point = get_mouse_point(input_manager->screen), + }; + struct control_msg msg; + if (mouse_wheel_from_sdl_to_android(event, position, &msg)) { + if (!controller_push_msg(input_manager->controller, &msg)) { + LOGW("Cannot request 'inject mouse wheel event'"); } } } diff --git a/app/src/input_manager.h b/app/src/input_manager.h index af4cbc69..83cb7405 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -1,71 +1,40 @@ -#ifndef SC_INPUTMANAGER_H -#define SC_INPUTMANAGER_H - -#include "common.h" +#ifndef INPUTMANAGER_H +#define INPUTMANAGER_H #include -#include -#include -#include +#include "common.h" #include "controller.h" -#include "file_pusher.h" -#include "options.h" -#include "trait/gamepad_processor.h" -#include "trait/key_processor.h" -#include "trait/mouse_processor.h" +#include "fps_counter.h" +#include "video_buffer.h" +#include "screen.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; - struct sc_gamepad_processor *gp; - - struct sc_mouse_bindings mouse_bindings; - bool legacy_paste; - bool clipboard_autosync; - - uint16_t sdl_shortcut_mods; - - bool vfinger_down; - bool vfinger_invert_x; - bool vfinger_invert_y; - - uint8_t mouse_buttons_state; // OR of enum sc_mouse_button values - - // 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; - struct sc_gamepad_processor *gp; - - struct sc_mouse_bindings mouse_bindings; - bool legacy_paste; - bool clipboard_autosync; - uint8_t shortcut_mods; // OR of enum sc_shortcut_mod values +struct input_manager { + struct controller *controller; + struct video_buffer *video_buffer; + struct screen *screen; }; void -sc_input_manager_init(struct sc_input_manager *im, - const struct sc_input_manager_params *params); +input_manager_process_text_input(struct input_manager *input_manager, + const SDL_TextInputEvent *event); void -sc_input_manager_handle_event(struct sc_input_manager *im, - const SDL_Event *event); +input_manager_process_key(struct input_manager *input_manager, + const SDL_KeyboardEvent *event, + bool control); + +void +input_manager_process_mouse_motion(struct input_manager *input_manager, + const SDL_MouseMotionEvent *event); + +void +input_manager_process_mouse_button(struct input_manager *input_manager, + const SDL_MouseButtonEvent *event, + bool control); + +void +input_manager_process_mouse_wheel(struct input_manager *input_manager, + const SDL_MouseWheelEvent *event); #endif diff --git a/app/src/keyboard_sdk.c b/app/src/keyboard_sdk.c deleted file mode 100644 index 466a1aeb..00000000 --- a/app/src/keyboard_sdk.c +++ /dev/null @@ -1,350 +0,0 @@ -#include "keyboard_sdk.h" - -#include -#include -#include -#include -#include - -#include "android/input.h" -#include "android/keycodes.h" -#include "control_msg.h" -#include "controller.h" -#include "input_events.h" -#include "util/intmap.h" -#include "util/log.h" - -/** Downcast key processor to sc_keyboard_sdk */ -#define DOWNCAST(KP) container_of(KP, struct sc_keyboard_sdk, key_processor) - -static enum android_keyevent_action -convert_keycode_action(enum sc_action action) { - if (action == SC_ACTION_DOWN) { - return AKEY_EVENT_ACTION_DOWN; - } - assert(action == SC_ACTION_UP); - return AKEY_EVENT_ACTION_UP; -} - -static bool -convert_keycode(enum sc_keycode from, enum android_keycode *to, uint16_t mod, - enum sc_key_inject_mode key_inject_mode) { - // Navigation keys and ENTER. - // Used in all modes. - static const struct sc_intmap_entry special_keys[] = { - {SC_KEYCODE_RETURN, AKEYCODE_ENTER}, - {SC_KEYCODE_KP_ENTER, AKEYCODE_NUMPAD_ENTER}, - {SC_KEYCODE_ESCAPE, AKEYCODE_ESCAPE}, - {SC_KEYCODE_BACKSPACE, AKEYCODE_DEL}, - {SC_KEYCODE_TAB, AKEYCODE_TAB}, - {SC_KEYCODE_PAGEUP, AKEYCODE_PAGE_UP}, - {SC_KEYCODE_DELETE, AKEYCODE_FORWARD_DEL}, - {SC_KEYCODE_HOME, AKEYCODE_MOVE_HOME}, - {SC_KEYCODE_END, AKEYCODE_MOVE_END}, - {SC_KEYCODE_PAGEDOWN, AKEYCODE_PAGE_DOWN}, - {SC_KEYCODE_RIGHT, AKEYCODE_DPAD_RIGHT}, - {SC_KEYCODE_LEFT, AKEYCODE_DPAD_LEFT}, - {SC_KEYCODE_DOWN, AKEYCODE_DPAD_DOWN}, - {SC_KEYCODE_UP, AKEYCODE_DPAD_UP}, - {SC_KEYCODE_LCTRL, AKEYCODE_CTRL_LEFT}, - {SC_KEYCODE_RCTRL, AKEYCODE_CTRL_RIGHT}, - {SC_KEYCODE_LSHIFT, AKEYCODE_SHIFT_LEFT}, - {SC_KEYCODE_RSHIFT, AKEYCODE_SHIFT_RIGHT}, - {SC_KEYCODE_LALT, AKEYCODE_ALT_LEFT}, - {SC_KEYCODE_RALT, AKEYCODE_ALT_RIGHT}, - {SC_KEYCODE_LGUI, AKEYCODE_META_LEFT}, - {SC_KEYCODE_RGUI, AKEYCODE_META_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; - } - - // Handle letters and space - entry = SC_INTMAP_FIND_ENTRY(alphaspace_keys, from); - if (entry) { - *to = entry->value; - return true; - } - - if (key_inject_mode == SC_KEY_INJECT_MODE_RAW) { - entry = SC_INTMAP_FIND_ENTRY(numbers_punct_keys, from); - if (entry) { - *to = entry->value; - return true; - } - } - - return false; -} - -static enum android_metastate -autocomplete_metastate(enum android_metastate metastate) { - // fill dependent flags - if (metastate & (AMETA_SHIFT_LEFT_ON | AMETA_SHIFT_RIGHT_ON)) { - metastate |= AMETA_SHIFT_ON; - } - if (metastate & (AMETA_CTRL_LEFT_ON | AMETA_CTRL_RIGHT_ON)) { - metastate |= AMETA_CTRL_ON; - } - if (metastate & (AMETA_ALT_LEFT_ON | AMETA_ALT_RIGHT_ON)) { - metastate |= AMETA_ALT_ON; - } - if (metastate & (AMETA_META_LEFT_ON | AMETA_META_RIGHT_ON)) { - metastate |= AMETA_META_ON; - } - - return metastate; -} - -static enum android_metastate -convert_meta_state(uint16_t mod) { - enum android_metastate metastate = 0; - if (mod & SC_MOD_LSHIFT) { - metastate |= AMETA_SHIFT_LEFT_ON; - } - if (mod & SC_MOD_RSHIFT) { - metastate |= AMETA_SHIFT_RIGHT_ON; - } - if (mod & SC_MOD_LCTRL) { - metastate |= AMETA_CTRL_LEFT_ON; - } - if (mod & SC_MOD_RCTRL) { - metastate |= AMETA_CTRL_RIGHT_ON; - } - if (mod & SC_MOD_LALT) { - metastate |= AMETA_ALT_LEFT_ON; - } - if (mod & SC_MOD_RALT) { - metastate |= AMETA_ALT_RIGHT_ON; - } - if (mod & SC_MOD_LGUI) { // Windows key - metastate |= AMETA_META_LEFT_ON; - } - if (mod & SC_MOD_RGUI) { // Windows key - metastate |= AMETA_META_RIGHT_ON; - } - if (mod & SC_MOD_NUM) { - metastate |= AMETA_NUM_LOCK_ON; - } - if (mod & SC_MOD_CAPS) { - metastate |= AMETA_CAPS_LOCK_ON; - } - - // fill the dependent fields - return autocomplete_metastate(metastate); -} - -static bool -convert_input_key(const struct sc_key_event *event, struct sc_control_msg *msg, - enum sc_key_inject_mode key_inject_mode, uint32_t repeat) { - msg->type = SC_CONTROL_MSG_TYPE_INJECT_KEYCODE; - - if (!convert_keycode(event->keycode, &msg->inject_keycode.keycode, - event->mods_state, key_inject_mode)) { - return false; - } - - msg->inject_keycode.action = convert_keycode_action(event->action); - msg->inject_keycode.repeat = repeat; - msg->inject_keycode.metastate = convert_meta_state(event->mods_state); - - return true; -} - -static void -sc_key_processor_process_key(struct sc_key_processor *kp, - const struct sc_key_event *event, - uint64_t ack_to_wait) { - // The device clipboard synchronization and the key event messages are - // serialized, there is nothing special to do to ensure that the clipboard - // is set before injecting Ctrl+v. - (void) ack_to_wait; - - struct sc_keyboard_sdk *kb = DOWNCAST(kp); - - if (event->repeat) { - if (!kb->forward_key_repeat) { - return; - } - ++kb->repeat; - } else { - kb->repeat = 0; - } - - struct sc_control_msg msg; - if (convert_input_key(event, &msg, kb->key_inject_mode, kb->repeat)) { - if (!sc_controller_push_msg(kb->controller, &msg)) { - LOGW("Could not request 'inject keycode'"); - } - } -} - -static void -sc_key_processor_process_text(struct sc_key_processor *kp, - const struct sc_text_event *event) { - struct sc_keyboard_sdk *kb = DOWNCAST(kp); - - if (kb->key_inject_mode == SC_KEY_INJECT_MODE_RAW) { - // Never inject text events - return; - } - - if (kb->key_inject_mode == SC_KEY_INJECT_MODE_MIXED) { - char c = event->text[0]; - if (isalpha(c) || c == ' ') { - assert(event->text[1] == '\0'); - // Letters and space are handled as raw key events - return; - } - } - - struct sc_control_msg msg; - msg.type = SC_CONTROL_MSG_TYPE_INJECT_TEXT; - msg.inject_text.text = strdup(event->text); - if (!msg.inject_text.text) { - LOGW("Could not strdup input text"); - return; - } - if (!sc_controller_push_msg(kb->controller, &msg)) { - free(msg.inject_text.text); - LOGW("Could not request 'inject text'"); - } -} - -void -sc_keyboard_sdk_init(struct sc_keyboard_sdk *kb, - struct sc_controller *controller, - enum sc_key_inject_mode key_inject_mode, - bool forward_key_repeat) { - kb->controller = controller; - kb->key_inject_mode = key_inject_mode; - kb->forward_key_repeat = forward_key_repeat; - - kb->repeat = 0; - - static const struct sc_key_processor_ops ops = { - .process_key = sc_key_processor_process_key, - .process_text = sc_key_processor_process_text, - }; - - // Key injection and clipboard synchronization are serialized - kb->key_processor.async_paste = false; - kb->key_processor.hid = false; - kb->key_processor.ops = &ops; -} diff --git a/app/src/keyboard_sdk.h b/app/src/keyboard_sdk.h deleted file mode 100644 index 700ba90b..00000000 --- a/app/src/keyboard_sdk.h +++ /dev/null @@ -1,31 +0,0 @@ -#ifndef SC_KEYBOARD_SDK_H -#define SC_KEYBOARD_SDK_H - -#include "common.h" - -#include - -#include "controller.h" -#include "options.h" -#include "trait/key_processor.h" - -struct sc_keyboard_sdk { - struct sc_key_processor key_processor; // key processor trait - - struct sc_controller *controller; - - // SDL reports repeated events as a boolean, but Android expects the actual - // number of repetitions. This variable keeps track of the count. - unsigned repeat; - - enum sc_key_inject_mode key_inject_mode; - bool forward_key_repeat; -}; - -void -sc_keyboard_sdk_init(struct sc_keyboard_sdk *kb, - struct sc_controller *controller, - enum sc_key_inject_mode key_inject_mode, - bool forward_key_repeat); - -#endif diff --git a/app/src/lock_util.h b/app/src/lock_util.h new file mode 100644 index 00000000..d1ca7336 --- /dev/null +++ b/app/src/lock_util.h @@ -0,0 +1,51 @@ +#ifndef LOCKUTIL_H +#define LOCKUTIL_H + +#include +#include + +#include "log.h" + +static inline void +mutex_lock(SDL_mutex *mutex) { + if (SDL_LockMutex(mutex)) { + LOGC("Could not lock mutex"); + abort(); + } +} + +static inline void +mutex_unlock(SDL_mutex *mutex) { + if (SDL_UnlockMutex(mutex)) { + LOGC("Could not unlock mutex"); + abort(); + } +} + +static inline void +cond_wait(SDL_cond *cond, SDL_mutex *mutex) { + if (SDL_CondWait(cond, mutex)) { + LOGC("Could not wait on condition"); + abort(); + } +} + +static inline int +cond_wait_timeout(SDL_cond *cond, SDL_mutex *mutex, uint32_t ms) { + int r = SDL_CondWaitTimeout(cond, mutex, ms); + if (r < 0) { + LOGC("Could not wait on condition with timeout"); + abort(); + } + return r; +} + +static inline void +cond_signal(SDL_cond *cond) { + if (SDL_CondSignal(cond)) { + LOGC("Could not signal a condition"); + abort(); + } +} + +#endif diff --git a/app/src/log.h b/app/src/log.h new file mode 100644 index 00000000..5955c7fb --- /dev/null +++ b/app/src/log.h @@ -0,0 +1,13 @@ +#ifndef LOG_H +#define LOG_H + +#include + +#define LOGV(...) SDL_LogVerbose(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) +#define LOGD(...) SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) +#define LOGI(...) SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) +#define LOGW(...) SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) +#define LOGE(...) SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) +#define LOGC(...) SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) + +#endif diff --git a/app/src/main.c b/app/src/main.c index c58e0be7..bf3b7a50 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -1,152 +1,498 @@ -#include "common.h" +#include "scrcpy.h" +#include #include -#ifdef HAVE_V4L2 -# include -#endif +#include +#include +#include #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 "util/net.h" -#include "util/thread.h" -#include "version.h" +#include "compat.h" +#include "config.h" +#include "log.h" +#include "recorder.h" -#ifdef _WIN32 -#include -#include "util/str.h" -#endif +struct args { + const char *serial; + const char *crop; + const char *record_filename; + enum recorder_format record_format; + bool fullscreen; + bool no_control; + bool no_display; + bool help; + bool version; + bool show_touches; + uint16_t port; + uint16_t max_size; + uint32_t bit_rate; + bool always_on_top; + bool turn_screen_off; + bool render_expired_frames; +}; -static int -main_scrcpy(int argc, char *argv[]) { -#ifdef _WIN32 +static void usage(const char *arg0) { + fprintf(stderr, + "Usage: %s [options]\n" + "\n" + "Options:\n" + "\n" + " -b, --bit-rate value\n" + " Encode the video at the given bit-rate, expressed in bits/s.\n" + " Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n" + " Default is %d.\n" + "\n" + " -c, --crop width:height:x:y\n" + " Crop the device screen on the server.\n" + " The values are expressed in the device natural orientation\n" + " (typically, portrait for a phone, landscape for a tablet).\n" + " Any --max-size value is computed on the cropped size.\n" + "\n" + " -f, --fullscreen\n" + " Start in fullscreen.\n" + "\n" + " -F, --record-format\n" + " Force recording format (either mp4 or mkv).\n" + "\n" + " -h, --help\n" + " Print this help.\n" + "\n" + " -m, --max-size value\n" + " Limit both the width and height of the video to value. The\n" + " other dimension is computed so that the device aspect-ratio\n" + " is preserved.\n" + " Default is %d%s.\n" + "\n" + " -n, --no-control\n" + " Disable device control (mirror the device in read-only).\n" + "\n" + " -N, --no-display\n" + " Do not display device (only when screen recording is\n" + " enabled).\n" + "\n" + " -p, --port port\n" + " Set the TCP port the client listens on.\n" + " Default is %d.\n" + "\n" + " -r, --record file.mp4\n" + " Record screen to file.\n" + " The format is determined by the -F/--record-format option if\n" + " set, or by the file extension (.mp4 or .mkv).\n" + "\n" + " --render-expired-frames\n" + " By default, to minimize latency, scrcpy always renders the\n" + " last available decoded frame, and drops any previous ones.\n" + " This flag forces to render all frames, at a cost of a\n" + " possible increased latency.\n" + "\n" + " -s, --serial\n" + " The device serial number. Mandatory only if several devices\n" + " are connected to adb.\n" + "\n" + " -S, --turn-screen-off\n" + " Turn the device screen off immediately.\n" + "\n" + " -t, --show-touches\n" + " Enable \"show touches\" on start, disable on quit.\n" + " It only shows physical touches (not clicks from scrcpy).\n" + "\n" + " -T, --always-on-top\n" + " Make scrcpy window always on top (above other windows).\n" + "\n" + " -v, --version\n" + " Print the version of scrcpy.\n" + "\n" + "Shortcuts:\n" + "\n" + " Ctrl+f\n" + " switch fullscreen mode\n" + "\n" + " Ctrl+g\n" + " resize window to 1:1 (pixel-perfect)\n" + "\n" + " Ctrl+x\n" + " Double-click on black borders\n" + " resize window to remove black borders\n" + "\n" + " Ctrl+h\n" + " Middle-click\n" + " click on HOME\n" + "\n" + " Ctrl+b\n" + " Ctrl+Backspace\n" + " Right-click (when screen is on)\n" + " click on BACK\n" + "\n" + " Ctrl+s\n" + " click on APP_SWITCH\n" + "\n" + " Ctrl+m\n" + " click on MENU\n" + "\n" + " Ctrl+Up\n" + " click on VOLUME_UP\n" + "\n" + " Ctrl+Down\n" + " click on VOLUME_DOWN\n" + "\n" + " Ctrl+p\n" + " click on POWER (turn screen on/off)\n" + "\n" + " Right-click (when screen is off)\n" + " power on\n" + "\n" + " Ctrl+o\n" + " turn device screen off (keep mirroring)\n" + "\n" + " Ctrl+n\n" + " expand notification panel\n" + "\n" + " Ctrl+Shift+n\n" + " collapse notification panel\n" + "\n" + " Ctrl+c\n" + " copy device clipboard to computer\n" + "\n" + " Ctrl+v\n" + " paste computer clipboard to device\n" + "\n" + " Ctrl+Shift+v\n" + " copy computer clipboard to device\n" + "\n" + " Ctrl+i\n" + " enable/disable FPS counter (print frames/second in logs)\n" + "\n" + " Drag & drop APK file\n" + " install APK from computer\n" + "\n", + arg0, + DEFAULT_BIT_RATE, + DEFAULT_MAX_SIZE, DEFAULT_MAX_SIZE ? "" : " (unlimited)", + DEFAULT_LOCAL_PORT); +} + +static void +print_version(void) { + fprintf(stderr, "scrcpy %s\n\n", SCRCPY_VERSION); + + fprintf(stderr, "dependencies:\n"); + fprintf(stderr, " - SDL %d.%d.%d\n", SDL_MAJOR_VERSION, SDL_MINOR_VERSION, + SDL_PATCHLEVEL); + fprintf(stderr, " - libavcodec %d.%d.%d\n", LIBAVCODEC_VERSION_MAJOR, + LIBAVCODEC_VERSION_MINOR, + LIBAVCODEC_VERSION_MICRO); + fprintf(stderr, " - libavformat %d.%d.%d\n", LIBAVFORMAT_VERSION_MAJOR, + LIBAVFORMAT_VERSION_MINOR, + LIBAVFORMAT_VERSION_MICRO); + fprintf(stderr, " - libavutil %d.%d.%d\n", LIBAVUTIL_VERSION_MAJOR, + LIBAVUTIL_VERSION_MINOR, + LIBAVUTIL_VERSION_MICRO); +} + +static bool +parse_bit_rate(char *optarg, uint32_t *bit_rate) { + char *endptr; + if (*optarg == '\0') { + LOGE("Bit-rate parameter is empty"); + return false; + } + long value = strtol(optarg, &endptr, 0); + int mul = 1; + if (*endptr != '\0') { + if (optarg == endptr) { + LOGE("Invalid bit-rate: %s", optarg); + return false; + } + if ((*endptr == 'M' || *endptr == 'm') && endptr[1] == '\0') { + mul = 1000000; + } else if ((*endptr == 'K' || *endptr == 'k') && endptr[1] == '\0') { + mul = 1000; + } else { + LOGE("Invalid bit-rate unit: %s", optarg); + return false; + } + } + if (value < 0 || ((uint32_t) -1) / mul < value) { + LOGE("Bitrate must be positive and less than 2^32: %s", optarg); + return false; + } + + *bit_rate = (uint32_t) value * mul; + return true; +} + +static bool +parse_max_size(char *optarg, uint16_t *max_size) { + char *endptr; + if (*optarg == '\0') { + LOGE("Max size parameter is empty"); + return false; + } + long value = strtol(optarg, &endptr, 0); + if (*endptr != '\0') { + LOGE("Invalid max size: %s", optarg); + return false; + } + if (value & ~0xffff) { + LOGE("Max size must be between 0 and 65535: %ld", value); + return false; + } + + *max_size = (uint16_t) value; + return true; +} + +static bool +parse_port(char *optarg, uint16_t *port) { + char *endptr; + if (*optarg == '\0') { + LOGE("Invalid port parameter is empty"); + return false; + } + long value = strtol(optarg, &endptr, 0); + if (*endptr != '\0') { + LOGE("Invalid port: %s", optarg); + return false; + } + if (value & ~0xffff) { + LOGE("Port out of range: %ld", value); + return false; + } + + *port = (uint16_t) value; + return true; +} + +static bool +parse_record_format(const char *optarg, enum recorder_format *format) { + if (!strcmp(optarg, "mp4")) { + *format = RECORDER_FORMAT_MP4; + return true; + } + if (!strcmp(optarg, "mkv")) { + *format = RECORDER_FORMAT_MKV; + return true; + } + LOGE("Unsupported format: %s (expected mp4 or mkv)", optarg); + return false; +} + +static enum recorder_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 RECORDER_FORMAT_MP4; + } + if (!strcmp(ext, ".mkv")) { + return RECORDER_FORMAT_MKV; + } + return 0; +} + +#define OPT_RENDER_EXPIRED_FRAMES 1000 + +static bool +parse_args(struct args *args, int argc, char *argv[]) { + static const struct option long_options[] = { + {"always-on-top", no_argument, NULL, 'T'}, + {"bit-rate", required_argument, NULL, 'b'}, + {"crop", required_argument, NULL, 'c'}, + {"fullscreen", no_argument, NULL, 'f'}, + {"help", no_argument, NULL, 'h'}, + {"max-size", required_argument, NULL, 'm'}, + {"no-control", no_argument, NULL, 'n'}, + {"no-display", no_argument, NULL, 'N'}, + {"port", required_argument, NULL, 'p'}, + {"record", required_argument, NULL, 'r'}, + {"record-format", required_argument, NULL, 'f'}, + {"render-expired-frames", no_argument, NULL, + OPT_RENDER_EXPIRED_FRAMES}, + {"serial", required_argument, NULL, 's'}, + {"show-touches", no_argument, NULL, 't'}, + {"turn-screen-off", no_argument, NULL, 'S'}, + {"version", no_argument, NULL, 'v'}, + {NULL, 0, NULL, 0 }, + }; + int c; + while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTv", long_options, + NULL)) != -1) { + switch (c) { + case 'b': + if (!parse_bit_rate(optarg, &args->bit_rate)) { + return false; + } + break; + case 'c': + args->crop = optarg; + break; + case 'f': + args->fullscreen = true; + break; + case 'F': + if (!parse_record_format(optarg, &args->record_format)) { + return false; + } + break; + case 'h': + args->help = true; + break; + case 'm': + if (!parse_max_size(optarg, &args->max_size)) { + return false; + } + break; + case 'n': + args->no_control = true; + break; + case 'N': + args->no_display = true; + break; + case 'p': + if (!parse_port(optarg, &args->port)) { + return false; + } + break; + case 'r': + args->record_filename = optarg; + break; + case 's': + args->serial = optarg; + break; + case 'S': + args->turn_screen_off = true; + break; + case 't': + args->show_touches = true; + break; + case 'T': + args->always_on_top = true; + break; + case 'v': + args->version = true; + break; + case OPT_RENDER_EXPIRED_FRAMES: + args->render_expired_frames = true; + break; + default: + // getopt prints the error message on stderr + return false; + } + } + + if (args->no_display && !args->record_filename) { + LOGE("-N/--no-display requires screen recording (-r/--record)"); + return false; + } + + if (args->no_display && args->fullscreen) { + LOGE("-f/--fullscreen-window is incompatible with -N/--no-display"); + return false; + } + + int index = optind; + if (index < argc) { + LOGE("Unexpected additional argument: %s", argv[index]); + return false; + } + + if (args->record_format && !args->record_filename) { + LOGE("Record format specified without recording"); + return false; + } + + if (args->record_filename && !args->record_format) { + args->record_format = guess_record_format(args->record_filename); + if (!args->record_format) { + LOGE("No format specified for \"%s\" (try with -F mkv)", + args->record_filename); + return false; + } + } + + return true; +} + +int +main(int argc, char *argv[]) { +#ifdef __WINDOWS__ // disable buffering, we want logs immediately // even line buffering (setvbuf() with mode _IOLBF) is not sufficient setbuf(stdout, NULL); setbuf(stderr, NULL); #endif - - printf("scrcpy " SCRCPY_VERSION - " \n"); - - struct scrcpy_cli_args args = { - .opts = scrcpy_options_default, + struct args args = { + .serial = NULL, + .crop = NULL, + .record_filename = NULL, + .record_format = 0, .help = false, .version = false, - .pause_on_exit = SC_PAUSE_ON_EXIT_FALSE, + .show_touches = false, + .port = DEFAULT_LOCAL_PORT, + .max_size = DEFAULT_MAX_SIZE, + .bit_rate = DEFAULT_BIT_RATE, + .always_on_top = false, + .no_control = false, + .no_display = false, + .turn_screen_off = false, + .render_expired_frames = false, }; - -#ifndef NDEBUG - args.opts.log_level = SC_LOG_LEVEL_DEBUG; -#endif - - enum scrcpy_exit_code ret; - - if (!scrcpy_parse_args(&args, argc, argv)) { - ret = SCRCPY_EXIT_FAILURE; - goto end; + if (!parse_args(&args, argc, argv)) { + return 1; } - sc_set_log_level(args.opts.log_level); - if (args.help) { - scrcpy_print_usage(argv[0]); - ret = SCRCPY_EXIT_SUCCESS; - goto end; + usage(argv[0]); + return 0; } if (args.version) { - scrcpy_print_version(); - ret = SCRCPY_EXIT_SUCCESS; - goto end; + print_version(); + return 0; } - // The current thread is the main thread - SC_MAIN_THREAD_ID = sc_thread_get_id(); - #ifdef SCRCPY_LAVF_REQUIRES_REGISTER_ALL av_register_all(); #endif -#ifdef HAVE_V4L2 - if (args.opts.v4l2_device) { - avdevice_register_all(); - } -#endif - - if (!net_init()) { - ret = SCRCPY_EXIT_FAILURE; - goto end; + if (avformat_network_init()) { + return 1; } - sc_log_configure(); - -#ifdef HAVE_USB - ret = args.opts.otg ? scrcpy_otg(&args.opts) : scrcpy(&args.opts); -#else - ret = scrcpy(&args.opts); +#ifdef BUILD_DEBUG + SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG); #endif -end: - if (args.pause_on_exit == SC_PAUSE_ON_EXIT_TRUE || - (args.pause_on_exit == SC_PAUSE_ON_EXIT_IF_ERROR && - ret != SCRCPY_EXIT_SUCCESS)) { - printf("Press Enter to continue...\n"); + struct scrcpy_options options = { + .serial = args.serial, + .crop = args.crop, + .port = args.port, + .record_filename = args.record_filename, + .record_format = args.record_format, + .max_size = args.max_size, + .bit_rate = args.bit_rate, + .show_touches = args.show_touches, + .fullscreen = args.fullscreen, + .always_on_top = args.always_on_top, + .control = !args.no_control, + .display = !args.no_display, + .turn_screen_off = args.turn_screen_off, + .render_expired_frames = args.render_expired_frames, + }; + int res = scrcpy(&options) ? 0 : 1; + + avformat_network_deinit(); // ignore failure + +#if defined (__WINDOWS__) && ! defined (WINDOWS_NOCONSOLE) + if (res != 0) { + fprintf(stderr, "Press any key to continue...\n"); getchar(); } - - return ret; -} - -int -main(int argc, char *argv[]) { -#ifndef _WIN32 - return main_scrcpy(argc, argv); -#else - (void) argc; - (void) argv; - int wargc; - wchar_t **wargv = CommandLineToArgvW(GetCommandLineW(), &wargc); - if (!wargv) { - LOG_OOM(); - return SCRCPY_EXIT_FAILURE; - } - - char **argv_utf8 = malloc((wargc + 1) * sizeof(*argv_utf8)); - if (!argv_utf8) { - LOG_OOM(); - LocalFree(wargv); - return SCRCPY_EXIT_FAILURE; - } - - argv_utf8[wargc] = NULL; - - for (int i = 0; i < wargc; ++i) { - argv_utf8[i] = sc_str_from_wchars(wargv[i]); - if (!argv_utf8[i]) { - LOG_OOM(); - for (int j = 0; j < i; ++j) { - free(argv_utf8[j]); - } - LocalFree(wargv); - free(argv_utf8); - return SCRCPY_EXIT_FAILURE; - } - } - - 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 + return res; } diff --git a/app/src/mouse_capture.c b/app/src/mouse_capture.c deleted file mode 100644 index 25345faa..00000000 --- a/app/src/mouse_capture.c +++ /dev/null @@ -1,123 +0,0 @@ -#include "mouse_capture.h" - -#include "shortcut_mod.h" -#include "util/log.h" - -void -sc_mouse_capture_init(struct sc_mouse_capture *mc, SDL_Window *window, - uint8_t shortcut_mods) { - mc->window = window; - mc->sdl_mouse_capture_keys = sc_shortcut_mods_to_sdl(shortcut_mods); - mc->mouse_capture_key_pressed = SDLK_UNKNOWN; -} - -static inline bool -sc_mouse_capture_is_capture_key(struct sc_mouse_capture *mc, SDL_Keycode key) { - return sc_shortcut_mods_is_shortcut_key(mc->sdl_mouse_capture_keys, key); -} - -bool -sc_mouse_capture_handle_event(struct sc_mouse_capture *mc, - const SDL_Event *event) { - switch (event->type) { - case SDL_WINDOWEVENT: - if (event->window.event == SDL_WINDOWEVENT_FOCUS_LOST) { - sc_mouse_capture_set_active(mc, false); - return true; - } - break; - case SDL_KEYDOWN: { - SDL_Keycode key = event->key.keysym.sym; - if (sc_mouse_capture_is_capture_key(mc, key)) { - if (!mc->mouse_capture_key_pressed) { - mc->mouse_capture_key_pressed = key; - } else { - // Another mouse capture key has been pressed, cancel - // mouse (un)capture - mc->mouse_capture_key_pressed = 0; - } - // Mouse capture keys are never forwarded to the device - return true; - } - break; - } - case SDL_KEYUP: { - SDL_Keycode key = event->key.keysym.sym; - SDL_Keycode cap = mc->mouse_capture_key_pressed; - mc->mouse_capture_key_pressed = 0; - if (sc_mouse_capture_is_capture_key(mc, key)) { - if (key == cap) { - // A mouse capture key has been pressed then released: - // toggle the capture mouse mode - sc_mouse_capture_toggle(mc); - } - // Mouse capture keys are never forwarded to the device - return true; - } - break; - } - case SDL_MOUSEWHEEL: - case SDL_MOUSEMOTION: - case SDL_MOUSEBUTTONDOWN: - if (!sc_mouse_capture_is_active(mc)) { - // The mouse will be captured on SDL_MOUSEBUTTONUP, so consume - // the event - return true; - } - break; - case SDL_MOUSEBUTTONUP: - if (!sc_mouse_capture_is_active(mc)) { - sc_mouse_capture_set_active(mc, true); - return true; - } - break; - case SDL_FINGERMOTION: - case SDL_FINGERDOWN: - case SDL_FINGERUP: - // Touch events are not compatible with relative mode - // (coordinates are not relative), so consume the event - return true; - } - - return false; -} - -void -sc_mouse_capture_set_active(struct sc_mouse_capture *mc, 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(mc->window, &x, &y); - SDL_GetWindowSize(mc->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(mc->window, w / 2, h / 2); - } - } -#else - (void) mc; -#endif - if (SDL_SetRelativeMouseMode(capture)) { - LOGE("Could not set relative mouse mode to %s: %s", - capture ? "true" : "false", SDL_GetError()); - } -} - -bool -sc_mouse_capture_is_active(struct sc_mouse_capture *mc) { - (void) mc; - return SDL_GetRelativeMouseMode(); -} - -void -sc_mouse_capture_toggle(struct sc_mouse_capture *mc) { - bool new_value = !sc_mouse_capture_is_active(mc); - sc_mouse_capture_set_active(mc, new_value); -} diff --git a/app/src/mouse_capture.h b/app/src/mouse_capture.h deleted file mode 100644 index f352cc13..00000000 --- a/app/src/mouse_capture.h +++ /dev/null @@ -1,38 +0,0 @@ -#ifndef SC_MOUSE_CAPTURE_H -#define SC_MOUSE_CAPTURE_H - -#include "common.h" - -#include - -#include - -struct sc_mouse_capture { - SDL_Window *window; - uint16_t sdl_mouse_capture_keys; - - // 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; - -}; - -void -sc_mouse_capture_init(struct sc_mouse_capture *mc, SDL_Window *window, - uint8_t shortcut_mods); - -void -sc_mouse_capture_set_active(struct sc_mouse_capture *mc, bool capture); - -bool -sc_mouse_capture_is_active(struct sc_mouse_capture *mc); - -void -sc_mouse_capture_toggle(struct sc_mouse_capture *mc); - -// Return true if it consumed the event -bool -sc_mouse_capture_handle_event(struct sc_mouse_capture *mc, - const SDL_Event *event); - -#endif diff --git a/app/src/mouse_sdk.c b/app/src/mouse_sdk.c deleted file mode 100644 index 7eceffa7..00000000 --- a/app/src/mouse_sdk.c +++ /dev/null @@ -1,164 +0,0 @@ -#include "mouse_sdk.h" - -#include -#include - -#include "android/input.h" -#include "control_msg.h" -#include "controller.h" -#include "input_events.h" -#include "util/log.h" - -/** Downcast mouse processor to sc_mouse_sdk */ -#define DOWNCAST(MP) container_of(MP, struct sc_mouse_sdk, mouse_processor) - -static enum android_motionevent_buttons -convert_mouse_buttons(uint32_t state) { - enum android_motionevent_buttons buttons = 0; - if (state & SC_MOUSE_BUTTON_LEFT) { - buttons |= AMOTION_EVENT_BUTTON_PRIMARY; - } - if (state & SC_MOUSE_BUTTON_RIGHT) { - buttons |= AMOTION_EVENT_BUTTON_SECONDARY; - } - if (state & SC_MOUSE_BUTTON_MIDDLE) { - buttons |= AMOTION_EVENT_BUTTON_TERTIARY; - } - if (state & SC_MOUSE_BUTTON_X1) { - buttons |= AMOTION_EVENT_BUTTON_BACK; - } - if (state & SC_MOUSE_BUTTON_X2) { - buttons |= AMOTION_EVENT_BUTTON_FORWARD; - } - return buttons; -} - -static enum android_motionevent_action -convert_mouse_action(enum sc_action action) { - if (action == SC_ACTION_DOWN) { - return AMOTION_EVENT_ACTION_DOWN; - } - assert(action == SC_ACTION_UP); - return AMOTION_EVENT_ACTION_UP; -} - -static enum android_motionevent_action -convert_touch_action(enum sc_touch_action action) { - switch (action) { - case SC_TOUCH_ACTION_MOVE: - return AMOTION_EVENT_ACTION_MOVE; - case SC_TOUCH_ACTION_DOWN: - return AMOTION_EVENT_ACTION_DOWN; - default: - assert(action == SC_TOUCH_ACTION_UP); - return AMOTION_EVENT_ACTION_UP; - } -} - -static void -sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, - const struct sc_mouse_motion_event *event) { - struct sc_mouse_sdk *m = DOWNCAST(mp); - - if (!m->mouse_hover && !event->buttons_state) { - // Do not send motion events when no click is pressed - return; - } - - struct sc_control_msg msg = { - .type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, - .inject_touch_event = { - .action = event->buttons_state ? AMOTION_EVENT_ACTION_MOVE - : AMOTION_EVENT_ACTION_HOVER_MOVE, - .pointer_id = event->pointer_id, - .position = event->position, - .pressure = 1.f, - .buttons = convert_mouse_buttons(event->buttons_state), - }, - }; - - if (!sc_controller_push_msg(m->controller, &msg)) { - LOGW("Could not request 'inject mouse motion event'"); - } -} - -static void -sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, - const struct sc_mouse_click_event *event) { - struct sc_mouse_sdk *m = DOWNCAST(mp); - - struct sc_control_msg msg = { - .type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, - .inject_touch_event = { - .action = convert_mouse_action(event->action), - .pointer_id = event->pointer_id, - .position = event->position, - .pressure = event->action == SC_ACTION_DOWN ? 1.f : 0.f, - .action_button = convert_mouse_buttons(event->button), - .buttons = convert_mouse_buttons(event->buttons_state), - }, - }; - - if (!sc_controller_push_msg(m->controller, &msg)) { - LOGW("Could not request 'inject mouse click event'"); - } -} - -static void -sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, - const struct sc_mouse_scroll_event *event) { - struct sc_mouse_sdk *m = DOWNCAST(mp); - - struct sc_control_msg msg = { - .type = SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, - .inject_scroll_event = { - .position = event->position, - .hscroll = event->hscroll, - .vscroll = event->vscroll, - .buttons = convert_mouse_buttons(event->buttons_state), - }, - }; - - if (!sc_controller_push_msg(m->controller, &msg)) { - LOGW("Could not request 'inject mouse scroll event'"); - } -} - -static void -sc_mouse_processor_process_touch(struct sc_mouse_processor *mp, - const struct sc_touch_event *event) { - struct sc_mouse_sdk *m = DOWNCAST(mp); - - struct sc_control_msg msg = { - .type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, - .inject_touch_event = { - .action = convert_touch_action(event->action), - .pointer_id = event->pointer_id, - .position = event->position, - .pressure = event->pressure, - .buttons = 0, - }, - }; - - if (!sc_controller_push_msg(m->controller, &msg)) { - LOGW("Could not request 'inject touch event'"); - } -} - -void -sc_mouse_sdk_init(struct sc_mouse_sdk *m, struct sc_controller *controller, - bool mouse_hover) { - m->controller = controller; - m->mouse_hover = mouse_hover; - - static const struct sc_mouse_processor_ops ops = { - .process_mouse_motion = sc_mouse_processor_process_mouse_motion, - .process_mouse_click = sc_mouse_processor_process_mouse_click, - .process_mouse_scroll = sc_mouse_processor_process_mouse_scroll, - .process_touch = sc_mouse_processor_process_touch, - }; - - m->mouse_processor.ops = &ops; - - m->mouse_processor.relative_mode = false; -} diff --git a/app/src/mouse_sdk.h b/app/src/mouse_sdk.h deleted file mode 100644 index fe92a2d7..00000000 --- a/app/src/mouse_sdk.h +++ /dev/null @@ -1,22 +0,0 @@ -#ifndef SC_MOUSE_SDK_H -#define SC_MOUSE_SDK_H - -#include "common.h" - -#include - -#include "controller.h" -#include "trait/mouse_processor.h" - -struct sc_mouse_sdk { - struct sc_mouse_processor mouse_processor; // mouse processor trait - - struct sc_controller *controller; - bool mouse_hover; -}; - -void -sc_mouse_sdk_init(struct sc_mouse_sdk *m, struct sc_controller *controller, - bool mouse_hover); - -#endif diff --git a/app/src/net.c b/app/src/net.c new file mode 100644 index 00000000..a0bc38f2 --- /dev/null +++ b/app/src/net.c @@ -0,0 +1,116 @@ +#include "net.h" + +#include + +#include "log.h" + +#ifdef __WINDOWS__ + typedef int socklen_t; +#else +# 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; +#endif + +socket_t +net_connect(uint32_t addr, uint16_t port) { + socket_t sock = socket(AF_INET, SOCK_STREAM, 0); + if (sock == INVALID_SOCKET) { + perror("socket"); + return INVALID_SOCKET; + } + + SOCKADDR_IN sin; + sin.sin_family = AF_INET; + sin.sin_addr.s_addr = htonl(addr); + sin.sin_port = htons(port); + + if (connect(sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) { + perror("connect"); + net_close(sock); + return INVALID_SOCKET; + } + + return sock; +} + +socket_t +net_listen(uint32_t addr, uint16_t port, int backlog) { + socket_t sock = socket(AF_INET, SOCK_STREAM, 0); + if (sock == INVALID_SOCKET) { + perror("socket"); + return INVALID_SOCKET; + } + + int reuse = 1; + if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const void *) &reuse, + sizeof(reuse)) == -1) { + perror("setsockopt(SO_REUSEADDR)"); + } + + SOCKADDR_IN sin; + sin.sin_family = AF_INET; + sin.sin_addr.s_addr = htonl(addr); // htonl() harmless on INADDR_ANY + sin.sin_port = htons(port); + + if (bind(sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) { + perror("bind"); + net_close(sock); + return INVALID_SOCKET; + } + + if (listen(sock, backlog) == SOCKET_ERROR) { + perror("listen"); + net_close(sock); + return INVALID_SOCKET; + } + + return sock; +} + +socket_t +net_accept(socket_t server_socket) { + SOCKADDR_IN csin; + socklen_t sinsize = sizeof(csin); + return accept(server_socket, (SOCKADDR *) &csin, &sinsize); +} + +ssize_t +net_recv(socket_t socket, void *buf, size_t len) { + return recv(socket, buf, len, 0); +} + +ssize_t +net_recv_all(socket_t socket, void *buf, size_t len) { + return recv(socket, buf, len, MSG_WAITALL); +} + +ssize_t +net_send(socket_t socket, const void *buf, size_t len) { + return send(socket, buf, len, 0); +} + +ssize_t +net_send_all(socket_t socket, const void *buf, size_t len) { + ssize_t w = 0; + while (len > 0) { + w = send(socket, buf, len, 0); + if (w == -1) { + return -1; + } + len -= w; + buf = (char *) buf + w; + } + return w; +} + +bool +net_shutdown(socket_t socket, int how) { + return !shutdown(socket, how); +} diff --git a/app/src/net.h b/app/src/net.h new file mode 100644 index 00000000..dd82c083 --- /dev/null +++ b/app/src/net.h @@ -0,0 +1,55 @@ +#ifndef NET_H +#define NET_H + +#include +#include +#include + +#ifdef __WINDOWS__ +# include + #define SHUT_RD SD_RECEIVE + #define SHUT_WR SD_SEND + #define SHUT_RDWR SD_BOTH + typedef SOCKET socket_t; +#else +# include +# define INVALID_SOCKET -1 + typedef int socket_t; +#endif + +bool +net_init(void); + +void +net_cleanup(void); + +socket_t +net_connect(uint32_t addr, uint16_t port); + +socket_t +net_listen(uint32_t addr, uint16_t port, int backlog); + +socket_t +net_accept(socket_t server_socket); + +// the _all versions wait/retry until len bytes have been written/read +ssize_t +net_recv(socket_t socket, void *buf, size_t len); + +ssize_t +net_recv_all(socket_t socket, void *buf, size_t len); + +ssize_t +net_send(socket_t socket, const void *buf, size_t len); + +ssize_t +net_send_all(socket_t socket, const void *buf, size_t len); + +// how is SHUT_RD (read), SHUT_WR (write) or SHUT_RDWR (both) +bool +net_shutdown(socket_t socket, int how); + +bool +net_close(socket_t socket); + +#endif diff --git a/app/src/opengl.c b/app/src/opengl.c deleted file mode 100644 index 0cb83ed7..00000000 --- a/app/src/opengl.c +++ /dev/null @@ -1,57 +0,0 @@ -#include "opengl.h" - -#include -#include -#include -#include - -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); -} diff --git a/app/src/opengl.h b/app/src/opengl.h deleted file mode 100644 index 81163704..00000000 --- a/app/src/opengl.h +++ /dev/null @@ -1,36 +0,0 @@ -#ifndef SC_OPENGL_H -#define SC_OPENGL_H - -#include "common.h" - -#include -#include - -struct sc_opengl { - const char *version; - bool is_opengles; - int version_major; - int version_minor; - - const GLubyte * - (*GetString)(GLenum name); - - void - (*TexParameterf)(GLenum target, GLenum pname, GLfloat param); - - void - (*TexParameteri)(GLenum target, GLenum pname, GLint param); - - void - (*GenerateMipmap)(GLenum target); -}; - -void -sc_opengl_init(struct sc_opengl *gl); - -bool -sc_opengl_version_at_least(struct sc_opengl *gl, - int minver_major, int minver_minor, - int minver_es_major, int minver_es_minor); - -#endif diff --git a/app/src/options.c b/app/src/options.c deleted file mode 100644 index 0fe82d29..00000000 --- a/app/src/options.c +++ /dev/null @@ -1,152 +0,0 @@ -#include "options.h" - -#include - -const struct scrcpy_options scrcpy_options_default = { - .serial = NULL, - .crop = NULL, - .record_filename = NULL, - .window_title = NULL, - .push_target = NULL, - .render_driver = NULL, - .video_codec_options = NULL, - .audio_codec_options = NULL, - .video_encoder = NULL, - .audio_encoder = NULL, - .camera_id = NULL, - .camera_size = NULL, - .camera_ar = NULL, - .camera_fps = 0, - .log_level = SC_LOG_LEVEL_INFO, - .video_codec = SC_CODEC_H264, - .audio_codec = SC_CODEC_OPUS, - .video_source = SC_VIDEO_SOURCE_DISPLAY, - .audio_source = SC_AUDIO_SOURCE_AUTO, - .record_format = SC_RECORD_FORMAT_AUTO, - .keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_AUTO, - .mouse_input_mode = SC_MOUSE_INPUT_MODE_AUTO, - .gamepad_input_mode = SC_GAMEPAD_INPUT_MODE_DISABLED, - .mouse_bindings = { - .pri = { - .right_click = SC_MOUSE_BINDING_AUTO, - .middle_click = SC_MOUSE_BINDING_AUTO, - .click4 = SC_MOUSE_BINDING_AUTO, - .click5 = SC_MOUSE_BINDING_AUTO, - }, - .sec = { - .right_click = SC_MOUSE_BINDING_AUTO, - .middle_click = SC_MOUSE_BINDING_AUTO, - .click4 = SC_MOUSE_BINDING_AUTO, - .click5 = SC_MOUSE_BINDING_AUTO, - }, - }, - .camera_facing = SC_CAMERA_FACING_ANY, - .port_range = { - .first = DEFAULT_LOCAL_PORT_RANGE_FIRST, - .last = DEFAULT_LOCAL_PORT_RANGE_LAST, - }, - .tunnel_host = 0, - .tunnel_port = 0, - .shortcut_mods = SC_SHORTCUT_MOD_LALT | SC_SHORTCUT_MOD_LSUPER, - .max_size = 0, - .video_bit_rate = 0, - .audio_bit_rate = 0, - .max_fps = NULL, - .capture_orientation = SC_ORIENTATION_0, - .capture_orientation_lock = SC_ORIENTATION_UNLOCKED, - .display_orientation = SC_ORIENTATION_0, - .record_orientation = SC_ORIENTATION_0, - .display_ime_policy = SC_DISPLAY_IME_POLICY_UNDEFINED, - .window_x = SC_WINDOW_POSITION_UNDEFINED, - .window_y = SC_WINDOW_POSITION_UNDEFINED, - .window_width = 0, - .window_height = 0, - .display_id = 0, - .video_buffer = 0, - .audio_buffer = -1, // depends on the audio format, - .audio_output_buffer = SC_TICK_FROM_MS(5), - .time_limit = 0, - .screen_off_timeout = -1, -#ifdef HAVE_V4L2 - .v4l2_device = NULL, - .v4l2_buffer = 0, -#endif -#ifdef HAVE_USB - .otg = false, -#endif - .show_touches = false, - .fullscreen = false, - .always_on_top = false, - .control = true, - .video_playback = true, - .audio_playback = true, - .turn_screen_off = false, - .key_inject_mode = SC_KEY_INJECT_MODE_MIXED, - .window_borderless = false, - .mipmaps = true, - .stay_awake = false, - .force_adb_forward = false, - .disable_screensaver = false, - .forward_key_repeat = true, - .legacy_paste = false, - .power_off_on_close = false, - .clipboard_autosync = true, - .downsize_on_error = true, - .tcpip = false, - .tcpip_dst = NULL, - .select_tcpip = false, - .select_usb = false, - .cleanup = true, - .start_fps_counter = false, - .power_on = true, - .video = true, - .audio = true, - .require_audio = false, - .kill_adb_on_close = false, - .camera_high_speed = false, - .list = 0, - .window = true, - .mouse_hover = true, - .audio_dup = false, - .new_display = NULL, - .start_app = NULL, - .angle = NULL, - .vd_destroy_content = true, - .vd_system_decorations = true, -}; - -enum sc_orientation -sc_orientation_apply(enum sc_orientation src, enum sc_orientation transform) { - assert(!(src & ~7)); - assert(!(transform & ~7)); - - unsigned transform_hflip = transform & 4; - unsigned transform_rotation = transform & 3; - unsigned src_hflip = src & 4; - unsigned src_rotation = src & 3; - unsigned src_swap = src & 1; - if (src_swap && transform_hflip) { - // If the src is rotated by 90 or 270 degrees, applying a flipped - // transformation requires an additional 180 degrees rotation to - // compensate for the inversion of the order of multiplication: - // - // hflip1 × rotate1 × hflip2 × rotate2 - // `--------------' `--------------' - // src transform - // - // In the final result, we want all the hflips then all the rotations, - // so we must move hflip2 to the left: - // - // hflip1 × hflip2 × rotate1' × rotate2 - // - // with rotate1' = | rotate1 if src is 0° or 180° - // | rotate1 + 180° if src is 90° or 270° - - src_rotation += 2; - } - - unsigned result_hflip = src_hflip ^ transform_hflip; - unsigned result_rotation = (transform_rotation + src_rotation) % 4; - enum sc_orientation result = result_hflip | result_rotation; - return result; -} diff --git a/app/src/options.h b/app/src/options.h deleted file mode 100644 index 03b42913..00000000 --- a/app/src/options.h +++ /dev/null @@ -1,334 +0,0 @@ -#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, - SC_RECORD_FORMAT_M4A, - SC_RECORD_FORMAT_MKA, - SC_RECORD_FORMAT_OPUS, - SC_RECORD_FORMAT_AAC, - SC_RECORD_FORMAT_FLAC, - SC_RECORD_FORMAT_WAV, -}; - -static inline bool -sc_record_format_is_audio_only(enum sc_record_format fmt) { - return fmt == SC_RECORD_FORMAT_M4A - || fmt == SC_RECORD_FORMAT_MKA - || fmt == SC_RECORD_FORMAT_OPUS - || fmt == SC_RECORD_FORMAT_AAC - || fmt == SC_RECORD_FORMAT_FLAC - || fmt == SC_RECORD_FORMAT_WAV; -} - -enum sc_codec { - SC_CODEC_H264, - SC_CODEC_H265, - SC_CODEC_AV1, - SC_CODEC_OPUS, - SC_CODEC_AAC, - SC_CODEC_FLAC, - SC_CODEC_RAW, -}; - -enum sc_video_source { - SC_VIDEO_SOURCE_DISPLAY, - SC_VIDEO_SOURCE_CAMERA, -}; - -enum sc_audio_source { - SC_AUDIO_SOURCE_AUTO, // OUTPUT for video DISPLAY, MIC for video CAMERA - SC_AUDIO_SOURCE_OUTPUT, - SC_AUDIO_SOURCE_MIC, - SC_AUDIO_SOURCE_PLAYBACK, - SC_AUDIO_SOURCE_MIC_UNPROCESSED, - SC_AUDIO_SOURCE_MIC_CAMCORDER, - SC_AUDIO_SOURCE_MIC_VOICE_RECOGNITION, - SC_AUDIO_SOURCE_MIC_VOICE_COMMUNICATION, - SC_AUDIO_SOURCE_VOICE_CALL, - SC_AUDIO_SOURCE_VOICE_CALL_UPLINK, - SC_AUDIO_SOURCE_VOICE_CALL_DOWNLINK, - SC_AUDIO_SOURCE_VOICE_PERFORMANCE, -}; - -enum sc_camera_facing { - SC_CAMERA_FACING_ANY, - SC_CAMERA_FACING_FRONT, - SC_CAMERA_FACING_BACK, - SC_CAMERA_FACING_EXTERNAL, -}; - - // ,----- hflip (applied before the rotation) - // | ,--- 180° - // | | ,- 90° clockwise - // | | | -enum sc_orientation { // v v v - SC_ORIENTATION_0, // 0 0 0 - SC_ORIENTATION_90, // 0 0 1 - SC_ORIENTATION_180, // 0 1 0 - SC_ORIENTATION_270, // 0 1 1 - SC_ORIENTATION_FLIP_0, // 1 0 0 - SC_ORIENTATION_FLIP_90, // 1 0 1 - SC_ORIENTATION_FLIP_180, // 1 1 0 - SC_ORIENTATION_FLIP_270, // 1 1 1 -}; - -enum sc_orientation_lock { - SC_ORIENTATION_UNLOCKED, - SC_ORIENTATION_LOCKED_VALUE, // lock to specified orientation - SC_ORIENTATION_LOCKED_INITIAL, // lock to initial device orientation -}; - -enum sc_display_ime_policy { - SC_DISPLAY_IME_POLICY_UNDEFINED, - SC_DISPLAY_IME_POLICY_LOCAL, - SC_DISPLAY_IME_POLICY_FALLBACK, - SC_DISPLAY_IME_POLICY_HIDE, -}; - -static inline bool -sc_orientation_is_mirror(enum sc_orientation orientation) { - assert(!(orientation & ~7)); - return orientation & 4; -} - -// Does the orientation swap width and height? -static inline bool -sc_orientation_is_swap(enum sc_orientation orientation) { - assert(!(orientation & ~7)); - return orientation & 1; -} - -static inline enum sc_orientation -sc_orientation_get_rotation(enum sc_orientation orientation) { - assert(!(orientation & ~7)); - return orientation & 3; -} - -enum sc_orientation -sc_orientation_apply(enum sc_orientation src, enum sc_orientation transform); - -static inline const char * -sc_orientation_get_name(enum sc_orientation orientation) { - switch (orientation) { - case SC_ORIENTATION_0: - return "0"; - case SC_ORIENTATION_90: - return "90"; - case SC_ORIENTATION_180: - return "180"; - case SC_ORIENTATION_270: - return "270"; - case SC_ORIENTATION_FLIP_0: - return "flip0"; - case SC_ORIENTATION_FLIP_90: - return "flip90"; - case SC_ORIENTATION_FLIP_180: - return "flip180"; - case SC_ORIENTATION_FLIP_270: - return "flip270"; - default: - return "(unknown)"; - } -} - -enum sc_keyboard_input_mode { - SC_KEYBOARD_INPUT_MODE_AUTO, - SC_KEYBOARD_INPUT_MODE_UHID_OR_AOA, // normal vs otg mode - SC_KEYBOARD_INPUT_MODE_DISABLED, - SC_KEYBOARD_INPUT_MODE_SDK, - SC_KEYBOARD_INPUT_MODE_UHID, - SC_KEYBOARD_INPUT_MODE_AOA, -}; - -enum sc_mouse_input_mode { - SC_MOUSE_INPUT_MODE_AUTO, - SC_MOUSE_INPUT_MODE_UHID_OR_AOA, // normal vs otg mode - SC_MOUSE_INPUT_MODE_DISABLED, - SC_MOUSE_INPUT_MODE_SDK, - SC_MOUSE_INPUT_MODE_UHID, - SC_MOUSE_INPUT_MODE_AOA, -}; - -enum sc_gamepad_input_mode { - SC_GAMEPAD_INPUT_MODE_DISABLED, - SC_GAMEPAD_INPUT_MODE_UHID_OR_AOA, // normal vs otg mode - SC_GAMEPAD_INPUT_MODE_UHID, - SC_GAMEPAD_INPUT_MODE_AOA, -}; - -enum sc_mouse_binding { - SC_MOUSE_BINDING_AUTO, - SC_MOUSE_BINDING_DISABLED, - SC_MOUSE_BINDING_CLICK, - SC_MOUSE_BINDING_BACK, - SC_MOUSE_BINDING_HOME, - SC_MOUSE_BINDING_APP_SWITCH, - SC_MOUSE_BINDING_EXPAND_NOTIFICATION_PANEL, -}; - -struct sc_mouse_binding_set { - enum sc_mouse_binding right_click; - enum sc_mouse_binding middle_click; - enum sc_mouse_binding click4; - enum sc_mouse_binding click5; -}; - -struct sc_mouse_bindings { - struct sc_mouse_binding_set pri; - struct sc_mouse_binding_set sec; // When Shift is pressed -}; - -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, -}; - -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_port_range { - uint16_t first; - uint16_t last; -}; - -#define SC_WINDOW_POSITION_UNDEFINED (-0x8000) - -struct scrcpy_options { - const char *serial; - const char *crop; - const char *record_filename; - const char *window_title; - const char *push_target; - const char *render_driver; - const char *video_codec_options; - const char *audio_codec_options; - const char *video_encoder; - const char *audio_encoder; - const char *camera_id; - const char *camera_size; - const char *camera_ar; - uint16_t camera_fps; - enum sc_log_level log_level; - enum sc_codec video_codec; - enum sc_codec audio_codec; - enum sc_video_source video_source; - enum sc_audio_source audio_source; - enum sc_record_format record_format; - enum sc_keyboard_input_mode keyboard_input_mode; - enum sc_mouse_input_mode mouse_input_mode; - enum sc_gamepad_input_mode gamepad_input_mode; - struct sc_mouse_bindings mouse_bindings; - enum sc_camera_facing camera_facing; - struct sc_port_range port_range; - uint32_t tunnel_host; - uint16_t tunnel_port; - uint8_t shortcut_mods; // OR of enum sc_shortcut_mod values - uint16_t max_size; - uint32_t video_bit_rate; - uint32_t audio_bit_rate; - const char *max_fps; // float to be parsed by the server - const char *angle; // float to be parsed by the server - enum sc_orientation capture_orientation; - enum sc_orientation_lock capture_orientation_lock; - enum sc_orientation display_orientation; - enum sc_orientation record_orientation; - enum sc_display_ime_policy display_ime_policy; - 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 video_buffer; - sc_tick audio_buffer; - sc_tick audio_output_buffer; - sc_tick time_limit; - sc_tick screen_off_timeout; -#ifdef HAVE_V4L2 - const char *v4l2_device; - sc_tick v4l2_buffer; -#endif -#ifdef HAVE_USB - bool otg; -#endif - bool show_touches; - bool fullscreen; - bool always_on_top; - bool control; - bool video_playback; - bool audio_playback; - bool turn_screen_off; - enum sc_key_inject_mode key_inject_mode; - bool window_borderless; - bool mipmaps; - bool stay_awake; - bool force_adb_forward; - bool disable_screensaver; - bool forward_key_repeat; - bool legacy_paste; - bool power_off_on_close; - bool clipboard_autosync; - bool downsize_on_error; - bool tcpip; - const char *tcpip_dst; - bool select_usb; - bool select_tcpip; - bool cleanup; - bool start_fps_counter; - bool power_on; - bool video; - bool audio; - bool require_audio; - bool kill_adb_on_close; - bool camera_high_speed; -#define SC_OPTION_LIST_ENCODERS 0x1 -#define SC_OPTION_LIST_DISPLAYS 0x2 -#define SC_OPTION_LIST_CAMERAS 0x4 -#define SC_OPTION_LIST_CAMERA_SIZES 0x8 -#define SC_OPTION_LIST_APPS 0x10 - uint8_t list; - bool window; - bool mouse_hover; - bool audio_dup; - const char *new_display; // [x][/] parsed by the server - const char *start_app; - bool vd_destroy_content; - bool vd_system_decorations; -}; - -extern const struct scrcpy_options scrcpy_options_default; - -#endif diff --git a/app/src/packet_merger.c b/app/src/packet_merger.c deleted file mode 100644 index dea038b6..00000000 --- a/app/src/packet_merger.c +++ /dev/null @@ -1,52 +0,0 @@ -#include "packet_merger.h" - -#include -#include -#include - -#include "util/log.h" - -void -sc_packet_merger_init(struct sc_packet_merger *merger) { - merger->config = NULL; -} - -void -sc_packet_merger_destroy(struct sc_packet_merger *merger) { - free(merger->config); -} - -bool -sc_packet_merger_merge(struct sc_packet_merger *merger, AVPacket *packet) { - bool is_config = packet->pts == AV_NOPTS_VALUE; - - if (is_config) { - free(merger->config); - - merger->config = malloc(packet->size); - if (!merger->config) { - LOG_OOM(); - return false; - } - - memcpy(merger->config, packet->data, packet->size); - merger->config_size = packet->size; - } else if (merger->config) { - size_t config_size = merger->config_size; - size_t media_size = packet->size; - - if (av_grow_packet(packet, config_size)) { - LOG_OOM(); - return false; - } - - memmove(packet->data + config_size, packet->data, media_size); - memcpy(packet->data, merger->config, config_size); - - free(merger->config); - merger->config = NULL; - // merger->size is meaningless when merger->config is NULL - } - - return true; -} diff --git a/app/src/packet_merger.h b/app/src/packet_merger.h deleted file mode 100644 index 3f9972ce..00000000 --- a/app/src/packet_merger.h +++ /dev/null @@ -1,43 +0,0 @@ -#ifndef SC_PACKET_MERGER_H -#define SC_PACKET_MERGER_H - -#include "common.h" - -#include -#include -#include - -/** - * Config packets (containing the SPS/PPS) are sent in-band. A new config - * packet is sent whenever a new encoding session is started (on start and on - * device orientation change). - * - * Every time a config packet is received, it must be sent alone (for recorder - * extradata), then concatenated to the next media packet (for correct decoding - * and recording). - * - * This helper reads every input packet and modifies each media packet which - * immediately follows a config packet to prepend the config packet payload. - */ - -struct sc_packet_merger { - uint8_t *config; - size_t config_size; -}; - -void -sc_packet_merger_init(struct sc_packet_merger *merger); - -void -sc_packet_merger_destroy(struct sc_packet_merger *merger); - -/** - * If the packet is a config packet, then keep its data for later. - * Otherwise (if the packet is a media packet), then if a config packet is - * pending, prepend the config packet to this packet (so the packet is - * modified!). - */ -bool -sc_packet_merger_merge(struct sc_packet_merger *merger, AVPacket *packet); - -#endif diff --git a/app/src/receiver.c b/app/src/receiver.c index 2ccb8a8b..1c80bb00 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -1,164 +1,43 @@ #include "receiver.h" -#include -#include +#include #include +#include "config.h" #include "device_msg.h" -#include "events.h" -#include "util/log.h" -#include "util/str.h" -#include "util/thread.h" - -struct sc_uhid_output_task_data { - struct sc_uhid_devices *uhid_devices; - uint16_t id; - uint16_t size; - uint8_t *data; -}; +#include "lock_util.h" +#include "log.h" bool -sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket, - const struct sc_receiver_callbacks *cbs, void *cbs_userdata) { - bool ok = sc_mutex_init(&receiver->mutex); - if (!ok) { +receiver_init(struct receiver *receiver, socket_t control_socket) { + if (!(receiver->mutex = SDL_CreateMutex())) { return false; } - receiver->control_socket = control_socket; - receiver->acksync = NULL; - receiver->uhid_devices = NULL; - - assert(cbs && cbs->on_ended); - receiver->cbs = cbs; - receiver->cbs_userdata = cbs_userdata; - return true; } void -sc_receiver_destroy(struct sc_receiver *receiver) { - sc_mutex_destroy(&receiver->mutex); +receiver_destroy(struct receiver *receiver) { + SDL_DestroyMutex(receiver->mutex); } static void -task_set_clipboard(void *userdata) { - assert(sc_thread_get_id() == SC_MAIN_THREAD_ID); - - char *text = userdata; - - char *current = SDL_GetClipboardText(); - bool same = current && !strcmp(current, text); - SDL_free(current); - if (same) { - LOGD("Computer clipboard unchanged"); - } else { - LOGI("Device clipboard copied"); - SDL_SetClipboardText(text); - } - - free(text); -} - -static void -task_uhid_output(void *userdata) { - assert(sc_thread_get_id() == SC_MAIN_THREAD_ID); - - struct sc_uhid_output_task_data *data = userdata; - - sc_uhid_devices_process_hid_output(data->uhid_devices, data->id, data->data, - data->size); - - free(data->data); - free(data); -} - -static void -process_msg(struct sc_receiver *receiver, struct sc_device_msg *msg) { +process_msg(struct receiver *receiver, struct device_msg *msg) { switch (msg->type) { - case DEVICE_MSG_TYPE_CLIPBOARD: { - // Take ownership of the text (do not destroy the msg) - char *text = msg->clipboard.text; - - bool ok = sc_post_to_main_thread(task_set_clipboard, text); - if (!ok) { - LOGW("Could not post clipboard to main thread"); - free(text); - return; - } - - break; - } - case DEVICE_MSG_TYPE_ACK_CLIPBOARD: - LOGD("Ack device clipboard sequence=%" PRIu64_, - msg->ack_clipboard.sequence); - - // This is a programming error to receive this message if there is - // no ACK synchronization mechanism - assert(receiver->acksync); - - // Also check at runtime (do not trust the server) - if (!receiver->acksync) { - LOGE("Received unexpected ack"); - return; - } - - sc_acksync_ack(receiver->acksync, msg->ack_clipboard.sequence); - // No allocation to free in the msg - break; - case DEVICE_MSG_TYPE_UHID_OUTPUT: - if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) { - char *hex = sc_str_to_hex_string(msg->uhid_output.data, - msg->uhid_output.size); - if (hex) { - LOGV("UHID output [%" PRIu16 "] %s", - msg->uhid_output.id, hex); - free(hex); - } else { - LOGV("UHID output [%" PRIu16 "] size=%" PRIu16, - msg->uhid_output.id, msg->uhid_output.size); - } - } - - if (!receiver->uhid_devices) { - LOGE("Received unexpected HID output message"); - sc_device_msg_destroy(msg); - return; - } - - struct sc_uhid_output_task_data *data = malloc(sizeof(*data)); - if (!data) { - LOG_OOM(); - return; - } - - // It is guaranteed that these pointers will still be valid when - // the main thread will process them (the main thread will stop - // processing SC_EVENT_RUN_ON_MAIN_THREAD on exit, when everything - // gets deinitialized) - data->uhid_devices = receiver->uhid_devices; - data->id = msg->uhid_output.id; - data->data = msg->uhid_output.data; // take ownership - data->size = msg->uhid_output.size; - - bool ok = sc_post_to_main_thread(task_uhid_output, data); - if (!ok) { - LOGW("Could not post UHID output to main thread"); - free(data->data); - free(data); - return; - } - + case DEVICE_MSG_TYPE_CLIPBOARD: + LOGI("Device clipboard copied"); + SDL_SetClipboardText(msg->clipboard.text); break; } } static ssize_t -process_msgs(struct sc_receiver *receiver, const uint8_t *buf, size_t len) { +process_msgs(struct receiver *receiver, const unsigned char *buf, size_t len) { size_t head = 0; for (;;) { - struct sc_device_msg msg; - ssize_t r = sc_device_msg_deserialize(&buf[head], len - head, &msg); + struct device_msg msg; + ssize_t r = device_msg_deserialize(&buf[head], len - head, &msg); if (r == -1) { return -1; } @@ -167,10 +46,10 @@ process_msgs(struct sc_receiver *receiver, const uint8_t *buf, size_t len) { } process_msg(receiver, &msg); - // the device msg must be destroyed by process_msg() + device_msg_destroy(&msg); head += r; - assert(head <= len); + SDL_assert(head <= len); if (head == len) { return head; } @@ -179,51 +58,43 @@ process_msgs(struct sc_receiver *receiver, const uint8_t *buf, size_t len) { static int run_receiver(void *data) { - struct sc_receiver *receiver = data; + struct receiver *receiver = data; - static uint8_t buf[DEVICE_MSG_MAX_SIZE]; + unsigned char buf[DEVICE_MSG_SERIALIZED_MAX_SIZE]; size_t head = 0; - bool error = false; - for (;;) { - assert(head < DEVICE_MSG_MAX_SIZE); - ssize_t r = net_recv(receiver->control_socket, buf + head, - DEVICE_MSG_MAX_SIZE - head); + SDL_assert(head < DEVICE_MSG_SERIALIZED_MAX_SIZE); + ssize_t r = net_recv(receiver->control_socket, buf, + DEVICE_MSG_SERIALIZED_MAX_SIZE - head); if (r <= 0) { LOGD("Receiver stopped"); - // device disconnected: keep error=false break; } - head += r; - ssize_t consumed = process_msgs(receiver, buf, head); + ssize_t consumed = process_msgs(receiver, buf, r); if (consumed == -1) { // an error occurred - error = true; break; } if (consumed) { - head -= consumed; // shift the remaining data in the buffer - memmove(buf, &buf[consumed], head); + memmove(buf, &buf[consumed], r - consumed); + head = r - consumed; } } - receiver->cbs->on_ended(receiver, error, receiver->cbs_userdata); - return 0; } bool -sc_receiver_start(struct sc_receiver *receiver) { +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"); + receiver->thread = SDL_CreateThread(run_receiver, "receiver", receiver); + if (!receiver->thread) { + LOGC("Could not start receiver thread"); return false; } @@ -231,6 +102,6 @@ sc_receiver_start(struct sc_receiver *receiver) { } void -sc_receiver_join(struct sc_receiver *receiver) { - sc_thread_join(&receiver->thread, NULL); +receiver_join(struct receiver *receiver) { + SDL_WaitThread(receiver->thread, NULL); } diff --git a/app/src/receiver.h b/app/src/receiver.h index b1ae4fde..c119b827 100644 --- a/app/src/receiver.h +++ b/app/src/receiver.h @@ -1,46 +1,32 @@ -#ifndef SC_RECEIVER_H -#define SC_RECEIVER_H - -#include "common.h" +#ifndef RECEIVER_H +#define RECEIVER_H #include +#include +#include -#include "uhid/uhid_output.h" -#include "util/acksync.h" -#include "util/net.h" -#include "util/thread.h" +#include "net.h" // receive events from the device // managed by the controller -struct sc_receiver { - sc_socket control_socket; - sc_thread thread; - sc_mutex mutex; - - struct sc_acksync *acksync; - struct sc_uhid_devices *uhid_devices; - - const struct sc_receiver_callbacks *cbs; - void *cbs_userdata; -}; - -struct sc_receiver_callbacks { - void (*on_ended)(struct sc_receiver *receiver, bool error, void *userdata); +struct receiver { + socket_t control_socket; + SDL_Thread *thread; + SDL_mutex *mutex; }; bool -sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket, - const struct sc_receiver_callbacks *cbs, void *cbs_userdata); +receiver_init(struct receiver *receiver, socket_t control_socket); void -sc_receiver_destroy(struct sc_receiver *receiver); +receiver_destroy(struct receiver *receiver); bool -sc_receiver_start(struct sc_receiver *receiver); +receiver_start(struct receiver *receiver); -// no sc_receiver_stop(), it will automatically stop on control_socket shutdown +// no receiver_stop(), it will automatically stop on control_socket shutdown void -sc_receiver_join(struct sc_receiver *receiver); +receiver_join(struct receiver *receiver); #endif diff --git a/app/src/recorder.c b/app/src/recorder.c index c26f8f2d..321a17ee 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -1,22 +1,11 @@ #include "recorder.h" -#include -#include -#include -#include -#include -#include #include -#include +#include -#include "util/log.h" -#include "util/str.h" - -/** Downcast packet sinks to recorder */ -#define DOWNCAST_VIDEO(SINK) \ - container_of(SINK, struct sc_recorder, video_packet_sink) -#define DOWNCAST_AUDIO(SINK) \ - container_of(SINK, struct sc_recorder, audio_packet_sink) +#include "compat.h" +#include "config.h" +#include "log.h" static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us @@ -32,108 +21,47 @@ find_muxer(const char *name) { #else oformat = av_oformat_next(oformat); #endif - // until null or containing the requested name - } while (oformat && !sc_str_list_contains(oformat->name, ',', name)); + // until null or with name "mp4" + } while (oformat && strcmp(oformat->name, name)); return oformat; } -static AVPacket * -sc_recorder_packet_ref(const AVPacket *packet) { - AVPacket *p = av_packet_alloc(); - if (!p) { - LOG_OOM(); - return NULL; - } - - if (av_packet_ref(p, packet)) { - av_packet_free(&p); - return NULL; - } - - return p; -} - -static void -sc_recorder_queue_clear(struct sc_recorder_queue *queue) { - while (!sc_vecdeque_is_empty(queue)) { - AVPacket *p = sc_vecdeque_pop(queue); - av_packet_free(&p); - } -} - -static const char * -sc_recorder_get_format_name(enum sc_record_format format) { - switch (format) { - case SC_RECORD_FORMAT_MP4: - case SC_RECORD_FORMAT_M4A: - case SC_RECORD_FORMAT_AAC: - return "mp4"; - case SC_RECORD_FORMAT_MKV: - case SC_RECORD_FORMAT_MKA: - return "matroska"; - case SC_RECORD_FORMAT_OPUS: - return "opus"; - case SC_RECORD_FORMAT_FLAC: - return "flac"; - case SC_RECORD_FORMAT_WAV: - return "wav"; - default: - return NULL; - } -} - -static bool -sc_recorder_set_extradata(AVStream *ostream, const AVPacket *packet) { - uint8_t *extradata = av_malloc(packet->size * sizeof(uint8_t)); - if (!extradata) { - LOG_OOM(); +bool +recorder_init(struct recorder *recorder, + const char *filename, + enum recorder_format format, + struct size declared_frame_size) { + recorder->filename = SDL_strdup(filename); + if (!recorder->filename) { + LOGE("Cannot strdup filename"); return false; } - // copy the first packet to the extra data - memcpy(extradata, packet->data, packet->size); + recorder->format = format; + recorder->declared_frame_size = declared_frame_size; + recorder->header_written = false; - ostream->codecpar->extradata = extradata; - ostream->codecpar->extradata_size = packet->size; return true; } -static inline void -sc_recorder_rescale_packet(AVStream *stream, AVPacket *packet) { - av_packet_rescale_ts(packet, SCRCPY_TIME_BASE, stream->time_base); +void +recorder_destroy(struct recorder *recorder) { + SDL_free(recorder->filename); } -static bool -sc_recorder_write_stream(struct sc_recorder *recorder, - struct sc_recorder_stream *st, AVPacket *packet) { - AVStream *stream = recorder->ctx->streams[st->index]; - sc_recorder_rescale_packet(stream, packet); - if (st->last_pts != AV_NOPTS_VALUE && packet->pts <= st->last_pts) { - LOGD("Fixing PTS non monotonically increasing in stream %d " - "(%" PRIi64 " >= %" PRIi64 ")", - st->index, st->last_pts, packet->pts); - packet->pts = ++st->last_pts; - packet->dts = packet->pts; - } else { - st->last_pts = packet->pts; +static const char * +recorder_get_format_name(enum recorder_format format) { + switch (format) { + case RECORDER_FORMAT_MP4: return "mp4"; + case RECORDER_FORMAT_MKV: return "matroska"; + default: return NULL; } - return av_interleaved_write_frame(recorder->ctx, packet) >= 0; } -static inline bool -sc_recorder_write_video(struct sc_recorder *recorder, AVPacket *packet) { - return sc_recorder_write_stream(recorder, &recorder->video_stream, packet); -} - -static inline bool -sc_recorder_write_audio(struct sc_recorder *recorder, AVPacket *packet) { - return sc_recorder_write_stream(recorder, &recorder->audio_stream, packet); -} - -static bool -sc_recorder_open_output_file(struct sc_recorder *recorder) { - const char *format_name = sc_recorder_get_format_name(recorder->format); - assert(format_name); +bool +recorder_open(struct recorder *recorder, const AVCodec *input_codec) { + const char *format_name = recorder_get_format_name(recorder->format); + SDL_assert(format_name); const AVOutputFormat *format = find_muxer(format_name); if (!format) { LOGE("Could not find muxer"); @@ -142,21 +70,7 @@ sc_recorder_open_output_file(struct sc_recorder *recorder) { recorder->ctx = avformat_alloc_context(); if (!recorder->ctx) { - LOG_OOM(); - return false; - } - - char *file_url = sc_str_concat("file:", recorder->filename); - if (!file_url) { - avformat_free_context(recorder->ctx); - return false; - } - - int ret = avio_open(&recorder->ctx->pb, file_url, AVIO_FLAG_WRITE); - free(file_url); - if (ret < 0) { - LOGE("Failed to open output file: %s", recorder->filename); - avformat_free_context(recorder->ctx); + LOGE("Could not allocate output context"); return false; } @@ -166,688 +80,102 @@ sc_recorder_open_output_file(struct sc_recorder *recorder) { // recorder->ctx->oformat = (AVOutputFormat *) format; - av_dict_set(&recorder->ctx->metadata, "comment", - "Recorded by scrcpy " SCRCPY_VERSION, 0); + AVStream *ostream = avformat_new_stream(recorder->ctx, input_codec); + if (!ostream) { + avformat_free_context(recorder->ctx); + return false; + } + +#ifdef SCRCPY_LAVF_HAS_NEW_CODEC_PARAMS_API + ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; + ostream->codecpar->codec_id = input_codec->id; + ostream->codecpar->format = AV_PIX_FMT_YUV420P; + ostream->codecpar->width = recorder->declared_frame_size.width; + ostream->codecpar->height = recorder->declared_frame_size.height; +#else + ostream->codec->codec_type = AVMEDIA_TYPE_VIDEO; + ostream->codec->codec_id = input_codec->id; + ostream->codec->pix_fmt = AV_PIX_FMT_YUV420P; + ostream->codec->width = recorder->declared_frame_size.width; + ostream->codec->height = recorder->declared_frame_size.height; +#endif + + int ret = avio_open(&recorder->ctx->pb, recorder->filename, + AVIO_FLAG_WRITE); + if (ret < 0) { + LOGE("Failed to open output file: %s", recorder->filename); + // ostream will be cleaned up during context cleaning + avformat_free_context(recorder->ctx); + return false; + } LOGI("Recording started to %s file: %s", format_name, recorder->filename); + return true; } -static void -sc_recorder_close_output_file(struct sc_recorder *recorder) { - avio_close(recorder->ctx->pb); - avformat_free_context(recorder->ctx); -} - -static inline bool -sc_recorder_must_wait_for_config_packets(struct sc_recorder *recorder) { - if (recorder->video && sc_vecdeque_is_empty(&recorder->video_queue)) { - // The video queue is empty - return true; - } - - if (recorder->audio && recorder->audio_expects_config_packet - && sc_vecdeque_is_empty(&recorder->audio_queue)) { - // The audio queue is empty (when audio is enabled) - return true; - } - - // No queue is empty - return false; -} - -static bool -sc_recorder_process_header(struct sc_recorder *recorder) { - sc_mutex_lock(&recorder->mutex); - - while (!recorder->stopped && - ((recorder->video && !recorder->video_init) - || (recorder->audio && !recorder->audio_init) - || sc_recorder_must_wait_for_config_packets(recorder))) { - sc_cond_wait(&recorder->cond, &recorder->mutex); - } - - if (recorder->video && sc_vecdeque_is_empty(&recorder->video_queue)) { - assert(recorder->stopped); - // If the recorder is stopped, don't process anything if there are not - // at least video packets - sc_mutex_unlock(&recorder->mutex); - return false; - } - - AVPacket *video_pkt = NULL; - if (!sc_vecdeque_is_empty(&recorder->video_queue)) { - assert(recorder->video); - video_pkt = sc_vecdeque_pop(&recorder->video_queue); - } - - AVPacket *audio_pkt = NULL; - if (recorder->audio_expects_config_packet && - !sc_vecdeque_is_empty(&recorder->audio_queue)) { - assert(recorder->audio); - audio_pkt = sc_vecdeque_pop(&recorder->audio_queue); - } - - sc_mutex_unlock(&recorder->mutex); - - int ret = false; - - if (video_pkt) { - if (video_pkt->pts != AV_NOPTS_VALUE) { - LOGE("The first video packet is not a config packet"); - goto end; - } - - assert(recorder->video_stream.index >= 0); - AVStream *video_stream = - recorder->ctx->streams[recorder->video_stream.index]; - bool ok = sc_recorder_set_extradata(video_stream, video_pkt); - if (!ok) { - goto end; - } - } - - if (audio_pkt) { - if (audio_pkt->pts != AV_NOPTS_VALUE) { - LOGE("The first audio packet is not a config packet"); - goto end; - } - - assert(recorder->audio_stream.index >= 0); - AVStream *audio_stream = - recorder->ctx->streams[recorder->audio_stream.index]; - bool ok = sc_recorder_set_extradata(audio_stream, audio_pkt); - if (!ok) { - goto end; - } - } - - bool ok = avformat_write_header(recorder->ctx, NULL) >= 0; - if (!ok) { - LOGE("Failed to write header to %s", recorder->filename); - goto end; - } - - ret = true; - -end: - if (video_pkt) { - av_packet_free(&video_pkt); - } - if (audio_pkt) { - av_packet_free(&audio_pkt); - } - - return ret; -} - -static bool -sc_recorder_process_packets(struct sc_recorder *recorder) { - int64_t pts_origin = AV_NOPTS_VALUE; - - bool header_written = sc_recorder_process_header(recorder); - if (!header_written) { - return false; - } - - AVPacket *video_pkt = NULL; - AVPacket *audio_pkt = NULL; - - // We can write a video packet only once we received the next one so that - // we can set its duration (next_pts - current_pts) - AVPacket *video_pkt_previous = NULL; - - bool error = false; - - for (;;) { - sc_mutex_lock(&recorder->mutex); - - while (!recorder->stopped) { - if (recorder->video && !video_pkt && - !sc_vecdeque_is_empty(&recorder->video_queue)) { - // A new packet may be assigned to video_pkt and be processed - break; - } - if (recorder->audio && !audio_pkt - && !sc_vecdeque_is_empty(&recorder->audio_queue)) { - // A new packet may be assigned to audio_pkt and be processed - break; - } - sc_cond_wait(&recorder->cond, &recorder->mutex); - } - - // If stopped is set, continue to process the remaining events (to - // finish the recording) before actually stopping. - - // If there is no video, then the video_queue will remain empty forever - // and video_pkt will always be NULL. - assert(recorder->video || (!video_pkt - && sc_vecdeque_is_empty(&recorder->video_queue))); - - // If there is no audio, then the audio_queue will remain empty forever - // and audio_pkt will always be NULL. - assert(recorder->audio || (!audio_pkt - && sc_vecdeque_is_empty(&recorder->audio_queue))); - - if (!video_pkt && !sc_vecdeque_is_empty(&recorder->video_queue)) { - video_pkt = sc_vecdeque_pop(&recorder->video_queue); - } - - if (!audio_pkt && !sc_vecdeque_is_empty(&recorder->audio_queue)) { - audio_pkt = sc_vecdeque_pop(&recorder->audio_queue); - } - - if (recorder->stopped && !video_pkt && !audio_pkt) { - assert(sc_vecdeque_is_empty(&recorder->video_queue)); - assert(sc_vecdeque_is_empty(&recorder->audio_queue)); - sc_mutex_unlock(&recorder->mutex); - break; - } - - assert(video_pkt || audio_pkt); // at least one - - sc_mutex_unlock(&recorder->mutex); - - // Ignore further config packets (e.g. on device orientation - // change). The next non-config packet will have the config packet - // data prepended. - if (video_pkt && video_pkt->pts == AV_NOPTS_VALUE) { - av_packet_free(&video_pkt); - video_pkt = NULL; - } - - if (audio_pkt && audio_pkt->pts == AV_NOPTS_VALUE) { - av_packet_free(&audio_pkt); - audio_pkt = NULL; - } - - if (pts_origin == AV_NOPTS_VALUE) { - if (!recorder->audio) { - assert(video_pkt); - pts_origin = video_pkt->pts; - } else if (!recorder->video) { - assert(audio_pkt); - pts_origin = audio_pkt->pts; - } else if (video_pkt && audio_pkt) { - pts_origin = MIN(video_pkt->pts, audio_pkt->pts); - } else if (recorder->stopped) { - if (video_pkt) { - // The recorder is stopped without audio, record the video - // packets - pts_origin = video_pkt->pts; - } else { - // Fail if there is no video - error = true; - goto end; - } - } else { - // We need both video and audio packets to initialize pts_origin - continue; - } - } - - assert(pts_origin != AV_NOPTS_VALUE); - - if (video_pkt) { - video_pkt->pts -= pts_origin; - video_pkt->dts = video_pkt->pts; - - if (video_pkt_previous) { - // we now know the duration of the previous packet - video_pkt_previous->duration = video_pkt->pts - - video_pkt_previous->pts; - - bool ok = sc_recorder_write_video(recorder, video_pkt_previous); - av_packet_free(&video_pkt_previous); - if (!ok) { - LOGE("Could not record video packet"); - error = true; - goto end; - } - } - - video_pkt_previous = video_pkt; - video_pkt = NULL; - } - - if (audio_pkt) { - audio_pkt->pts -= pts_origin; - audio_pkt->dts = audio_pkt->pts; - - bool ok = sc_recorder_write_audio(recorder, audio_pkt); - if (!ok) { - LOGE("Could not record audio packet"); - error = true; - goto end; - } - - av_packet_free(&audio_pkt); - audio_pkt = NULL; - } - } - - // Write the last video packet - AVPacket *last = video_pkt_previous; - if (last) { - // assign an arbitrary duration to the last packet - last->duration = 100000; - bool ok = sc_recorder_write_video(recorder, last); - if (!ok) { - // failing to write the last frame is not very serious, no - // future frame may depend on it, so the resulting file - // will still be valid - LOGW("Could not record last packet"); - } - av_packet_free(&last); - } - +void +recorder_close(struct recorder *recorder) { int ret = av_write_trailer(recorder->ctx); if (ret < 0) { LOGE("Failed to write trailer to %s", recorder->filename); - error = false; } + avio_close(recorder->ctx->pb); + avformat_free_context(recorder->ctx); -end: - if (video_pkt) { - av_packet_free(&video_pkt); - } - if (audio_pkt) { - av_packet_free(&audio_pkt); - } - - return !error; + const char *format_name = recorder_get_format_name(recorder->format); + LOGI("Recording complete to %s file: %s", format_name, recorder->filename); } static bool -sc_recorder_record(struct sc_recorder *recorder) { - bool ok = sc_recorder_open_output_file(recorder); - if (!ok) { +recorder_write_header(struct recorder *recorder, const AVPacket *packet) { + AVStream *ostream = recorder->ctx->streams[0]; + + uint8_t *extradata = av_malloc(packet->size * sizeof(uint8_t)); + if (!extradata) { + LOGC("Cannot allocate extradata"); return false; } - ok = sc_recorder_process_packets(recorder); - sc_recorder_close_output_file(recorder); - return ok; -} + // copy the first packet to the extra data + memcpy(extradata, packet->data, packet->size); -static int -run_recorder(void *data) { - struct sc_recorder *recorder = data; - - // Recording is a background task - bool ok = sc_thread_set_priority(SC_THREAD_PRIORITY_LOW); - (void) ok; // We don't care if it worked - - bool success = sc_recorder_record(recorder); - - sc_mutex_lock(&recorder->mutex); - // Prevent the producer to push any new packet - recorder->stopped = true; - // Discard pending packets - sc_recorder_queue_clear(&recorder->video_queue); - sc_recorder_queue_clear(&recorder->audio_queue); - sc_mutex_unlock(&recorder->mutex); - - if (success) { - const char *format_name = sc_recorder_get_format_name(recorder->format); - LOGI("Recording complete to %s file: %s", format_name, - recorder->filename); - } else { - LOGE("Recording failed to %s", recorder->filename); - } - - LOGD("Recorder thread ended"); - - recorder->cbs->on_ended(recorder, success, recorder->cbs_userdata); - - return 0; -} - -static bool -sc_recorder_set_orientation(AVStream *stream, enum sc_orientation orientation) { - assert(!sc_orientation_is_mirror(orientation)); - - uint8_t *raw_data; -#ifdef SCRCPY_LAVC_HAS_CODECPAR_CODEC_SIDEDATA - AVPacketSideData *sd = - av_packet_side_data_new(&stream->codecpar->coded_side_data, - &stream->codecpar->nb_coded_side_data, - AV_PKT_DATA_DISPLAYMATRIX, - sizeof(int32_t) * 9, 0); - if (!sd) { - LOG_OOM(); - return false; - } - - raw_data = sd->data; +#ifdef SCRCPY_LAVF_HAS_NEW_CODEC_PARAMS_API + ostream->codecpar->extradata = extradata; + ostream->codecpar->extradata_size = packet->size; #else - raw_data = av_stream_new_side_data(stream, AV_PKT_DATA_DISPLAYMATRIX, - sizeof(int32_t) * 9); - if (!raw_data) { - LOG_OOM(); - return false; - } + ostream->codec->extradata = extradata; + ostream->codec->extradata_size = packet->size; #endif - int32_t *matrix = (int32_t *) raw_data; - - unsigned rotation = orientation; - unsigned angle = rotation * 90; - - av_display_rotation_set(matrix, angle); + int ret = avformat_write_header(recorder->ctx, NULL); + if (ret < 0) { + LOGE("Failed to write header to %s", recorder->filename); + SDL_free(extradata); + avio_closep(&recorder->ctx->pb); + avformat_free_context(recorder->ctx); + return false; + } return true; } -static bool -sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink, - AVCodecContext *ctx) { - struct sc_recorder *recorder = DOWNCAST_VIDEO(sink); - // only written from this thread, no need to lock - assert(!recorder->video_init); +static void +recorder_rescale_packet(struct recorder *recorder, AVPacket *packet) { + AVStream *ostream = recorder->ctx->streams[0]; + av_packet_rescale_ts(packet, SCRCPY_TIME_BASE, ostream->time_base); +} - sc_mutex_lock(&recorder->mutex); - if (recorder->stopped) { - sc_mutex_unlock(&recorder->mutex); - return false; - } - - AVStream *stream = avformat_new_stream(recorder->ctx, ctx->codec); - if (!stream) { - sc_mutex_unlock(&recorder->mutex); - return false; - } - - int r = avcodec_parameters_from_context(stream->codecpar, ctx); - if (r < 0) { - sc_mutex_unlock(&recorder->mutex); - return false; - } - - recorder->video_stream.index = stream->index; - - if (recorder->orientation != SC_ORIENTATION_0) { - if (!sc_recorder_set_orientation(stream, recorder->orientation)) { - sc_mutex_unlock(&recorder->mutex); +bool +recorder_write(struct recorder *recorder, AVPacket *packet) { + if (!recorder->header_written) { + bool ok = recorder_write_header(recorder, packet); + if (!ok) { return false; } - - LOGI("Record orientation set to %s", - sc_orientation_get_name(recorder->orientation)); + recorder->header_written = true; } - recorder->video_init = true; - sc_cond_signal(&recorder->cond); - sc_mutex_unlock(&recorder->mutex); - - return true; -} - -static void -sc_recorder_video_packet_sink_close(struct sc_packet_sink *sink) { - struct sc_recorder *recorder = DOWNCAST_VIDEO(sink); - // only written from this thread, no need to lock - assert(recorder->video_init); - - sc_mutex_lock(&recorder->mutex); - // EOS also stops the recorder - recorder->stopped = true; - sc_cond_signal(&recorder->cond); - sc_mutex_unlock(&recorder->mutex); -} - -static bool -sc_recorder_video_packet_sink_push(struct sc_packet_sink *sink, - const AVPacket *packet) { - struct sc_recorder *recorder = DOWNCAST_VIDEO(sink); - // only written from this thread, no need to lock - assert(recorder->video_init); - - sc_mutex_lock(&recorder->mutex); - - if (recorder->stopped) { - // reject any new packet - sc_mutex_unlock(&recorder->mutex); - return false; - } - - AVPacket *rec = sc_recorder_packet_ref(packet); - if (!rec) { - LOG_OOM(); - sc_mutex_unlock(&recorder->mutex); - return false; - } - - rec->stream_index = recorder->video_stream.index; - - bool ok = sc_vecdeque_push(&recorder->video_queue, rec); - if (!ok) { - LOG_OOM(); - sc_mutex_unlock(&recorder->mutex); - return false; - } - - sc_cond_signal(&recorder->cond); - - sc_mutex_unlock(&recorder->mutex); - return true; -} - -static bool -sc_recorder_audio_packet_sink_open(struct sc_packet_sink *sink, - AVCodecContext *ctx) { - struct sc_recorder *recorder = DOWNCAST_AUDIO(sink); - assert(recorder->audio); - // only written from this thread, no need to lock - assert(!recorder->audio_init); - - sc_mutex_lock(&recorder->mutex); - - AVStream *stream = avformat_new_stream(recorder->ctx, ctx->codec); - if (!stream) { - sc_mutex_unlock(&recorder->mutex); - return false; - } - - int r = avcodec_parameters_from_context(stream->codecpar, ctx); - if (r < 0) { - sc_mutex_unlock(&recorder->mutex); - return false; - } - - recorder->audio_stream.index = stream->index; - - // A config packet is provided for all supported formats except raw audio - recorder->audio_expects_config_packet = - ctx->codec_id != AV_CODEC_ID_PCM_S16LE; - - recorder->audio_init = true; - sc_cond_signal(&recorder->cond); - sc_mutex_unlock(&recorder->mutex); - - return true; -} - -static void -sc_recorder_audio_packet_sink_close(struct sc_packet_sink *sink) { - struct sc_recorder *recorder = DOWNCAST_AUDIO(sink); - assert(recorder->audio); - // only written from this thread, no need to lock - assert(recorder->audio_init); - - sc_mutex_lock(&recorder->mutex); - // EOS also stops the recorder - recorder->stopped = true; - sc_cond_signal(&recorder->cond); - sc_mutex_unlock(&recorder->mutex); -} - -static bool -sc_recorder_audio_packet_sink_push(struct sc_packet_sink *sink, - const AVPacket *packet) { - struct sc_recorder *recorder = DOWNCAST_AUDIO(sink); - assert(recorder->audio); - // only written from this thread, no need to lock - assert(recorder->audio_init); - - sc_mutex_lock(&recorder->mutex); - - if (recorder->stopped) { - // reject any new packet - sc_mutex_unlock(&recorder->mutex); - return false; - } - - AVPacket *rec = sc_recorder_packet_ref(packet); - if (!rec) { - LOG_OOM(); - sc_mutex_unlock(&recorder->mutex); - return false; - } - - rec->stream_index = recorder->audio_stream.index; - - bool ok = sc_vecdeque_push(&recorder->audio_queue, rec); - if (!ok) { - LOG_OOM(); - sc_mutex_unlock(&recorder->mutex); - return false; - } - - sc_cond_signal(&recorder->cond); - - sc_mutex_unlock(&recorder->mutex); - return true; -} - -static void -sc_recorder_audio_packet_sink_disable(struct sc_packet_sink *sink) { - struct sc_recorder *recorder = DOWNCAST_AUDIO(sink); - assert(recorder->audio); - // only written from this thread, no need to lock - assert(!recorder->audio_init); - - LOGW("Audio stream recording disabled"); - - sc_mutex_lock(&recorder->mutex); - recorder->audio = false; - recorder->audio_init = true; - sc_cond_signal(&recorder->cond); - sc_mutex_unlock(&recorder->mutex); -} - -static void -sc_recorder_stream_init(struct sc_recorder_stream *stream) { - stream->index = -1; - stream->last_pts = AV_NOPTS_VALUE; -} - -bool -sc_recorder_init(struct sc_recorder *recorder, const char *filename, - enum sc_record_format format, bool video, bool audio, - enum sc_orientation orientation, - const struct sc_recorder_callbacks *cbs, void *cbs_userdata) { - assert(!sc_orientation_is_mirror(orientation)); - - recorder->filename = strdup(filename); - if (!recorder->filename) { - LOG_OOM(); - return false; - } - - bool ok = sc_mutex_init(&recorder->mutex); - if (!ok) { - goto error_free_filename; - } - - ok = sc_cond_init(&recorder->cond); - if (!ok) { - goto error_mutex_destroy; - } - - assert(video || audio); - recorder->video = video; - recorder->audio = audio; - - recorder->orientation = orientation; - - sc_vecdeque_init(&recorder->video_queue); - sc_vecdeque_init(&recorder->audio_queue); - recorder->stopped = false; - - recorder->video_init = false; - recorder->audio_init = false; - - recorder->audio_expects_config_packet = false; - - sc_recorder_stream_init(&recorder->video_stream); - sc_recorder_stream_init(&recorder->audio_stream); - - recorder->format = format; - - assert(cbs && cbs->on_ended); - recorder->cbs = cbs; - recorder->cbs_userdata = cbs_userdata; - - if (video) { - static const struct sc_packet_sink_ops video_ops = { - .open = sc_recorder_video_packet_sink_open, - .close = sc_recorder_video_packet_sink_close, - .push = sc_recorder_video_packet_sink_push, - }; - - recorder->video_packet_sink.ops = &video_ops; - } - - if (audio) { - static const struct sc_packet_sink_ops audio_ops = { - .open = sc_recorder_audio_packet_sink_open, - .close = sc_recorder_audio_packet_sink_close, - .push = sc_recorder_audio_packet_sink_push, - .disable = sc_recorder_audio_packet_sink_disable, - }; - - recorder->audio_packet_sink.ops = &audio_ops; - } - - return true; - -error_mutex_destroy: - sc_mutex_destroy(&recorder->mutex); -error_free_filename: - free(recorder->filename); - - return false; -} - -bool -sc_recorder_start(struct sc_recorder *recorder) { - bool ok = sc_thread_create(&recorder->thread, run_recorder, - "scrcpy-recorder", recorder); - if (!ok) { - LOGE("Could not start recorder thread"); - return false; - } - - return true; -} - -void -sc_recorder_stop(struct sc_recorder *recorder) { - sc_mutex_lock(&recorder->mutex); - recorder->stopped = true; - sc_cond_signal(&recorder->cond); - sc_mutex_unlock(&recorder->mutex); -} - -void -sc_recorder_join(struct sc_recorder *recorder) { - sc_thread_join(&recorder->thread, NULL); -} - -void -sc_recorder_destroy(struct sc_recorder *recorder) { - sc_cond_destroy(&recorder->cond); - sc_mutex_destroy(&recorder->mutex); - free(recorder->filename); + recorder_rescale_packet(recorder, packet); + return av_write_frame(recorder->ctx, packet) >= 0; } diff --git a/app/src/recorder.h b/app/src/recorder.h index 70b73836..8a8e3310 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -1,88 +1,38 @@ -#ifndef SC_RECORDER_H -#define SC_RECORDER_H +#ifndef RECORDER_H +#define RECORDER_H + +#include +#include #include "common.h" -#include -#include -#include -#include - -#include "options.h" -#include "trait/packet_sink.h" -#include "util/thread.h" -#include "util/vecdeque.h" - -struct sc_recorder_queue SC_VECDEQUE(AVPacket *); - -struct sc_recorder_stream { - int index; - int64_t last_pts; +enum recorder_format { + RECORDER_FORMAT_MP4 = 1, + RECORDER_FORMAT_MKV, }; -struct sc_recorder { - struct sc_packet_sink video_packet_sink; - struct sc_packet_sink audio_packet_sink; - - /* The audio flag is unprotected: - * - it is initialized from sc_recorder_init() from the main thread; - * - it may be reset once from the recorder thread if the audio is - * disabled dynamically. - * - * Therefore, once the recorder thread is started, only the recorder thread - * may access it without data races. - */ - bool audio; - bool video; - - enum sc_orientation orientation; - +struct recorder { char *filename; - enum sc_record_format format; + enum recorder_format format; AVFormatContext *ctx; - - sc_thread thread; - sc_mutex mutex; - sc_cond cond; - // set on sc_recorder_stop(), packet_sink close or recording failure - bool stopped; - struct sc_recorder_queue video_queue; - struct sc_recorder_queue audio_queue; - - // wake up the recorder thread once the video or audio codec is known - bool video_init; - bool audio_init; - - bool audio_expects_config_packet; - - struct sc_recorder_stream video_stream; - struct sc_recorder_stream audio_stream; - - const struct sc_recorder_callbacks *cbs; - void *cbs_userdata; -}; - -struct sc_recorder_callbacks { - void (*on_ended)(struct sc_recorder *recorder, bool success, - void *userdata); + struct size declared_frame_size; + bool header_written; }; bool -sc_recorder_init(struct sc_recorder *recorder, const char *filename, - enum sc_record_format format, bool video, bool audio, - enum sc_orientation orientation, - const struct sc_recorder_callbacks *cbs, void *cbs_userdata); +recorder_init(struct recorder *recorder, const char *filename, + enum recorder_format format, struct size declared_frame_size); + +void +recorder_destroy(struct recorder *recorder); bool -sc_recorder_start(struct sc_recorder *recorder); +recorder_open(struct recorder *recorder, const AVCodec *input_codec); void -sc_recorder_stop(struct sc_recorder *recorder); +recorder_close(struct recorder *recorder); -void -sc_recorder_join(struct sc_recorder *recorder); - -void -sc_recorder_destroy(struct sc_recorder *recorder); +bool +recorder_write(struct recorder *recorder, AVPacket *packet); #endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index a4c8c340..761edb69 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -1,140 +1,73 @@ #include "scrcpy.h" -#include -#include -#include #include -#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 "audio_player.h" +#include "command.h" +#include "common.h" +#include "compat.h" #include "controller.h" #include "decoder.h" -#include "delay_buffer.h" -#include "demuxer.h" +#include "device.h" #include "events.h" -#include "file_pusher.h" -#include "keyboard_sdk.h" -#include "mouse_sdk.h" +#include "file_handler.h" +#include "fps_counter.h" +#include "input_manager.h" +#include "log.h" +#include "lock_util.h" +#include "net.h" #include "recorder.h" #include "screen.h" #include "server.h" -#include "uhid/gamepad_uhid.h" -#include "uhid/keyboard_uhid.h" -#include "uhid/mouse_uhid.h" -#ifdef HAVE_USB -# include "usb/aoa_hid.h" -# include "usb/gamepad_aoa.h" -# include "usb/keyboard_aoa.h" -# include "usb/mouse_aoa.h" -# include "usb/usb.h" -#endif -#include "util/acksync.h" -#include "util/log.h" -#include "util/rand.h" -#include "util/timeout.h" -#include "util/tick.h" -#ifdef HAVE_V4L2 -# include "v4l2_sink.h" -#endif +#include "stream.h" +#include "tiny_xpm.h" +#include "video_buffer.h" -struct scrcpy { - struct sc_server server; - struct sc_screen screen; - struct sc_audio_player audio_player; - struct sc_demuxer video_demuxer; - struct sc_demuxer audio_demuxer; - struct sc_decoder video_decoder; - struct sc_decoder audio_decoder; - struct sc_recorder recorder; - struct sc_delay_buffer video_buffer; -#ifdef HAVE_V4L2 - struct sc_v4l2_sink v4l2_sink; - struct sc_delay_buffer v4l2_buffer; -#endif - struct sc_controller controller; - struct sc_file_pusher file_pusher; -#ifdef HAVE_USB - struct sc_usb usb; - struct sc_aoa aoa; - // sequence/ack helper to synchronize clipboard and Ctrl+v via HID - struct sc_acksync acksync; -#endif - struct sc_uhid_devices uhid_devices; - union { - struct sc_keyboard_sdk keyboard_sdk; - struct sc_keyboard_uhid keyboard_uhid; -#ifdef HAVE_USB - struct sc_keyboard_aoa keyboard_aoa; -#endif - }; - union { - struct sc_mouse_sdk mouse_sdk; - struct sc_mouse_uhid mouse_uhid; -#ifdef HAVE_USB - struct sc_mouse_aoa mouse_aoa; -#endif - }; - union { - struct sc_gamepad_uhid gamepad_uhid; -#ifdef HAVE_USB - struct sc_gamepad_aoa gamepad_aoa; -#endif - }; - struct sc_timeout timeout; +static struct server server = SERVER_INITIALIZER; +static struct screen screen = SCREEN_INITIALIZER; +static struct fps_counter fps_counter; +static struct video_buffer video_buffer; +static struct stream stream; +static struct decoder decoder; +static struct recorder recorder; +static struct controller controller; +static struct file_handler file_handler; + +static struct input_manager input_manager = { + .controller = &controller, + .video_buffer = &video_buffer, + .screen = &screen, }; -#ifdef _WIN32 -static BOOL WINAPI windows_ctrl_handler(DWORD ctrl_type) { - if (ctrl_type == CTRL_C_EVENT) { - sc_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"); +// init SDL and set appropriate hints +static bool +sdl_init_and_configure(bool display) { + uint32_t flags = display ? SDL_INIT_VIDEO : SDL_INIT_EVENTS; + if (SDL_Init(flags)) { + LOGC("Could not initialize SDL: %s", SDL_GetError()); + return false; } - // App name used in various contexts (such as PulseAudio) -#if defined(SCRCPY_SDL_HAS_HINT_APP_NAME) - if (!SDL_SetHint(SDL_HINT_APP_NAME, "scrcpy")) { - LOGW("Could not set app name"); - } -#elif defined(SCRCPY_SDL_HAS_HINT_AUDIO_DEVICE_APP_NAME) - if (!SDL_SetHint(SDL_HINT_AUDIO_DEVICE_APP_NAME, "scrcpy")) { - LOGW("Could not set audio device app name"); - } -#endif + atexit(SDL_Quit); - // Linear filtering - if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1")) { - LOGW("Could not enable linear filtering"); + if (!display) { + return true; } + // Use the best available scale quality + if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "2")) { + LOGW("Could not enable bilinear filtering"); + } + +#ifdef SCRCPY_SDL_HAS_HINT_MOUSE_FOCUS_CLICKTHROUGH // Handle a click to gain focus as any other click if (!SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1")) { LOGW("Could not enable mouse focus clickthrough"); } - -#ifdef SCRCPY_SDL_HAS_HINT_TOUCH_MOUSE_EVENTS - // Disable synthetic mouse events from touch events - // Touch events with id SDL_TOUCH_MOUSEID are ignored anyway, but it is - // better not to generate them in the first place. - if (!SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0")) { - LOGW("Could not disable synthetic mouse events"); - } #endif #ifdef SCRCPY_SDL_HAS_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR @@ -149,920 +82,394 @@ sdl_set_hints(const char *render_driver) { LOGW("Could not disable minimize on focus loss"); } - if (!SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1")) { - LOGW("Could not allow joystick background events"); - } + // Do not disable the screensaver when scrcpy is running + SDL_EnableScreenSaver(); + + return true; } -static void -sdl_configure(bool video_playback, bool disable_screensaver) { -#ifdef _WIN32 - // Clean up properly on Ctrl+C on Windows - bool ok = SetConsoleCtrlHandler(windows_ctrl_handler, TRUE); - if (!ok) { - LOGW("Could not set Ctrl+C handler"); - } -#endif // _WIN32 - if (!video_playback) { - return; - } +#if defined(__APPLE__) || defined(__WINDOWS__) +# define CONTINUOUS_RESIZING_WORKAROUND +#endif - if (disable_screensaver) { - SDL_DisableScreenSaver(); - } else { - SDL_EnableScreenSaver(); +#ifdef CONTINUOUS_RESIZING_WORKAROUND +// On Windows and MacOS, resizing blocks the event loop, so resizing events are +// not triggered. As a workaround, handle them in an event handler. +// +// +// +static int +event_watcher(void *data, SDL_Event *event) { + if (event->type == SDL_WINDOWEVENT + && event->window.event == SDL_WINDOWEVENT_RESIZED) { + // called from another thread, not very safe, but it's a workaround! + screen_render(&screen); } + return 0; +} +#endif + +static bool +is_apk(const char *file) { + const char *ext = strrchr(file, '.'); + return ext && !strcmp(ext, ".apk"); } -static enum scrcpy_exit_code -event_loop(struct scrcpy *s, bool has_screen) { - SDL_Event event; - while (SDL_WaitEvent(&event)) { - switch (event.type) { - case SC_EVENT_DEVICE_DISCONNECTED: - LOGW("Device disconnected"); - return SCRCPY_EXIT_DISCONNECTED; - case SC_EVENT_DEMUXER_ERROR: - LOGE("Demuxer error"); - return SCRCPY_EXIT_FAILURE; - case SC_EVENT_CONTROLLER_ERROR: - LOGE("Controller error"); - return SCRCPY_EXIT_FAILURE; - case SC_EVENT_RECORDER_ERROR: - LOGE("Recorder error"); - return SCRCPY_EXIT_FAILURE; - case SC_EVENT_AOA_OPEN_ERROR: - LOGE("AOA open error"); - return SCRCPY_EXIT_FAILURE; - case SC_EVENT_TIME_LIMIT_REACHED: - LOGI("Time limit reached"); - return SCRCPY_EXIT_SUCCESS; - case SDL_QUIT: - LOGD("User requested to quit"); - return SCRCPY_EXIT_SUCCESS; - case SC_EVENT_RUN_ON_MAIN_THREAD: { - sc_runnable_fn run = event.user.data1; - void *userdata = event.user.data2; - run(userdata); +enum event_result { + EVENT_RESULT_CONTINUE, + EVENT_RESULT_STOPPED_BY_USER, + EVENT_RESULT_STOPPED_BY_EOS, +}; + +static enum event_result +handle_event(SDL_Event *event, bool control) { + switch (event->type) { + case EVENT_STREAM_STOPPED: + LOGD("Video stream stopped"); + return EVENT_RESULT_STOPPED_BY_EOS; + case SDL_QUIT: + LOGD("User requested to quit"); + return EVENT_RESULT_STOPPED_BY_USER; + case EVENT_NEW_FRAME: + if (!screen.has_frame) { + screen.has_frame = true; + // this is the very first frame, show the window + screen_show_window(&screen); + } + if (!screen_update_frame(&screen, &video_buffer)) { + return EVENT_RESULT_CONTINUE; + } + break; + case SDL_WINDOWEVENT: + switch (event->window.event) { + case SDL_WINDOWEVENT_EXPOSED: + case SDL_WINDOWEVENT_SIZE_CHANGED: + screen_render(&screen); + break; + } + break; + case SDL_TEXTINPUT: + if (!control) { break; } - default: - if (has_screen && !sc_screen_handle_event(&s->screen, &event)) { - return SCRCPY_EXIT_FAILURE; - } + input_manager_process_text_input(&input_manager, &event->text); + break; + case SDL_KEYDOWN: + case SDL_KEYUP: + // some key events do not interact with the device, so process the + // event even if control is disabled + input_manager_process_key(&input_manager, &event->key, control); + break; + case SDL_MOUSEMOTION: + if (!control) { break; + } + input_manager_process_mouse_motion(&input_manager, &event->motion); + break; + case SDL_MOUSEWHEEL: + if (!control) { + break; + } + input_manager_process_mouse_wheel(&input_manager, &event->wheel); + break; + case SDL_MOUSEBUTTONDOWN: + case SDL_MOUSEBUTTONUP: + // some mouse events do not interact with the device, so process + // the event even if control is disabled + input_manager_process_mouse_button(&input_manager, &event->button, + control); + break; + case SDL_DROPFILE: { + if (!control) { + break; + } + file_handler_action_t action; + if (is_apk(event->drop.file)) { + action = ACTION_INSTALL_APK; + } else { + action = ACTION_PUSH_FILE; + } + file_handler_request(&file_handler, action, event->drop.file); + break; } } - return SCRCPY_EXIT_FAILURE; + return EVENT_RESULT_CONTINUE; } -static void -terminate_event_loop(void) { - sc_reject_new_runnables(); - - SDL_Event event; - while (SDL_PollEvent(&event)) { - if (event.type == SC_EVENT_RUN_ON_MAIN_THREAD) { - // Make sure all posted runnables are run, to avoid memory leaks - sc_runnable_fn run = event.user.data1; - void *userdata = event.user.data2; - run(userdata); - } - } -} - -// Return true on success, false on error static bool -await_for_server(bool *connected) { +event_loop(bool display, bool control) { +#ifdef CONTINUOUS_RESIZING_WORKAROUND + if (display) { + SDL_AddEventWatch(event_watcher, NULL); + } +#endif SDL_Event event; while (SDL_WaitEvent(&event)) { - switch (event.type) { - case SDL_QUIT: - if (connected) { - *connected = false; - } + enum event_result result = handle_event(&event, control); + switch (result) { + case EVENT_RESULT_STOPPED_BY_USER: return true; - case SC_EVENT_SERVER_CONNECTION_FAILED: + case EVENT_RESULT_STOPPED_BY_EOS: return false; - case SC_EVENT_SERVER_CONNECTED: - if (connected) { - *connected = true; - } - return true; - default: + case EVENT_RESULT_CONTINUE: break; } } - - LOGE("SDL_WaitEvent() error: %s", SDL_GetError()); return false; } -static void -sc_recorder_on_ended(struct sc_recorder *recorder, bool success, - void *userdata) { - (void) recorder; - (void) userdata; +static process_t +set_show_touches_enabled(const char *serial, bool enabled) { + const char *value = enabled ? "1" : "0"; + const char *const adb_cmd[] = { + "shell", "settings", "put", "system", "show_touches", value + }; + return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); +} - if (!success) { - sc_push_event(SC_EVENT_RECORDER_ERROR); +static void +wait_show_touches(process_t process) { + // reap the process, ignore the result + process_check_success(process, "show_touches"); +} + +static SDL_LogPriority +sdl_priority_from_av_level(int level) { + switch (level) { + case AV_LOG_PANIC: + case AV_LOG_FATAL: + return SDL_LOG_PRIORITY_CRITICAL; + case AV_LOG_ERROR: + return SDL_LOG_PRIORITY_ERROR; + case AV_LOG_WARNING: + return SDL_LOG_PRIORITY_WARN; + case AV_LOG_INFO: + return SDL_LOG_PRIORITY_INFO; } + // do not forward others, which are too verbose + return 0; } static void -sc_video_demuxer_on_ended(struct sc_demuxer *demuxer, - enum sc_demuxer_status status, void *userdata) { - (void) demuxer; - (void) userdata; - - // The device may not decide to disable the video - assert(status != SC_DEMUXER_STATUS_DISABLED); - - if (status == SC_DEMUXER_STATUS_EOS) { - sc_push_event(SC_EVENT_DEVICE_DISCONNECTED); - } else { - sc_push_event(SC_EVENT_DEMUXER_ERROR); +av_log_callback(void *avcl, int level, const char *fmt, va_list vl) { + SDL_LogPriority priority = sdl_priority_from_av_level(level); + if (priority == 0) { + return; } -} - -static void -sc_audio_demuxer_on_ended(struct sc_demuxer *demuxer, - enum sc_demuxer_status status, void *userdata) { - (void) demuxer; - - const struct scrcpy_options *options = userdata; - - // Contrary to the video demuxer, keep mirroring if only the audio fails - // (unless --require-audio is set). - if (status == SC_DEMUXER_STATUS_EOS) { - sc_push_event(SC_EVENT_DEVICE_DISCONNECTED); - } else if (status == SC_DEMUXER_STATUS_ERROR - || (status == SC_DEMUXER_STATUS_DISABLED - && options->require_audio)) { - sc_push_event(SC_EVENT_DEMUXER_ERROR); + char *local_fmt = SDL_malloc(strlen(fmt) + 10); + if (!local_fmt) { + LOGC("Cannot allocate string"); + return; } + // strcpy is safe here, the destination is large enough + strcpy(local_fmt, "[FFmpeg] "); + strcpy(local_fmt + 9, fmt); + SDL_LogMessageV(SDL_LOG_CATEGORY_VIDEO, priority, local_fmt, vl); + SDL_free(local_fmt); } -static void -sc_controller_on_ended(struct sc_controller *controller, bool error, - void *userdata) { - // Note: this function may be called twice, once from the controller thread - // and once from the receiver thread - (void) controller; - (void) userdata; - - if (error) { - sc_push_event(SC_EVENT_CONTROLLER_ERROR); - } else { - sc_push_event(SC_EVENT_DEVICE_DISCONNECTED); - } -} - -static void -sc_server_on_connection_failed(struct sc_server *server, void *userdata) { - (void) server; - (void) userdata; - - sc_push_event(SC_EVENT_SERVER_CONNECTION_FAILED); -} - -static void -sc_server_on_connected(struct sc_server *server, void *userdata) { - (void) server; - (void) userdata; - - sc_push_event(SC_EVENT_SERVER_CONNECTED); -} - -static void -sc_server_on_disconnected(struct sc_server *server, void *userdata) { - (void) server; - (void) userdata; - - LOGD("Server disconnected"); - // Do nothing, the disconnection will be handled by the "stream stopped" - // event -} - -static void -sc_timeout_on_timeout(struct sc_timeout *timeout, void *userdata) { - (void) timeout; - (void) userdata; - - sc_push_event(SC_EVENT_TIME_LIMIT_REACHED); -} - -// Generate a scrcpy id to differentiate multiple running scrcpy instances -static uint32_t -scrcpy_generate_scid(void) { - struct sc_rand rand; - sc_rand_init(&rand); - // Only use 31 bits to avoid issues with signed values on the Java-side - return sc_rand_u32(&rand) & 0x7FFFFFFF; -} - -static void -init_sdl_gamepads(void) { - // Trigger a SDL_CONTROLLERDEVICEADDED event for all gamepads already - // connected - int num_joysticks = SDL_NumJoysticks(); - for (int i = 0; i < num_joysticks; ++i) { - if (SDL_IsGameController(i)) { - SDL_Event event; - event.cdevice.type = SDL_CONTROLLERDEVICEADDED; - event.cdevice.which = i; - SDL_PushEvent(&event); - } - } -} - -enum scrcpy_exit_code -scrcpy(struct scrcpy_options *options) { - static struct scrcpy scrcpy; -#ifndef NDEBUG - // Detect missing initializations - memset(&scrcpy, 42, sizeof(scrcpy)); -#endif - struct scrcpy *s = &scrcpy; - - // Minimal SDL initialization - if (SDL_Init(SDL_INIT_EVENTS)) { - LOGE("Could not initialize SDL: %s", SDL_GetError()); - return SCRCPY_EXIT_FAILURE; +bool +scrcpy(const struct scrcpy_options *options) { + bool record = !!options->record_filename; + struct server_params params = { + .crop = options->crop, + .local_port = options->port, + .max_size = options->max_size, + .bit_rate = options->bit_rate, + .send_frame_meta = record, + .control = options->control, + }; + if (!server_start(&server, options->serial, ¶ms)) { + return false; } - atexit(SDL_Quit); + process_t proc_show_touches = PROCESS_NONE; + bool show_touches_waited; + if (options->show_touches) { + LOGI("Enable show_touches"); + proc_show_touches = set_show_touches_enabled(options->serial, true); + show_touches_waited = false; + } - enum scrcpy_exit_code ret = SCRCPY_EXIT_FAILURE; + bool ret = false; - bool server_started = false; - bool file_pusher_initialized = false; + bool fps_counter_initialized = false; + bool video_buffer_initialized = false; + bool file_handler_initialized = false; bool recorder_initialized = false; - bool recorder_started = false; -#ifdef HAVE_V4L2 - bool v4l2_sink_initialized = false; -#endif - bool video_demuxer_started = false; - bool audio_demuxer_started = false; -#ifdef HAVE_USB - bool aoa_hid_initialized = false; - bool keyboard_aoa_initialized = false; - bool mouse_aoa_initialized = false; - bool gamepad_aoa_initialized = false; -#endif + bool stream_started = false; bool controller_initialized = false; bool controller_started = false; - bool screen_initialized = false; - bool timeout_initialized = false; - bool timeout_started = false; - struct sc_acksync *acksync = NULL; - - uint32_t scid = scrcpy_generate_scid(); - - struct sc_server_params params = { - .scid = scid, - .req_serial = options->serial, - .select_usb = options->select_usb, - .select_tcpip = options->select_tcpip, - .log_level = options->log_level, - .video_codec = options->video_codec, - .audio_codec = options->audio_codec, - .video_source = options->video_source, - .audio_source = options->audio_source, - .camera_facing = options->camera_facing, - .crop = options->crop, - .port_range = options->port_range, - .tunnel_host = options->tunnel_host, - .tunnel_port = options->tunnel_port, - .max_size = options->max_size, - .video_bit_rate = options->video_bit_rate, - .audio_bit_rate = options->audio_bit_rate, - .max_fps = options->max_fps, - .angle = options->angle, - .screen_off_timeout = options->screen_off_timeout, - .capture_orientation = options->capture_orientation, - .capture_orientation_lock = options->capture_orientation_lock, - .control = options->control, - .display_id = options->display_id, - .new_display = options->new_display, - .display_ime_policy = options->display_ime_policy, - .video = options->video, - .audio = options->audio, - .audio_dup = options->audio_dup, - .show_touches = options->show_touches, - .stay_awake = options->stay_awake, - .video_codec_options = options->video_codec_options, - .audio_codec_options = options->audio_codec_options, - .video_encoder = options->video_encoder, - .audio_encoder = options->audio_encoder, - .camera_id = options->camera_id, - .camera_size = options->camera_size, - .camera_ar = options->camera_ar, - .camera_fps = options->camera_fps, - .force_adb_forward = options->force_adb_forward, - .power_off_on_close = options->power_off_on_close, - .clipboard_autosync = options->clipboard_autosync, - .downsize_on_error = options->downsize_on_error, - .tcpip = options->tcpip, - .tcpip_dst = options->tcpip_dst, - .cleanup = options->cleanup, - .power_on = options->power_on, - .kill_adb_on_close = options->kill_adb_on_close, - .camera_high_speed = options->camera_high_speed, - .vd_destroy_content = options->vd_destroy_content, - .vd_system_decorations = options->vd_system_decorations, - .list = options->list, - }; - - static const struct sc_server_callbacks cbs = { - .on_connection_failed = sc_server_on_connection_failed, - .on_connected = sc_server_on_connected, - .on_disconnected = sc_server_on_disconnected, - }; - if (!sc_server_init(&s->server, ¶ms, &cbs, NULL)) { - return SCRCPY_EXIT_FAILURE; - } - - if (options->window) { - // Set hints before starting the server thread to avoid race conditions - // in SDL - sdl_set_hints(options->render_driver); - } - - if (!sc_server_start(&s->server)) { + if (!sdl_init_and_configure(options->display)) { goto end; } - server_started = true; - - if (options->list) { - bool ok = await_for_server(NULL); - ret = ok ? SCRCPY_EXIT_SUCCESS : SCRCPY_EXIT_FAILURE; + if (!server_connect_to(&server)) { goto end; } - // playback implies capture - assert(!options->video_playback || options->video); - assert(!options->audio_playback || options->audio); + char device_name[DEVICE_NAME_FIELD_LENGTH]; + struct size frame_size; - if (options->window || - (options->control && options->clipboard_autosync)) { - // Initialize the video subsystem even if --no-video or - // --no-video-playback is passed so that clipboard synchronization - // still works. - // - if (SDL_Init(SDL_INIT_VIDEO)) { - // If it fails, it is an error only if video playback is enabled - if (options->video_playback) { - LOGE("Could not initialize SDL video: %s", SDL_GetError()); + // screenrecord does not send frames when the screen content does not + // change therefore, we transmit the screen size before the video stream, + // to be able to init the window immediately + if (!device_read_info(server.video_socket, device_name, &frame_size)) { + goto end; + } + + struct decoder *dec = NULL; + if (options->display) { + if (!fps_counter_init(&fps_counter)) { + goto end; + } + fps_counter_initialized = true; + + if (!video_buffer_init(&video_buffer, &fps_counter, + options->render_expired_frames)) { + goto end; + } + video_buffer_initialized = true; + + if (options->control) { + if (!file_handler_init(&file_handler, server.serial)) { goto end; - } else { - LOGW("Could not initialize SDL video: %s", SDL_GetError()); } + file_handler_initialized = true; } + + decoder_init(&decoder, &video_buffer); + dec = &decoder; } - if (options->audio_playback) { - if (SDL_Init(SDL_INIT_AUDIO)) { - LOGE("Could not initialize SDL audio: %s", SDL_GetError()); - goto end; - } - } - - if (options->gamepad_input_mode != SC_GAMEPAD_INPUT_MODE_DISABLED) { - if (SDL_Init(SDL_INIT_GAMECONTROLLER)) { - LOGE("Could not initialize SDL gamepad: %s", SDL_GetError()); - goto end; - } - } - - sdl_configure(options->video_playback, options->disable_screensaver); - - // Await for server without blocking Ctrl+C handling - bool connected; - if (!await_for_server(&connected)) { - LOGE("Server connection failed"); - goto end; - } - - if (!connected) { - // This is not an error, user requested to quit - LOGD("User requested to quit"); - ret = SCRCPY_EXIT_SUCCESS; - goto end; - } - - LOGD("Server connected"); - - // It is necessarily initialized here, since the device is connected - struct sc_server_info *info = &s->server.info; - - const char *serial = s->server.serial; - assert(serial); - - struct sc_file_pusher *fp = NULL; - - if (options->video_playback && options->control) { - if (!sc_file_pusher_init(&s->file_pusher, serial, - options->push_target)) { - goto end; - } - fp = &s->file_pusher; - file_pusher_initialized = true; - } - - if (options->video) { - static const struct sc_demuxer_callbacks video_demuxer_cbs = { - .on_ended = sc_video_demuxer_on_ended, - }; - sc_demuxer_init(&s->video_demuxer, "video", s->server.video_socket, - &video_demuxer_cbs, NULL); - } - - if (options->audio) { - static const struct sc_demuxer_callbacks audio_demuxer_cbs = { - .on_ended = sc_audio_demuxer_on_ended, - }; - sc_demuxer_init(&s->audio_demuxer, "audio", s->server.audio_socket, - &audio_demuxer_cbs, options); - } - - bool needs_video_decoder = options->video_playback; - bool needs_audio_decoder = options->audio_playback; -#ifdef HAVE_V4L2 - needs_video_decoder |= !!options->v4l2_device; -#endif - if (needs_video_decoder) { - sc_decoder_init(&s->video_decoder, "video"); - sc_packet_source_add_sink(&s->video_demuxer.packet_source, - &s->video_decoder.packet_sink); - } - if (needs_audio_decoder) { - sc_decoder_init(&s->audio_decoder, "audio"); - sc_packet_source_add_sink(&s->audio_demuxer.packet_source, - &s->audio_decoder.packet_sink); - } - - if (options->record_filename) { - static const struct sc_recorder_callbacks recorder_cbs = { - .on_ended = sc_recorder_on_ended, - }; - if (!sc_recorder_init(&s->recorder, options->record_filename, - options->record_format, options->video, - options->audio, options->record_orientation, - &recorder_cbs, NULL)) { + struct recorder *rec = NULL; + if (record) { + if (!recorder_init(&recorder, + options->record_filename, + options->record_format, + frame_size)) { goto end; } + rec = &recorder; recorder_initialized = true; - - if (!sc_recorder_start(&s->recorder)) { - goto end; - } - recorder_started = true; - - if (options->video) { - sc_packet_source_add_sink(&s->video_demuxer.packet_source, - &s->recorder.video_packet_sink); - } - if (options->audio) { - sc_packet_source_add_sink(&s->audio_demuxer.packet_source, - &s->recorder.audio_packet_sink); - } } - struct sc_controller *controller = NULL; - struct sc_key_processor *kp = NULL; - struct sc_mouse_processor *mp = NULL; - struct sc_gamepad_processor *gp = NULL; + av_log_set_callback(av_log_callback); - if (options->control) { - static const struct sc_controller_callbacks controller_cbs = { - .on_ended = sc_controller_on_ended, - }; + stream_init(&stream, server.video_socket, dec, rec); - if (!sc_controller_init(&s->controller, s->server.control_socket, - &controller_cbs, NULL)) { - goto end; - } - controller_initialized = true; + // now we consumed the header values, the socket receives the video stream + // start the stream + if (!stream_start(&stream)) { + goto end; + } + stream_started = true; - controller = &s->controller; - -#ifdef HAVE_USB - bool use_keyboard_aoa = - options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AOA; - bool use_mouse_aoa = - options->mouse_input_mode == SC_MOUSE_INPUT_MODE_AOA; - bool use_gamepad_aoa = - options->gamepad_input_mode == SC_GAMEPAD_INPUT_MODE_AOA; - if (use_keyboard_aoa || use_mouse_aoa || use_gamepad_aoa) { - bool ok = sc_acksync_init(&s->acksync); - if (!ok) { + if (options->display) { + if (options->control) { + if (!controller_init(&controller, server.control_socket)) { goto end; } + controller_initialized = true; - ok = sc_usb_init(&s->usb); - if (!ok) { - LOGE("Failed to initialize USB"); - sc_acksync_destroy(&s->acksync); + if (!controller_start(&controller)) { goto end; } - - assert(serial); - struct sc_usb_device usb_device; - ok = sc_usb_select_device(&s->usb, serial, &usb_device); - if (!ok) { - sc_usb_destroy(&s->usb); - goto end; - } - - LOGI("USB device: %s (%04" PRIx16 ":%04" PRIx16 ") %s %s", - usb_device.serial, usb_device.vid, usb_device.pid, - usb_device.manufacturer, usb_device.product); - - ok = sc_usb_connect(&s->usb, usb_device.device, NULL, NULL); - sc_usb_device_destroy(&usb_device); - if (!ok) { - LOGE("Failed to connect to USB device %s", serial); - sc_usb_destroy(&s->usb); - sc_acksync_destroy(&s->acksync); - goto end; - } - - ok = sc_aoa_init(&s->aoa, &s->usb, &s->acksync); - if (!ok) { - LOGE("Failed to enable HID over AOA"); - sc_usb_disconnect(&s->usb); - sc_usb_destroy(&s->usb); - sc_acksync_destroy(&s->acksync); - goto end; - } - - bool aoa_fail = false; - if (use_keyboard_aoa) { - if (sc_keyboard_aoa_init(&s->keyboard_aoa, &s->aoa)) { - keyboard_aoa_initialized = true; - kp = &s->keyboard_aoa.key_processor; - } else { - LOGE("Could not initialize HID keyboard"); - aoa_fail = true; - goto aoa_complete; - } - } - - if (use_mouse_aoa) { - if (sc_mouse_aoa_init(&s->mouse_aoa, &s->aoa)) { - mouse_aoa_initialized = true; - mp = &s->mouse_aoa.mouse_processor; - } else { - LOGE("Could not initialized HID mouse"); - aoa_fail = true; - goto aoa_complete; - } - } - - if (use_gamepad_aoa) { - sc_gamepad_aoa_init(&s->gamepad_aoa, &s->aoa); - gp = &s->gamepad_aoa.gamepad_processor; - gamepad_aoa_initialized = true; - } - -aoa_complete: - if (aoa_fail || !sc_aoa_start(&s->aoa)) { - sc_acksync_destroy(&s->acksync); - sc_usb_disconnect(&s->usb); - sc_usb_destroy(&s->usb); - sc_aoa_destroy(&s->aoa); - goto end; - } - - acksync = &s->acksync; - - aoa_hid_initialized = true; - } -#else - assert(options->keyboard_input_mode != SC_KEYBOARD_INPUT_MODE_AOA); - assert(options->mouse_input_mode != SC_MOUSE_INPUT_MODE_AOA); -#endif - - struct sc_keyboard_uhid *uhid_keyboard = NULL; - - if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_SDK) { - sc_keyboard_sdk_init(&s->keyboard_sdk, &s->controller, - options->key_inject_mode, - options->forward_key_repeat); - kp = &s->keyboard_sdk.key_processor; - } else if (options->keyboard_input_mode - == SC_KEYBOARD_INPUT_MODE_UHID) { - bool ok = sc_keyboard_uhid_init(&s->keyboard_uhid, &s->controller); - if (!ok) { - goto end; - } - kp = &s->keyboard_uhid.key_processor; - uhid_keyboard = &s->keyboard_uhid; + controller_started = true; } - if (options->mouse_input_mode == SC_MOUSE_INPUT_MODE_SDK) { - sc_mouse_sdk_init(&s->mouse_sdk, &s->controller, - options->mouse_hover); - mp = &s->mouse_sdk.mouse_processor; - } else if (options->mouse_input_mode == SC_MOUSE_INPUT_MODE_UHID) { - bool ok = sc_mouse_uhid_init(&s->mouse_uhid, &s->controller); - if (!ok) { - goto end; - } - mp = &s->mouse_uhid.mouse_processor; - } - - if (options->gamepad_input_mode == SC_GAMEPAD_INPUT_MODE_UHID) { - sc_gamepad_uhid_init(&s->gamepad_uhid, &s->controller); - gp = &s->gamepad_uhid.gamepad_processor; - } - - struct sc_uhid_devices *uhid_devices = NULL; - if (uhid_keyboard) { - sc_uhid_devices_init(&s->uhid_devices, uhid_keyboard); - uhid_devices = &s->uhid_devices; - } - - sc_controller_configure(&s->controller, acksync, uhid_devices); - - if (!sc_controller_start(&s->controller)) { - goto end; - } - controller_started = true; - } - - // There is a controller if and only if control is enabled - assert(options->control == !!controller); - - if (options->window) { - const char *window_title = - options->window_title ? options->window_title : info->device_name; - - struct sc_screen_params screen_params = { - .video = options->video_playback, - .controller = controller, - .fp = fp, - .kp = kp, - .mp = mp, - .gp = gp, - .mouse_bindings = options->mouse_bindings, - .legacy_paste = options->legacy_paste, - .clipboard_autosync = options->clipboard_autosync, - .shortcut_mods = options->shortcut_mods, - .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, - .orientation = options->display_orientation, - .mipmaps = options->mipmaps, - .fullscreen = options->fullscreen, - .start_fps_counter = options->start_fps_counter, - }; - - if (!sc_screen_init(&s->screen, &screen_params)) { - goto end; - } - screen_initialized = true; - - if (options->video_playback) { - struct sc_frame_source *src = &s->video_decoder.frame_source; - if (options->video_buffer) { - sc_delay_buffer_init(&s->video_buffer, - options->video_buffer, true); - sc_frame_source_add_sink(src, &s->video_buffer.frame_sink); - src = &s->video_buffer.frame_source; - } - - sc_frame_source_add_sink(src, &s->screen.frame_sink); - } - } - - if (options->audio_playback) { - sc_audio_player_init(&s->audio_player, options->audio_buffer, - options->audio_output_buffer); - sc_frame_source_add_sink(&s->audio_decoder.frame_source, - &s->audio_player.frame_sink); - } - -#ifdef HAVE_V4L2 - if (options->v4l2_device) { - if (!sc_v4l2_sink_init(&s->v4l2_sink, options->v4l2_device)) { + if (!screen_init_rendering(&screen, device_name, frame_size, + options->always_on_top)) { goto end; } - struct sc_frame_source *src = &s->video_decoder.frame_source; - if (options->v4l2_buffer) { - sc_delay_buffer_init(&s->v4l2_buffer, options->v4l2_buffer, true); - sc_frame_source_add_sink(src, &s->v4l2_buffer.frame_sink); - src = &s->v4l2_buffer.frame_source; + if (options->turn_screen_off) { + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; + msg.set_screen_power_mode.mode = SCREEN_POWER_MODE_OFF; + + if (!controller_push_msg(&controller, &msg)) { + LOGW("Cannot request 'set screen power mode'"); + } } - sc_frame_source_add_sink(src, &s->v4l2_sink.frame_sink); - - v4l2_sink_initialized = true; - } -#endif - - // Now that the header values have been consumed, the socket(s) will - // receive the stream(s). Start the demuxer(s). - - if (options->video) { - if (!sc_demuxer_start(&s->video_demuxer)) { - goto end; - } - video_demuxer_started = true; - } - - if (options->audio) { - if (!sc_demuxer_start(&s->audio_demuxer)) { - goto end; - } - audio_demuxer_started = true; - } - - // If the device screen is to be turned off, send the control message after - // everything is set up - if (options->control && options->turn_screen_off) { - struct sc_control_msg msg; - msg.type = SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER; - msg.set_display_power.on = false; - - if (!sc_controller_push_msg(&s->controller, &msg)) { - LOGW("Could not request 'set display power'"); + if (options->fullscreen) { + screen_switch_fullscreen(&screen); } } - if (options->time_limit) { - bool ok = sc_timeout_init(&s->timeout); - if (!ok) { - goto end; - } - - timeout_initialized = true; - - sc_tick deadline = sc_tick_now() + options->time_limit; - static const struct sc_timeout_callbacks cbs = { - .on_timeout = sc_timeout_on_timeout, - }; - - ok = sc_timeout_start(&s->timeout, deadline, &cbs, NULL); - if (!ok) { - goto end; - } - - timeout_started = true; + if (options->show_touches) { + wait_show_touches(proc_show_touches); + show_touches_waited = true; } - if (options->control - && options->gamepad_input_mode != SC_GAMEPAD_INPUT_MODE_DISABLED) { - init_sdl_gamepads(); - } - - if (options->control && options->start_app) { - assert(controller); - - char *name = strdup(options->start_app); - if (!name) { - LOG_OOM(); - goto end; - } - - struct sc_control_msg msg; - msg.type = SC_CONTROL_MSG_TYPE_START_APP; - msg.start_app.name = name; - - if (!sc_controller_push_msg(controller, &msg)) { - LOGW("Could not request start app '%s'", name); - free(name); - } - } - - ret = event_loop(s, options->window); - terminate_event_loop(); + ret = event_loop(options->display, options->control); LOGD("quit..."); - if (options->video_playback) { - // Close the window immediately on closing, because screen_destroy() - // may only be called once the video demuxer thread is joined (it may - // take time) - sc_screen_hide_window(&s->screen); - } + screen_destroy(&screen); end: - if (timeout_started) { - sc_timeout_stop(&s->timeout); + // stop stream and controller so that they don't continue once their socket + // is shutdown + if (stream_started) { + stream_stop(&stream); } - - // The demuxer is not stopped explicitly, because it will stop by itself on - // end-of-stream -#ifdef HAVE_USB - if (aoa_hid_initialized) { - if (keyboard_aoa_initialized) { - sc_keyboard_aoa_destroy(&s->keyboard_aoa); - } - if (mouse_aoa_initialized) { - sc_mouse_aoa_destroy(&s->mouse_aoa); - } - if (gamepad_aoa_initialized) { - sc_gamepad_aoa_destroy(&s->gamepad_aoa); - } - 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); + controller_stop(&controller); } - if (file_pusher_initialized) { - sc_file_pusher_stop(&s->file_pusher); + if (file_handler_initialized) { + file_handler_stop(&file_handler); } - if (recorder_initialized) { - sc_recorder_stop(&s->recorder); - } - if (screen_initialized) { - sc_screen_interrupt(&s->screen); + if (fps_counter_initialized) { + fps_counter_interrupt(&fps_counter); } - if (server_started) { - // shutdown the sockets and kill the server - sc_server_stop(&s->server); - } + // shutdown the sockets and kill the server + server_stop(&server); - if (timeout_started) { - sc_timeout_join(&s->timeout); - } - if (timeout_initialized) { - sc_timeout_destroy(&s->timeout); - } - - // now that the sockets are shutdown, the demuxer and controller are + // now that the sockets are shutdown, the stream and controller are // interrupted, we can join them - if (video_demuxer_started) { - sc_demuxer_join(&s->video_demuxer); + if (stream_started) { + stream_join(&stream); } - - if (audio_demuxer_started) { - sc_demuxer_join(&s->audio_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 video 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); + controller_join(&controller); } if (controller_initialized) { - sc_controller_destroy(&s->controller); + controller_destroy(&controller); } - if (recorder_started) { - sc_recorder_join(&s->recorder); - } if (recorder_initialized) { - sc_recorder_destroy(&s->recorder); + recorder_destroy(&recorder); } - if (file_pusher_initialized) { - sc_file_pusher_join(&s->file_pusher); - sc_file_pusher_destroy(&s->file_pusher); + if (file_handler_initialized) { + file_handler_join(&file_handler); + file_handler_destroy(&file_handler); } - if (server_started) { - sc_server_join(&s->server); + if (video_buffer_initialized) { + video_buffer_destroy(&video_buffer); } - sc_server_destroy(&s->server); + if (fps_counter_initialized) { + fps_counter_join(&fps_counter); + fps_counter_destroy(&fps_counter); + } + + if (options->show_touches) { + if (!show_touches_waited) { + // wait the process which enabled "show touches" + wait_show_touches(proc_show_touches); + } + LOGI("Disable show_touches"); + proc_show_touches = set_show_touches_enabled(options->serial, false); + wait_show_touches(proc_show_touches); + } + + server_destroy(&server); return ret; } diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 7f6a0fb2..d705d2db 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -1,22 +1,28 @@ #ifndef SCRCPY_H #define SCRCPY_H -#include "common.h" +#include +#include +#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, +struct scrcpy_options { + const char *serial; + const char *crop; + const char *record_filename; + enum recorder_format record_format; + uint16_t port; + uint16_t max_size; + uint32_t bit_rate; + bool show_touches; + bool fullscreen; + bool always_on_top; + bool control; + bool display; + bool turn_screen_off; + bool render_expired_frames; }; -enum scrcpy_exit_code -scrcpy(struct scrcpy_options *options); +bool +scrcpy(const struct scrcpy_options *options); #endif diff --git a/app/src/screen.c b/app/src/screen.c index 1d694f12..67b268c5 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -1,70 +1,63 @@ #include "screen.h" -#include #include #include -#include "events.h" -#include "icon.h" -#include "options.h" -#include "util/log.h" +#include "common.h" +#include "compat.h" +#include "icon.xpm" +#include "lock_util.h" +#include "log.h" +#include "tiny_xpm.h" +#include "video_buffer.h" #define DISPLAY_MARGINS 96 -#define DOWNCAST(SINK) container_of(SINK, struct sc_screen, frame_sink) - -static inline struct sc_size -get_oriented_size(struct sc_size size, enum sc_orientation orientation) { - struct sc_size oriented_size; - if (sc_orientation_is_swap(orientation)) { - oriented_size.width = size.height; - oriented_size.height = size.width; - } else { - oriented_size.width = size.width; - oriented_size.height = size.height; - } - return oriented_size; -} - -// get the window size in a struct sc_size -static struct sc_size -get_window_size(const struct sc_screen *screen) { +// get the window size in a struct size +static struct size +get_native_window_size(SDL_Window *window) { int width; int height; - SDL_GetWindowSize(screen->window, &width, &height); + SDL_GetWindowSize(window, &width, &height); - struct sc_size size; + struct 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; +// get the windowed window size +static struct size +get_window_size(const struct screen *screen) { + if (screen->fullscreen) { + return screen->windowed_window_size; + } + return get_native_window_size(screen->window); } // 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); - assert(!screen->minimized); - SDL_SetWindowSize(screen->window, new_size.width, new_size.height); +set_window_size(struct screen *screen, struct size new_size) { + // setting the window size during fullscreen is implementation defined, + // so apply the resize only after fullscreen is disabled + if (screen->fullscreen) { + // SDL_SetWindowSize will be called when fullscreen will be disabled + screen->windowed_window_size = new_size; + } else { + 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) { +get_preferred_display_bounds(struct size *bounds) { SDL_Rect rect; - if (SDL_GetDisplayUsableBounds(0, &rect)) { +#ifdef SCRCPY_SDL_HAS_GET_DISPLAY_USABLE_BOUNDS +# define GET_DISPLAY_BOUNDS(i, r) SDL_GetDisplayUsableBounds((i), (r)) +#else +# define GET_DISPLAY_BOUNDS(i, r) SDL_GetDisplayBounds((i), (r)) +#endif + if (GET_DISPLAY_BOUNDS(0, &rect)) { LOGW("Could not get display usable bounds: %s", SDL_GetError()); return false; } @@ -74,247 +67,184 @@ get_preferred_display_bounds(struct sc_size *bounds) { 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) { +static struct size +get_optimal_size(struct size current_size, struct size frame_size) { + if (frame_size.width == 0 || frame_size.height == 0) { // avoid division by 0 return current_size; } - struct sc_size window_size; + struct size display_size; + // 32 bits because we need to multiply two 16 bits values + uint32_t w; + uint32_t h; - struct sc_size display_size; - if (!within_display_bounds || - !get_preferred_display_bounds(&display_size)) { - // do not constraint the size - window_size = current_size; + if (!get_preferred_display_bounds(&display_size)) { + // cannot get display bounds, do not constraint the size + w = current_size.width; + h = current_size.height; } else { - window_size.width = MIN(current_size.width, display_size.width); - window_size.height = MIN(current_size.height, display_size.height); + w = MIN(current_size.width, display_size.width); + h = 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; + bool keep_width = frame_size.width * h > frame_size.height * w; if (keep_width) { // remove black borders on top and bottom - window_size.height = content_size.height * window_size.width - / content_size.width; + h = frame_size.height * w / frame_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; + w = frame_size.width * h / frame_size.height; } - return window_size; + // w and h must fit into 16 bits + SDL_assert_release(w < 0x10000 && h < 0x10000); + return (struct size) {w, h}; +} + +// same as get_optimal_size(), but read the current size from the window +static inline struct size +get_optimal_window_size(const struct screen *screen, struct size frame_size) { + struct size current_size = get_window_size(screen); + return get_optimal_size(current_size, frame_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 struct size +get_initial_optimal_size(struct size frame_size) { + return get_optimal_size(frame_size, frame_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; +void +screen_init(struct screen *screen) { + *screen = (struct screen) SCREEN_INITIALIZER; } -static void -sc_screen_update_content_rect(struct sc_screen *screen) { - assert(screen->video); - - 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(SDL_Renderer *renderer, struct size frame_size) { + return SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12, + SDL_TEXTUREACCESS_STREAMING, + frame_size.width, frame_size.height); } -// 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) { - assert(screen->video); +bool +screen_init_rendering(struct screen *screen, const char *device_name, + struct size frame_size, bool always_on_top) { + screen->frame_size = frame_size; - if (update_content_rect) { - sc_screen_update_content_rect(screen); - } - - enum sc_display_result res = - sc_display_render(&screen->display, &screen->rect, screen->orientation); - (void) res; // any error already logged -} - -static void -sc_screen_render_novideo(struct sc_screen *screen) { - enum sc_display_result res = - sc_display_render(&screen->display, NULL, SC_ORIENTATION_0); - (void) res; // any error already logged -} - -#if defined(__APPLE__) || defined(__WINDOWS__) -# define CONTINUOUS_RESIZING_WORKAROUND + struct size window_size = get_initial_optimal_size(frame_size); + uint32_t window_flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE; +#ifdef HIDPI_SUPPORT + window_flags |= SDL_WINDOW_ALLOW_HIGHDPI; #endif - -#ifdef CONTINUOUS_RESIZING_WORKAROUND -// On Windows and MacOS, resizing blocks the event loop, so resizing events are -// not triggered. As a workaround, handle them in an event handler. -// -// -// -static int -event_watcher(void *data, SDL_Event *event) { - struct sc_screen *screen = data; - assert(screen->video); - - 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; -} + if (always_on_top) { +#ifdef SCRCPY_SDL_HAS_WINDOW_ALWAYS_ON_TOP + window_flags |= SDL_WINDOW_ALWAYS_ON_TOP; +#else + LOGW("The 'always on top' flag is not available " + "(compile with SDL >= 2.0.5 to enable it)"); #endif + } -static bool -sc_screen_frame_sink_open(struct sc_frame_sink *sink, - const AVCodecContext *ctx) { - assert(ctx->pix_fmt == AV_PIX_FMT_YUV420P); - (void) ctx; - - struct sc_screen *screen = DOWNCAST(sink); - - if (ctx->width <= 0 || ctx->width > 0xFFFF - || ctx->height <= 0 || ctx->height > 0xFFFF) { - LOGE("Invalid video size: %dx%d", ctx->width, ctx->height); + screen->window = SDL_CreateWindow(device_name, SDL_WINDOWPOS_UNDEFINED, + SDL_WINDOWPOS_UNDEFINED, + window_size.width, window_size.height, + window_flags); + if (!screen->window) { + LOGC("Could not create window: %s", SDL_GetError()); return false; } - assert(ctx->width > 0 && ctx->width <= 0xFFFF); - assert(ctx->height > 0 && ctx->height <= 0xFFFF); - // screen->frame_size is never used before the event is pushed, and the - // event acts as a memory barrier so it is safe without mutex - screen->frame_size.width = ctx->width; - screen->frame_size.height = ctx->height; - - // Post the event on the UI thread (the texture must be created from there) - bool ok = sc_push_event(SC_EVENT_SCREEN_INIT_SIZE); - if (!ok) { + screen->renderer = SDL_CreateRenderer(screen->window, -1, + SDL_RENDERER_ACCELERATED); + if (!screen->renderer) { + LOGC("Could not create renderer: %s", SDL_GetError()); + screen_destroy(screen); return false; } -#ifndef NDEBUG - screen->open = true; -#endif + if (SDL_RenderSetLogicalSize(screen->renderer, frame_size.width, + frame_size.height)) { + LOGE("Could not set renderer logical size: %s", SDL_GetError()); + screen_destroy(screen); + return false; + } + + SDL_Surface *icon = read_xpm(icon_xpm); + if (icon) { + SDL_SetWindowIcon(screen->window, icon); + SDL_FreeSurface(icon); + } else { + LOGW("Could not load icon"); + } + + LOGI("Initial texture: %" PRIu16 "x%" PRIu16, frame_size.width, + frame_size.height); + screen->texture = create_texture(screen->renderer, frame_size); + if (!screen->texture) { + LOGC("Could not create texture: %s", SDL_GetError()); + screen_destroy(screen); + return false; + } - // 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 +void +screen_show_window(struct screen *screen) { + SDL_ShowWindow(screen->window); } -static bool -sc_screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { - struct sc_screen *screen = DOWNCAST(sink); - assert(screen->video); - - bool previous_skipped; - bool ok = sc_frame_buffer_push(&screen->fb, frame, &previous_skipped); - if (!ok) { - return false; +void +screen_destroy(struct screen *screen) { + if (screen->texture) { + SDL_DestroyTexture(screen->texture); } + if (screen->renderer) { + SDL_DestroyRenderer(screen->renderer); + } + if (screen->window) { + SDL_DestroyWindow(screen->window); + } +} - if (previous_skipped) { - sc_fps_counter_add_skipped_frame(&screen->fps_counter); - // The SC_EVENT_NEW_FRAME triggered for the previous frame will consume - // this new frame instead - } else { - // Post the event on the UI thread - bool ok = sc_push_event(SC_EVENT_NEW_FRAME); - if (!ok) { +// recreate the texture and resize the window if the frame size has changed +static bool +prepare_for_frame(struct screen *screen, struct size new_frame_size) { + if (screen->frame_size.width != new_frame_size.width + || screen->frame_size.height != new_frame_size.height) { + if (SDL_RenderSetLogicalSize(screen->renderer, new_frame_size.width, + new_frame_size.height)) { + LOGE("Could not set renderer logical size: %s", SDL_GetError()); + return false; + } + + // frame dimension changed, destroy texture + SDL_DestroyTexture(screen->texture); + + struct size current_size = get_window_size(screen); + struct size target_size = { + (uint32_t) current_size.width * new_frame_size.width + / screen->frame_size.width, + (uint32_t) current_size.height * new_frame_size.height + / screen->frame_size.height, + }; + target_size = get_optimal_size(target_size, new_frame_size); + set_window_size(screen, target_size); + + screen->frame_size = new_frame_size; + + LOGI("New texture: %" PRIu16 "x%" PRIu16, + screen->frame_size.width, screen->frame_size.height); + screen->texture = create_texture(screen->renderer, new_frame_size); + if (!screen->texture) { + LOGC("Could not create texture: %s", SDL_GetError()); return false; } } @@ -322,422 +252,44 @@ sc_screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { return true; } +// write the frame into the texture +static void +update_texture(struct screen *screen, const AVFrame *frame) { + SDL_UpdateYUVTexture(screen->texture, NULL, + frame->data[0], frame->linesize[0], + frame->data[1], frame->linesize[1], + frame->data[2], frame->linesize[2]); +} + 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->minimized = false; - screen->paused = false; - screen->resume_frame = NULL; - screen->orientation = SC_ORIENTATION_0; - - screen->video = params->video; - - screen->req.x = params->window_x; - screen->req.y = params->window_y; - screen->req.width = params->window_width; - screen->req.height = params->window_height; - screen->req.fullscreen = params->fullscreen; - screen->req.start_fps_counter = params->start_fps_counter; - - bool ok = sc_frame_buffer_init(&screen->fb); - if (!ok) { +screen_update_frame(struct screen *screen, struct video_buffer *vb) { + mutex_lock(vb->mutex); + const AVFrame *frame = video_buffer_consume_rendered_frame(vb); + struct size new_frame_size = {frame->width, frame->height}; + if (!prepare_for_frame(screen, new_frame_size)) { + mutex_unlock(vb->mutex); return false; } + update_texture(screen, frame); + mutex_unlock(vb->mutex); - if (!sc_fps_counter_init(&screen->fps_counter)) { - goto error_destroy_frame_buffer; - } - - if (screen->video) { - screen->orientation = params->orientation; - if (screen->orientation != SC_ORIENTATION_0) { - LOGI("Initial display orientation set to %s", - sc_orientation_get_name(screen->orientation)); - } - } - - 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; - } - if (params->video) { - // The window will be shown on first frame - window_flags |= SDL_WINDOW_HIDDEN - | SDL_WINDOW_RESIZABLE; - } - - const char *title = params->window_title; - assert(title); - - int x = SDL_WINDOWPOS_UNDEFINED; - int y = SDL_WINDOWPOS_UNDEFINED; - int width = 256; - int height = 256; - if (params->window_x != SC_WINDOW_POSITION_UNDEFINED) { - x = params->window_x; - } - if (params->window_y != SC_WINDOW_POSITION_UNDEFINED) { - y = params->window_y; - } - if (params->window_width) { - width = params->window_width; - } - if (params->window_height) { - height = params->window_height; - } - - // The window will be positioned and sized on first video frame - screen->window = SDL_CreateWindow(title, x, y, width, height, window_flags); - if (!screen->window) { - LOGE("Could not create window: %s", SDL_GetError()); - goto error_destroy_fps_counter; - } - - SDL_Surface *icon = scrcpy_icon_load(); - if (icon) { - SDL_SetWindowIcon(screen->window, icon); - } else if (params->video) { - // just a warning - LOGW("Could not load icon"); - } else { - // without video, the icon is used as window content, it must be present - LOGE("Could not load icon"); - goto error_destroy_fps_counter; - } - - SDL_Surface *icon_novideo = params->video ? NULL : icon; - bool mipmaps = params->video && params->mipmaps; - ok = sc_display_init(&screen->display, screen->window, icon_novideo, - mipmaps); - if (icon) { - scrcpy_icon_destroy(icon); - } - if (!ok) { - goto error_destroy_window; - } - - screen->frame = av_frame_alloc(); - if (!screen->frame) { - LOG_OOM(); - goto error_destroy_display; - } - - struct sc_input_manager_params im_params = { - .controller = params->controller, - .fp = params->fp, - .screen = screen, - .kp = params->kp, - .mp = params->mp, - .gp = params->gp, - .mouse_bindings = params->mouse_bindings, - .legacy_paste = params->legacy_paste, - .clipboard_autosync = params->clipboard_autosync, - .shortcut_mods = params->shortcut_mods, - }; - - sc_input_manager_init(&screen->im, &im_params); - - // Initialize even if not used for simplicity - sc_mouse_capture_init(&screen->mc, screen->window, params->shortcut_mods); - -#ifdef CONTINUOUS_RESIZING_WORKAROUND - if (screen->video) { - 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 - - if (!screen->video && sc_screen_is_relative_mode(screen)) { - // Capture mouse immediately if video mirroring is disabled - sc_mouse_capture_set_active(&screen->mc, true); - } - - return true; - -error_destroy_display: - sc_display_destroy(&screen->display); -error_destroy_window: - SDL_DestroyWindow(screen->window); -error_destroy_fps_counter: - sc_fps_counter_destroy(&screen->fps_counter); -error_destroy_frame_buffer: - sc_frame_buffer_destroy(&screen->fb); - - 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_toggle_fullscreen(screen); - } - - if (screen->req.start_fps_counter) { - sc_fps_counter_start(&screen->fps_counter); - } - - SDL_ShowWindow(screen->window); - sc_screen_update_content_rect(screen); -} - -void -sc_screen_hide_window(struct sc_screen *screen) { - SDL_HideWindow(screen->window); -} - -void -sc_screen_interrupt(struct sc_screen *screen) { - sc_fps_counter_interrupt(&screen->fps_counter); -} - -void -sc_screen_join(struct sc_screen *screen) { - sc_fps_counter_join(&screen->fps_counter); -} - -void -sc_screen_destroy(struct sc_screen *screen) { -#ifndef NDEBUG - assert(!screen->open); -#endif - sc_display_destroy(&screen->display); - av_frame_free(&screen->frame); - SDL_DestroyWindow(screen->window); - sc_fps_counter_destroy(&screen->fps_counter); - sc_frame_buffer_destroy(&screen->fb); -} - -static void -resize_for_content(struct sc_screen *screen, struct sc_size old_content_size, - struct sc_size new_content_size) { - assert(screen->video); - - 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) { - assert(screen->video); - - if (!screen->fullscreen && !screen->maximized && !screen->minimized) { - resize_for_content(screen, screen->content_size, new_content_size); - } else if (!screen->resize_pending) { - // Store the windowed size to be able to compute the optimal size once - // fullscreen/maximized/minimized 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->video); - - assert(!screen->fullscreen); - assert(!screen->maximized); - assert(!screen->minimized); - if (screen->resize_pending) { - resize_for_content(screen, screen->windowed_content_size, - screen->content_size); - screen->resize_pending = false; - } -} - -void -sc_screen_set_orientation(struct sc_screen *screen, - enum sc_orientation orientation) { - assert(screen->video); - - if (orientation == screen->orientation) { - return; - } - - struct sc_size new_content_size = - get_oriented_size(screen->frame_size, orientation); - - set_content_size(screen, new_content_size); - - screen->orientation = orientation; - LOGI("Display orientation set to %s", sc_orientation_get_name(orientation)); - - sc_screen_render(screen, true); -} - -static bool -sc_screen_init_size(struct sc_screen *screen) { - // Before first frame - assert(!screen->has_frame); - - // The requested size is passed via screen->frame_size - - struct sc_size content_size = - get_oriented_size(screen->frame_size, screen->orientation); - screen->content_size = content_size; - - enum sc_display_result res = - sc_display_set_texture_size(&screen->display, screen->frame_size); - return res != SC_DISPLAY_RESULT_ERROR; -} - -// recreate the texture and resize the window if the frame size has changed -static enum sc_display_result -prepare_for_frame(struct sc_screen *screen, struct sc_size new_frame_size) { - assert(screen->video); - - if (screen->frame_size.width == new_frame_size.width - && screen->frame_size.height == new_frame_size.height) { - return SC_DISPLAY_RESULT_OK; - } - - // frame dimension changed - screen->frame_size = new_frame_size; - - struct sc_size new_content_size = - get_oriented_size(new_frame_size, screen->orientation); - set_content_size(screen, new_content_size); - - sc_screen_update_content_rect(screen); - - return sc_display_set_texture_size(&screen->display, screen->frame_size); -} - -static bool -sc_screen_apply_frame(struct sc_screen *screen) { - assert(screen->video); - - sc_fps_counter_add_rendered_frame(&screen->fps_counter); - - AVFrame *frame = screen->frame; - struct sc_size new_frame_size = {frame->width, frame->height}; - enum sc_display_result res = prepare_for_frame(screen, new_frame_size); - if (res == SC_DISPLAY_RESULT_ERROR) { - return false; - } - if (res == SC_DISPLAY_RESULT_PENDING) { - // Not an error, but do not continue - return true; - } - - res = sc_display_update_texture(&screen->display, frame); - if (res == SC_DISPLAY_RESULT_ERROR) { - return false; - } - if (res == SC_DISPLAY_RESULT_PENDING) { - // Not an error, but do not continue - return true; - } - - 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_mouse_capture_set_active(&screen->mc, true); - } - } - - sc_screen_render(screen, false); + screen_render(screen); return true; } -static bool -sc_screen_update_frame(struct sc_screen *screen) { - assert(screen->video); - - if (screen->paused) { - if (!screen->resume_frame) { - screen->resume_frame = av_frame_alloc(); - if (!screen->resume_frame) { - LOG_OOM(); - return false; - } - } else { - av_frame_unref(screen->resume_frame); - } - sc_frame_buffer_consume(&screen->fb, screen->resume_frame); - return true; - } - - av_frame_unref(screen->frame); - sc_frame_buffer_consume(&screen->fb, screen->frame); - return sc_screen_apply_frame(screen); +void +screen_render(struct screen *screen) { + SDL_RenderClear(screen->renderer); + SDL_RenderCopy(screen->renderer, screen->texture, NULL, NULL); + SDL_RenderPresent(screen->renderer); } void -sc_screen_set_paused(struct sc_screen *screen, bool paused) { - assert(screen->video); - - if (!paused && !screen->paused) { - // nothing to do - return; +screen_switch_fullscreen(struct screen *screen) { + if (!screen->fullscreen) { + // going to fullscreen, store the current windowed window size + screen->windowed_window_size = get_native_window_size(screen->window); } - - if (screen->paused && screen->resume_frame) { - // If display screen was paused, refresh the frame immediately, even if - // the new state is also paused. - av_frame_free(&screen->frame); - screen->frame = screen->resume_frame; - screen->resume_frame = NULL; - sc_screen_apply_frame(screen); - } - - if (!paused) { - LOGI("Display screen unpaused"); - } else if (!screen->paused) { - LOGI("Display screen paused"); - } else { - LOGI("Display screen re-paused"); - } - - screen->paused = paused; -} - -void -sc_screen_toggle_fullscreen(struct sc_screen *screen) { - assert(screen->video); - 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()); @@ -745,203 +297,32 @@ sc_screen_toggle_fullscreen(struct sc_screen *screen) { } screen->fullscreen = !screen->fullscreen; - if (!screen->fullscreen && !screen->maximized && !screen->minimized) { - apply_pending_resize(screen); + if (!screen->fullscreen) { + // fullscreen disabled, restore expected windowed window size + SDL_SetWindowSize(screen->window, screen->windowed_window_size.width, + screen->windowed_window_size.height); } LOGD("Switched to %s mode", screen->fullscreen ? "fullscreen" : "windowed"); - sc_screen_render(screen, true); + screen_render(screen); } void -sc_screen_resize_to_fit(struct sc_screen *screen) { - assert(screen->video); - - if (screen->fullscreen || screen->maximized || screen->minimized) { - return; +screen_resize_to_fit(struct screen *screen) { + if (!screen->fullscreen) { + struct size optimal_size = get_optimal_window_size(screen, + screen->frame_size); + SDL_SetWindowSize(screen->window, optimal_size.width, + optimal_size.height); + LOGD("Resized to optimal size"); } - - 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) { - assert(screen->video); - - if (screen->fullscreen || screen->minimized) { - return; +screen_resize_to_pixel_perfect(struct screen *screen) { + if (!screen->fullscreen) { + SDL_SetWindowSize(screen->window, screen->frame_size.width, + screen->frame_size.height); + LOGD("Resized to pixel-perfect"); } - - if (screen->maximized) { - SDL_RestoreWindow(screen->window); - screen->maximized = false; - } - - struct sc_size content_size = screen->content_size; - SDL_SetWindowSize(screen->window, content_size.width, content_size.height); - LOGD("Resized to pixel-perfect: %ux%u", content_size.width, - content_size.height); -} - -bool -sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) { - switch (event->type) { - case SC_EVENT_SCREEN_INIT_SIZE: { - // The initial size is passed via screen->frame_size - bool ok = sc_screen_init_size(screen); - if (!ok) { - LOGE("Could not initialize screen size"); - return false; - } - return true; - } - case SC_EVENT_NEW_FRAME: { - bool ok = sc_screen_update_frame(screen); - if (!ok) { - LOGE("Frame update failed\n"); - return false; - } - return true; - } - case SDL_WINDOWEVENT: - if (!screen->video - && event->window.event == SDL_WINDOWEVENT_EXPOSED) { - sc_screen_render_novideo(screen); - } - - // !video implies !has_frame - assert(screen->video || !screen->has_frame); - if (!screen->has_frame) { - // Do nothing - return true; - } - 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_MINIMIZED: - screen->minimized = 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; - screen->minimized = false; - apply_pending_resize(screen); - sc_screen_render(screen, true); - break; - } - return true; - } - - if (sc_screen_is_relative_mode(screen) - && sc_mouse_capture_handle_event(&screen->mc, event)) { - // The mouse capture handler consumed the event - return true; - } - - sc_input_manager_handle_event(&screen->im, event); - return true; -} - -struct sc_point -sc_screen_convert_drawable_to_frame_coords(struct sc_screen *screen, - int32_t x, int32_t y) { - assert(screen->video); - - enum sc_orientation orientation = screen->orientation; - - int32_t w = screen->content_size.width; - int32_t h = screen->content_size.height; - - // screen->rect must be initialized to avoid a division by zero - assert(screen->rect.w && screen->rect.h); - - x = (int64_t) (x - screen->rect.x) * w / screen->rect.w; - y = (int64_t) (y - screen->rect.y) * h / screen->rect.h; - - struct sc_point result; - switch (orientation) { - case SC_ORIENTATION_0: - result.x = x; - result.y = y; - break; - case SC_ORIENTATION_90: - result.x = y; - result.y = w - x; - break; - case SC_ORIENTATION_180: - result.x = w - x; - result.y = h - y; - break; - case SC_ORIENTATION_270: - result.x = h - y; - result.y = x; - break; - case SC_ORIENTATION_FLIP_0: - result.x = w - x; - result.y = y; - break; - case SC_ORIENTATION_FLIP_90: - result.x = h - y; - result.y = w - x; - break; - case SC_ORIENTATION_FLIP_180: - result.x = x; - result.y = h - y; - break; - default: - assert(orientation == SC_ORIENTATION_FLIP_270); - result.x = y; - result.y = 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; } diff --git a/app/src/screen.h b/app/src/screen.h index 6621b2d2..5734fdc2 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -1,174 +1,78 @@ -#ifndef SC_SCREEN_H -#define SC_SCREEN_H +#ifndef SCREEN_H +#define SCREEN_H + +#include +#include +#include #include "common.h" -#include -#include -#include -#include -#include -#include - -#include "controller.h" -#include "coords.h" -#include "display.h" -#include "fps_counter.h" -#include "frame_buffer.h" -#include "input_manager.h" -#include "mouse_capture.h" -#include "options.h" -#include "trait/key_processor.h" -#include "trait/frame_sink.h" -#include "trait/mouse_processor.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 - - bool video; - - struct sc_display display; - struct sc_input_manager im; - struct sc_mouse_capture mc; // only used in mouse relative mode - struct sc_frame_buffer fb; - struct sc_fps_counter fps_counter; - - // The initial requested window properties - struct { - int16_t x; - int16_t y; - uint16_t width; - uint16_t height; - bool fullscreen; - bool start_fps_counter; - } req; +struct video_buffer; +struct screen { SDL_Window *window; - 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 orientation - enum sc_orientation orientation; - // rectangle of the content (excluding black borders) - struct SDL_Rect rect; + SDL_Renderer *renderer; + SDL_Texture *texture; + struct size frame_size; + //used only in fullscreen mode to know the windowed window size + struct size windowed_window_size; bool has_frame; bool fullscreen; - bool maximized; - bool minimized; - - AVFrame *frame; - - bool paused; - AVFrame *resume_frame; + bool no_window; }; -struct sc_screen_params { - bool video; +#define SCREEN_INITIALIZER { \ + .window = NULL, \ + .renderer = NULL, \ + .texture = NULL, \ + .frame_size = { \ + .width = 0, \ + .height = 0, \ + }, \ + .windowed_window_size = { \ + .width = 0, \ + .height = 0, \ + }, \ + .has_frame = false, \ + .fullscreen = false, \ + .no_window = false, \ +} - struct sc_controller *controller; - struct sc_file_pusher *fp; - struct sc_key_processor *kp; - struct sc_mouse_processor *mp; - struct sc_gamepad_processor *gp; - - struct sc_mouse_bindings mouse_bindings; - bool legacy_paste; - bool clipboard_autosync; - uint8_t shortcut_mods; // OR of enum sc_shortcut_mod values - - 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; - - enum sc_orientation orientation; - bool mipmaps; - - bool fullscreen; - bool start_fps_counter; -}; +// initialize default values +void +screen_init(struct screen *screen); // initialize screen, create window, renderer and texture (window is hidden) bool -sc_screen_init(struct sc_screen *screen, const struct sc_screen_params *params); +screen_init_rendering(struct screen *screen, const char *device_name, + struct size frame_size, bool always_on_top); -// request to interrupt any inner thread -// must be called before screen_join() +// show the window void -sc_screen_interrupt(struct sc_screen *screen); - -// join any inner thread -void -sc_screen_join(struct sc_screen *screen); +screen_show_window(struct screen *screen); // destroy window, renderer and texture (if any) void -sc_screen_destroy(struct sc_screen *screen); +screen_destroy(struct 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); +// resize if necessary and write the rendered frame into the texture +bool +screen_update_frame(struct screen *screen, struct video_buffer *vb); -// toggle the fullscreen mode +// render the texture to the renderer void -sc_screen_toggle_fullscreen(struct sc_screen *screen); +screen_render(struct screen *screen); + +// switch the fullscreen mode +void +screen_switch_fullscreen(struct screen *screen); // resize window to optimal size (remove black borders) void -sc_screen_resize_to_fit(struct sc_screen *screen); +screen_resize_to_fit(struct screen *screen); // resize window to 1:1 (pixel-perfect) void -sc_screen_resize_to_pixel_perfect(struct sc_screen *screen); - -// set the display orientation -void -sc_screen_set_orientation(struct sc_screen *screen, - enum sc_orientation orientation); - -// set the display pause state -void -sc_screen_set_paused(struct sc_screen *screen, bool paused); - -// react to SDL events -// If this function returns false, scrcpy must exit with an error. -bool -sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event); - -// convert point from window coordinates to frame coordinates -// x and y are expressed in pixels -struct 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); +screen_resize_to_pixel_perfect(struct screen *screen); #endif diff --git a/app/src/server.c b/app/src/server.c index 153219c3..d0599bef 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -1,1200 +1,323 @@ #include "server.h" -#include +#include #include +#include #include -#include -#include -#include +#include +#include -#include "adb/adb.h" -#include "util/env.h" -#include "util/file.h" -#include "util/log.h" -#include "util/net_intr.h" -#include "util/process.h" -#include "util/str.h" +#include "config.h" +#include "command.h" +#include "log.h" +#include "net.h" -#define SC_SERVER_FILENAME "scrcpy-server" +#define SOCKET_NAME "scrcpy" +#define SERVER_FILENAME "scrcpy-server.jar" -#define SC_SERVER_PATH_DEFAULT PREFIX "/share/scrcpy/" SC_SERVER_FILENAME -#define SC_DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar" +#define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FLENAME +#define DEVICE_SERVER_PATH "/data/local/tmp/" SERVER_FILENAME -#define SC_ADB_PORT_DEFAULT 5555 -#define SC_SOCKET_NAME_PREFIX "scrcpy_" - -static char * +static const char * get_server_path(void) { - char *server_path = sc_get_env("SCRCPY_SERVER_PATH"); - if (server_path) { + const char *server_path_env = getenv("SCRCPY_SERVER_PATH"); + if (server_path_env) { + LOGD("Using SCRCPY_SERVER_PATH: %s", server_path_env); // if the envvar is set, use it - LOGD("Using SCRCPY_SERVER_PATH: %s", server_path); - return server_path; + return server_path_env; } #ifndef PORTABLE - LOGD("Using server: " SC_SERVER_PATH_DEFAULT); - server_path = strdup(SC_SERVER_PATH_DEFAULT); - if (!server_path) { - LOG_OOM(); - return NULL; - } + LOGD("Using server: " DEFAULT_SERVER_PATH); + // the absolute path is hardcoded + return DEFAULT_SERVER_PATH; #else - 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); + // use scrcpy-server.jar in the same directory as the executable + char *executable_path = get_executable_path(); + if (!executable_path) { + LOGE("Cannot get executable path, " + "using " SERVER_FILENAME " from current directory"); + // not found, use current directory + return SERVER_FILENAME; } + char *dir = dirname(executable_path); + size_t dirlen = strlen(dir); + + // sizeof(SERVER_FILENAME) gives statically the size including the null byte + size_t len = dirlen + 1 + sizeof(SERVER_FILENAME); + char *server_path = SDL_malloc(len); + if (!server_path) { + LOGE("Cannot alloc server path string, " + "using " SERVER_FILENAME " from current directory"); + SDL_free(executable_path); + return SERVER_FILENAME; + } + + memcpy(server_path, dir, dirlen); + server_path[dirlen] = PATH_SEPARATOR; + memcpy(&server_path[dirlen + 1], SERVER_FILENAME, sizeof(SERVER_FILENAME)); + // the final null byte has been copied with SERVER_FILENAME + + SDL_free(executable_path); LOGD("Using server (portable): %s", server_path); -#endif - return server_path; -} - -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 NULL; - } -} - -static bool -sc_server_sleep(struct sc_server *server, sc_tick deadline) { - sc_mutex_lock(&server->mutex); - bool timed_out = false; - while (!server->stopped && !timed_out) { - timed_out = !sc_cond_timedwait(&server->cond_stopped, - &server->mutex, deadline); - } - bool stopped = server->stopped; - sc_mutex_unlock(&server->mutex); - - return !stopped; -} - -static const char * -sc_server_get_codec_name(enum sc_codec codec) { - switch (codec) { - case SC_CODEC_H264: - return "h264"; - case SC_CODEC_H265: - return "h265"; - case SC_CODEC_AV1: - return "av1"; - case SC_CODEC_OPUS: - return "opus"; - case SC_CODEC_AAC: - return "aac"; - case SC_CODEC_FLAC: - return "flac"; - case SC_CODEC_RAW: - return "raw"; - default: - assert(!"unexpected codec"); - return NULL; - } -} - -static const char * -sc_server_get_camera_facing_name(enum sc_camera_facing camera_facing) { - switch (camera_facing) { - case SC_CAMERA_FACING_FRONT: - return "front"; - case SC_CAMERA_FACING_BACK: - return "back"; - case SC_CAMERA_FACING_EXTERNAL: - return "external"; - default: - assert(!"unexpected camera facing"); - return NULL; - } -} - -static const char * -sc_server_get_audio_source_name(enum sc_audio_source audio_source) { - switch (audio_source) { - case SC_AUDIO_SOURCE_OUTPUT: - return "output"; - case SC_AUDIO_SOURCE_MIC: - return "mic"; - case SC_AUDIO_SOURCE_PLAYBACK: - return "playback"; - case SC_AUDIO_SOURCE_MIC_UNPROCESSED: - return "mic-unprocessed"; - case SC_AUDIO_SOURCE_MIC_CAMCORDER: - return "mic-camcorder"; - case SC_AUDIO_SOURCE_MIC_VOICE_RECOGNITION: - return "mic-voice-recognition"; - case SC_AUDIO_SOURCE_MIC_VOICE_COMMUNICATION: - return "mic-voice-communication"; - case SC_AUDIO_SOURCE_VOICE_CALL: - return "voice-call"; - case SC_AUDIO_SOURCE_VOICE_CALL_UPLINK: - return "voice-call-uplink"; - case SC_AUDIO_SOURCE_VOICE_CALL_DOWNLINK: - return "voice-call-downlink"; - case SC_AUDIO_SOURCE_VOICE_PERFORMANCE: - return "voice-performance"; - default: - assert(!"unexpected audio source"); - return NULL; - } -} - -static const char * -sc_server_get_display_ime_policy_name(enum sc_display_ime_policy policy) { - switch (policy) { - case SC_DISPLAY_IME_POLICY_LOCAL: - return "local"; - case SC_DISPLAY_IME_POLICY_FALLBACK: - return "fallback"; - case SC_DISPLAY_IME_POLICY_HIDE: - return "hide"; - default: - assert(!"unexpected display IME policy"); - return NULL; - } -} - -static bool -validate_string(const char *s) { - // The parameters values are passed as command line arguments to adb, so - // they must either be properly escaped, or they must not contain any - // special shell characters. - // Since they are not properly escaped on Windows anyway (see - // sys/win/process.c), just forbid special shell characters. - if (strpbrk(s, " ;'\"*$?&`#\\|<>[]{}()!~\r\n")) { - LOGE("Invalid server param: [%s]", s); - return false; - } - - return true; -} - -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 - uint16_t sdk_version = sc_adb_get_device_sdk_version(&server->intr, serial); - if (!sdk_version) { - LOGE("Could not determine SDK version"); - return 0; - } - -# define SERVER_DEBUGGER_PORT "5005" - const char *dbg; - if (sdk_version < 28) { - // Android < 9 - dbg = "-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address=" - SERVER_DEBUGGER_PORT; - } else if (sdk_version < 30) { - // Android >= 9 && Android < 11 - dbg = "-XjdwpProvider:internal -XjdwpOptions:transport=dt_socket," - "suspend=y,server=y,address=" SERVER_DEBUGGER_PORT; - } else { - // Android >= 11 - // Contrary to the other methods, this does not suspend on start. - // - dbg = "-XjdwpProvider:adbconnection"; - } - cmd[count++] = dbg; #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, ...) do { \ - char *p; \ - if (asprintf(&p, fmt, ## __VA_ARGS__) == -1) { \ - goto end; \ - } \ - cmd[count++] = p; \ - } while(0) -#define VALIDATE_STRING(s) do { \ - if (!validate_string(s)) { \ - goto end; \ - } \ - } while(0) - - ADD_PARAM("scid=%08x", params->scid); - ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level)); - - if (!params->video) { - ADD_PARAM("video=false"); - } - if (params->video_bit_rate) { - ADD_PARAM("video_bit_rate=%" PRIu32, params->video_bit_rate); - } - if (!params->audio) { - ADD_PARAM("audio=false"); - } - if (params->audio_bit_rate) { - ADD_PARAM("audio_bit_rate=%" PRIu32, params->audio_bit_rate); - } - if (params->video_codec != SC_CODEC_H264) { - ADD_PARAM("video_codec=%s", - sc_server_get_codec_name(params->video_codec)); - } - if (params->audio_codec != SC_CODEC_OPUS) { - ADD_PARAM("audio_codec=%s", - sc_server_get_codec_name(params->audio_codec)); - } - if (params->video_source != SC_VIDEO_SOURCE_DISPLAY) { - assert(params->video_source == SC_VIDEO_SOURCE_CAMERA); - ADD_PARAM("video_source=camera"); - } - // If audio is enabled, an "auto" audio source must have been resolved - assert(params->audio_source != SC_AUDIO_SOURCE_AUTO || !params->audio); - if (params->audio_source != SC_AUDIO_SOURCE_OUTPUT && params->audio) { - ADD_PARAM("audio_source=%s", - sc_server_get_audio_source_name(params->audio_source)); - } - if (params->audio_dup) { - ADD_PARAM("audio_dup=true"); - } - if (params->max_size) { - ADD_PARAM("max_size=%" PRIu16, params->max_size); - } - if (params->max_fps) { - VALIDATE_STRING(params->max_fps); - ADD_PARAM("max_fps=%s", params->max_fps); - } - if (params->angle) { - VALIDATE_STRING(params->angle); - ADD_PARAM("angle=%s", params->angle); - } - if (params->capture_orientation_lock != SC_ORIENTATION_UNLOCKED - || params->capture_orientation != SC_ORIENTATION_0) { - if (params->capture_orientation_lock == SC_ORIENTATION_LOCKED_INITIAL) { - ADD_PARAM("capture_orientation=@"); - } else { - const char *orient = - sc_orientation_get_name(params->capture_orientation); - bool locked = - params->capture_orientation_lock != SC_ORIENTATION_UNLOCKED; - ADD_PARAM("capture_orientation=%s%s", locked ? "@" : "", orient); - } - } - if (server->tunnel.forward) { - ADD_PARAM("tunnel_forward=true"); - } - if (params->crop) { - VALIDATE_STRING(params->crop); - ADD_PARAM("crop=%s", params->crop); - } - if (!params->control) { - // By default, control is true - ADD_PARAM("control=false"); - } - if (params->display_id) { - ADD_PARAM("display_id=%" PRIu32, params->display_id); - } - if (params->camera_id) { - VALIDATE_STRING(params->camera_id); - ADD_PARAM("camera_id=%s", params->camera_id); - } - if (params->camera_size) { - VALIDATE_STRING(params->camera_size); - ADD_PARAM("camera_size=%s", params->camera_size); - } - if (params->camera_facing != SC_CAMERA_FACING_ANY) { - ADD_PARAM("camera_facing=%s", - sc_server_get_camera_facing_name(params->camera_facing)); - } - if (params->camera_ar) { - VALIDATE_STRING(params->camera_ar); - ADD_PARAM("camera_ar=%s", params->camera_ar); - } - if (params->camera_fps) { - ADD_PARAM("camera_fps=%" PRIu16, params->camera_fps); - } - if (params->camera_high_speed) { - ADD_PARAM("camera_high_speed=true"); - } - if (params->show_touches) { - ADD_PARAM("show_touches=true"); - } - if (params->stay_awake) { - ADD_PARAM("stay_awake=true"); - } - if (params->screen_off_timeout != -1) { - assert(params->screen_off_timeout >= 0); - uint64_t ms = SC_TICK_TO_MS(params->screen_off_timeout); - ADD_PARAM("screen_off_timeout=%" PRIu64, ms); - } - if (params->video_codec_options) { - VALIDATE_STRING(params->video_codec_options); - ADD_PARAM("video_codec_options=%s", params->video_codec_options); - } - if (params->audio_codec_options) { - VALIDATE_STRING(params->audio_codec_options); - ADD_PARAM("audio_codec_options=%s", params->audio_codec_options); - } - if (params->video_encoder) { - VALIDATE_STRING(params->video_encoder); - ADD_PARAM("video_encoder=%s", params->video_encoder); - } - if (params->audio_encoder) { - VALIDATE_STRING(params->audio_encoder); - ADD_PARAM("audio_encoder=%s", params->audio_encoder); - } - if (params->power_off_on_close) { - ADD_PARAM("power_off_on_close=true"); - } - if (!params->clipboard_autosync) { - // By default, clipboard_autosync is true - ADD_PARAM("clipboard_autosync=false"); - } - if (!params->downsize_on_error) { - // By default, downsize_on_error is true - ADD_PARAM("downsize_on_error=false"); - } - if (!params->cleanup) { - // By default, cleanup is true - ADD_PARAM("cleanup=false"); - } - if (!params->power_on) { - // By default, power_on is true - ADD_PARAM("power_on=false"); - } - if (params->new_display) { - VALIDATE_STRING(params->new_display); - ADD_PARAM("new_display=%s", params->new_display); - } - if (params->display_ime_policy != SC_DISPLAY_IME_POLICY_UNDEFINED) { - ADD_PARAM("display_ime_policy=%s", - sc_server_get_display_ime_policy_name(params->display_ime_policy)); - } - if (!params->vd_destroy_content) { - ADD_PARAM("vd_destroy_content=false"); - } - if (!params->vd_system_decorations) { - ADD_PARAM("vd_system_decorations=false"); - } - if (params->list & SC_OPTION_LIST_ENCODERS) { - ADD_PARAM("list_encoders=true"); - } - if (params->list & SC_OPTION_LIST_DISPLAYS) { - ADD_PARAM("list_displays=true"); - } - if (params->list & SC_OPTION_LIST_CAMERAS) { - ADD_PARAM("list_cameras=true"); - } - if (params->list & SC_OPTION_LIST_CAMERA_SIZES) { - ADD_PARAM("list_camera_sizes=true"); - } - if (params->list & SC_OPTION_LIST_APPS) { - ADD_PARAM("list_apps=true"); - } - -#undef ADD_PARAM - - cmd[count++] = NULL; - -#ifdef SERVER_DEBUGGER - LOGI("Server debugger listening%s...", - sdk_version < 30 ? " on port " SERVER_DEBUGGER_PORT : ""); - // For Android < 11, from the computer: - // - run `adb forward tcp:5005 tcp:5005` - // For Android >= 11: - // - execute `adb jdwp` to get the jdwp port - // - run `adb forward tcp:5005 jdwp:XXXX` (replace XXXX) - // - // 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; +push_server(const char *serial) { + process_t process = adb_push(serial, get_server_path(), DEVICE_SERVER_PATH); + return process_check_success(process, "adb push"); +} + +static bool +enable_tunnel_reverse(const char *serial, uint16_t local_port) { + process_t process = adb_reverse(serial, SOCKET_NAME, local_port); + return process_check_success(process, "adb reverse"); +} + +static bool +disable_tunnel_reverse(const char *serial) { + process_t process = adb_reverse_remove(serial, SOCKET_NAME); + return process_check_success(process, "adb reverse --remove"); +} + +static bool +enable_tunnel_forward(const char *serial, uint16_t local_port) { + process_t process = adb_forward(serial, local_port, SOCKET_NAME); + return process_check_success(process, "adb forward"); +} + +static bool +disable_tunnel_forward(const char *serial, uint16_t local_port) { + process_t process = adb_forward_remove(serial, local_port); + return process_check_success(process, "adb forward --remove"); +} + +static bool +enable_tunnel(struct server *server) { + if (enable_tunnel_reverse(server->serial, server->local_port)) { + return true; + } + + LOGW("'adb reverse' failed, fallback to 'adb forward'"); + server->tunnel_forward = true; + return enable_tunnel_forward(server->serial, server->local_port); +} + +static bool +disable_tunnel(struct server *server) { + if (server->tunnel_forward) { + return disable_tunnel_forward(server->serial, server->local_port); + } + return disable_tunnel_reverse(server->serial); +} + +static process_t +execute_server(struct server *server, const struct server_params *params) { + char max_size_string[6]; + char bit_rate_string[11]; + sprintf(max_size_string, "%"PRIu16, params->max_size); + sprintf(bit_rate_string, "%"PRIu32, params->bit_rate); + const char *const cmd[] = { + "shell", + "CLASSPATH=/data/local/tmp/" SERVER_FILENAME, + "app_process", + "/", // unused + "com.genymobile.scrcpy.Server", + max_size_string, + bit_rate_string, + server->tunnel_forward ? "true" : "false", + params->crop ? params->crop : "-", + params->send_frame_meta ? "true" : "false", + params->control ? "true" : "false", + }; + return adb_execute(server->serial, cmd, sizeof(cmd) / sizeof(cmd[0])); +} + +#define IPV4_LOCALHOST 0x7F000001 + +static socket_t +listen_on_port(uint16_t port) { + return net_listen(IPV4_LOCALHOST, port, 1); +} + +static socket_t +connect_and_read_byte(uint16_t port) { + socket_t socket = net_connect(IPV4_LOCALHOST, port); + if (socket == INVALID_SOCKET) { + return INVALID_SOCKET; } 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) { + if (net_recv(socket, &byte, 1) != 1) { // the server is not listening yet behind the adb tunnel - return false; + return INVALID_SOCKET; } - - return true; + return socket; } -static sc_socket -connect_to_server(struct sc_server *server, unsigned attempts, sc_tick delay, - uint32_t host, uint16_t port) { +static socket_t +connect_to_server(uint16_t port, uint32_t attempts, uint32_t delay) { 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); + LOGD("Remaining connection attempts: %d", (int) attempts); + socket_t socket = connect_and_read_byte(port); + if (socket != INVALID_SOCKET) { + // it worked! + return 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; - } + SDL_Delay(delay); } - } while (--attempts); - return SC_SOCKET_NONE; + } while (--attempts > 0); + return INVALID_SOCKET; +} + +static void +close_socket(socket_t *socket) { + SDL_assert(*socket != INVALID_SOCKET); + net_shutdown(*socket, SHUT_RDWR); + if (!net_close(*socket)) { + LOGW("Cannot close socket"); + return; + } + *socket = INVALID_SOCKET; +} + +void +server_init(struct server *server) { + *server = (struct server) SERVER_INITIALIZER; } bool -sc_server_init(struct sc_server *server, const struct sc_server_params *params, - const struct sc_server_callbacks *cbs, void *cbs_userdata) { - // The allocated data in params (const char *) must remain valid until the - // end of the program - server->params = *params; +server_start(struct server *server, const char *serial, + const struct server_params *params) { + server->local_port = params->local_port; - bool ok = sc_adb_init(); - if (!ok) { - return false; - } - - ok = sc_mutex_init(&server->mutex); - if (!ok) { - sc_adb_destroy(); - return false; - } - - ok = sc_cond_init(&server->cond_stopped); - if (!ok) { - sc_mutex_destroy(&server->mutex); - sc_adb_destroy(); - return false; - } - - ok = sc_intr_init(&server->intr); - if (!ok) { - sc_cond_destroy(&server->cond_stopped); - sc_mutex_destroy(&server->mutex); - sc_adb_destroy(); - return false; - } - - server->serial = NULL; - server->device_socket_name = NULL; - server->stopped = false; - - server->video_socket = SC_SOCKET_NONE; - server->audio_socket = SC_SOCKET_NONE; - server->control_socket = SC_SOCKET_NONE; - - 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) { - uint8_t buf[SC_DEVICE_NAME_FIELD_LENGTH]; - ssize_t r = net_recv_all_intr(intr, device_socket, buf, sizeof(buf)); - if (r < SC_DEVICE_NAME_FIELD_LENGTH) { - LOGE("Could not retrieve device information"); - return false; - } - // in case the client sends garbage - buf[SC_DEVICE_NAME_FIELD_LENGTH - 1] = '\0'; - memcpy(info->device_name, (char *) buf, sizeof(info->device_name)); - - return true; -} - -static bool -sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) { - struct sc_adb_tunnel *tunnel = &server->tunnel; - - assert(tunnel->enabled); - - const char *serial = server->serial; - assert(serial); - - bool video = server->params.video; - bool audio = server->params.audio; - bool control = server->params.control; - - sc_socket video_socket = SC_SOCKET_NONE; - sc_socket audio_socket = SC_SOCKET_NONE; - sc_socket control_socket = SC_SOCKET_NONE; - if (!tunnel->forward) { - if (video) { - video_socket = - net_accept_intr(&server->intr, tunnel->server_socket); - if (video_socket == SC_SOCKET_NONE) { - goto fail; - } - } - - if (audio) { - audio_socket = - net_accept_intr(&server->intr, tunnel->server_socket); - if (audio_socket == SC_SOCKET_NONE) { - goto fail; - } - } - - if (control) { - control_socket = - net_accept_intr(&server->intr, tunnel->server_socket); - if (control_socket == SC_SOCKET_NONE) { - goto fail; - } - } - } else { - uint32_t tunnel_host = server->params.tunnel_host; - if (!tunnel_host) { - tunnel_host = IPV4_LOCALHOST; - } - - uint16_t tunnel_port = server->params.tunnel_port; - if (!tunnel_port) { - tunnel_port = tunnel->local_port; - } - - unsigned attempts = 100; - sc_tick delay = SC_TICK_FROM_MS(100); - sc_socket first_socket = connect_to_server(server, attempts, delay, - tunnel_host, tunnel_port); - if (first_socket == SC_SOCKET_NONE) { - goto fail; - } - - if (video) { - video_socket = first_socket; - } - - if (audio) { - if (!video) { - audio_socket = first_socket; - } else { - audio_socket = net_socket(); - if (audio_socket == SC_SOCKET_NONE) { - goto fail; - } - bool ok = net_connect_intr(&server->intr, audio_socket, - tunnel_host, tunnel_port); - if (!ok) { - goto fail; - } - } - } - - if (control) { - if (!video && !audio) { - control_socket = first_socket; - } else { - control_socket = net_socket(); - if (control_socket == SC_SOCKET_NONE) { - goto fail; - } - bool ok = net_connect_intr(&server->intr, control_socket, - tunnel_host, tunnel_port); - if (!ok) { - goto fail; - } - } - } - } - - if (control_socket != SC_SOCKET_NONE) { - // Disable Nagle's algorithm for the control socket - // (it only impacts the sending side, so it is useless to set it - // for the other sockets) - bool ok = net_set_tcp_nodelay(control_socket, true); - (void) ok; // error already logged - } - - // we don't need the adb tunnel anymore - sc_adb_tunnel_close(tunnel, &server->intr, serial, - server->device_socket_name); - - sc_socket first_socket = video ? video_socket - : audio ? audio_socket - : control_socket; - - // The sockets will be closed on stop if device_read_info() fails - bool ok = device_read_info(&server->intr, first_socket, info); - if (!ok) { - goto fail; - } - - assert(!video || video_socket != SC_SOCKET_NONE); - assert(!audio || audio_socket != SC_SOCKET_NONE); - assert(!control || control_socket != SC_SOCKET_NONE); - - server->video_socket = video_socket; - server->audio_socket = audio_socket; - server->control_socket = control_socket; - - return true; - -fail: - if (video_socket != SC_SOCKET_NONE) { - if (!net_close(video_socket)) { - LOGW("Could not close video socket"); - } - } - - if (audio_socket != SC_SOCKET_NONE) { - if (!net_close(audio_socket)) { - LOGW("Could not close audio 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, - server->device_socket_name); - } - - return false; -} - -static void -sc_server_on_terminated(void *userdata) { - struct sc_server *server = userdata; - - // If the server process dies before connecting to the server socket, - // then the client will be stuck forever on accept(). To avoid the problem, - // wake up the accept() call (or any other) when the server dies, like on - // stop() (it is safe to call interrupt() twice). - sc_intr_interrupt(&server->intr); - - server->cbs->on_disconnected(server, server->cbs_userdata); - - LOGD("Server terminated"); -} - -static uint16_t -get_adb_tcp_port(struct sc_server *server, const char *serial) { - struct sc_intr *intr = &server->intr; - - char *current_port = - sc_adb_getprop(intr, serial, "service.adb.tcp.port", SC_ADB_SILENT); - if (!current_port) { - return 0; - } - - long value; - bool ok = sc_str_parse_integer(current_port, &value); - free(current_port); - if (!ok) { - 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, - bool disconnect) { - struct sc_intr *intr = &server->intr; - - if (disconnect) { - // 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, bool disconnect) { - // 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, disconnect); -} - -static bool -sc_server_configure_tcpip_unknown_address(struct sc_server *server, - const char *serial) { - bool is_already_tcpip = - sc_adb_device_get_type(serial) == SC_ADB_DEVICE_TYPE_TCPIP; - if (is_already_tcpip) { - // Nothing to do - LOGI("Device already connected via TCP/IP: %s", serial); - server->serial = strdup(serial); + if (serial) { + server->serial = SDL_strdup(serial); if (!server->serial) { - LOG_OOM(); return false; } - return true; } - char *ip_port = sc_server_switch_to_tcpip(server, serial); - if (!ip_port) { + if (!push_server(serial)) { + SDL_free(server->serial); return false; } - server->serial = ip_port; - return sc_server_connect_to_tcpip(server, ip_port, false); -} - -static void -sc_server_kill_adb_if_requested(struct sc_server *server) { - if (server->params.kill_adb_on_close) { - LOGI("Killing adb server..."); - unsigned flags = SC_ADB_NO_STDOUT | SC_ADB_NO_STDERR | SC_ADB_NO_LOGERR; - sc_adb_kill_server(&server->intr, flags); - } -} - -static int -run_server(void *data) { - struct sc_server *server = data; - - const struct sc_server_params *params = &server->params; - - // Execute "adb start-server" before "adb devices" so that daemon starting - // output/errors is correctly printed in the console ("adb devices" output - // is parsed, so it is not output) - bool ok = sc_adb_start_server(&server->intr, 0); - if (!ok) { - LOGE("Could not start adb server"); - goto error_connection_failed; + if (!enable_tunnel(server)) { + SDL_free(server->serial); + return false; } - // params->tcpip_dst implies params->tcpip - assert(!params->tcpip_dst || params->tcpip); + // if "adb reverse" does not work (e.g. over "adb connect"), it fallbacks to + // "adb forward", so the app socket is the client + if (!server->tunnel_forward) { + // At the application level, the device part is "the server" because it + // serves video stream and control. However, at the network level, the + // client listens and the server connects to the client. That way, the + // client can listen before starting the server app, so there is no + // need to try to connect until the server socket is listening on the + // device. - // 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; - } + server->server_socket = listen_on_port(params->local_port); + if (server->server_socket == INVALID_SOCKET) { + LOGE("Could not listen on port %" PRIu16, params->local_port); + disable_tunnel(server); + SDL_free(server->serial); + return false; } - 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 { - // If the user passed a '+' (--tcpip=+ip), then disconnect first - const char *tcpip_dst = params->tcpip_dst; - bool plus = tcpip_dst[0] == '+'; - if (plus) { - ++tcpip_dst; - } - ok = sc_server_configure_tcpip_known_address(server, tcpip_dst, plus); - 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; - } - - // If --list-* is passed, then the server just prints the requested data - // then exits. - if (params->list) { - sc_pid pid = execute_server(server, params); - if (pid == SC_PROCESS_NONE) { - goto error_connection_failed; - } - sc_process_wait(pid, NULL); // ignore exit code - sc_process_close(pid); - // Wake up await_for_server() - server->cbs->on_connected(server, server->cbs_userdata); - return 0; - } - - int r = asprintf(&server->device_socket_name, SC_SOCKET_NAME_PREFIX "%08x", - params->scid); - if (r == -1) { - LOG_OOM(); - goto error_connection_failed; - } - assert(r == sizeof(SC_SOCKET_NAME_PREFIX) - 1 + 8); - assert(server->device_socket_name); - - ok = sc_adb_tunnel_open(&server->tunnel, &server->intr, serial, - server->device_socket_name, params->port_range, - params->force_adb_forward); - if (!ok) { - goto error_connection_failed; } // server will connect to our server socket - sc_pid pid = execute_server(server, params); - if (pid == SC_PROCESS_NONE) { - sc_adb_tunnel_close(&server->tunnel, &server->intr, serial, - server->device_socket_name); - goto error_connection_failed; + server->process = execute_server(server, params); + + if (server->process == PROCESS_NONE) { + if (!server->tunnel_forward) { + close_socket(&server->server_socket); + } + disable_tunnel(server); + SDL_free(server->serial); + return false; } - static const struct sc_process_listener listener = { - .on_terminated = sc_server_on_terminated, - }; - struct sc_process_observer observer; - ok = sc_process_observer_init(&observer, pid, &listener, server); - if (!ok) { - sc_process_terminate(pid); - sc_process_wait(pid, true); // ignore exit code - sc_adb_tunnel_close(&server->tunnel, &server->intr, serial, - server->device_socket_name); - goto error_connection_failed; - } + server->tunnel_enabled = true; - 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 - - if (server->video_socket != SC_SOCKET_NONE) { - // There is no video_socket if --no-video is set - net_interrupt(server->video_socket); - } - - if (server->audio_socket != SC_SOCKET_NONE) { - // There is no audio_socket if --no-audio is set - net_interrupt(server->audio_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); - - sc_server_kill_adb_if_requested(server); - - return 0; - -error_connection_failed: - sc_server_kill_adb_if_requested(server); - server->cbs->on_connection_failed(server, server->cbs_userdata); - return -1; + return true; } 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; +server_connect_to(struct server *server) { + if (!server->tunnel_forward) { + server->video_socket = net_accept(server->server_socket); + if (server->video_socket == INVALID_SOCKET) { + return false; + } + + server->control_socket = net_accept(server->server_socket); + if (server->control_socket == INVALID_SOCKET) { + // the video_socket will be clean up on destroy + return false; + } + + // we don't need the server socket anymore + close_socket(&server->server_socket); + } else { + uint32_t attempts = 100; + uint32_t delay = 100; // ms + server->video_socket = + connect_to_server(server->local_port, attempts, delay); + if (server->video_socket == INVALID_SOCKET) { + return false; + } + + // we know that the device is listening, we don't need several attempts + server->control_socket = + net_connect(IPV4_LOCALHOST, server->local_port); + if (server->control_socket == INVALID_SOCKET) { + return false; + } } + // we don't need the adb tunnel anymore + disable_tunnel(server); // ignore failure + server->tunnel_enabled = 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); +server_stop(struct server *server) { + if (server->server_socket != INVALID_SOCKET) { + close_socket(&server->server_socket); + } + if (server->video_socket != INVALID_SOCKET) { + close_socket(&server->video_socket); + } + if (server->control_socket != INVALID_SOCKET) { + close_socket(&server->control_socket); + } + + SDL_assert(server->process != PROCESS_NONE); + + if (!cmd_terminate(server->process)) { + LOGW("Cannot terminate server"); + } + + cmd_simple_wait(server->process, NULL); // ignore exit code + LOGD("Server terminated"); + + if (server->tunnel_enabled) { + // ignore failure + disable_tunnel(server); + } } void -sc_server_join(struct sc_server *server) { - sc_thread_join(&server->thread, NULL); -} - -void -sc_server_destroy(struct sc_server *server) { - if (server->video_socket != SC_SOCKET_NONE) { - net_close(server->video_socket); - } - if (server->audio_socket != SC_SOCKET_NONE) { - net_close(server->audio_socket); - } - if (server->control_socket != SC_SOCKET_NONE) { - net_close(server->control_socket); - } - - free(server->serial); - free(server->device_socket_name); - sc_intr_destroy(&server->intr); - sc_cond_destroy(&server->cond_stopped); - sc_mutex_destroy(&server->mutex); - - sc_adb_destroy(); +server_destroy(struct server *server) { + SDL_free(server->serial); } diff --git a/app/src/server.h b/app/src/server.h index 5f4592de..74a6cac8 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -1,141 +1,62 @@ -#ifndef SC_SERVER_H -#define SC_SERVER_H - -#include "common.h" +#ifndef SERVER_H +#define SERVER_H #include #include -#include "adb/adb_tunnel.h" -#include "options.h" -#include "util/intr.h" -#include "util/net.h" -#include "util/thread.h" -#include "util/tick.h" +#include "command.h" +#include "net.h" -#define SC_DEVICE_NAME_FIELD_LENGTH 64 -struct sc_server_info { - char device_name[SC_DEVICE_NAME_FIELD_LENGTH]; -}; - -struct sc_server_params { - uint32_t scid; - const char *req_serial; - enum sc_log_level log_level; - enum sc_codec video_codec; - enum sc_codec audio_codec; - enum sc_video_source video_source; - enum sc_audio_source audio_source; - enum sc_camera_facing camera_facing; - const char *crop; - const char *video_codec_options; - const char *audio_codec_options; - const char *video_encoder; - const char *audio_encoder; - const char *camera_id; - const char *camera_size; - const char *camera_ar; - uint16_t camera_fps; - struct sc_port_range port_range; - uint32_t tunnel_host; - uint16_t tunnel_port; - uint16_t max_size; - uint32_t video_bit_rate; - uint32_t audio_bit_rate; - const char *max_fps; // float to be parsed by the server - const char *angle; // float to be parsed by the server - sc_tick screen_off_timeout; - enum sc_orientation capture_orientation; - enum sc_orientation_lock capture_orientation_lock; - bool control; - uint32_t display_id; - const char *new_display; - enum sc_display_ime_policy display_ime_policy; - bool video; - bool audio; - bool audio_dup; - bool show_touches; - bool stay_awake; - bool force_adb_forward; - bool power_off_on_close; - bool clipboard_autosync; - bool downsize_on_error; - bool tcpip; - const char *tcpip_dst; - bool select_usb; - bool select_tcpip; - bool cleanup; - bool power_on; - bool kill_adb_on_close; - bool camera_high_speed; - bool vd_destroy_content; - bool vd_system_decorations; - uint8_t list; -}; - -struct sc_server { - // The internal allocated strings are copies owned by the server - struct sc_server_params params; +struct server { char *serial; - char *device_socket_name; - - sc_thread thread; - struct sc_server_info info; // initialized once connected - - sc_mutex mutex; - sc_cond cond_stopped; - bool stopped; - - struct sc_intr intr; - struct sc_adb_tunnel tunnel; - - sc_socket video_socket; - sc_socket audio_socket; - sc_socket control_socket; - - const struct sc_server_callbacks *cbs; - void *cbs_userdata; + process_t process; + socket_t server_socket; // only used if !tunnel_forward + socket_t video_socket; + socket_t control_socket; + uint16_t local_port; + bool tunnel_enabled; + bool tunnel_forward; // use "adb forward" instead of "adb reverse" }; -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); +#define SERVER_INITIALIZER { \ + .serial = NULL, \ + .process = PROCESS_NONE, \ + .server_socket = INVALID_SOCKET, \ + .video_socket = INVALID_SOCKET, \ + .control_socket = INVALID_SOCKET, \ + .local_port = 0, \ + .tunnel_enabled = false, \ + .tunnel_forward = false, \ +} - /** - * 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); +struct server_params { + const char *crop; + uint16_t local_port; + uint16_t max_size; + uint32_t bit_rate; + bool send_frame_meta; + bool control; }; -// 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); +// init default values +void +server_init(struct server *server); -// start the server asynchronously +// push, enable tunnel et start the server bool -sc_server_start(struct sc_server *server); +server_start(struct server *server, const char *serial, + const struct server_params *params); + +// block until the communication with the server is established +bool +server_connect_to(struct server *server); // disconnect and kill the server process void -sc_server_stop(struct sc_server *server); - -// join the server thread -void -sc_server_join(struct sc_server *server); +server_stop(struct server *server); // close and release sockets void -sc_server_destroy(struct sc_server *server); +server_destroy(struct server *server); #endif diff --git a/app/src/shortcut_mod.h b/app/src/shortcut_mod.h deleted file mode 100644 index f6c13f03..00000000 --- a/app/src/shortcut_mod.h +++ /dev/null @@ -1,61 +0,0 @@ -#ifndef SC_SHORTCUT_MOD_H -#define SC_SHORTCUT_MOD_H - -#include "common.h" - -#include -#include -#include -#include - -#include "options.h" - -#define SC_SDL_SHORTCUT_MODS_MASK (KMOD_CTRL | KMOD_ALT | KMOD_GUI) - -// input: OR of enum sc_shortcut_mod -// output: OR of SDL_Keymod -static inline uint16_t -sc_shortcut_mods_to_sdl(uint8_t shortcut_mods) { - uint16_t sdl_mod = 0; - if (shortcut_mods & SC_SHORTCUT_MOD_LCTRL) { - sdl_mod |= KMOD_LCTRL; - } - if (shortcut_mods & SC_SHORTCUT_MOD_RCTRL) { - sdl_mod |= KMOD_RCTRL; - } - if (shortcut_mods & SC_SHORTCUT_MOD_LALT) { - sdl_mod |= KMOD_LALT; - } - if (shortcut_mods & SC_SHORTCUT_MOD_RALT) { - sdl_mod |= KMOD_RALT; - } - if (shortcut_mods & SC_SHORTCUT_MOD_LSUPER) { - sdl_mod |= KMOD_LGUI; - } - if (shortcut_mods & SC_SHORTCUT_MOD_RSUPER) { - sdl_mod |= KMOD_RGUI; - } - return sdl_mod; -} - -static inline bool -sc_shortcut_mods_is_shortcut_mod(uint16_t sdl_shortcut_mods, uint16_t sdl_mod) { - // sdl_shortcut_mods must be within the mask - assert(!(sdl_shortcut_mods & ~SC_SDL_SHORTCUT_MODS_MASK)); - - // at least one shortcut mod pressed? - return sdl_mod & sdl_shortcut_mods; -} - -static inline bool -sc_shortcut_mods_is_shortcut_key(uint16_t sdl_shortcut_mods, - SDL_Keycode keycode) { - return (sdl_shortcut_mods & KMOD_LCTRL && keycode == SDLK_LCTRL) - || (sdl_shortcut_mods & KMOD_RCTRL && keycode == SDLK_RCTRL) - || (sdl_shortcut_mods & KMOD_LALT && keycode == SDLK_LALT) - || (sdl_shortcut_mods & KMOD_RALT && keycode == SDLK_RALT) - || (sdl_shortcut_mods & KMOD_LGUI && keycode == SDLK_LGUI) - || (sdl_shortcut_mods & KMOD_RGUI && keycode == SDLK_RGUI); -} - -#endif diff --git a/app/src/str_util.c b/app/src/str_util.c new file mode 100644 index 00000000..7d46a1a0 --- /dev/null +++ b/app/src/str_util.c @@ -0,0 +1,111 @@ +#include "str_util.h" + +#include +#include + +#ifdef _WIN32 +# include +# include +#endif + +#include + +size_t +xstrncpy(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 +xstrjoin(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 = xstrncpy(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 * +strquote(const char *src) { + size_t len = strlen(src); + char *quoted = SDL_malloc(len + 3); + if (!quoted) { + return NULL; + } + memcpy("ed[1], src, len); + quoted[0] = '"'; + quoted[len + 1] = '"'; + quoted[len + 2] = '\0'; + return quoted; +} + +size_t +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 * +utf8_to_wide_char(const char *utf8) { + int len = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, NULL, 0); + if (!len) { + return NULL; + } + + wchar_t *wide = SDL_malloc(len * sizeof(wchar_t)); + if (!wide) { + return NULL; + } + + MultiByteToWideChar(CP_UTF8, 0, utf8, -1, wide, len); + return wide; +} + +char * +utf8_from_wide_char(const wchar_t *ws) { + int len = WideCharToMultiByte(CP_UTF8, 0, ws, -1, NULL, 0, NULL, NULL); + if (!len) { + return NULL; + } + + char *utf8 = SDL_malloc(len); + if (!utf8) { + return NULL; + } + + WideCharToMultiByte(CP_UTF8, 0, ws, -1, utf8, len, NULL, NULL); + return utf8; +} + +#endif diff --git a/app/src/str_util.h b/app/src/str_util.h new file mode 100644 index 00000000..0b7a571a --- /dev/null +++ b/app/src/str_util.h @@ -0,0 +1,40 @@ +#ifndef STRUTIL_H +#define STRUTIL_H + +#include + +// like strncpy, except: +// - it copies at most n-1 chars +// - the dest string is nul-terminated +// - it does not write useless bytes if strlen(src) < n +// - it returns the number of chars actually written (max n-1) if src has +// been copied completely, or n if src has been truncated +size_t +xstrncpy(char *dest, const char *src, size_t n); + +// join tokens by sep into dst +// returns the number of chars actually written (max n-1) if no trucation +// occurred, or n if truncated +size_t +xstrjoin(char *dst, const char *const tokens[], char sep, size_t n); + +// quote a string +// returns the new allocated string, to be freed by the caller +char * +strquote(const char *src); + +// return the index to truncate a UTF-8 string at a valid position +size_t +utf8_truncation_index(const char *utf8, size_t max_len); + +#ifdef _WIN32 +// convert a UTF-8 string to a wchar_t string +// returns the new allocated string, to be freed by the caller +wchar_t * +utf8_to_wide_char(const char *utf8); + +char * +utf8_from_wide_char(const wchar_t *s); +#endif + +#endif diff --git a/app/src/stream.c b/app/src/stream.c new file mode 100644 index 00000000..4f38cecf --- /dev/null +++ b/app/src/stream.c @@ -0,0 +1,295 @@ +#include "stream.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "compat.h" +#include "config.h" +#include "buffer_util.h" +#include "decoder.h" +#include "events.h" +#include "lock_util.h" +#include "log.h" +#include "recorder.h" + +#define BUFSIZE 0x10000 + +#define HEADER_SIZE 12 +#define NO_PTS UINT64_C(-1) + +static struct frame_meta * +frame_meta_new(uint64_t pts) { + struct frame_meta *meta = SDL_malloc(sizeof(*meta)); + if (!meta) { + return meta; + } + meta->pts = pts; + meta->next = NULL; + return meta; +} + +static void +frame_meta_delete(struct frame_meta *frame_meta) { + SDL_free(frame_meta); +} + +static bool +receiver_state_push_meta(struct receiver_state *state, uint64_t pts) { + struct frame_meta *frame_meta = frame_meta_new(pts); + if (!frame_meta) { + return false; + } + + // append to the list + // (iterate to find the last item, in practice the list should be tiny) + struct frame_meta **p = &state->frame_meta_queue; + while (*p) { + p = &(*p)->next; + } + *p = frame_meta; + return true; +} + +static uint64_t +receiver_state_take_meta(struct receiver_state *state) { + struct frame_meta *frame_meta = state->frame_meta_queue; // first item + SDL_assert(frame_meta); // must not be empty + uint64_t pts = frame_meta->pts; + state->frame_meta_queue = frame_meta->next; // remove the item + frame_meta_delete(frame_meta); + return pts; +} + +static int +read_packet_with_meta(void *opaque, uint8_t *buf, int buf_size) { + struct stream *stream = opaque; + struct receiver_state *state = &stream->receiver_state; + + // 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. + + if (!state->remaining) { +#define HEADER_SIZE 12 + uint8_t header[HEADER_SIZE]; + ssize_t r = net_recv_all(stream->socket, header, HEADER_SIZE); + if (r == -1) { + return AVERROR(errno); + } + if (r == 0) { + return AVERROR_EOF; + } + // no partial read (net_recv_all()) + SDL_assert_release(r == HEADER_SIZE); + + uint64_t pts = buffer_read64be(header); + state->remaining = buffer_read32be(&header[8]); + + if (pts != NO_PTS && !receiver_state_push_meta(state, pts)) { + LOGE("Could not store PTS for recording"); + // we cannot save the PTS, the recording would be broken + return AVERROR(ENOMEM); + } + } + + SDL_assert(state->remaining); + + if (buf_size > state->remaining) { + buf_size = state->remaining; + } + + ssize_t r = net_recv(stream->socket, buf, buf_size); + if (r == -1) { + return errno ? AVERROR(errno) : AVERROR_EOF; + } + if (r == 0) { + return AVERROR_EOF; + } + + SDL_assert(state->remaining >= r); + state->remaining -= r; + + return r; +} + +static int +read_raw_packet(void *opaque, uint8_t *buf, int buf_size) { + struct stream *stream = opaque; + ssize_t r = net_recv(stream->socket, buf, buf_size); + if (r == -1) { + return errno ? AVERROR(errno) : AVERROR_EOF; + } + if (r == 0) { + return AVERROR_EOF; + } + return r; +} + +static void +notify_stopped(void) { + SDL_Event stop_event; + stop_event.type = EVENT_STREAM_STOPPED; + SDL_PushEvent(&stop_event); +} + +static int +run_stream(void *data) { + struct stream *stream = data; + + AVFormatContext *format_ctx = avformat_alloc_context(); + if (!format_ctx) { + LOGC("Could not allocate format context"); + goto end; + } + + unsigned char *buffer = av_malloc(BUFSIZE); + if (!buffer) { + LOGC("Could not allocate buffer"); + goto finally_free_format_ctx; + } + + // initialize the receiver state + stream->receiver_state.frame_meta_queue = NULL; + stream->receiver_state.remaining = 0; + + // if recording is enabled, a "header" is sent between raw packets + int (*read_packet)(void *, uint8_t *, int) = + stream->recorder ? read_packet_with_meta : read_raw_packet; + AVIOContext *avio_ctx = avio_alloc_context(buffer, BUFSIZE, 0, stream, + read_packet, NULL, NULL); + if (!avio_ctx) { + LOGC("Could not allocate avio context"); + // avformat_open_input takes ownership of 'buffer' + // so only free the buffer before avformat_open_input() + av_free(buffer); + goto finally_free_format_ctx; + } + + format_ctx->pb = avio_ctx; + + if (avformat_open_input(&format_ctx, NULL, NULL, NULL) < 0) { + LOGE("Could not open video stream"); + goto finally_free_avio_ctx; + } + + AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264); + if (!codec) { + LOGE("H.264 decoder not found"); + goto end; + } + + if (stream->decoder && !decoder_open(stream->decoder, codec)) { + LOGE("Could not open decoder"); + goto finally_close_input; + } + + if (stream->recorder && !recorder_open(stream->recorder, codec)) { + LOGE("Could not open recorder"); + goto finally_close_input; + } + + AVPacket packet; + av_init_packet(&packet); + packet.data = NULL; + packet.size = 0; + + while (!av_read_frame(format_ctx, &packet)) { + if (SDL_AtomicGet(&stream->stopped)) { + // if the stream is stopped, the socket had been shutdown, so the + // last packet is probably corrupted (but not detected as such by + // FFmpeg) and will not be decoded correctly + av_packet_unref(&packet); + goto quit; + } + if (stream->decoder && !decoder_push(stream->decoder, &packet)) { + av_packet_unref(&packet); + goto quit; + } + + if (stream->recorder) { + // we retrieve the PTS in order they were received, so they will + // be assigned to the correct frame + uint64_t pts = receiver_state_take_meta(&stream->receiver_state); + packet.pts = pts; + packet.dts = pts; + + // no need to rescale with av_packet_rescale_ts(), the timestamps + // are in microseconds both in input and output + if (!recorder_write(stream->recorder, &packet)) { + LOGE("Could not write frame to output file"); + av_packet_unref(&packet); + goto quit; + } + } + + av_packet_unref(&packet); + + if (avio_ctx->eof_reached) { + break; + } + } + + LOGD("End of frames"); + +quit: + if (stream->recorder) { + recorder_close(stream->recorder); + } +finally_close_input: + avformat_close_input(&format_ctx); +finally_free_avio_ctx: + av_free(avio_ctx->buffer); + av_free(avio_ctx); +finally_free_format_ctx: + avformat_free_context(format_ctx); +end: + notify_stopped(); + return 0; +} + +void +stream_init(struct stream *stream, socket_t socket, + struct decoder *decoder, struct recorder *recorder) { + stream->socket = socket; + stream->decoder = decoder, + stream->recorder = recorder; + SDL_AtomicSet(&stream->stopped, 0); +} + +bool +stream_start(struct stream *stream) { + LOGD("Starting stream thread"); + + stream->thread = SDL_CreateThread(run_stream, "stream", stream); + if (!stream->thread) { + LOGC("Could not start stream thread"); + return false; + } + return true; +} + +void +stream_stop(struct stream *stream) { + SDL_AtomicSet(&stream->stopped, 1); + if (stream->decoder) { + decoder_interrupt(stream->decoder); + } +} + +void +stream_join(struct stream *stream) { + SDL_WaitThread(stream->thread, NULL); +} diff --git a/app/src/stream.h b/app/src/stream.h new file mode 100644 index 00000000..1ebff1a0 --- /dev/null +++ b/app/src/stream.h @@ -0,0 +1,45 @@ +#ifndef STREAM_H +#define STREAM_H + +#include +#include +#include +#include + +#include "net.h" + +struct video_buffer; + +struct frame_meta { + uint64_t pts; + struct frame_meta *next; +}; + +struct stream { + socket_t socket; + struct video_buffer *video_buffer; + SDL_Thread *thread; + SDL_atomic_t stopped; + struct decoder *decoder; + struct recorder *recorder; + struct receiver_state { + // meta (in order) for frames not consumed yet + struct frame_meta *frame_meta_queue; + size_t remaining; // remaining bytes to receive for the current frame + } receiver_state; +}; + +void +stream_init(struct stream *stream, socket_t socket, + struct decoder *decoder, struct recorder *recorder); + +bool +stream_start(struct stream *stream); + +void +stream_stop(struct stream *stream); + +void +stream_join(struct stream *stream); + +#endif diff --git a/app/src/sys/unix/command.c b/app/src/sys/unix/command.c new file mode 100644 index 00000000..55aea5e8 --- /dev/null +++ b/app/src/sys/unix/command.c @@ -0,0 +1,126 @@ +// for portability +#define _POSIX_SOURCE // for kill() +#define _BSD_SOURCE // for readlink() + +// modern glibc will complain without this +#define _DEFAULT_SOURCE + +#include "command.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include "log.h" + +enum process_result +cmd_execute(const char *path, const char *const argv[], pid_t *pid) { + int fd[2]; + + if (pipe(fd) == -1) { + perror("pipe"); + return PROCESS_ERROR_GENERIC; + } + + enum process_result ret = PROCESS_SUCCESS; + + *pid = fork(); + if (*pid == -1) { + perror("fork"); + ret = PROCESS_ERROR_GENERIC; + goto end; + } + + if (*pid > 0) { + // parent close write side + close(fd[1]); + fd[1] = -1; + // wait for EOF or receive errno from child + if (read(fd[0], &ret, sizeof(ret)) == -1) { + perror("read"); + ret = PROCESS_ERROR_GENERIC; + goto end; + } + } else if (*pid == 0) { + // child close read side + close(fd[0]); + if (fcntl(fd[1], F_SETFD, FD_CLOEXEC) == 0) { + execvp(path, (char *const *)argv); + if (errno == ENOENT) { + ret = PROCESS_ERROR_MISSING_BINARY; + } else { + ret = PROCESS_ERROR_GENERIC; + } + perror("exec"); + } else { + perror("fcntl"); + ret = PROCESS_ERROR_GENERIC; + } + // send ret to the parent + if (write(fd[1], &ret, sizeof(ret)) == -1) { + perror("write"); + } + // close write side before exiting + close(fd[1]); + _exit(1); + } + +end: + if (fd[0] != -1) { + close(fd[0]); + } + if (fd[1] != -1) { + close(fd[1]); + } + return ret; +} + +bool +cmd_terminate(pid_t pid) { + if (pid <= 0) { + LOGC("Requested to kill %d, this is an error. Please report the bug.\n", + (int) pid); + abort(); + } + return kill(pid, SIGTERM) != -1; +} + +bool +cmd_simple_wait(pid_t pid, int *exit_code) { + int status; + int code; + if (waitpid(pid, &status, 0) == -1 || !WIFEXITED(status)) { + // cannot wait, or exited unexpectedly, probably by a signal + code = -1; + } else { + code = WEXITSTATUS(status); + } + if (exit_code) { + *exit_code = code; + } + return !code; +} + +char * +get_executable_path(void) { +// +#ifdef __linux__ + char buf[PATH_MAX + 1]; // +1 for the null byte + ssize_t len = readlink("/proc/self/exe", buf, PATH_MAX); + if (len == -1) { + perror("readlink"); + return NULL; + } + buf[len] = '\0'; + return SDL_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 +} diff --git a/app/src/sys/unix/file.c b/app/src/sys/unix/file.c deleted file mode 100644 index 8f7fb074..00000000 --- a/app/src/sys/unix/file.c +++ /dev/null @@ -1,96 +0,0 @@ -#include "util/file.h" - -#include -#include -#include -#include -#include -#include -#include -#ifdef __APPLE__ -# include // for _NSGetExecutablePath() -#endif - -#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); -#elif defined(__APPLE__) - char buf[PATH_MAX]; - uint32_t bufsize = PATH_MAX; - if (_NSGetExecutablePath(buf, &bufsize) != 0) { - LOGE("Executable path buffer too small; need %u bytes", bufsize); - return NULL; - } - return realpath(buf, NULL); -#else - // "_" is often used to store the full path of the command being executed - char *path = getenv("_"); - if (!path) { - LOGE("Could not determine executable path"); - return NULL; - } - return strdup(path); -#endif -} - -bool -sc_file_is_regular(const char *path) { - struct stat path_stat; - - if (stat(path, &path_stat)) { - perror("stat"); - return false; - } - return S_ISREG(path_stat.st_mode); -} - diff --git a/app/src/sys/unix/net.c b/app/src/sys/unix/net.c new file mode 100644 index 00000000..199cd7c2 --- /dev/null +++ b/app/src/sys/unix/net.c @@ -0,0 +1,19 @@ +#include "net.h" + +#include + +bool +net_init(void) { + // do nothing + return true; +} + +void +net_cleanup(void) { + // do nothing +} + +bool +net_close(socket_t socket) { + return !close(socket); +} diff --git a/app/src/sys/unix/process.c b/app/src/sys/unix/process.c deleted file mode 100644 index 36d1ff7d..00000000 --- a/app/src/sys/unix/process.c +++ /dev/null @@ -1,236 +0,0 @@ -#include "util/process.h" - -#include -#include -#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"); - } -} diff --git a/app/src/sys/win/command.c b/app/src/sys/win/command.c new file mode 100644 index 00000000..484ce9f0 --- /dev/null +++ b/app/src/sys/win/command.c @@ -0,0 +1,92 @@ +#include "command.h" + +#include "config.h" +#include "log.h" +#include "str_util.h" + +static int +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 = xstrjoin(cmd, argv, ' ', len); + if (ret >= len) { + LOGE("Command too long (%" PRIsizet " chars)", len - 1); + return -1; + } + return 0; +} + +enum process_result +cmd_execute(const char *path, const char *const argv[], HANDLE *handle) { + STARTUPINFOW si; + PROCESS_INFORMATION pi; + memset(&si, 0, sizeof(si)); + si.cb = sizeof(si); + + char cmd[256]; + if (build_cmd(cmd, sizeof(cmd), argv)) { + *handle = NULL; + return PROCESS_ERROR_GENERIC; + } + + wchar_t *wide = utf8_to_wide_char(cmd); + if (!wide) { + LOGC("Cannot allocate wide char string"); + return PROCESS_ERROR_GENERIC; + } + +#ifdef WINDOWS_NOCONSOLE + int flags = CREATE_NO_WINDOW; +#else + int flags = 0; +#endif + if (!CreateProcessW(NULL, wide, NULL, NULL, FALSE, flags, NULL, NULL, &si, + &pi)) { + SDL_free(wide); + *handle = NULL; + if (GetLastError() == ERROR_FILE_NOT_FOUND) { + return PROCESS_ERROR_MISSING_BINARY; + } + return PROCESS_ERROR_GENERIC; + } + + SDL_free(wide); + *handle = pi.hProcess; + return PROCESS_SUCCESS; +} + +bool +cmd_terminate(HANDLE handle) { + return TerminateProcess(handle, 1) && CloseHandle(handle); +} + +bool +cmd_simple_wait(HANDLE handle, DWORD *exit_code) { + DWORD code; + if (WaitForSingleObject(handle, INFINITE) != WAIT_OBJECT_0 + || !GetExitCodeProcess(handle, &code)) { + // cannot wait or retrieve the exit code + code = -1; // max value, it's unsigned + } + if (exit_code) { + *exit_code = code; + } + return !code; +} + +char * +get_executable_path(void) { + HMODULE hModule = GetModuleHandleW(NULL); + if (!hModule) { + return NULL; + } + WCHAR buf[MAX_PATH + 1]; // +1 for the null byte + int len = GetModuleFileNameW(hModule, buf, MAX_PATH); + if (!len) { + return NULL; + } + buf[len] = '\0'; + return utf8_from_wide_char(buf); +} diff --git a/app/src/sys/win/file.c b/app/src/sys/win/file.c deleted file mode 100644 index d3cf1760..00000000 --- a/app/src/sys/win/file.c +++ /dev/null @@ -1,43 +0,0 @@ -#include "util/file.h" - -#include - -#include - -#include "util/log.h" -#include "util/str.h" - -char * -sc_file_get_executable_path(void) { - HMODULE hModule = GetModuleHandleW(NULL); - if (!hModule) { - return NULL; - } - WCHAR buf[MAX_PATH + 1]; // +1 for the null byte - int len = GetModuleFileNameW(hModule, buf, MAX_PATH); - if (!len) { - return NULL; - } - buf[len] = '\0'; - return sc_str_from_wchars(buf); -} - -bool -sc_file_is_regular(const char *path) { - wchar_t *wide_path = sc_str_to_wchars(path); - if (!wide_path) { - LOG_OOM(); - return false; - } - - struct _stat path_stat; - int r = _wstat(wide_path, &path_stat); - free(wide_path); - - if (r) { - perror("stat"); - return false; - } - return S_ISREG(path_stat.st_mode); -} - diff --git a/app/src/sys/win/net.c b/app/src/sys/win/net.c new file mode 100644 index 00000000..dc483682 --- /dev/null +++ b/app/src/sys/win/net.c @@ -0,0 +1,24 @@ +#include "net.h" + +#include "log.h" + +bool +net_init(void) { + WSADATA wsa; + int res = WSAStartup(MAKEWORD(2, 2), &wsa) < 0; + if (res < 0) { + LOGC("WSAStartup failed with error %d", res); + return false; + } + return true; +} + +void +net_cleanup(void) { + WSACleanup(); +} + +bool +net_close(socket_t socket) { + return !closesocket(socket); +} diff --git a/app/src/sys/win/process.c b/app/src/sys/win/process.c deleted file mode 100644 index 6ae33d86..00000000 --- a/app/src/sys/win/process.c +++ /dev/null @@ -1,260 +0,0 @@ -#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); - } - - CloseHandle(pi.hThread); - - // 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"); - } -} diff --git a/app/src/tiny_xpm.c b/app/src/tiny_xpm.c new file mode 100644 index 00000000..0fb410f3 --- /dev/null +++ b/app/src/tiny_xpm.c @@ -0,0 +1,115 @@ +#include "tiny_xpm.h" + +#include +#include +#include +#include + +#include "log.h" + +struct index { + char c; + uint32_t color; +}; + +static bool +find_color(struct index *index, int len, char c, uint32_t *color) { + // there are typically very few color, so it's ok to iterate over the array + for (int i = 0; i < len; ++i) { + if (index[i].c == c) { + *color = index[i].color; + return true; + } + } + *color = 0; + return false; +} + +// We encounter some problems with SDL2_image on MSYS2 (Windows), +// so here is our own XPM parsing not to depend on SDL_image. +// +// We do not hardcode the binary image to keep some flexibility to replace the +// icon easily (just by replacing icon.xpm). +// +// Parameter is not "const char *" because XPM formats are generally stored in a +// (non-const) "char *" +SDL_Surface * +read_xpm(char *xpm[]) { +#if SDL_ASSERT_LEVEL >= 2 + // patch the XPM to change the icon color in debug mode + xpm[2] = ". c #CC00CC"; +#endif + + char *endptr; + // *** No error handling, assume the XPM source is valid *** + // (it's in our source repo) + // Assertions are only checked in debug + int width = strtol(xpm[0], &endptr, 10); + int height = strtol(endptr + 1, &endptr, 10); + int colors = strtol(endptr + 1, &endptr, 10); + int chars = strtol(endptr + 1, &endptr, 10); + + // sanity checks + SDL_assert(0 <= width && width < 256); + SDL_assert(0 <= height && height < 256); + SDL_assert(0 <= colors && colors < 256); + SDL_assert(chars == 1); // this implementation does not support more + + // init index + struct index index[colors]; + for (int i = 0; i < colors; ++i) { + const char *line = xpm[1+i]; + index[i].c = line[0]; + SDL_assert(line[1] == '\t'); + SDL_assert(line[2] == 'c'); + SDL_assert(line[3] == ' '); + if (line[4] == '#') { + index[i].color = 0xff000000 | strtol(&line[5], &endptr, 0x10); + SDL_assert(*endptr == '\0'); + } else { + SDL_assert(!strcmp("None", &line[4])); + index[i].color = 0; + } + } + + // parse image + uint32_t *pixels = SDL_malloc(4 * width * height); + if (!pixels) { + LOGE("Could not allocate icon memory"); + return NULL; + } + for (int y = 0; y < height; ++y) { + const char *line = xpm[1 + colors + y]; + for (int x = 0; x < width; ++x) { + char c = line[x]; + uint32_t color; + bool color_found = find_color(index, colors, c, &color); + SDL_assert(color_found); + pixels[y * width + x] = color; + } + } + +#if SDL_BYTEORDER == SDL_BIG_ENDIAN + uint32_t amask = 0x000000ff; + uint32_t rmask = 0x0000ff00; + uint32_t gmask = 0x00ff0000; + uint32_t bmask = 0xff000000; +#else // little endian, like x86 + uint32_t amask = 0xff000000; + uint32_t rmask = 0x00ff0000; + uint32_t gmask = 0x0000ff00; + uint32_t bmask = 0x000000ff; +#endif + + SDL_Surface *surface = SDL_CreateRGBSurfaceFrom(pixels, + width, height, + 32, 4 * width, + rmask, gmask, bmask, amask); + if (!surface) { + LOGE("Could not create icon surface"); + return NULL; + } + // make the surface own the raw pixels + surface->flags &= ~SDL_PREALLOC; + return surface; +} diff --git a/app/src/tiny_xpm.h b/app/src/tiny_xpm.h new file mode 100644 index 00000000..85dea5c2 --- /dev/null +++ b/app/src/tiny_xpm.h @@ -0,0 +1,9 @@ +#ifndef TINYXPM_H +#define TINYXPM_H + +#include + +SDL_Surface * +read_xpm(char *xpm[]); + +#endif diff --git a/app/src/trait/frame_sink.h b/app/src/trait/frame_sink.h deleted file mode 100644 index 67be4d46..00000000 --- a/app/src/trait/frame_sink.h +++ /dev/null @@ -1,25 +0,0 @@ -#ifndef SC_FRAME_SINK_H -#define SC_FRAME_SINK_H - -#include "common.h" - -#include -#include - -/** - * 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 { - /* The codec context is valid until the sink is closed */ - bool (*open)(struct sc_frame_sink *sink, const AVCodecContext *ctx); - void (*close)(struct sc_frame_sink *sink); - bool (*push)(struct sc_frame_sink *sink, const AVFrame *frame); -}; - -#endif diff --git a/app/src/trait/frame_source.c b/app/src/trait/frame_source.c deleted file mode 100644 index 56848309..00000000 --- a/app/src/trait/frame_source.c +++ /dev/null @@ -1,61 +0,0 @@ -#include "frame_source.h" - -#include - -void -sc_frame_source_init(struct sc_frame_source *source) { - source->sink_count = 0; -} - -void -sc_frame_source_add_sink(struct sc_frame_source *source, - struct sc_frame_sink *sink) { - assert(source->sink_count < SC_FRAME_SOURCE_MAX_SINKS); - assert(sink); - assert(sink->ops); - source->sinks[source->sink_count++] = sink; -} - -static void -sc_frame_source_sinks_close_firsts(struct sc_frame_source *source, - unsigned count) { - while (count) { - struct sc_frame_sink *sink = source->sinks[--count]; - sink->ops->close(sink); - } -} - -bool -sc_frame_source_sinks_open(struct sc_frame_source *source, - const AVCodecContext *ctx) { - assert(source->sink_count); - for (unsigned i = 0; i < source->sink_count; ++i) { - struct sc_frame_sink *sink = source->sinks[i]; - if (!sink->ops->open(sink, ctx)) { - sc_frame_source_sinks_close_firsts(source, i); - return false; - } - } - - return true; -} - -void -sc_frame_source_sinks_close(struct sc_frame_source *source) { - assert(source->sink_count); - sc_frame_source_sinks_close_firsts(source, source->sink_count); -} - -bool -sc_frame_source_sinks_push(struct sc_frame_source *source, - const AVFrame *frame) { - assert(source->sink_count); - for (unsigned i = 0; i < source->sink_count; ++i) { - struct sc_frame_sink *sink = source->sinks[i]; - if (!sink->ops->push(sink, frame)) { - return false; - } - } - - return true; -} diff --git a/app/src/trait/frame_source.h b/app/src/trait/frame_source.h deleted file mode 100644 index cb1ef905..00000000 --- a/app/src/trait/frame_source.h +++ /dev/null @@ -1,40 +0,0 @@ -#ifndef SC_FRAME_SOURCE_H -#define SC_FRAME_SOURCE_H - -#include "common.h" - -#include - -#include "trait/frame_sink.h" - -#define SC_FRAME_SOURCE_MAX_SINKS 2 - -/** - * Frame source trait - * - * Component able to send AVFrames should implement this trait. - */ -struct sc_frame_source { - struct sc_frame_sink *sinks[SC_FRAME_SOURCE_MAX_SINKS]; - unsigned sink_count; -}; - -void -sc_frame_source_init(struct sc_frame_source *source); - -void -sc_frame_source_add_sink(struct sc_frame_source *source, - struct sc_frame_sink *sink); - -bool -sc_frame_source_sinks_open(struct sc_frame_source *source, - const AVCodecContext *ctx); - -void -sc_frame_source_sinks_close(struct sc_frame_source *source); - -bool -sc_frame_source_sinks_push(struct sc_frame_source *source, - const AVFrame *frame); - -#endif diff --git a/app/src/trait/gamepad_processor.h b/app/src/trait/gamepad_processor.h deleted file mode 100644 index 5e8dc2a4..00000000 --- a/app/src/trait/gamepad_processor.h +++ /dev/null @@ -1,56 +0,0 @@ -#ifndef SC_GAMEPAD_PROCESSOR_H -#define SC_GAMEPAD_PROCESSOR_H - -#include "common.h" - -#include "input_events.h" - -/** - * Gamepad processor trait. - * - * Component able to handle gamepads devices and inject buttons and axis events. - */ -struct sc_gamepad_processor { - const struct sc_gamepad_processor_ops *ops; -}; - -struct sc_gamepad_processor_ops { - - /** - * Process a gamepad device added event - * - * This function is mandatory. - */ - void - (*process_gamepad_added)(struct sc_gamepad_processor *gp, - const struct sc_gamepad_device_event *event); - - /** - * Process a gamepad device removed event - * - * This function is mandatory. - */ - void - (*process_gamepad_removed)(struct sc_gamepad_processor *gp, - const struct sc_gamepad_device_event *event); - - /** - * Process a gamepad axis event - * - * This function is mandatory. - */ - void - (*process_gamepad_axis)(struct sc_gamepad_processor *gp, - const struct sc_gamepad_axis_event *event); - - /** - * Process a gamepad button event - * - * This function is mandatory. - */ - void - (*process_gamepad_button)(struct sc_gamepad_processor *gp, - const struct sc_gamepad_button_event *event); -}; - -#endif diff --git a/app/src/trait/key_processor.h b/app/src/trait/key_processor.h deleted file mode 100644 index 9e9bb86e..00000000 --- a/app/src/trait/key_processor.h +++ /dev/null @@ -1,61 +0,0 @@ -#ifndef SC_KEY_PROCESSOR_H -#define SC_KEY_PROCESSOR_H - -#include "common.h" - -#include - -#include "input_events.h" - -/** - * Key processor trait. - * - * Component able to process and inject keys should implement this trait. - */ -struct sc_key_processor { - /** - * Set by the implementation to indicate that it must explicitly wait for - * the clipboard to be set on the device before injecting Ctrl+v to avoid - * race conditions. If it is set, the input_manager will pass a valid - * ack_to_wait to process_key() in case of clipboard synchronization - * resulting of the key event. - */ - bool async_paste; - - /** - * Set by the implementation to indicate that the keyboard is HID. In - * practice, it is used to react on a shortcut to open the hard keyboard - * settings only if the keyboard is HID. - */ - bool hid; - - const struct sc_key_processor_ops *ops; -}; - -struct sc_key_processor_ops { - - /** - * Process a keyboard event - * - * The `sequence` number (if different from `SC_SEQUENCE_INVALID`) indicates - * the acknowledgement number to wait for before injecting this event. - * This allows to ensure that the device clipboard is set before injecting - * Ctrl+v on the device. - * - * This function is mandatory. - */ - void - (*process_key)(struct sc_key_processor *kp, - const struct sc_key_event *event, uint64_t ack_to_wait); - - /** - * Process an input text - * - * This function is optional. - */ - void - (*process_text)(struct sc_key_processor *kp, - const struct sc_text_event *event); -}; - -#endif diff --git a/app/src/trait/mouse_processor.h b/app/src/trait/mouse_processor.h deleted file mode 100644 index d0a96e7c..00000000 --- a/app/src/trait/mouse_processor.h +++ /dev/null @@ -1,65 +0,0 @@ -#ifndef SC_MOUSE_PROCESSOR_H -#define SC_MOUSE_PROCESSOR_H - -#include "common.h" - -#include - -#include "input_events.h" - -/** - * Mouse processor trait. - * - * Component able to process and inject mouse events should implement this - * trait. - */ -struct sc_mouse_processor { - const struct sc_mouse_processor_ops *ops; - - /** - * If set, the mouse processor works in relative mode (the absolute - * position is irrelevant). In particular, it indicates that the mouse - * pointer must be "captured" by the UI. - */ - bool relative_mode; -}; - -struct sc_mouse_processor_ops { - /** - * Process a mouse motion event - * - * This function is mandatory. - */ - void - (*process_mouse_motion)(struct sc_mouse_processor *mp, - const struct sc_mouse_motion_event *event); - - /** - * Process a mouse click event - * - * This function is mandatory. - */ - void - (*process_mouse_click)(struct sc_mouse_processor *mp, - const struct sc_mouse_click_event *event); - - /** - * Process a mouse scroll event - * - * This function is optional. - */ - void - (*process_mouse_scroll)(struct sc_mouse_processor *mp, - const struct sc_mouse_scroll_event *event); - - /** - * Process a touch event - * - * This function is optional. - */ - void - (*process_touch)(struct sc_mouse_processor *mp, - const struct sc_touch_event *event); -}; - -#endif diff --git a/app/src/trait/packet_sink.h b/app/src/trait/packet_sink.h deleted file mode 100644 index e12dea12..00000000 --- a/app/src/trait/packet_sink.h +++ /dev/null @@ -1,35 +0,0 @@ -#ifndef SC_PACKET_SINK_H -#define SC_PACKET_SINK_H - -#include "common.h" - -#include -#include - -/** - * 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 { - /* The codec context is valid until the sink is closed */ - bool (*open)(struct sc_packet_sink *sink, AVCodecContext *ctx); - void (*close)(struct sc_packet_sink *sink); - bool (*push)(struct sc_packet_sink *sink, const AVPacket *packet); - - /*/ - * Called when the input stream has been disabled at runtime. - * - * If it is called, then open(), close() and push() will never be called. - * - * It is useful to notify the recorder that the requested audio stream has - * finally been disabled because the device could not capture it. - */ - void (*disable)(struct sc_packet_sink *sink); -}; - -#endif diff --git a/app/src/trait/packet_source.c b/app/src/trait/packet_source.c deleted file mode 100644 index 0a2c6c4d..00000000 --- a/app/src/trait/packet_source.c +++ /dev/null @@ -1,72 +0,0 @@ -#include "packet_source.h" - -#include - -void -sc_packet_source_init(struct sc_packet_source *source) { - source->sink_count = 0; -} - -void -sc_packet_source_add_sink(struct sc_packet_source *source, - struct sc_packet_sink *sink) { - assert(source->sink_count < SC_PACKET_SOURCE_MAX_SINKS); - assert(sink); - assert(sink->ops); - source->sinks[source->sink_count++] = sink; -} - -static void -sc_packet_source_sinks_close_firsts(struct sc_packet_source *source, - unsigned count) { - while (count) { - struct sc_packet_sink *sink = source->sinks[--count]; - sink->ops->close(sink); - } -} - -bool -sc_packet_source_sinks_open(struct sc_packet_source *source, - AVCodecContext *ctx) { - assert(source->sink_count); - for (unsigned i = 0; i < source->sink_count; ++i) { - struct sc_packet_sink *sink = source->sinks[i]; - if (!sink->ops->open(sink, ctx)) { - sc_packet_source_sinks_close_firsts(source, i); - return false; - } - } - - return true; -} - -void -sc_packet_source_sinks_close(struct sc_packet_source *source) { - assert(source->sink_count); - sc_packet_source_sinks_close_firsts(source, source->sink_count); -} - -bool -sc_packet_source_sinks_push(struct sc_packet_source *source, - const AVPacket *packet) { - assert(source->sink_count); - for (unsigned i = 0; i < source->sink_count; ++i) { - struct sc_packet_sink *sink = source->sinks[i]; - if (!sink->ops->push(sink, packet)) { - return false; - } - } - - return true; -} - -void -sc_packet_source_sinks_disable(struct sc_packet_source *source) { - assert(source->sink_count); - for (unsigned i = 0; i < source->sink_count; ++i) { - struct sc_packet_sink *sink = source->sinks[i]; - if (sink->ops->disable) { - sink->ops->disable(sink); - } - } -} diff --git a/app/src/trait/packet_source.h b/app/src/trait/packet_source.h deleted file mode 100644 index 8788021a..00000000 --- a/app/src/trait/packet_source.h +++ /dev/null @@ -1,43 +0,0 @@ -#ifndef SC_PACKET_SOURCE_H -#define SC_PACKET_SOURCE_H - -#include "common.h" - -#include - -#include "trait/packet_sink.h" - -#define SC_PACKET_SOURCE_MAX_SINKS 2 - -/** - * Packet source trait - * - * Component able to send AVPackets should implement this trait. - */ -struct sc_packet_source { - struct sc_packet_sink *sinks[SC_PACKET_SOURCE_MAX_SINKS]; - unsigned sink_count; -}; - -void -sc_packet_source_init(struct sc_packet_source *source); - -void -sc_packet_source_add_sink(struct sc_packet_source *source, - struct sc_packet_sink *sink); - -bool -sc_packet_source_sinks_open(struct sc_packet_source *source, - AVCodecContext *ctx); - -void -sc_packet_source_sinks_close(struct sc_packet_source *source); - -bool -sc_packet_source_sinks_push(struct sc_packet_source *source, - const AVPacket *packet); - -void -sc_packet_source_sinks_disable(struct sc_packet_source *source); - -#endif diff --git a/app/src/uhid/gamepad_uhid.c b/app/src/uhid/gamepad_uhid.c deleted file mode 100644 index c64feb18..00000000 --- a/app/src/uhid/gamepad_uhid.c +++ /dev/null @@ -1,146 +0,0 @@ -#include "gamepad_uhid.h" - -#include -#include -#include -#include - -#include "hid/hid_gamepad.h" -#include "input_events.h" -#include "util/log.h" - -/** Downcast gamepad processor to sc_gamepad_uhid */ -#define DOWNCAST(GP) container_of(GP, struct sc_gamepad_uhid, gamepad_processor) - -// Xbox 360 -#define SC_GAMEPAD_UHID_VENDOR_ID UINT16_C(0x045e) -#define SC_GAMEPAD_UHID_PRODUCT_ID UINT16_C(0x028e) -#define SC_GAMEPAD_UHID_NAME "Microsoft X-Box 360 Pad" - -static void -sc_gamepad_uhid_send_input(struct sc_gamepad_uhid *gamepad, - const struct sc_hid_input *hid_input, - const char *name) { - struct sc_control_msg msg; - msg.type = SC_CONTROL_MSG_TYPE_UHID_INPUT; - msg.uhid_input.id = hid_input->hid_id; - - assert(hid_input->size <= SC_HID_MAX_SIZE); - memcpy(msg.uhid_input.data, hid_input->data, hid_input->size); - msg.uhid_input.size = hid_input->size; - - if (!sc_controller_push_msg(gamepad->controller, &msg)) { - LOGE("Could not push UHID_INPUT message (%s)", name); - } -} - -static void -sc_gamepad_uhid_send_open(struct sc_gamepad_uhid *gamepad, - const struct sc_hid_open *hid_open) { - struct sc_control_msg msg; - msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE; - msg.uhid_create.id = hid_open->hid_id; - msg.uhid_create.vendor_id = SC_GAMEPAD_UHID_VENDOR_ID; - msg.uhid_create.product_id = SC_GAMEPAD_UHID_PRODUCT_ID; - msg.uhid_create.name = SC_GAMEPAD_UHID_NAME; - msg.uhid_create.report_desc = hid_open->report_desc; - msg.uhid_create.report_desc_size = hid_open->report_desc_size; - - if (!sc_controller_push_msg(gamepad->controller, &msg)) { - LOGE("Could not push UHID_CREATE message (gamepad)"); - } -} - -static void -sc_gamepad_uhid_send_close(struct sc_gamepad_uhid *gamepad, - const struct sc_hid_close *hid_close) { - struct sc_control_msg msg; - msg.type = SC_CONTROL_MSG_TYPE_UHID_DESTROY; - msg.uhid_create.id = hid_close->hid_id; - - if (!sc_controller_push_msg(gamepad->controller, &msg)) { - LOGE("Could not push UHID_DESTROY message (gamepad)"); - } -} - -static void -sc_gamepad_processor_process_gamepad_added(struct sc_gamepad_processor *gp, - const struct sc_gamepad_device_event *event) { - struct sc_gamepad_uhid *gamepad = DOWNCAST(gp); - - struct sc_hid_open hid_open; - if (!sc_hid_gamepad_generate_open(&gamepad->hid, &hid_open, - event->gamepad_id)) { - return; - } - - SDL_GameController* game_controller = - SDL_GameControllerFromInstanceID(event->gamepad_id); - assert(game_controller); - const char *name = SDL_GameControllerName(game_controller); - LOGI("Gamepad added: [%" PRIu32 "] %s", event->gamepad_id, name); - - sc_gamepad_uhid_send_open(gamepad, &hid_open); -} - -static void -sc_gamepad_processor_process_gamepad_removed(struct sc_gamepad_processor *gp, - const struct sc_gamepad_device_event *event) { - struct sc_gamepad_uhid *gamepad = DOWNCAST(gp); - - struct sc_hid_close hid_close; - if (!sc_hid_gamepad_generate_close(&gamepad->hid, &hid_close, - event->gamepad_id)) { - return; - } - - LOGI("Gamepad removed: [%" PRIu32 "]", event->gamepad_id); - - sc_gamepad_uhid_send_close(gamepad, &hid_close); -} - -static void -sc_gamepad_processor_process_gamepad_axis(struct sc_gamepad_processor *gp, - const struct sc_gamepad_axis_event *event) { - struct sc_gamepad_uhid *gamepad = DOWNCAST(gp); - - struct sc_hid_input hid_input; - if (!sc_hid_gamepad_generate_input_from_axis(&gamepad->hid, &hid_input, - event)) { - return; - } - - sc_gamepad_uhid_send_input(gamepad, &hid_input, "gamepad axis"); -} - -static void -sc_gamepad_processor_process_gamepad_button(struct sc_gamepad_processor *gp, - const struct sc_gamepad_button_event *event) { - struct sc_gamepad_uhid *gamepad = DOWNCAST(gp); - - struct sc_hid_input hid_input; - if (!sc_hid_gamepad_generate_input_from_button(&gamepad->hid, &hid_input, - event)) { - return; - } - - sc_gamepad_uhid_send_input(gamepad, &hid_input, "gamepad button"); - -} - -void -sc_gamepad_uhid_init(struct sc_gamepad_uhid *gamepad, - struct sc_controller *controller) { - sc_hid_gamepad_init(&gamepad->hid); - - gamepad->controller = controller; - - static const struct sc_gamepad_processor_ops ops = { - .process_gamepad_added = sc_gamepad_processor_process_gamepad_added, - .process_gamepad_removed = sc_gamepad_processor_process_gamepad_removed, - .process_gamepad_axis = sc_gamepad_processor_process_gamepad_axis, - .process_gamepad_button = sc_gamepad_processor_process_gamepad_button, - }; - - gamepad->gamepad_processor.ops = &ops; -} diff --git a/app/src/uhid/gamepad_uhid.h b/app/src/uhid/gamepad_uhid.h deleted file mode 100644 index ad747604..00000000 --- a/app/src/uhid/gamepad_uhid.h +++ /dev/null @@ -1,21 +0,0 @@ -#ifndef SC_GAMEPAD_UHID_H -#define SC_GAMEPAD_UHID_H - -#include "common.h" - -#include "controller.h" -#include "hid/hid_gamepad.h" -#include "trait/gamepad_processor.h" - -struct sc_gamepad_uhid { - struct sc_gamepad_processor gamepad_processor; // gamepad processor trait - - struct sc_hid_gamepad hid; - struct sc_controller *controller; -}; - -void -sc_gamepad_uhid_init(struct sc_gamepad_uhid *mouse, - struct sc_controller *controller); - -#endif diff --git a/app/src/uhid/keyboard_uhid.c b/app/src/uhid/keyboard_uhid.c deleted file mode 100644 index 70082990..00000000 --- a/app/src/uhid/keyboard_uhid.c +++ /dev/null @@ -1,161 +0,0 @@ -#include "keyboard_uhid.h" - -#include -#include -#include -#include - -#include "util/log.h" -#include "util/thread.h" - -/** Downcast key processor to keyboard_uhid */ -#define DOWNCAST(KP) container_of(KP, struct sc_keyboard_uhid, key_processor) - -/** Downcast uhid_receiver to keyboard_uhid */ -#define DOWNCAST_RECEIVER(UR) \ - container_of(UR, struct sc_keyboard_uhid, uhid_receiver) - -static void -sc_keyboard_uhid_send_input(struct sc_keyboard_uhid *kb, - const struct sc_hid_input *hid_input) { - struct sc_control_msg msg; - msg.type = SC_CONTROL_MSG_TYPE_UHID_INPUT; - msg.uhid_input.id = hid_input->hid_id; - - assert(hid_input->size <= SC_HID_MAX_SIZE); - memcpy(msg.uhid_input.data, hid_input->data, hid_input->size); - msg.uhid_input.size = hid_input->size; - - if (!sc_controller_push_msg(kb->controller, &msg)) { - LOGE("Could not push UHID_INPUT message (key)"); - } -} - -static void -sc_keyboard_uhid_synchronize_mod(struct sc_keyboard_uhid *kb) { - SDL_Keymod sdl_mod = SDL_GetModState(); - uint16_t mod = sc_mods_state_from_sdl(sdl_mod) & (SC_MOD_CAPS | SC_MOD_NUM); - uint16_t diff = mod ^ kb->device_mod; - - if (diff) { - // Inherently racy (the HID output reports arrive asynchronously in - // response to key presses), but will re-synchronize on next key press - // or HID output anyway - kb->device_mod = mod; - - struct sc_hid_input hid_input; - if (!sc_hid_keyboard_generate_input_from_mods(&hid_input, diff)) { - return; - } - - LOGV("HID keyboard state synchronized"); - - sc_keyboard_uhid_send_input(kb, &hid_input); - } -} - -static void -sc_key_processor_process_key(struct sc_key_processor *kp, - const struct sc_key_event *event, - uint64_t ack_to_wait) { - (void) ack_to_wait; - - assert(sc_thread_get_id() == SC_MAIN_THREAD_ID); - - if (event->repeat) { - // In USB HID protocol, key repeat is handled by the host (Android), so - // just ignore key repeat here. - return; - } - - struct sc_keyboard_uhid *kb = DOWNCAST(kp); - - struct sc_hid_input hid_input; - - // Not all keys are supported, just ignore unsupported keys - if (sc_hid_keyboard_generate_input_from_key(&kb->hid, &hid_input, event)) { - if (event->scancode == SC_SCANCODE_CAPSLOCK) { - kb->device_mod ^= SC_MOD_CAPS; - } else if (event->scancode == SC_SCANCODE_NUMLOCK) { - kb->device_mod ^= SC_MOD_NUM; - } else { - // Synchronize modifiers (only if the scancode itself does not - // change the modifiers) - sc_keyboard_uhid_synchronize_mod(kb); - } - sc_keyboard_uhid_send_input(kb, &hid_input); - } -} - -static unsigned -sc_keyboard_uhid_to_sc_mod(uint8_t hid_led) { - // - // (chapter 11: LED page) - unsigned mod = 0; - if (hid_led & 0x01) { - mod |= SC_MOD_NUM; - } - if (hid_led & 0x02) { - mod |= SC_MOD_CAPS; - } - return mod; -} - -void -sc_keyboard_uhid_process_hid_output(struct sc_keyboard_uhid *kb, - const uint8_t *data, size_t size) { - assert(sc_thread_get_id() == SC_MAIN_THREAD_ID); - - assert(size); - - // Also check at runtime (do not trust the server) - if (!size) { - LOGE("Unexpected empty HID output message"); - return; - } - - uint8_t hid_led = data[0]; - uint16_t device_mod = sc_keyboard_uhid_to_sc_mod(hid_led); - kb->device_mod = device_mod; -} - -bool -sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb, - struct sc_controller *controller) { - sc_hid_keyboard_init(&kb->hid); - - kb->controller = controller; - kb->device_mod = 0; - - static const struct sc_key_processor_ops ops = { - .process_key = sc_key_processor_process_key, - // Never forward text input via HID (all the keys are injected - // separately) - .process_text = NULL, - }; - - // Clipboard synchronization is requested over the same control socket, so - // there is no need for a specific synchronization mechanism - kb->key_processor.async_paste = false; - kb->key_processor.hid = true; - kb->key_processor.ops = &ops; - - struct sc_hid_open hid_open; - sc_hid_keyboard_generate_open(&hid_open); - assert(hid_open.hid_id == SC_HID_ID_KEYBOARD); - - struct sc_control_msg msg; - msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE; - msg.uhid_create.id = SC_HID_ID_KEYBOARD; - msg.uhid_create.vendor_id = 0; - msg.uhid_create.product_id = 0; - msg.uhid_create.name = NULL; - msg.uhid_create.report_desc = hid_open.report_desc; - msg.uhid_create.report_desc_size = hid_open.report_desc_size; - if (!sc_controller_push_msg(controller, &msg)) { - LOGE("Could not send UHID_CREATE message (keyboard)"); - return false; - } - - return true; -} diff --git a/app/src/uhid/keyboard_uhid.h b/app/src/uhid/keyboard_uhid.h deleted file mode 100644 index 1628a678..00000000 --- a/app/src/uhid/keyboard_uhid.h +++ /dev/null @@ -1,28 +0,0 @@ -#ifndef SC_KEYBOARD_UHID_H -#define SC_KEYBOARD_UHID_H - -#include "common.h" - -#include - -#include "controller.h" -#include "hid/hid_keyboard.h" -#include "trait/key_processor.h" - -struct sc_keyboard_uhid { - struct sc_key_processor key_processor; // key processor trait - - struct sc_hid_keyboard hid; - struct sc_controller *controller; - uint16_t device_mod; -}; - -bool -sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb, - struct sc_controller *controller); - -void -sc_keyboard_uhid_process_hid_output(struct sc_keyboard_uhid *kb, - const uint8_t *data, size_t size); - -#endif diff --git a/app/src/uhid/mouse_uhid.c b/app/src/uhid/mouse_uhid.c deleted file mode 100644 index 869e48a4..00000000 --- a/app/src/uhid/mouse_uhid.c +++ /dev/null @@ -1,100 +0,0 @@ -#include "mouse_uhid.h" - -#include -#include - -#include "hid/hid_mouse.h" -#include "input_events.h" -#include "util/log.h" - -/** Downcast mouse processor to mouse_uhid */ -#define DOWNCAST(MP) container_of(MP, struct sc_mouse_uhid, mouse_processor) - -static void -sc_mouse_uhid_send_input(struct sc_mouse_uhid *mouse, - const struct sc_hid_input *hid_input, - const char *name) { - struct sc_control_msg msg; - msg.type = SC_CONTROL_MSG_TYPE_UHID_INPUT; - msg.uhid_input.id = hid_input->hid_id; - - assert(hid_input->size <= SC_HID_MAX_SIZE); - memcpy(msg.uhid_input.data, hid_input->data, hid_input->size); - msg.uhid_input.size = hid_input->size; - - if (!sc_controller_push_msg(mouse->controller, &msg)) { - LOGE("Could not push UHID_INPUT message (%s)", name); - } -} - -static void -sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, - const struct sc_mouse_motion_event *event) { - struct sc_mouse_uhid *mouse = DOWNCAST(mp); - - struct sc_hid_input hid_input; - sc_hid_mouse_generate_input_from_motion(&hid_input, event); - - sc_mouse_uhid_send_input(mouse, &hid_input, "mouse motion"); -} - -static void -sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, - const struct sc_mouse_click_event *event) { - struct sc_mouse_uhid *mouse = DOWNCAST(mp); - - struct sc_hid_input hid_input; - sc_hid_mouse_generate_input_from_click(&hid_input, event); - - sc_mouse_uhid_send_input(mouse, &hid_input, "mouse click"); -} - -static void -sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, - const struct sc_mouse_scroll_event *event) { - struct sc_mouse_uhid *mouse = DOWNCAST(mp); - - struct sc_hid_input hid_input; - if (!sc_hid_mouse_generate_input_from_scroll(&hid_input, event)) { - return; - } - - sc_mouse_uhid_send_input(mouse, &hid_input, "mouse scroll"); -} - -bool -sc_mouse_uhid_init(struct sc_mouse_uhid *mouse, - struct sc_controller *controller) { - mouse->controller = controller; - - static const struct sc_mouse_processor_ops ops = { - .process_mouse_motion = sc_mouse_processor_process_mouse_motion, - .process_mouse_click = sc_mouse_processor_process_mouse_click, - .process_mouse_scroll = sc_mouse_processor_process_mouse_scroll, - // Touch events not supported (coordinates are not relative) - .process_touch = NULL, - }; - - mouse->mouse_processor.ops = &ops; - - mouse->mouse_processor.relative_mode = true; - - struct sc_hid_open hid_open; - sc_hid_mouse_generate_open(&hid_open); - assert(hid_open.hid_id == SC_HID_ID_MOUSE); - - struct sc_control_msg msg; - msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE; - msg.uhid_create.id = SC_HID_ID_MOUSE; - msg.uhid_create.vendor_id = 0; - msg.uhid_create.product_id = 0; - msg.uhid_create.name = NULL; - msg.uhid_create.report_desc = hid_open.report_desc; - msg.uhid_create.report_desc_size = hid_open.report_desc_size; - if (!sc_controller_push_msg(controller, &msg)) { - LOGE("Could not push UHID_CREATE message (mouse)"); - return false; - } - - return true; -} diff --git a/app/src/uhid/mouse_uhid.h b/app/src/uhid/mouse_uhid.h deleted file mode 100644 index f117ba97..00000000 --- a/app/src/uhid/mouse_uhid.h +++ /dev/null @@ -1,19 +0,0 @@ -#ifndef SC_MOUSE_UHID_H -#define SC_MOUSE_UHID_H - -#include - -#include "controller.h" -#include "trait/mouse_processor.h" - -struct sc_mouse_uhid { - struct sc_mouse_processor mouse_processor; // mouse processor trait - - struct sc_controller *controller; -}; - -bool -sc_mouse_uhid_init(struct sc_mouse_uhid *mouse, - struct sc_controller *controller); - -#endif diff --git a/app/src/uhid/uhid_output.c b/app/src/uhid/uhid_output.c deleted file mode 100644 index e743a73c..00000000 --- a/app/src/uhid/uhid_output.c +++ /dev/null @@ -1,26 +0,0 @@ -#include "uhid_output.h" - -#include - -#include "uhid/keyboard_uhid.h" -#include "util/log.h" - -void -sc_uhid_devices_init(struct sc_uhid_devices *devices, - struct sc_keyboard_uhid *keyboard) { - devices->keyboard = keyboard; -} - -void -sc_uhid_devices_process_hid_output(struct sc_uhid_devices *devices, uint16_t id, - const uint8_t *data, size_t size) { - if (id == SC_HID_ID_KEYBOARD) { - if (devices->keyboard) { - sc_keyboard_uhid_process_hid_output(devices->keyboard, data, size); - } else { - LOGW("Unexpected keyboard HID output without UHID keyboard"); - } - } else { - LOGW("HID output ignored for id %" PRIu16, id); - } -} diff --git a/app/src/uhid/uhid_output.h b/app/src/uhid/uhid_output.h deleted file mode 100644 index ed028b58..00000000 --- a/app/src/uhid/uhid_output.h +++ /dev/null @@ -1,27 +0,0 @@ -#ifndef SC_UHID_OUTPUT_H -#define SC_UHID_OUTPUT_H - -#include "common.h" - -#include -#include - -/** - * The communication with UHID devices is bidirectional. - * - * This component dispatches HID outputs to the expected processor. - */ - -struct sc_uhid_devices { - struct sc_keyboard_uhid *keyboard; -}; - -void -sc_uhid_devices_init(struct sc_uhid_devices *devices, - struct sc_keyboard_uhid *keyboard); - -void -sc_uhid_devices_process_hid_output(struct sc_uhid_devices *devices, uint16_t id, - const uint8_t *data, size_t size); - -#endif diff --git a/app/src/usb/aoa_hid.c b/app/src/usb/aoa_hid.c deleted file mode 100644 index 8cb62bfd..00000000 --- a/app/src/usb/aoa_hid.c +++ /dev/null @@ -1,475 +0,0 @@ -#include "aoa_hid.h" - -#include -#include -#include -#include -#include -#include - -#include "events.h" -#include "util/log.h" -#include "util/str.h" -#include "util/tick.h" -#include "util/vector.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 - -// Drop droppable events above this limit -#define SC_AOA_EVENT_QUEUE_LIMIT 60 - -struct sc_vec_hid_ids SC_VECTOR(uint16_t); - -static void -sc_hid_input_log(const struct sc_hid_input *hid_input) { - // HID input: [00] FF FF FF FF... - assert(hid_input->size); - char *hex = sc_str_to_hex_string(hid_input->data, hid_input->size); - if (!hex) { - return; - } - LOGV("HID input: [%" PRIu16 "] %s", hid_input->hid_id, hex); - free(hex); -} - -static void -sc_hid_open_log(const struct sc_hid_open *hid_open) { - // HID open: [00] FF FF FF FF... - assert(hid_open->report_desc_size); - char *hex = sc_str_to_hex_string(hid_open->report_desc, - hid_open->report_desc_size); - if (!hex) { - return; - } - LOGV("HID open: [%" PRIu16 "] %s", hid_open->hid_id, hex); - free(hex); -} - -static void -sc_hid_close_log(const struct sc_hid_close *hid_close) { - // HID close: [00] - LOGV("HID close: [%" PRIu16 "]", hid_close->hid_id); -} - -bool -sc_aoa_init(struct sc_aoa *aoa, struct sc_usb *usb, - struct sc_acksync *acksync) { - sc_vecdeque_init(&aoa->queue); - - // Add 4 to support 4 non-droppable events without re-allocation - if (!sc_vecdeque_reserve(&aoa->queue, SC_AOA_EVENT_QUEUE_LIMIT + 4)) { - return false; - } - - if (!sc_mutex_init(&aoa->mutex)) { - sc_vecdeque_destroy(&aoa->queue); - return false; - } - - if (!sc_cond_init(&aoa->event_cond)) { - sc_mutex_destroy(&aoa->mutex); - sc_vecdeque_destroy(&aoa->queue); - return false; - } - - aoa->stopped = false; - aoa->acksync = acksync; - aoa->usb = usb; - - return true; -} - -void -sc_aoa_destroy(struct sc_aoa *aoa) { - sc_vecdeque_destroy(&aoa->queue); - - sc_cond_destroy(&aoa->event_cond); - sc_mutex_destroy(&aoa->mutex); -} - -static bool -sc_aoa_register_hid(struct sc_aoa *aoa, uint16_t accessory_id, - uint16_t report_desc_size) { - uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR; - uint8_t request = ACCESSORY_REGISTER_HID; - // - // value (arg0): accessory assigned ID for the HID device - // index (arg1): total length of the HID report descriptor - uint16_t value = accessory_id; - uint16_t index = report_desc_size; - unsigned char *data = NULL; - uint16_t length = 0; - int result = libusb_control_transfer(aoa->usb->handle, request_type, - request, value, index, data, length, - DEFAULT_TIMEOUT); - if (result < 0) { - LOGE("REGISTER_HID: libusb error: %s", libusb_strerror(result)); - sc_usb_check_disconnected(aoa->usb, result); - return false; - } - - return true; -} - -static bool -sc_aoa_set_hid_report_desc(struct sc_aoa *aoa, uint16_t accessory_id, - const uint8_t *report_desc, - uint16_t report_desc_size) { - uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR; - uint8_t request = ACCESSORY_SET_HID_REPORT_DESC; - /** - * If the HID descriptor is longer than the endpoint zero max packet size, - * the descriptor will be sent in multiple ACCESSORY_SET_HID_REPORT_DESC - * commands. The data for the descriptor must be sent sequentially - * if multiple packets are needed. - * - * - * libusb handles packet abstraction internally, so we don't need to care - * about bMaxPacketSize0 here. - * - * See - */ - // value (arg0): accessory assigned ID for the HID device - // index (arg1): offset of data in descriptor - uint16_t value = accessory_id; - uint16_t index = 0; - // libusb_control_transfer expects a pointer to non-const - unsigned char *data = (unsigned char *) report_desc; - uint16_t length = report_desc_size; - int result = libusb_control_transfer(aoa->usb->handle, request_type, - request, value, index, data, length, - DEFAULT_TIMEOUT); - if (result < 0) { - LOGE("SET_HID_REPORT_DESC: libusb error: %s", libusb_strerror(result)); - sc_usb_check_disconnected(aoa->usb, result); - return false; - } - - return true; -} - -static bool -sc_aoa_send_hid_event(struct sc_aoa *aoa, - const struct sc_hid_input *hid_input) { - 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 = hid_input->hid_id; - uint16_t index = 0; - unsigned char *data = (uint8_t *) hid_input->data; // discard const - uint16_t length = hid_input->size; - int result = libusb_control_transfer(aoa->usb->handle, request_type, - request, value, index, data, length, - DEFAULT_TIMEOUT); - if (result < 0) { - LOGE("SEND_HID_EVENT: libusb error: %s", libusb_strerror(result)); - sc_usb_check_disconnected(aoa->usb, result); - return false; - } - - return true; -} - -static bool -sc_aoa_unregister_hid(struct sc_aoa *aoa, uint16_t accessory_id) { - uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR; - uint8_t request = ACCESSORY_UNREGISTER_HID; - // - // value (arg0): accessory assigned ID for the HID device - // index (arg1): 0 - uint16_t value = accessory_id; - uint16_t index = 0; - unsigned char *data = NULL; - uint16_t length = 0; - int result = libusb_control_transfer(aoa->usb->handle, request_type, - request, value, index, data, length, - DEFAULT_TIMEOUT); - if (result < 0) { - LOGE("UNREGISTER_HID: libusb error: %s", libusb_strerror(result)); - sc_usb_check_disconnected(aoa->usb, result); - return false; - } - - return true; -} - -static bool -sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id, - const uint8_t *report_desc, uint16_t report_desc_size) { - bool ok = sc_aoa_register_hid(aoa, accessory_id, report_desc_size); - if (!ok) { - return false; - } - - ok = sc_aoa_set_hid_report_desc(aoa, accessory_id, report_desc, - report_desc_size); - if (!ok) { - if (!sc_aoa_unregister_hid(aoa, accessory_id)) { - LOGW("Could not unregister HID"); - } - return false; - } - - return true; -} - -bool -sc_aoa_push_input_with_ack_to_wait(struct sc_aoa *aoa, - const struct sc_hid_input *hid_input, - uint64_t ack_to_wait) { - if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) { - sc_hid_input_log(hid_input); - } - - sc_mutex_lock(&aoa->mutex); - - bool pushed = false; - - size_t size = sc_vecdeque_size(&aoa->queue); - if (size < SC_AOA_EVENT_QUEUE_LIMIT) { - bool was_empty = sc_vecdeque_is_empty(&aoa->queue); - - struct sc_aoa_event *aoa_event = - sc_vecdeque_push_hole_noresize(&aoa->queue); - aoa_event->type = SC_AOA_EVENT_TYPE_INPUT; - aoa_event->input.hid = *hid_input; - aoa_event->input.ack_to_wait = ack_to_wait; - pushed = true; - - if (was_empty) { - sc_cond_signal(&aoa->event_cond); - } - } - // Otherwise, the event is discarded - - sc_mutex_unlock(&aoa->mutex); - - return pushed; -} - -bool -sc_aoa_push_open(struct sc_aoa *aoa, const struct sc_hid_open *hid_open, - bool exit_on_open_error) { - if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) { - sc_hid_open_log(hid_open); - } - - sc_mutex_lock(&aoa->mutex); - bool was_empty = sc_vecdeque_is_empty(&aoa->queue); - - // an OPEN event is non-droppable, so push it to the queue even above the - // SC_AOA_EVENT_QUEUE_LIMIT - struct sc_aoa_event *aoa_event = sc_vecdeque_push_hole(&aoa->queue); - if (!aoa_event) { - LOG_OOM(); - sc_mutex_unlock(&aoa->mutex); - return false; - } - - aoa_event->type = SC_AOA_EVENT_TYPE_OPEN; - aoa_event->open.hid = *hid_open; - aoa_event->open.exit_on_error = exit_on_open_error; - - if (was_empty) { - sc_cond_signal(&aoa->event_cond); - } - - sc_mutex_unlock(&aoa->mutex); - - return true; -} - -bool -sc_aoa_push_close(struct sc_aoa *aoa, const struct sc_hid_close *hid_close) { - if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) { - sc_hid_close_log(hid_close); - } - - sc_mutex_lock(&aoa->mutex); - bool was_empty = sc_vecdeque_is_empty(&aoa->queue); - - // a CLOSE event is non-droppable, so push it to the queue even above the - // SC_AOA_EVENT_QUEUE_LIMIT - struct sc_aoa_event *aoa_event = sc_vecdeque_push_hole(&aoa->queue); - if (!aoa_event) { - LOG_OOM(); - sc_mutex_unlock(&aoa->mutex); - return false; - } - - aoa_event->type = SC_AOA_EVENT_TYPE_CLOSE; - aoa_event->close.hid = *hid_close; - - if (was_empty) { - sc_cond_signal(&aoa->event_cond); - } - - sc_mutex_unlock(&aoa->mutex); - - return true; -} - -static bool -sc_aoa_process_event(struct sc_aoa *aoa, struct sc_aoa_event *event, - struct sc_vec_hid_ids *vec_open) { - switch (event->type) { - case SC_AOA_EVENT_TYPE_INPUT: { - uint64_t ack_to_wait = event->input.ack_to_wait; - if (ack_to_wait != SC_SEQUENCE_INVALID) { - LOGD("Waiting ack from server sequence=%" PRIu64_, ack_to_wait); - - // If some events have ack_to_wait set, then sc_aoa must have - // been initialized with a non NULL acksync - assert(aoa->acksync); - - // Do not block the loop indefinitely if the ack never comes (it - // should never happen) - sc_tick deadline = sc_tick_now() + SC_TICK_FROM_MS(500); - enum sc_acksync_wait_result result = - sc_acksync_wait(aoa->acksync, ack_to_wait, deadline); - - if (result == SC_ACKSYNC_WAIT_TIMEOUT) { - LOGW("Ack not received after 500ms, discarding HID event"); - // continue to process events - return true; - } else if (result == SC_ACKSYNC_WAIT_INTR) { - // stopped - return false; - } - } - - struct sc_hid_input *hid_input = &event->input.hid; - bool ok = sc_aoa_send_hid_event(aoa, hid_input); - if (!ok) { - LOGW("Could not send HID event to USB device: %" PRIu16, - hid_input->hid_id); - } - - break; - } - case SC_AOA_EVENT_TYPE_OPEN: { - struct sc_hid_open *hid_open = &event->open.hid; - bool ok = sc_aoa_setup_hid(aoa, hid_open->hid_id, - hid_open->report_desc, - hid_open->report_desc_size); - if (ok) { - // The device is now open, add it to the list of devices to - // close automatically on exit - bool pushed = sc_vector_push(vec_open, hid_open->hid_id); - if (!pushed) { - LOG_OOM(); - // this is not fatal, the HID device will just not be - // explicitly unregistered - } - } else { - LOGW("Could not open AOA device: %" PRIu16, hid_open->hid_id); - if (event->open.exit_on_error) { - // Notify the error to the main thread, which will exit - sc_push_event(SC_EVENT_AOA_OPEN_ERROR); - } - } - - break; - } - case SC_AOA_EVENT_TYPE_CLOSE: { - struct sc_hid_close *hid_close = &event->close.hid; - bool ok = sc_aoa_unregister_hid(aoa, hid_close->hid_id); - if (ok) { - // The device is not open anymore, remove it from the list of - // devices to close automatically on exit - ssize_t idx = sc_vector_index_of(vec_open, hid_close->hid_id); - if (idx >= 0) { - sc_vector_remove(vec_open, idx); - } - } else { - LOGW("Could not close AOA device: %" PRIu16, hid_close->hid_id); - } - - break; - } - } - - // continue to process events - return true; -} - -static int -run_aoa_thread(void *data) { - struct sc_aoa *aoa = data; - - // Store the HID ids of opened devices to unregister them all before exiting - struct sc_vec_hid_ids vec_open = SC_VECTOR_INITIALIZER; - - for (;;) { - sc_mutex_lock(&aoa->mutex); - while (!aoa->stopped && sc_vecdeque_is_empty(&aoa->queue)) { - sc_cond_wait(&aoa->event_cond, &aoa->mutex); - } - if (aoa->stopped) { - // Stop immediately, do not process further events - sc_mutex_unlock(&aoa->mutex); - break; - } - - assert(!sc_vecdeque_is_empty(&aoa->queue)); - struct sc_aoa_event event = sc_vecdeque_pop(&aoa->queue); - sc_mutex_unlock(&aoa->mutex); - - bool cont = sc_aoa_process_event(aoa, &event, &vec_open); - if (!cont) { - // stopped - break; - } - } - - // Explicitly unregister all registered HID ids before exiting - for (size_t i = 0; i < vec_open.size; ++i) { - uint16_t hid_id = vec_open.data[i]; - LOGD("Unregistering AOA device %" PRIu16 "...", hid_id); - bool ok = sc_aoa_unregister_hid(aoa, hid_id); - if (!ok) { - LOGW("Could not close AOA device: %" PRIu16, hid_id); - } - } - sc_vector_destroy(&vec_open); - - return 0; -} - -bool -sc_aoa_start(struct sc_aoa *aoa) { - LOGD("Starting AOA thread"); - - bool ok = sc_thread_create(&aoa->thread, run_aoa_thread, "scrcpy-aoa", aoa); - if (!ok) { - LOGE("Could not start AOA thread"); - return false; - } - - return true; -} - -void -sc_aoa_stop(struct sc_aoa *aoa) { - sc_mutex_lock(&aoa->mutex); - aoa->stopped = true; - sc_cond_signal(&aoa->event_cond); - sc_mutex_unlock(&aoa->mutex); - - if (aoa->acksync) { - sc_acksync_interrupt(aoa->acksync); - } -} - -void -sc_aoa_join(struct sc_aoa *aoa) { - sc_thread_join(&aoa->thread, NULL); -} diff --git a/app/src/usb/aoa_hid.h b/app/src/usb/aoa_hid.h deleted file mode 100644 index 2755c957..00000000 --- a/app/src/usb/aoa_hid.h +++ /dev/null @@ -1,93 +0,0 @@ -#ifndef SC_AOA_HID_H -#define SC_AOA_HID_H - -#include "common.h" - -#include -#include - -#include "hid/hid_event.h" -#include "usb/usb.h" -#include "util/acksync.h" -#include "util/thread.h" -#include "util/vecdeque.h" - -enum sc_aoa_event_type { - SC_AOA_EVENT_TYPE_OPEN, - SC_AOA_EVENT_TYPE_INPUT, - SC_AOA_EVENT_TYPE_CLOSE, -}; - -struct sc_aoa_event { - enum sc_aoa_event_type type; - union { - struct { - struct sc_hid_open hid; - bool exit_on_error; - } open; - struct { - struct sc_hid_close hid; - } close; - struct { - struct sc_hid_input hid; - uint64_t ack_to_wait; - } input; - }; -}; - -struct sc_aoa_event_queue SC_VECDEQUE(struct sc_aoa_event); - -struct sc_aoa { - struct sc_usb *usb; - sc_thread thread; - sc_mutex mutex; - sc_cond event_cond; - bool stopped; - struct sc_aoa_event_queue queue; - - struct sc_acksync *acksync; -}; - -bool -sc_aoa_init(struct sc_aoa *aoa, struct sc_usb *usb, struct sc_acksync *acksync); - -void -sc_aoa_destroy(struct sc_aoa *aoa); - -bool -sc_aoa_start(struct sc_aoa *aoa); - -void -sc_aoa_stop(struct sc_aoa *aoa); - -void -sc_aoa_join(struct sc_aoa *aoa); - -//bool -//sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id, -// const uint8_t *report_desc, uint16_t report_desc_size); -// -//bool -//sc_aoa_unregister_hid(struct sc_aoa *aoa, uint16_t accessory_id); - -// report_desc must be a pointer to static memory, accessed at any time from -// another thread -bool -sc_aoa_push_open(struct sc_aoa *aoa, const struct sc_hid_open *hid_open, - bool exit_on_open_error); - -bool -sc_aoa_push_close(struct sc_aoa *aoa, const struct sc_hid_close *hid_close); - -bool -sc_aoa_push_input_with_ack_to_wait(struct sc_aoa *aoa, - const struct sc_hid_input *hid_input, - uint64_t ack_to_wait); - -static inline bool -sc_aoa_push_input(struct sc_aoa *aoa, const struct sc_hid_input *hid_input) { - return sc_aoa_push_input_with_ack_to_wait(aoa, hid_input, - SC_SEQUENCE_INVALID); -} - -#endif diff --git a/app/src/usb/gamepad_aoa.c b/app/src/usb/gamepad_aoa.c deleted file mode 100644 index d29b1a78..00000000 --- a/app/src/usb/gamepad_aoa.c +++ /dev/null @@ -1,96 +0,0 @@ -#include "gamepad_aoa.h" - -#include - -#include "input_events.h" -#include "util/log.h" - -/** Downcast gamepad processor to gamepad_aoa */ -#define DOWNCAST(GP) container_of(GP, struct sc_gamepad_aoa, gamepad_processor) - -static void -sc_gamepad_processor_process_gamepad_added(struct sc_gamepad_processor *gp, - const struct sc_gamepad_device_event *event) { - struct sc_gamepad_aoa *gamepad = DOWNCAST(gp); - - struct sc_hid_open hid_open; - if (!sc_hid_gamepad_generate_open(&gamepad->hid, &hid_open, - event->gamepad_id)) { - return; - } - - // exit_on_error: false (a gamepad open failure should not exit scrcpy) - if (!sc_aoa_push_open(gamepad->aoa, &hid_open, false)) { - LOGW("Could not push AOA HID open (gamepad)"); - } -} - -static void -sc_gamepad_processor_process_gamepad_removed(struct sc_gamepad_processor *gp, - const struct sc_gamepad_device_event *event) { - struct sc_gamepad_aoa *gamepad = DOWNCAST(gp); - - struct sc_hid_close hid_close; - if (!sc_hid_gamepad_generate_close(&gamepad->hid, &hid_close, - event->gamepad_id)) { - return; - } - - if (!sc_aoa_push_close(gamepad->aoa, &hid_close)) { - LOGW("Could not push AOA HID close (gamepad)"); - } -} - -static void -sc_gamepad_processor_process_gamepad_axis(struct sc_gamepad_processor *gp, - const struct sc_gamepad_axis_event *event) { - struct sc_gamepad_aoa *gamepad = DOWNCAST(gp); - - struct sc_hid_input hid_input; - if (!sc_hid_gamepad_generate_input_from_axis(&gamepad->hid, &hid_input, - event)) { - return; - } - - if (!sc_aoa_push_input(gamepad->aoa, &hid_input)) { - LOGW("Could not push AOA HID input (gamepad axis)"); - } -} - -static void -sc_gamepad_processor_process_gamepad_button(struct sc_gamepad_processor *gp, - const struct sc_gamepad_button_event *event) { - struct sc_gamepad_aoa *gamepad = DOWNCAST(gp); - - struct sc_hid_input hid_input; - if (!sc_hid_gamepad_generate_input_from_button(&gamepad->hid, &hid_input, - event)) { - return; - } - - if (!sc_aoa_push_input(gamepad->aoa, &hid_input)) { - LOGW("Could not push AOA HID input (gamepad button)"); - } -} - -void -sc_gamepad_aoa_init(struct sc_gamepad_aoa *gamepad, struct sc_aoa *aoa) { - gamepad->aoa = aoa; - - sc_hid_gamepad_init(&gamepad->hid); - - static const struct sc_gamepad_processor_ops ops = { - .process_gamepad_added = sc_gamepad_processor_process_gamepad_added, - .process_gamepad_removed = sc_gamepad_processor_process_gamepad_removed, - .process_gamepad_axis = sc_gamepad_processor_process_gamepad_axis, - .process_gamepad_button = sc_gamepad_processor_process_gamepad_button, - }; - - gamepad->gamepad_processor.ops = &ops; -} - -void -sc_gamepad_aoa_destroy(struct sc_gamepad_aoa *gamepad) { - (void) gamepad; - // Do nothing, gamepad->aoa will automatically unregister all devices -} diff --git a/app/src/usb/gamepad_aoa.h b/app/src/usb/gamepad_aoa.h deleted file mode 100644 index 0297a365..00000000 --- a/app/src/usb/gamepad_aoa.h +++ /dev/null @@ -1,23 +0,0 @@ -#ifndef SC_GAMEPAD_AOA_H -#define SC_GAMEPAD_AOA_H - -#include "common.h" - -#include "hid/hid_gamepad.h" -#include "usb/aoa_hid.h" -#include "trait/gamepad_processor.h" - -struct sc_gamepad_aoa { - struct sc_gamepad_processor gamepad_processor; // gamepad processor trait - - struct sc_hid_gamepad hid; - struct sc_aoa *aoa; -}; - -void -sc_gamepad_aoa_init(struct sc_gamepad_aoa *gamepad, struct sc_aoa *aoa); - -void -sc_gamepad_aoa_destroy(struct sc_gamepad_aoa *gamepad); - -#endif diff --git a/app/src/usb/keyboard_aoa.c b/app/src/usb/keyboard_aoa.c deleted file mode 100644 index 8f5cb755..00000000 --- a/app/src/usb/keyboard_aoa.c +++ /dev/null @@ -1,103 +0,0 @@ -#include "keyboard_aoa.h" - -#include - -#include "input_events.h" -#include "util/log.h" - -/** Downcast key processor to keyboard_aoa */ -#define DOWNCAST(KP) container_of(KP, struct sc_keyboard_aoa, key_processor) - -static bool -push_mod_lock_state(struct sc_keyboard_aoa *kb, uint16_t mods_state) { - struct sc_hid_input hid_input; - if (!sc_hid_keyboard_generate_input_from_mods(&hid_input, mods_state)) { - // Nothing to do - return true; - } - - if (!sc_aoa_push_input(kb->aoa, &hid_input)) { - LOGW("Could not push AOA HID input (mod lock state)"); - return false; - } - - LOGD("HID keyboard state synchronized"); - - return true; -} - -static void -sc_key_processor_process_key(struct sc_key_processor *kp, - const struct sc_key_event *event, - uint64_t ack_to_wait) { - if (event->repeat) { - // In USB HID protocol, key repeat is handled by the host (Android), so - // just ignore key repeat here. - return; - } - - struct sc_keyboard_aoa *kb = DOWNCAST(kp); - - struct sc_hid_input hid_input; - - // Not all keys are supported, just ignore unsupported keys - if (sc_hid_keyboard_generate_input_from_key(&kb->hid, &hid_input, event)) { - if (!kb->mod_lock_synchronized) { - // Inject CAPSLOCK and/or NUMLOCK if necessary to synchronize - // keyboard state - if (push_mod_lock_state(kb, event->mods_state)) { - kb->mod_lock_synchronized = true; - } - } - - // If ack_to_wait is != SC_SEQUENCE_INVALID, then Ctrl+v is pressed, so - // clipboard synchronization has been requested. Wait until clipboard - // synchronization is acknowledged by the server, otherwise it could - // paste the old clipboard content. - - if (!sc_aoa_push_input_with_ack_to_wait(kb->aoa, &hid_input, - ack_to_wait)) { - LOGW("Could not push AOA HID input (key)"); - } - } -} - -bool -sc_keyboard_aoa_init(struct sc_keyboard_aoa *kb, struct sc_aoa *aoa) { - kb->aoa = aoa; - - struct sc_hid_open hid_open; - sc_hid_keyboard_generate_open(&hid_open); - - bool ok = sc_aoa_push_open(aoa, &hid_open, true); - if (!ok) { - LOGW("Could not push AOA HID open (keyboard)"); - return false; - } - - sc_hid_keyboard_init(&kb->hid); - - kb->mod_lock_synchronized = false; - - static const struct sc_key_processor_ops ops = { - .process_key = sc_key_processor_process_key, - // Never forward text input via HID (all the keys are injected - // separately) - .process_text = NULL, - }; - - // Clipboard synchronization is requested over the control socket, while HID - // events are sent over AOA, so it must wait for clipboard synchronization - // to be acknowledged by the device before injecting Ctrl+v. - kb->key_processor.async_paste = true; - kb->key_processor.hid = true; - kb->key_processor.ops = &ops; - - return true; -} - -void -sc_keyboard_aoa_destroy(struct sc_keyboard_aoa *kb) { - (void) kb; - // Do nothing, kb->aoa will automatically unregister all devices -} diff --git a/app/src/usb/keyboard_aoa.h b/app/src/usb/keyboard_aoa.h deleted file mode 100644 index 9e9500a3..00000000 --- a/app/src/usb/keyboard_aoa.h +++ /dev/null @@ -1,27 +0,0 @@ -#ifndef SC_KEYBOARD_AOA_H -#define SC_KEYBOARD_AOA_H - -#include "common.h" - -#include - -#include "hid/hid_keyboard.h" -#include "usb/aoa_hid.h" -#include "trait/key_processor.h" - -struct sc_keyboard_aoa { - struct sc_key_processor key_processor; // key processor trait - - struct sc_hid_keyboard hid; - struct sc_aoa *aoa; - - bool mod_lock_synchronized; -}; - -bool -sc_keyboard_aoa_init(struct sc_keyboard_aoa *kb, struct sc_aoa *aoa); - -void -sc_keyboard_aoa_destroy(struct sc_keyboard_aoa *kb); - -#endif diff --git a/app/src/usb/mouse_aoa.c b/app/src/usb/mouse_aoa.c deleted file mode 100644 index fd5fa5e0..00000000 --- a/app/src/usb/mouse_aoa.c +++ /dev/null @@ -1,86 +0,0 @@ -#include "mouse_aoa.h" - -#include -#include - -#include "hid/hid_mouse.h" -#include "input_events.h" -#include "util/log.h" - -/** Downcast mouse processor to mouse_aoa */ -#define DOWNCAST(MP) container_of(MP, struct sc_mouse_aoa, mouse_processor) - -static void -sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, - const struct sc_mouse_motion_event *event) { - struct sc_mouse_aoa *mouse = DOWNCAST(mp); - - struct sc_hid_input hid_input; - sc_hid_mouse_generate_input_from_motion(&hid_input, event); - - if (!sc_aoa_push_input(mouse->aoa, &hid_input)) { - LOGW("Could not push AOA HID input (mouse motion)"); - } -} - -static void -sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, - const struct sc_mouse_click_event *event) { - struct sc_mouse_aoa *mouse = DOWNCAST(mp); - - struct sc_hid_input hid_input; - sc_hid_mouse_generate_input_from_click(&hid_input, event); - - if (!sc_aoa_push_input(mouse->aoa, &hid_input)) { - LOGW("Could not push AOA HID input (mouse click)"); - } -} - -static void -sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, - const struct sc_mouse_scroll_event *event) { - struct sc_mouse_aoa *mouse = DOWNCAST(mp); - - struct sc_hid_input hid_input; - if (!sc_hid_mouse_generate_input_from_scroll(&hid_input, event)) { - return; - } - - if (!sc_aoa_push_input(mouse->aoa, &hid_input)) { - LOGW("Could not push AOA HID input (mouse scroll)"); - } -} - -bool -sc_mouse_aoa_init(struct sc_mouse_aoa *mouse, struct sc_aoa *aoa) { - mouse->aoa = aoa; - - struct sc_hid_open hid_open; - sc_hid_mouse_generate_open(&hid_open); - - bool ok = sc_aoa_push_open(aoa, &hid_open, true); - if (!ok) { - LOGW("Could not push AOA HID open (mouse)"); - return false; - } - - static const struct sc_mouse_processor_ops ops = { - .process_mouse_motion = sc_mouse_processor_process_mouse_motion, - .process_mouse_click = sc_mouse_processor_process_mouse_click, - .process_mouse_scroll = sc_mouse_processor_process_mouse_scroll, - // Touch events not supported (coordinates are not relative) - .process_touch = NULL, - }; - - mouse->mouse_processor.ops = &ops; - - mouse->mouse_processor.relative_mode = true; - - return true; -} - -void -sc_mouse_aoa_destroy(struct sc_mouse_aoa *mouse) { - (void) mouse; - // Do nothing, mouse->aoa will automatically unregister all devices -} diff --git a/app/src/usb/mouse_aoa.h b/app/src/usb/mouse_aoa.h deleted file mode 100644 index 506286ba..00000000 --- a/app/src/usb/mouse_aoa.h +++ /dev/null @@ -1,23 +0,0 @@ -#ifndef SC_MOUSE_AOA_H -#define SC_MOUSE_AOA_H - -#include "common.h" - -#include - -#include "usb/aoa_hid.h" -#include "trait/mouse_processor.h" - -struct sc_mouse_aoa { - struct sc_mouse_processor mouse_processor; // mouse processor trait - - struct sc_aoa *aoa; -}; - -bool -sc_mouse_aoa_init(struct sc_mouse_aoa *mouse, struct sc_aoa *aoa); - -void -sc_mouse_aoa_destroy(struct sc_mouse_aoa *mouse); - -#endif diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c deleted file mode 100644 index 1a9cc46e..00000000 --- a/app/src/usb/scrcpy_otg.c +++ /dev/null @@ -1,251 +0,0 @@ -#include "scrcpy_otg.h" - -#include -#include -#include -#include - -#ifdef _WIN32 -# include "adb/adb.h" -#endif -#include "events.h" -#include "usb/screen_otg.h" -#include "usb/aoa_hid.h" -#include "usb/gamepad_aoa.h" -#include "usb/keyboard_aoa.h" -#include "usb/mouse_aoa.h" -#include "util/log.h" - -struct scrcpy_otg { - struct sc_usb usb; - struct sc_aoa aoa; - struct sc_keyboard_aoa keyboard; - struct sc_mouse_aoa mouse; - struct sc_gamepad_aoa gamepad; - - struct sc_screen_otg screen_otg; -}; - -static void -sc_usb_on_disconnected(struct sc_usb *usb, void *userdata) { - (void) usb; - (void) userdata; - - sc_push_event(SC_EVENT_USB_DEVICE_DISCONNECTED); -} - -static enum scrcpy_exit_code -event_loop(struct scrcpy_otg *s) { - SDL_Event event; - while (SDL_WaitEvent(&event)) { - switch (event.type) { - case SC_EVENT_USB_DEVICE_DISCONNECTED: - LOGW("Device disconnected"); - return SCRCPY_EXIT_DISCONNECTED; - case SC_EVENT_AOA_OPEN_ERROR: - LOGE("AOA open error"); - return SCRCPY_EXIT_FAILURE; - 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"); - } - - if (!SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1")) { - LOGW("Could not allow joystick background events"); - } - - // Minimal SDL initialization - if (SDL_Init(SDL_INIT_EVENTS)) { - LOGE("Could not initialize SDL: %s", SDL_GetError()); - return SCRCPY_EXIT_FAILURE; - } - - if (options->gamepad_input_mode != SC_GAMEPAD_INPUT_MODE_DISABLED) { - if (SDL_Init(SDL_INIT_GAMECONTROLLER)) { - LOGE("Could not initialize SDL controller: %s", SDL_GetError()); - // Not fatal, keyboard/mouse should still work - } - } - - atexit(SDL_Quit); - - if (!SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1")) { - LOGW("Could not enable mouse focus clickthrough"); - } - - enum scrcpy_exit_code ret = SCRCPY_EXIT_FAILURE; - - struct sc_keyboard_aoa *keyboard = NULL; - struct sc_mouse_aoa *mouse = NULL; - struct sc_gamepad_aoa *gamepad = NULL; - bool usb_device_initialized = false; - bool usb_connected = false; - bool aoa_started = false; - bool aoa_initialized = false; - -#ifdef _WIN32 - // On Windows, only one process could open a USB device - // - LOGI("Killing adb server (if any)..."); - if (sc_adb_init()) { - 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); - sc_adb_destroy(); - } else { - LOGW("Could not call adb executable, adb server not killed"); - } -#endif - - static const struct sc_usb_callbacks cbs = { - .on_disconnected = sc_usb_on_disconnected, - }; - bool ok = sc_usb_init(&s->usb); - if (!ok) { - return SCRCPY_EXIT_FAILURE; - } - - struct sc_usb_device usb_device; - ok = sc_usb_select_device(&s->usb, serial, &usb_device); - if (!ok) { - goto end; - } - - usb_device_initialized = true; - - ok = sc_usb_connect(&s->usb, usb_device.device, &cbs, NULL); - if (!ok) { - goto end; - } - usb_connected = true; - - ok = sc_aoa_init(&s->aoa, &s->usb, NULL); - if (!ok) { - goto end; - } - aoa_initialized = true; - - assert(options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AOA - || options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_DISABLED); - assert(options->mouse_input_mode == SC_MOUSE_INPUT_MODE_AOA - || options->mouse_input_mode == SC_MOUSE_INPUT_MODE_DISABLED); - assert(options->gamepad_input_mode == SC_GAMEPAD_INPUT_MODE_AOA - || options->gamepad_input_mode == SC_GAMEPAD_INPUT_MODE_DISABLED); - - bool enable_keyboard = - options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AOA; - bool enable_mouse = - options->mouse_input_mode == SC_MOUSE_INPUT_MODE_AOA; - bool enable_gamepad = - options->gamepad_input_mode == SC_GAMEPAD_INPUT_MODE_AOA; - - if (enable_keyboard) { - ok = sc_keyboard_aoa_init(&s->keyboard, &s->aoa); - if (!ok) { - goto end; - } - keyboard = &s->keyboard; - } - - if (enable_mouse) { - ok = sc_mouse_aoa_init(&s->mouse, &s->aoa); - if (!ok) { - goto end; - } - mouse = &s->mouse; - } - - if (enable_gamepad) { - sc_gamepad_aoa_init(&s->gamepad, &s->aoa); - gamepad = &s->gamepad; - } - - 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, - .gamepad = gamepad, - .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, - .shortcut_mods = options->shortcut_mods, - }; - - ok = sc_screen_otg_init(&s->screen_otg, ¶ms); - if (!ok) { - goto end; - } - - // usb_device not needed anymore - sc_usb_device_destroy(&usb_device); - usb_device_initialized = false; - - ret = event_loop(s); - LOGD("quit..."); - -end: - if (aoa_started) { - sc_aoa_stop(&s->aoa); - } - sc_usb_stop(&s->usb); - - if (mouse) { - sc_mouse_aoa_destroy(&s->mouse); - } - if (keyboard) { - sc_keyboard_aoa_destroy(&s->keyboard); - } - if (gamepad) { - sc_gamepad_aoa_destroy(&s->gamepad); - } - - if (aoa_initialized) { - sc_aoa_join(&s->aoa); - sc_aoa_destroy(&s->aoa); - } - - sc_usb_join(&s->usb); - - if (usb_connected) { - sc_usb_disconnect(&s->usb); - } - - if (usb_device_initialized) { - sc_usb_device_destroy(&usb_device); - } - - sc_usb_destroy(&s->usb); - - return ret; -} diff --git a/app/src/usb/scrcpy_otg.h b/app/src/usb/scrcpy_otg.h deleted file mode 100644 index e477660b..00000000 --- a/app/src/usb/scrcpy_otg.h +++ /dev/null @@ -1,12 +0,0 @@ -#ifndef SCRCPY_OTG_H -#define SCRCPY_OTG_H - -#include "common.h" - -#include "options.h" -#include "scrcpy.h" - -enum scrcpy_exit_code -scrcpy_otg(struct scrcpy_options *options); - -#endif diff --git a/app/src/usb/screen_otg.c b/app/src/usb/screen_otg.c deleted file mode 100644 index 5c580df9..00000000 --- a/app/src/usb/screen_otg.c +++ /dev/null @@ -1,326 +0,0 @@ -#include "screen_otg.h" - -#include -#include - -#include "icon.h" -#include "options.h" -#include "util/acksync.h" -#include "util/log.h" - -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->gamepad = params->gamepad; - - 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"); - } - - sc_mouse_capture_init(&screen->mc, screen->window, params->shortcut_mods); - - if (screen->mouse) { - // Capture mouse on start - sc_mouse_capture_set_active(&screen->mc, 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 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), - }; - - 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), - }; - - 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 -#if SDL_VERSION_ATLEAST(2, 0, 18) - .hscroll = event->preciseX, - .vscroll = event->preciseY, -#else - .hscroll = event->x, - .vscroll = event->y, -#endif - .hscroll_int = event->x, - .vscroll_int = event->y, - .buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state), - }; - - assert(mp->ops->process_mouse_scroll); - mp->ops->process_mouse_scroll(mp, &evt); -} - -static void -sc_screen_otg_process_gamepad_device(struct sc_screen_otg *screen, - const SDL_ControllerDeviceEvent *event) { - assert(screen->gamepad); - struct sc_gamepad_processor *gp = &screen->gamepad->gamepad_processor; - - if (event->type == SDL_CONTROLLERDEVICEADDED) { - SDL_GameController *gc = SDL_GameControllerOpen(event->which); - if (!gc) { - LOGW("Could not open game controller"); - return; - } - - SDL_Joystick *joystick = SDL_GameControllerGetJoystick(gc); - if (!joystick) { - LOGW("Could not get controller joystick"); - SDL_GameControllerClose(gc); - return; - } - - struct sc_gamepad_device_event evt = { - .gamepad_id = SDL_JoystickInstanceID(joystick), - }; - gp->ops->process_gamepad_added(gp, &evt); - } else if (event->type == SDL_CONTROLLERDEVICEREMOVED) { - SDL_JoystickID id = event->which; - - SDL_GameController *gc = SDL_GameControllerFromInstanceID(id); - if (gc) { - SDL_GameControllerClose(gc); - } else { - LOGW("Unknown gamepad device removed"); - } - - struct sc_gamepad_device_event evt = { - .gamepad_id = id, - }; - gp->ops->process_gamepad_removed(gp, &evt); - } -} - -static void -sc_screen_otg_process_gamepad_axis(struct sc_screen_otg *screen, - const SDL_ControllerAxisEvent *event) { - assert(screen->gamepad); - struct sc_gamepad_processor *gp = &screen->gamepad->gamepad_processor; - - enum sc_gamepad_axis axis = sc_gamepad_axis_from_sdl(event->axis); - if (axis == SC_GAMEPAD_AXIS_UNKNOWN) { - return; - } - - struct sc_gamepad_axis_event evt = { - .gamepad_id = event->which, - .axis = axis, - .value = event->value, - }; - gp->ops->process_gamepad_axis(gp, &evt); -} - -static void -sc_screen_otg_process_gamepad_button(struct sc_screen_otg *screen, - const SDL_ControllerButtonEvent *event) { - assert(screen->gamepad); - struct sc_gamepad_processor *gp = &screen->gamepad->gamepad_processor; - - enum sc_gamepad_button button = sc_gamepad_button_from_sdl(event->button); - if (button == SC_GAMEPAD_BUTTON_UNKNOWN) { - return; - } - - struct sc_gamepad_button_event evt = { - .gamepad_id = event->which, - .action = sc_action_from_sdl_controllerbutton_type(event->type), - .button = button, - }; - gp->ops->process_gamepad_button(gp, &evt); -} - -void -sc_screen_otg_handle_event(struct sc_screen_otg *screen, SDL_Event *event) { - if (sc_mouse_capture_handle_event(&screen->mc, event)) { - // The mouse capture handler consumed the event - return; - } - - switch (event->type) { - case SDL_WINDOWEVENT: - switch (event->window.event) { - case SDL_WINDOWEVENT_EXPOSED: - sc_screen_otg_render(screen); - break; - } - return; - case SDL_KEYDOWN: - if (screen->keyboard) { - sc_screen_otg_process_key(screen, &event->key); - } - break; - case SDL_KEYUP: - if (screen->keyboard) { - sc_screen_otg_process_key(screen, &event->key); - } - break; - case SDL_MOUSEMOTION: - if (screen->mouse) { - sc_screen_otg_process_mouse_motion(screen, &event->motion); - } - break; - case SDL_MOUSEBUTTONDOWN: - if (screen->mouse) { - sc_screen_otg_process_mouse_button(screen, &event->button); - } - break; - case SDL_MOUSEBUTTONUP: - if (screen->mouse) { - sc_screen_otg_process_mouse_button(screen, &event->button); - } - break; - case SDL_MOUSEWHEEL: - if (screen->mouse) { - sc_screen_otg_process_mouse_wheel(screen, &event->wheel); - } - break; - case SDL_CONTROLLERDEVICEADDED: - case SDL_CONTROLLERDEVICEREMOVED: - // Handle device added or removed even if paused - if (screen->gamepad) { - sc_screen_otg_process_gamepad_device(screen, &event->cdevice); - } - break; - case SDL_CONTROLLERAXISMOTION: - if (screen->gamepad) { - sc_screen_otg_process_gamepad_axis(screen, &event->caxis); - } - break; - case SDL_CONTROLLERBUTTONDOWN: - case SDL_CONTROLLERBUTTONUP: - if (screen->gamepad) { - sc_screen_otg_process_gamepad_button(screen, &event->cbutton); - } - break; - } -} diff --git a/app/src/usb/screen_otg.h b/app/src/usb/screen_otg.h deleted file mode 100644 index 08b76ae7..00000000 --- a/app/src/usb/screen_otg.h +++ /dev/null @@ -1,52 +0,0 @@ -#ifndef SC_SCREEN_OTG_H -#define SC_SCREEN_OTG_H - -#include "common.h" - -#include -#include -#include - -#include "mouse_capture.h" -#include "usb/gamepad_aoa.h" -#include "usb/keyboard_aoa.h" -#include "usb/mouse_aoa.h" - -struct sc_screen_otg { - struct sc_keyboard_aoa *keyboard; - struct sc_mouse_aoa *mouse; - struct sc_gamepad_aoa *gamepad; - - SDL_Window *window; - SDL_Renderer *renderer; - SDL_Texture *texture; - - struct sc_mouse_capture mc; -}; - -struct sc_screen_otg_params { - struct sc_keyboard_aoa *keyboard; - struct sc_mouse_aoa *mouse; - struct sc_gamepad_aoa *gamepad; - - 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; - uint8_t shortcut_mods; // OR of enum sc_shortcut_mod values -}; - -bool -sc_screen_otg_init(struct sc_screen_otg *screen, - const struct sc_screen_otg_params *params); - -void -sc_screen_otg_destroy(struct sc_screen_otg *screen); - -void -sc_screen_otg_handle_event(struct sc_screen_otg *screen, SDL_Event *event); - -#endif diff --git a/app/src/usb/usb.c b/app/src/usb/usb.c deleted file mode 100644 index 4f750581..00000000 --- a/app/src/usb/usb.c +++ /dev/null @@ -1,379 +0,0 @@ -#include "usb.h" - -#include - -#include "util/log.h" -#include "util/vector.h" - -struct sc_vec_usb_devices SC_VECTOR(struct sc_usb_device); - -static char * -read_string(libusb_device_handle *handle, uint8_t desc_index) { - char buffer[128]; - int result = - libusb_get_string_descriptor_ascii(handle, desc_index, - (unsigned char *) buffer, - sizeof(buffer)); - if (result < 0) { - LOGD("Read string: libusb error: %s", libusb_strerror(result)); - return NULL; - } - - assert((size_t) result <= sizeof(buffer)); - - // When non-negative, 'result' contains the number of bytes written - char *s = malloc(result + 1); - if (!s) { - LOG_OOM(); - return NULL; - } - - memcpy(s, buffer, result); - s[result] = '\0'; - return s; -} - -static bool -sc_usb_read_device(libusb_device *device, struct sc_usb_device *out) { - // Do not log any USB error in this function, it is expected that many USB - // devices available on the computer have permission restrictions - - struct libusb_device_descriptor desc; - int result = libusb_get_device_descriptor(device, &desc); - if (result < 0 || !desc.iSerialNumber) { - return false; - } - - libusb_device_handle *handle; - result = libusb_open(device, &handle); - if (result < 0) { - // Log at debug level because it is expected that some non-Android USB - // devices present on the computer require special permissions - LOGD("Open USB device %04x:%04x: libusb error: %s", - (unsigned) desc.idVendor, (unsigned) desc.idProduct, - libusb_strerror(result)); - return false; - } - - char *device_serial = read_string(handle, desc.iSerialNumber); - if (!device_serial) { - libusb_close(handle); - return false; - } - - out->device = libusb_ref_device(device); - out->serial = device_serial; - out->vid = desc.idVendor; - out->pid = desc.idProduct; - out->manufacturer = read_string(handle, desc.iManufacturer); - out->product = read_string(handle, desc.iProduct); - out->selected = false; - - libusb_close(handle); - - return true; -} - -void -sc_usb_device_destroy(struct sc_usb_device *usb_device) { - if (usb_device->device) { - libusb_unref_device(usb_device->device); - } - free(usb_device->serial); - free(usb_device->manufacturer); - free(usb_device->product); -} - -void -sc_usb_device_move(struct sc_usb_device *dst, struct sc_usb_device *src) { - *dst = *src; - src->device = NULL; - src->serial = NULL; - src->manufacturer = NULL; - src->product = NULL; -} - -static void -sc_usb_devices_destroy(struct sc_vec_usb_devices *usb_devices) { - for (size_t i = 0; i < usb_devices->size; ++i) { - sc_usb_device_destroy(&usb_devices->data[i]); - } - sc_vector_destroy(usb_devices); -} - -static bool -sc_usb_list_devices(struct sc_usb *usb, struct sc_vec_usb_devices *out_vec) { - libusb_device **list; - ssize_t count = libusb_get_device_list(usb->context, &list); - if (count < 0) { - LOGE("List USB devices: libusb error: %s", libusb_strerror(count)); - return false; - } - - for (size_t i = 0; i < (size_t) count; ++i) { - libusb_device *device = list[i]; - - struct sc_usb_device usb_device; - if (sc_usb_read_device(device, &usb_device)) { - bool ok = sc_vector_push(out_vec, usb_device); - if (!ok) { - LOG_OOM(); - LOGE("Could not push usb_device to vector"); - sc_usb_device_destroy(&usb_device); - // continue anyway - } - } - } - - libusb_free_device_list(list, 1); - return true; -} - -static bool -sc_usb_accept_device(const struct sc_usb_device *device, const char *serial) { - if (!serial) { - return true; - } - - return !strcmp(serial, device->serial); -} - -static size_t -sc_usb_devices_select(struct sc_usb_device *devices, size_t len, - const char *serial, size_t *idx_out) { - size_t count = 0; - for (size_t i = 0; i < len; ++i) { - struct sc_usb_device *device = &devices[i]; - device->selected = sc_usb_accept_device(device, serial); - if (device->selected) { - if (idx_out && !count) { - *idx_out = i; - } - ++count; - } - } - - return count; -} - -static void -sc_usb_devices_log(enum sc_log_level level, struct sc_usb_device *devices, - size_t count) { - for (size_t i = 0; i < count; ++i) { - struct sc_usb_device *d = &devices[i]; - const char *selection = d->selected ? "-->" : " "; - // Convert uint16_t to unsigned because PRIx16 may not exist on Windows - LOG(level, " %s %-18s (%04x:%04x) %s %s", - selection, d->serial, (unsigned) d->vid, (unsigned) d->pid, - d->manufacturer, d->product); - } -} - -bool -sc_usb_select_device(struct sc_usb *usb, const char *serial, - struct sc_usb_device *out_device) { - struct sc_vec_usb_devices vec = SC_VECTOR_INITIALIZER; - bool ok = sc_usb_list_devices(usb, &vec); - if (!ok) { - LOGE("Could not list USB devices"); - return false; - } - - if (vec.size == 0) { - LOGE("Could not find any USB device"); - return false; - } - - size_t sel_idx; // index of the single matching device if sel_count == 1 - size_t sel_count = - sc_usb_devices_select(vec.data, vec.size, serial, &sel_idx); - - if (sel_count == 0) { - // if count > 0 && sel_count == 0, then necessarily a serial is provided - assert(serial); - LOGE("Could not find USB device %s", serial); - sc_usb_devices_log(SC_LOG_LEVEL_ERROR, vec.data, vec.size); - sc_usb_devices_destroy(&vec); - return false; - } - - if (sel_count > 1) { - if (serial) { - LOGE("Multiple (%" SC_PRIsizet ") USB devices with serial %s:", - sel_count, serial); - } else { - LOGE("Multiple (%" SC_PRIsizet ") USB devices:", sel_count); - } - sc_usb_devices_log(SC_LOG_LEVEL_ERROR, vec.data, vec.size); - LOGE("Select a device via -s (--serial)"); - sc_usb_devices_destroy(&vec); - return false; - } - - assert(sel_count == 1); // sel_idx is valid only if sel_count == 1 - struct sc_usb_device *device = &vec.data[sel_idx]; - - LOGI("USB device found:"); - sc_usb_devices_log(SC_LOG_LEVEL_INFO, vec.data, vec.size); - - // Move device into out_device (do not destroy device) - sc_usb_device_move(out_device, device); - sc_usb_devices_destroy(&vec); - return true; -} - -bool -sc_usb_init(struct sc_usb *usb) { - usb->handle = NULL; - return libusb_init(&usb->context) == LIBUSB_SUCCESS; -} - -void -sc_usb_destroy(struct sc_usb *usb) { - libusb_exit(usb->context); -} - -static void -sc_usb_report_disconnected(struct sc_usb *usb) { - if (usb->cbs && !atomic_flag_test_and_set(&usb->disconnection_notified)) { - assert(usb->cbs && usb->cbs->on_disconnected); - usb->cbs->on_disconnected(usb, usb->cbs_userdata); - } -} - -bool -sc_usb_check_disconnected(struct sc_usb *usb, int result) { - if (result == LIBUSB_ERROR_NO_DEVICE || result == LIBUSB_ERROR_NOT_FOUND) { - sc_usb_report_disconnected(usb); - return false; - } - - return true; -} - -static LIBUSB_CALL int -sc_usb_libusb_callback(libusb_context *ctx, libusb_device *device, - libusb_hotplug_event event, void *userdata) { - (void) ctx; - (void) device; - (void) event; - - struct sc_usb *usb = userdata; - - libusb_device *dev = libusb_get_device(usb->handle); - assert(dev); - if (dev != device) { - // Not the connected device - return 0; - } - - sc_usb_report_disconnected(usb); - - // Do not automatically deregister the callback by returning 1. Instead, - // manually deregister to interrupt libusb_handle_events() from the libusb - // event thread: - return 0; -} - -static int -run_libusb_event_handler(void *data) { - struct sc_usb *usb = data; - while (!atomic_load(&usb->stopped)) { - // Interrupted by events or by libusb_hotplug_deregister_callback() - libusb_handle_events(usb->context); - } - return 0; -} - -static bool -sc_usb_register_callback(struct sc_usb *usb) { - if (!libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG)) { - LOGW("On this platform, libusb does not have hotplug capability; " - "device disconnection will not be detected properly"); - return false; - } - - libusb_device *device = libusb_get_device(usb->handle); - assert(device); - - struct libusb_device_descriptor desc; - int result = libusb_get_device_descriptor(device, &desc); - if (result < 0) { - LOGE("Device descriptor: libusb error: %s", libusb_strerror(result)); - return false; - } - - int events = LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT; - int flags = LIBUSB_HOTPLUG_NO_FLAGS; - int vendor_id = desc.idVendor; - int product_id = desc.idProduct; - int dev_class = LIBUSB_HOTPLUG_MATCH_ANY; - result = libusb_hotplug_register_callback(usb->context, events, flags, - vendor_id, product_id, dev_class, - sc_usb_libusb_callback, usb, - &usb->callback_handle); - if (result < 0) { - LOGE("Register hotplog callback: libusb error: %s", - libusb_strerror(result)); - return false; - } - - usb->has_callback_handle = true; - return true; -} - -bool -sc_usb_connect(struct sc_usb *usb, libusb_device *device, - const struct sc_usb_callbacks *cbs, void *cbs_userdata) { - int result = libusb_open(device, &usb->handle); - if (result < 0) { - LOGE("Open USB device: libusb error: %s", libusb_strerror(result)); - return false; - } - - usb->has_callback_handle = false; - usb->has_libusb_event_thread = false; - - // If cbs is set, then cbs->on_disconnected must be set - assert(!cbs || cbs->on_disconnected); - usb->cbs = cbs; - usb->cbs_userdata = cbs_userdata; - - if (cbs) { - atomic_init(&usb->stopped, false); - usb->disconnection_notified = (atomic_flag) ATOMIC_FLAG_INIT; - if (sc_usb_register_callback(usb)) { - // Create a thread to process libusb events, so that device - // disconnection could be detected immediately - usb->has_libusb_event_thread = - sc_thread_create(&usb->libusb_event_thread, - run_libusb_event_handler, "scrcpy-usbev", usb); - if (!usb->has_libusb_event_thread) { - LOGW("Libusb event thread handler could not be created, USB " - "device disconnection might not be detected immediately"); - } - } - } - - return true; -} - -void -sc_usb_disconnect(struct sc_usb *usb) { - libusb_close(usb->handle); -} - -void -sc_usb_stop(struct sc_usb *usb) { - if (usb->has_callback_handle) { - atomic_store(&usb->stopped, true); - libusb_hotplug_deregister_callback(usb->context, usb->callback_handle); - } -} - -void -sc_usb_join(struct sc_usb *usb) { - if (usb->has_libusb_event_thread) { - sc_thread_join(&usb->libusb_event_thread, NULL); - } -} diff --git a/app/src/usb/usb.h b/app/src/usb/usb.h deleted file mode 100644 index f0ebbd96..00000000 --- a/app/src/usb/usb.h +++ /dev/null @@ -1,88 +0,0 @@ -#ifndef SC_USB_H -#define SC_USB_H - -#include "common.h" - -#include -#include - -#include "util/thread.h" - -struct sc_usb { - libusb_context *context; - libusb_device_handle *handle; - - const struct sc_usb_callbacks *cbs; - void *cbs_userdata; - - bool has_callback_handle; - libusb_hotplug_callback_handle callback_handle; - - bool has_libusb_event_thread; - sc_thread libusb_event_thread; - - atomic_bool stopped; // only used if cbs != NULL - atomic_flag disconnection_notified; -}; - -struct sc_usb_callbacks { - void (*on_disconnected)(struct sc_usb *usb, void *userdata); -}; - -struct sc_usb_device { - libusb_device *device; - char *serial; - char *manufacturer; - char *product; - uint16_t vid; - uint16_t pid; - bool selected; -}; - -void -sc_usb_device_destroy(struct sc_usb_device *usb_device); - -/** - * Move src to dst - * - * After this call, the content of src is undefined, except that - * sc_usb_device_destroy() can be called. - * - * This is useful to take a device from a list that will be destroyed, without - * making unnecessary copies. - */ -void -sc_usb_device_move(struct sc_usb_device *dst, struct sc_usb_device *src); - -void -sc_usb_devices_destroy_all(struct sc_usb_device *usb_devices, size_t count); - -bool -sc_usb_init(struct sc_usb *usb); - -void -sc_usb_destroy(struct sc_usb *usb); - -bool -sc_usb_select_device(struct sc_usb *usb, const char *serial, - struct sc_usb_device *out_device); - -bool -sc_usb_connect(struct sc_usb *usb, libusb_device *device, - const struct sc_usb_callbacks *cbs, void *cbs_userdata); - -void -sc_usb_disconnect(struct sc_usb *usb); - -// A client should call this function with the return value of a libusb call -// to detect disconnection immediately -bool -sc_usb_check_disconnected(struct sc_usb *usb, int result); - -void -sc_usb_stop(struct sc_usb *usb); - -void -sc_usb_join(struct sc_usb *usb); - -#endif diff --git a/app/src/util/acksync.c b/app/src/util/acksync.c deleted file mode 100644 index 76ecee0d..00000000 --- a/app/src/util/acksync.c +++ /dev/null @@ -1,75 +0,0 @@ -#include "acksync.h" - -#include - -bool -sc_acksync_init(struct sc_acksync *as) { - bool ok = sc_mutex_init(&as->mutex); - if (!ok) { - return false; - } - - ok = sc_cond_init(&as->cond); - if (!ok) { - sc_mutex_destroy(&as->mutex); - return false; - } - - as->stopped = false; - as->ack = SC_SEQUENCE_INVALID; - - return true; -} - -void -sc_acksync_destroy(struct sc_acksync *as) { - sc_cond_destroy(&as->cond); - sc_mutex_destroy(&as->mutex); -} - -void -sc_acksync_ack(struct sc_acksync *as, uint64_t sequence) { - sc_mutex_lock(&as->mutex); - - // Acknowledgements must be monotonic - assert(sequence >= as->ack); - - as->ack = sequence; - sc_cond_signal(&as->cond); - - sc_mutex_unlock(&as->mutex); -} - -enum sc_acksync_wait_result -sc_acksync_wait(struct sc_acksync *as, uint64_t ack, sc_tick deadline) { - sc_mutex_lock(&as->mutex); - - bool timed_out = false; - while (!as->stopped && as->ack < ack && !timed_out) { - timed_out = !sc_cond_timedwait(&as->cond, &as->mutex, deadline); - } - - enum sc_acksync_wait_result ret; - if (as->stopped) { - ret = SC_ACKSYNC_WAIT_INTR; - } else if (as->ack >= ack) { - ret = SC_ACKSYNC_WAIT_OK; - } else { - assert(timed_out); - ret = SC_ACKSYNC_WAIT_TIMEOUT; - } - sc_mutex_unlock(&as->mutex); - - return ret; -} - -/** - * Interrupt any `sc_acksync_wait()` - */ -void -sc_acksync_interrupt(struct sc_acksync *as) { - sc_mutex_lock(&as->mutex); - as->stopped = true; - sc_cond_signal(&as->cond); - sc_mutex_unlock(&as->mutex); -} diff --git a/app/src/util/acksync.h b/app/src/util/acksync.h deleted file mode 100644 index 3d9c9b2f..00000000 --- a/app/src/util/acksync.h +++ /dev/null @@ -1,69 +0,0 @@ -#ifndef SC_ACK_SYNC_H -#define SC_ACK_SYNC_H - -#include "common.h" - -#include -#include -#include "util/thread.h" -#include "util/tick.h" - -#define SC_SEQUENCE_INVALID 0 - -/** - * Helper to wait for acknowledgments - * - * In practice, it is used to wait for device clipboard acknowledgement from the - * server before injecting Ctrl+v via AOA HID, in order to avoid pasting the - * content of the old device clipboard (if Ctrl+v was injected before the - * clipboard content was actually set). - */ -struct sc_acksync { - sc_mutex mutex; - sc_cond cond; - - bool stopped; - - // Last acked value, initially SC_SEQUENCE_INVALID - uint64_t ack; -}; - -enum sc_acksync_wait_result { - // Acknowledgment received - SC_ACKSYNC_WAIT_OK, - - // Timeout expired - SC_ACKSYNC_WAIT_TIMEOUT, - - // Interrupted from another thread by sc_acksync_interrupt() - SC_ACKSYNC_WAIT_INTR, -}; - -bool -sc_acksync_init(struct sc_acksync *as); - -void -sc_acksync_destroy(struct sc_acksync *as); - -/** - * Acknowledge `sequence` - * - * The `sequence` must be greater than (or equal to) any previous acknowledged - * sequence. - */ -void -sc_acksync_ack(struct sc_acksync *as, uint64_t sequence); - -/** - * Wait for acknowledgment of sequence `ack` (or higher) - */ -enum sc_acksync_wait_result -sc_acksync_wait(struct sc_acksync *as, uint64_t ack, sc_tick deadline); - -/** - * Interrupt any `sc_acksync_wait()` - */ -void -sc_acksync_interrupt(struct sc_acksync *as); - -#endif diff --git a/app/src/util/audiobuf.c b/app/src/util/audiobuf.c deleted file mode 100644 index eeb27514..00000000 --- a/app/src/util/audiobuf.c +++ /dev/null @@ -1,153 +0,0 @@ -#include "audiobuf.h" - -#include -#include -#include -#include - -bool -sc_audiobuf_init(struct sc_audiobuf *buf, size_t sample_size, - uint32_t capacity) { - assert(sample_size); - assert(capacity); - - // The actual capacity is (alloc_size - 1) so that head == tail is - // non-ambiguous - buf->alloc_size = capacity + 1; - buf->data = sc_allocarray(buf->alloc_size, sample_size); - if (!buf->data) { - LOG_OOM(); - return false; - } - - buf->sample_size = sample_size; - atomic_init(&buf->head, 0); - atomic_init(&buf->tail, 0); - - return true; -} - -void -sc_audiobuf_destroy(struct sc_audiobuf *buf) { - free(buf->data); -} - -uint32_t -sc_audiobuf_read(struct sc_audiobuf *buf, void *to_, uint32_t samples_count) { - assert(samples_count); - - uint8_t *to = to_; - - // Only the reader thread can write tail without synchronization, so - // memory_order_relaxed is sufficient - uint32_t tail = atomic_load_explicit(&buf->tail, memory_order_relaxed); - - // The head cursor is updated after the data is written to the array - uint32_t head = atomic_load_explicit(&buf->head, memory_order_acquire); - - uint32_t can_read = (buf->alloc_size + head - tail) % buf->alloc_size; - if (!can_read) { - return 0; - } - if (samples_count > can_read) { - samples_count = can_read; - } - - if (to) { - uint32_t right_count = buf->alloc_size - tail; - if (right_count > samples_count) { - right_count = samples_count; - } - memcpy(to, - buf->data + (tail * buf->sample_size), - right_count * buf->sample_size); - - if (samples_count > right_count) { - uint32_t left_count = samples_count - right_count; - memcpy(to + (right_count * buf->sample_size), - buf->data, - left_count * buf->sample_size); - } - } - - uint32_t new_tail = (tail + samples_count) % buf->alloc_size; - atomic_store_explicit(&buf->tail, new_tail, memory_order_release); - - return samples_count; -} - -uint32_t -sc_audiobuf_write(struct sc_audiobuf *buf, const void *from_, - uint32_t samples_count) { - const uint8_t *from = from_; - - // Only the writer thread can write head, so memory_order_relaxed is - // sufficient - uint32_t head = atomic_load_explicit(&buf->head, memory_order_relaxed); - - // The tail cursor is updated after the data is consumed by the reader - uint32_t tail = atomic_load_explicit(&buf->tail, memory_order_acquire); - - uint32_t can_write = (buf->alloc_size + tail - head - 1) % buf->alloc_size; - if (!can_write) { - return 0; - } - if (samples_count > can_write) { - samples_count = can_write; - } - - uint32_t right_count = buf->alloc_size - head; - if (right_count > samples_count) { - right_count = samples_count; - } - memcpy(buf->data + (head * buf->sample_size), - from, - right_count * buf->sample_size); - - if (samples_count > right_count) { - uint32_t left_count = samples_count - right_count; - memcpy(buf->data, - from + (right_count * buf->sample_size), - left_count * buf->sample_size); - } - - uint32_t new_head = (head + samples_count) % buf->alloc_size; - atomic_store_explicit(&buf->head, new_head, memory_order_release); - - return samples_count; -} - -uint32_t -sc_audiobuf_write_silence(struct sc_audiobuf *buf, uint32_t samples_count) { - // Only the writer thread can write head, so memory_order_relaxed is - // sufficient - uint32_t head = atomic_load_explicit(&buf->head, memory_order_relaxed); - - // The tail cursor is updated after the data is consumed by the reader - uint32_t tail = atomic_load_explicit(&buf->tail, memory_order_acquire); - - uint32_t can_write = (buf->alloc_size + tail - head - 1) % buf->alloc_size; - if (!can_write) { - return 0; - } - if (samples_count > can_write) { - samples_count = can_write; - } - - uint32_t right_count = buf->alloc_size - head; - if (right_count > samples_count) { - right_count = samples_count; - } - memset(buf->data + (head * buf->sample_size), 0, - right_count * buf->sample_size); - - if (samples_count > right_count) { - uint32_t left_count = samples_count - right_count; - memset(buf->data, 0, left_count * buf->sample_size); - } - - uint32_t new_head = (head + samples_count) % buf->alloc_size; - atomic_store_explicit(&buf->head, new_head, memory_order_release); - - return samples_count; -} diff --git a/app/src/util/audiobuf.h b/app/src/util/audiobuf.h deleted file mode 100644 index b55a5a59..00000000 --- a/app/src/util/audiobuf.h +++ /dev/null @@ -1,69 +0,0 @@ -#ifndef SC_AUDIOBUF_H -#define SC_AUDIOBUF_H - -#include "common.h" - -#include -#include -#include -#include -#include - -/** - * Wrapper around bytebuf to read and write samples - * - * Each sample takes sample_size bytes. - */ -struct sc_audiobuf { - uint8_t *data; - uint32_t alloc_size; // in samples - size_t sample_size; - - atomic_uint_least32_t head; // writer cursor, in samples - atomic_uint_least32_t tail; // reader cursor, in samples - // empty: tail == head - // full: ((tail + 1) % alloc_size) == head -}; - -static inline uint32_t -sc_audiobuf_to_samples(struct sc_audiobuf *buf, size_t bytes) { - assert(bytes % buf->sample_size == 0); - return bytes / buf->sample_size; -} - -static inline size_t -sc_audiobuf_to_bytes(struct sc_audiobuf *buf, uint32_t samples) { - return samples * buf->sample_size; -} - -bool -sc_audiobuf_init(struct sc_audiobuf *buf, size_t sample_size, - uint32_t capacity); - -void -sc_audiobuf_destroy(struct sc_audiobuf *buf); - -uint32_t -sc_audiobuf_read(struct sc_audiobuf *buf, void *to, uint32_t samples_count); - -uint32_t -sc_audiobuf_write(struct sc_audiobuf *buf, const void *from, - uint32_t samples_count); - -uint32_t -sc_audiobuf_write_silence(struct sc_audiobuf *buf, uint32_t samples); - -static inline uint32_t -sc_audiobuf_capacity(struct sc_audiobuf *buf) { - assert(buf->alloc_size); - return buf->alloc_size - 1; -} - -static inline uint32_t -sc_audiobuf_can_read(struct sc_audiobuf *buf) { - uint32_t head = atomic_load_explicit(&buf->head, memory_order_acquire); - uint32_t tail = atomic_load_explicit(&buf->tail, memory_order_acquire); - return (buf->alloc_size + head - tail) % buf->alloc_size; -} - -#endif diff --git a/app/src/util/average.c b/app/src/util/average.c deleted file mode 100644 index ace23d45..00000000 --- a/app/src/util/average.c +++ /dev/null @@ -1,26 +0,0 @@ -#include "average.h" - -#include - -void -sc_average_init(struct sc_average *avg, unsigned range) { - avg->range = range; - avg->avg = 0; - avg->count = 0; -} - -void -sc_average_push(struct sc_average *avg, float value) { - if (avg->count < avg->range) { - ++avg->count; - } - - assert(avg->count); - avg->avg = ((avg->count - 1) * avg->avg + value) / avg->count; -} - -float -sc_average_get(struct sc_average *avg) { - assert(avg->count); - return avg->avg; -} diff --git a/app/src/util/average.h b/app/src/util/average.h deleted file mode 100644 index eded9987..00000000 --- a/app/src/util/average.h +++ /dev/null @@ -1,37 +0,0 @@ -#ifndef SC_AVERAGE -#define SC_AVERAGE - -#include "common.h" - -struct sc_average { - // Current average value - float avg; - - // Target range, to update the average as follow: - // avg = ((range - 1) * avg + new_value) / range - unsigned range; - - // Number of values pushed when less than range (count <= range). - // The purpose is to handle the first (range - 1) values properly. - unsigned count; -}; - -void -sc_average_init(struct sc_average *avg, unsigned range); - -/** - * Push a new value to update the "rolling" average - */ -void -sc_average_push(struct sc_average *avg, float value); - -/** - * Get the current average value - * - * It is an error to call this function if sc_average_push() has not been - * called at least once. - */ -float -sc_average_get(struct sc_average *avg); - -#endif diff --git a/app/src/util/binary.h b/app/src/util/binary.h deleted file mode 100644 index b6ce3201..00000000 --- a/app/src/util/binary.h +++ /dev/null @@ -1,95 +0,0 @@ -#ifndef SC_BINARY_H -#define SC_BINARY_H - -#include "common.h" - -#include -#include - -static inline void -sc_write16be(uint8_t *buf, uint16_t value) { - buf[0] = value >> 8; - buf[1] = value; -} - -static inline void -sc_write16le(uint8_t *buf, uint16_t value) { - buf[0] = value; - buf[1] = value >> 8; -} - -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_write32le(uint8_t *buf, uint32_t value) { - buf[0] = value; - buf[1] = value >> 8; - buf[2] = value >> 16; - buf[3] = value >> 24; -} - -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 void -sc_write64le(uint8_t *buf, uint64_t value) { - sc_write32le(buf, (uint32_t) value); - sc_write32le(&buf[4], value >> 32); -} - -static inline uint16_t -sc_read16be(const uint8_t *buf) { - return (buf[0] << 8) | buf[1]; -} - -static inline uint32_t -sc_read32be(const uint8_t *buf) { - return ((uint32_t) buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3]; -} - -static inline uint64_t -sc_read64be(const uint8_t *buf) { - uint32_t msb = sc_read32be(buf); - uint32_t lsb = sc_read32be(&buf[4]); - return ((uint64_t) msb << 32) | lsb; -} - -/** - * Convert a float between 0 and 1 to an unsigned 16-bit fixed-point value - */ -static inline uint16_t -sc_float_to_u16fp(float f) { - assert(f >= 0.0f && f <= 1.0f); - uint32_t u = f * 0x1p16f; // 2^16 - if (u >= 0xffff) { - assert(u == 0x10000); // for f == 1.0f - u = 0xffff; - } - return (uint16_t) u; -} - -/** - * Convert a float between -1 and 1 to a signed 16-bit fixed-point value - */ -static inline int16_t -sc_float_to_i16fp(float f) { - assert(f >= -1.0f && f <= 1.0f); - int32_t i = f * 0x1p15f; // 2^15 - assert(i >= -0x8000); - if (i >= 0x7fff) { - assert(i == 0x8000); // for f == 1.0f - i = 0x7fff; - } - return (int16_t) i; -} - -#endif diff --git a/app/src/util/env.c b/app/src/util/env.c deleted file mode 100644 index 127f5a1f..00000000 --- a/app/src/util/env.c +++ /dev/null @@ -1,31 +0,0 @@ -#include "env.h" - -#include -#include -#ifdef _WIN32 -# include "util/str.h" -#endif - -char * -sc_get_env(const char *varname) { -#ifdef _WIN32 - wchar_t *w_varname = sc_str_to_wchars(varname); - if (!w_varname) { - return NULL; - } - const wchar_t *value = _wgetenv(w_varname); - free(w_varname); - if (!value) { - return NULL; - } - - return sc_str_from_wchars(value); -#else - const char *value = getenv(varname); - if (!value) { - return NULL; - } - - return strdup(value); -#endif -} diff --git a/app/src/util/env.h b/app/src/util/env.h deleted file mode 100644 index 50a31165..00000000 --- a/app/src/util/env.h +++ /dev/null @@ -1,12 +0,0 @@ -#ifndef SC_ENV_H -#define SC_ENV_H - -#include "common.h" - -// Return the value of the environment variable (may be NULL). -// -// The returned value must be freed by the caller. -char * -sc_get_env(const char *varname); - -#endif diff --git a/app/src/util/file.c b/app/src/util/file.c deleted file mode 100644 index 174e5efd..00000000 --- a/app/src/util/file.c +++ /dev/null @@ -1,48 +0,0 @@ -#include "file.h" - -#include -#include - -#include "util/log.h" - -char * -sc_file_get_local_path(const char *name) { - char *executable_path = sc_file_get_executable_path(); - if (!executable_path) { - return NULL; - } - - // dirname() does not work correctly everywhere, so get the parent - // directory manually. - // See - char *p = strrchr(executable_path, SC_PATH_SEPARATOR); - if (!p) { - LOGE("Unexpected executable path: \"%s\" (it should contain a '%c')", - executable_path, SC_PATH_SEPARATOR); - free(executable_path); - return NULL; - } - - *p = '\0'; // modify executable_path in place - char *dir = executable_path; - size_t dirlen = strlen(dir); - size_t namelen = strlen(name); - - size_t len = dirlen + namelen + 2; // +2: '/' and '\0' - char *file_path = malloc(len); - if (!file_path) { - LOG_OOM(); - free(executable_path); - return NULL; - } - - memcpy(file_path, dir, dirlen); - file_path[dirlen] = SC_PATH_SEPARATOR; - // namelen + 1 to copy the final '\0' - memcpy(&file_path[dirlen + 1], name, namelen + 1); - - free(executable_path); - - return file_path; -} - diff --git a/app/src/util/file.h b/app/src/util/file.h deleted file mode 100644 index 089f6f75..00000000 --- a/app/src/util/file.h +++ /dev/null @@ -1,49 +0,0 @@ -#ifndef SC_FILE_H -#define SC_FILE_H - -#include "common.h" - -#include - -#ifdef _WIN32 -# define SC_PATH_SEPARATOR '\\' -#else -# define SC_PATH_SEPARATOR '/' -#endif - -#ifndef _WIN32 -/** - * Indicate if an executable exists using $PATH - * - * In practice, it is only used to know if a package manager is available on - * the system. It is only implemented on Linux. - */ -bool -sc_file_executable_exists(const char *file); -#endif - -/** - * Return the absolute path of the executable (the scrcpy binary) - * - * The result must be freed by the caller using free(). It may return NULL on - * error. - */ -char * -sc_file_get_executable_path(void); - -/** - * Return the absolute path of a file in the same directory as the executable - * - * The result must be freed by the caller using free(). It may return NULL on - * error. - */ -char * -sc_file_get_local_path(const char *name); - -/** - * Indicate if the file exists and is not a directory - */ -bool -sc_file_is_regular(const char *path); - -#endif diff --git a/app/src/util/intmap.c b/app/src/util/intmap.c deleted file mode 100644 index fa11acef..00000000 --- a/app/src/util/intmap.c +++ /dev/null @@ -1,13 +0,0 @@ -#include "intmap.h" - -const struct sc_intmap_entry * -sc_intmap_find_entry(const struct sc_intmap_entry entries[], size_t len, - int32_t key) { - for (size_t i = 0; i < len; ++i) { - const struct sc_intmap_entry *entry = &entries[i]; - if (entry->key == key) { - return entry; - } - } - return NULL; -} diff --git a/app/src/util/intmap.h b/app/src/util/intmap.h deleted file mode 100644 index 7ab903ca..00000000 --- a/app/src/util/intmap.h +++ /dev/null @@ -1,25 +0,0 @@ -#ifndef SC_ARRAYMAP_H -#define SC_ARRAYMAP_H - -#include "common.h" - -#include -#include - -struct sc_intmap_entry { - int32_t key; - int32_t value; -}; - -const struct sc_intmap_entry * -sc_intmap_find_entry(const struct sc_intmap_entry entries[], size_t len, - int32_t key); - -/** - * MAP is expected to be a static array of sc_intmap_entry, so that - * ARRAY_LEN(MAP) can be computed statically. - */ -#define SC_INTMAP_FIND_ENTRY(MAP, KEY) \ - sc_intmap_find_entry(MAP, ARRAY_LEN(MAP), KEY) - -#endif diff --git a/app/src/util/intr.c b/app/src/util/intr.c deleted file mode 100644 index ddf4839f..00000000 --- a/app/src/util/intr.c +++ /dev/null @@ -1,83 +0,0 @@ -#include "intr.h" - -#include - -#include "util/log.h" - -bool -sc_intr_init(struct sc_intr *intr) { - bool ok = sc_mutex_init(&intr->mutex); - if (!ok) { - LOG_OOM(); - return false; - } - - intr->socket = SC_SOCKET_NONE; - intr->process = SC_PROCESS_NONE; - - atomic_store_explicit(&intr->interrupted, false, memory_order_relaxed); - - return true; -} - -bool -sc_intr_set_socket(struct sc_intr *intr, sc_socket socket) { - assert(intr->process == SC_PROCESS_NONE); - - sc_mutex_lock(&intr->mutex); - bool interrupted = - atomic_load_explicit(&intr->interrupted, memory_order_relaxed); - if (!interrupted) { - intr->socket = socket; - } - sc_mutex_unlock(&intr->mutex); - - return !interrupted; -} - -bool -sc_intr_set_process(struct sc_intr *intr, sc_pid pid) { - assert(intr->socket == SC_SOCKET_NONE); - - sc_mutex_lock(&intr->mutex); - bool interrupted = - atomic_load_explicit(&intr->interrupted, memory_order_relaxed); - if (!interrupted) { - intr->process = pid; - } - sc_mutex_unlock(&intr->mutex); - - return !interrupted; -} - -void -sc_intr_interrupt(struct sc_intr *intr) { - sc_mutex_lock(&intr->mutex); - - atomic_store_explicit(&intr->interrupted, true, memory_order_relaxed); - - // No more than one component to interrupt - assert(intr->socket == SC_SOCKET_NONE || - intr->process == SC_PROCESS_NONE); - - if (intr->socket != SC_SOCKET_NONE) { - LOGD("Interrupting socket"); - net_interrupt(intr->socket); - intr->socket = SC_SOCKET_NONE; - } - if (intr->process != SC_PROCESS_NONE) { - LOGD("Interrupting process"); - sc_process_terminate(intr->process); - intr->process = SC_PROCESS_NONE; - } - - sc_mutex_unlock(&intr->mutex); -} - -void -sc_intr_destroy(struct sc_intr *intr) { - assert(intr->socket == SC_SOCKET_NONE); - assert(intr->process == SC_PROCESS_NONE); - - sc_mutex_destroy(&intr->mutex); -} diff --git a/app/src/util/intr.h b/app/src/util/intr.h deleted file mode 100644 index 35bd3375..00000000 --- a/app/src/util/intr.h +++ /dev/null @@ -1,78 +0,0 @@ -#ifndef SC_INTR_H -#define SC_INTR_H - -#include "common.h" - -#include -#include - -#include "util/net.h" -#include "util/process.h" -#include "util/thread.h" - -/** - * Interruptor to wake up a blocking call from another thread - * - * It allows to register a socket or a process before a blocking call, and - * interrupt/close from another thread to wake up the blocking call. - */ -struct sc_intr { - sc_mutex mutex; - - sc_socket socket; - sc_pid process; - - // Written protected by the mutex to avoid race conditions against - // sc_intr_set_socket() and sc_intr_set_process(), but can be read - // (atomically) without mutex - atomic_bool interrupted; -}; - -/** - * Initialize an interruptor - */ -bool -sc_intr_init(struct sc_intr *intr); - -/** - * Set a socket as the interruptible component - * - * Call with SC_SOCKET_NONE to unset. - */ -bool -sc_intr_set_socket(struct sc_intr *intr, sc_socket socket); - -/** - * Set a process as the interruptible component - * - * Call with SC_PROCESS_NONE to unset. - */ -bool -sc_intr_set_process(struct sc_intr *intr, sc_pid socket); - -/** - * Interrupt the current interruptible component - * - * Must be called from a different thread. - */ -void -sc_intr_interrupt(struct sc_intr *intr); - -/** - * Read the interrupted state - * - * It is exposed as a static inline function because it just loads from an - * atomic. - */ -static inline bool -sc_intr_is_interrupted(struct sc_intr *intr) { - return atomic_load_explicit(&intr->interrupted, memory_order_relaxed); -} - -/** - * Destroy the interruptor - */ -void -sc_intr_destroy(struct sc_intr *intr); - -#endif diff --git a/app/src/util/log.c b/app/src/util/log.c deleted file mode 100644 index 9114a258..00000000 --- a/app/src/util/log.c +++ /dev/null @@ -1,157 +0,0 @@ -#include "log.h" - -#if _WIN32 -# include -#endif -#include -#include -#include -#include -#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); - SDL_LogSetPriority(SDL_LOG_CATEGORY_CUSTOM, 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 - -static SDL_LogPriority -sdl_priority_from_av_level(int level) { - switch (level) { - case AV_LOG_PANIC: - case AV_LOG_FATAL: - return SDL_LOG_PRIORITY_CRITICAL; - case AV_LOG_ERROR: - return SDL_LOG_PRIORITY_ERROR; - case AV_LOG_WARNING: - return SDL_LOG_PRIORITY_WARN; - case AV_LOG_INFO: - return SDL_LOG_PRIORITY_INFO; - } - // do not forward others, which are too verbose - return 0; -} - -static void -sc_av_log_callback(void *avcl, int level, const char *fmt, va_list vl) { - (void) avcl; - SDL_LogPriority priority = sdl_priority_from_av_level(level); - if (priority == 0) { - return; - } - - size_t fmt_len = strlen(fmt); - char *local_fmt = malloc(fmt_len + 10); - if (!local_fmt) { - LOG_OOM(); - return; - } - memcpy(local_fmt, "[FFmpeg] ", 9); // do not write the final '\0' - memcpy(local_fmt + 9, fmt, fmt_len + 1); // include '\0' - SDL_LogMessageV(SDL_LOG_CATEGORY_CUSTOM, priority, local_fmt, vl); - free(local_fmt); -} - -static const char *const sc_sdl_log_priority_names[SDL_NUM_LOG_PRIORITIES] = { - [SDL_LOG_PRIORITY_VERBOSE] = "VERBOSE", - [SDL_LOG_PRIORITY_DEBUG] = "DEBUG", - [SDL_LOG_PRIORITY_INFO] = "INFO", - [SDL_LOG_PRIORITY_WARN] = "WARN", - [SDL_LOG_PRIORITY_ERROR] = "ERROR", - [SDL_LOG_PRIORITY_CRITICAL] = "CRITICAL", -}; - -static void SDLCALL -sc_sdl_log_print(void *userdata, int category, SDL_LogPriority priority, - const char *message) { - (void) userdata; - (void) category; - - FILE *out = priority < SDL_LOG_PRIORITY_WARN ? stdout : stderr; - assert(priority < SDL_NUM_LOG_PRIORITIES); - const char *prio_name = sc_sdl_log_priority_names[priority]; - fprintf(out, "%s: %s\n", prio_name, message); -} - -void -sc_log_configure(void) { - SDL_LogSetOutputFunction(sc_sdl_log_print, NULL); - // Redirect FFmpeg logs to SDL logs - av_log_set_callback(sc_av_log_callback); -} diff --git a/app/src/util/log.h b/app/src/util/log.h deleted file mode 100644 index 0d79c9a4..00000000 --- a/app/src/util/log.h +++ /dev/null @@ -1,41 +0,0 @@ -#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 - -void -sc_log_configure(void); - -#endif diff --git a/app/src/util/memory.c b/app/src/util/memory.c deleted file mode 100644 index 64ee616e..00000000 --- a/app/src/util/memory.c +++ /dev/null @@ -1,14 +0,0 @@ -#include "memory.h" - -#include -#include - -void * -sc_allocarray(size_t nmemb, size_t size) { - size_t bytes; - if (__builtin_mul_overflow(nmemb, size, &bytes)) { - errno = ENOMEM; - return NULL; - } - return malloc(bytes); -} diff --git a/app/src/util/memory.h b/app/src/util/memory.h deleted file mode 100644 index 0fb6bc64..00000000 --- a/app/src/util/memory.h +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef SC_MEMORY_H -#define SC_MEMORY_H - -#include - -/** - * Allocate an array of `nmemb` items of `size` bytes each - * - * Like calloc(), but without initialization. - * Like reallocarray(), but without reallocation. - */ -void * -sc_allocarray(size_t nmemb, size_t size); - -#endif diff --git a/app/src/util/net.c b/app/src/util/net.c deleted file mode 100644 index 9562ff6b..00000000 --- a/app/src/util/net.c +++ /dev/null @@ -1,296 +0,0 @@ -#include "net.h" - -#include -#include - -#ifdef _WIN32 -# include - typedef int socklen_t; -#else -# include -# 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; -#endif - -#include "util/log.h" - -bool -net_init(void) { -#ifdef _WIN32 - WSADATA wsa; - int res = WSAStartup(MAKEWORD(1, 1), &wsa); - if (res) { - LOGE("WSAStartup failed with error %d", res); - return false; - } -#endif - return true; -} - -void -net_cleanup(void) { -#ifdef _WIN32 - WSACleanup(); -#endif -} - -static inline bool -sc_raw_socket_close(sc_raw_socket raw_sock) { -#ifndef _WIN32 - return !close(raw_sock); -#else - return !closesocket(raw_sock); -#endif -} - -static inline sc_socket -wrap(sc_raw_socket sock) { -#ifdef SC_SOCKET_CLOSE_ON_INTERRUPT - if (sock == SC_RAW_SOCKET_NONE) { - return SC_SOCKET_NONE; - } - - struct sc_socket_wrapper *socket = malloc(sizeof(*socket)); - if (!socket) { - LOG_OOM(); - sc_raw_socket_close(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 SC_SOCKET_CLOSE_ON_INTERRUPT - if (socket == SC_SOCKET_NONE) { - return SC_RAW_SOCKET_NONE; - } - - return socket->socket; -#else - return socket; -#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 SC_SOCKET_CLOSE_ON_INTERRUPT - if (!atomic_flag_test_and_set(&socket->closed)) { - return sc_raw_socket_close(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 SC_SOCKET_CLOSE_ON_INTERRUPT - bool ret = true; - if (!atomic_flag_test_and_set(&socket->closed)) { - ret = sc_raw_socket_close(raw_sock); - } - free(socket); - return ret; -#else - return sc_raw_socket_close(raw_sock); -#endif -} - -bool -net_set_tcp_nodelay(sc_socket socket, bool tcp_nodelay) { - sc_raw_socket raw_sock = unwrap(socket); - - int value = tcp_nodelay ? 1 : 0; - int ret = setsockopt(raw_sock, IPPROTO_TCP, TCP_NODELAY, - (const void *) &value, sizeof(value)); - if (ret == -1) { - net_perror("setsockopt(TCP_NODELAY)"); - return false; - } - - assert(ret == 0); - return true; -} - -bool -net_parse_ipv4(const char *s, uint32_t *ipv4) { - struct in_addr addr; - if (!inet_pton(AF_INET, s, &addr)) { - LOGE("Invalid IPv4 address: %s", s); - return false; - } - - *ipv4 = ntohl(addr.s_addr); - return true; -} diff --git a/app/src/util/net.h b/app/src/util/net.h deleted file mode 100644 index aa99bbc4..00000000 --- a/app/src/util/net.h +++ /dev/null @@ -1,96 +0,0 @@ -#ifndef SC_NET_H -#define SC_NET_H - -#include "common.h" - -#include -#include -#include -#include - -#ifdef _WIN32 -# include - typedef SOCKET sc_raw_socket; -# define SC_RAW_SOCKET_NONE INVALID_SOCKET -#else // not _WIN32 - typedef int sc_raw_socket; -# define SC_RAW_SOCKET_NONE -1 -#endif - -#if defined(_WIN32) || defined(__APPLE__) -// On Windows and macOS, shutdown() does not interrupt accept() or read() -// calls, so net_interrupt() must call close() instead, and net_close() must -// behave accordingly. -// This causes a small race condition (once the socket is closed, its -// handle becomes invalid and may in theory be reassigned before another -// thread calls accept() or read()), but it is deemed acceptable as a -// workaround. -# define SC_SOCKET_CLOSE_ON_INTERRUPT -#endif - -#ifdef SC_SOCKET_CLOSE_ON_INTERRUPT -# include -# define SC_SOCKET_NONE NULL - typedef struct sc_socket_wrapper { - sc_raw_socket socket; - atomic_flag closed; - } *sc_socket; -#else -# define SC_SOCKET_NONE -1 - typedef sc_raw_socket 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); - -// Disable Nagle's algorithm (if tcp_nodelay is true) -bool -net_set_tcp_nodelay(sc_socket socket, bool tcp_nodelay); - -/** - * Parse `ip` "xxx.xxx.xxx.xxx" to an IPv4 host representation - */ -bool -net_parse_ipv4(const char *ip, uint32_t *ipv4); - -#endif diff --git a/app/src/util/net_intr.c b/app/src/util/net_intr.c deleted file mode 100644 index 55286af6..00000000 --- a/app/src/util/net_intr.c +++ /dev/null @@ -1,97 +0,0 @@ -#include "net_intr.h" - -bool -net_connect_intr(struct sc_intr *intr, sc_socket socket, uint32_t addr, - uint16_t port) { - if (!sc_intr_set_socket(intr, socket)) { - // Already interrupted - return false; - } - - bool ret = net_connect(socket, addr, port); - - sc_intr_set_socket(intr, SC_SOCKET_NONE); - return ret; -} - -bool -net_listen_intr(struct sc_intr *intr, sc_socket server_socket, uint32_t addr, - uint16_t port, int backlog) { - if (!sc_intr_set_socket(intr, server_socket)) { - // Already interrupted - return false; - } - - bool ret = net_listen(server_socket, addr, port, backlog); - - sc_intr_set_socket(intr, SC_SOCKET_NONE); - return ret; -} - -sc_socket -net_accept_intr(struct sc_intr *intr, sc_socket server_socket) { - if (!sc_intr_set_socket(intr, server_socket)) { - // Already interrupted - return SC_SOCKET_NONE; - } - - sc_socket socket = net_accept(server_socket); - - sc_intr_set_socket(intr, SC_SOCKET_NONE); - return socket; -} - -ssize_t -net_recv_intr(struct sc_intr *intr, sc_socket socket, void *buf, size_t len) { - if (!sc_intr_set_socket(intr, socket)) { - // Already interrupted - return -1; - } - - ssize_t r = net_recv(socket, buf, len); - - sc_intr_set_socket(intr, SC_SOCKET_NONE); - return r; -} - -ssize_t -net_recv_all_intr(struct sc_intr *intr, sc_socket socket, void *buf, - size_t len) { - if (!sc_intr_set_socket(intr, socket)) { - // Already interrupted - return -1; - } - - ssize_t r = net_recv_all(socket, buf, len); - - sc_intr_set_socket(intr, SC_SOCKET_NONE); - return r; -} - -ssize_t -net_send_intr(struct sc_intr *intr, sc_socket socket, const void *buf, - size_t len) { - if (!sc_intr_set_socket(intr, socket)) { - // Already interrupted - return -1; - } - - ssize_t w = net_send(socket, buf, len); - - sc_intr_set_socket(intr, SC_SOCKET_NONE); - return w; -} - -ssize_t -net_send_all_intr(struct sc_intr *intr, sc_socket socket, const void *buf, - size_t len) { - if (!sc_intr_set_socket(intr, socket)) { - // Already interrupted - return -1; - } - - ssize_t w = net_send_all(socket, buf, len); - - sc_intr_set_socket(intr, SC_SOCKET_NONE); - return w; -} diff --git a/app/src/util/net_intr.h b/app/src/util/net_intr.h deleted file mode 100644 index e2bbee88..00000000 --- a/app/src/util/net_intr.h +++ /dev/null @@ -1,40 +0,0 @@ -#ifndef SC_NET_INTR_H -#define SC_NET_INTR_H - -#include "common.h" - -#include -#include -#include -#include - -#include "util/intr.h" -#include "util/net.h" - -bool -net_connect_intr(struct sc_intr *intr, sc_socket socket, uint32_t addr, - uint16_t port); - -bool -net_listen_intr(struct sc_intr *intr, sc_socket server_socket, uint32_t addr, - uint16_t port, int backlog); - -sc_socket -net_accept_intr(struct sc_intr *intr, sc_socket server_socket); - -ssize_t -net_recv_intr(struct sc_intr *intr, sc_socket socket, void *buf, size_t len); - -ssize_t -net_recv_all_intr(struct sc_intr *intr, sc_socket socket, void *buf, - size_t len); - -ssize_t -net_send_intr(struct sc_intr *intr, sc_socket socket, const void *buf, - size_t len); - -ssize_t -net_send_all_intr(struct sc_intr *intr, sc_socket socket, const void *buf, - size_t len); - -#endif diff --git a/app/src/util/process.c b/app/src/util/process.c deleted file mode 100644 index 29d89a54..00000000 --- a/app/src/util/process.c +++ /dev/null @@ -1,100 +0,0 @@ -#include "process.h" - -#include - -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); -} diff --git a/app/src/util/process.h b/app/src/util/process.h deleted file mode 100644 index eec51bcc..00000000 --- a/app/src/util/process.h +++ /dev/null @@ -1,179 +0,0 @@ -#ifndef SC_PROCESS_H -#define SC_PROCESS_H - -#include "common.h" - -#include -#include -#include "util/thread.h" -#include "util/tick.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 diff --git a/app/src/util/process_intr.c b/app/src/util/process_intr.c deleted file mode 100644 index 641440ab..00000000 --- a/app/src/util/process_intr.c +++ /dev/null @@ -1,35 +0,0 @@ -#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 -1; - } - - 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 -1; - } - - ssize_t ret = sc_pipe_read_all(pipe, data, len); - - if (intr) { - sc_intr_set_process(intr, SC_PROCESS_NONE); - } - - return ret; -} diff --git a/app/src/util/process_intr.h b/app/src/util/process_intr.h deleted file mode 100644 index 020eafa1..00000000 --- a/app/src/util/process_intr.h +++ /dev/null @@ -1,17 +0,0 @@ -#ifndef SC_PROCESS_INTR_H -#define SC_PROCESS_INTR_H - -#include "common.h" - -#include "util/intr.h" -#include "util/process.h" - -ssize_t -sc_pipe_read_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe, char *data, - size_t len); - -ssize_t -sc_pipe_read_all_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe, - char *data, size_t len); - -#endif diff --git a/app/src/util/rand.c b/app/src/util/rand.c deleted file mode 100644 index 590e4ca4..00000000 --- a/app/src/util/rand.c +++ /dev/null @@ -1,24 +0,0 @@ -#include "rand.h" - -#include - -#include "tick.h" - -void sc_rand_init(struct sc_rand *rand) { - sc_tick seed = sc_tick_now(); // microsecond precision - rand->xsubi[0] = (seed >> 32) & 0xFFFF; - rand->xsubi[1] = (seed >> 16) & 0xFFFF; - rand->xsubi[2] = seed & 0xFFFF; -} - -uint32_t sc_rand_u32(struct sc_rand *rand) { - // jrand returns a value in range [-2^31, 2^31] - // conversion from signed to unsigned is well-defined to wrap-around - return jrand48(rand->xsubi); -} - -uint64_t sc_rand_u64(struct sc_rand *rand) { - uint32_t msb = sc_rand_u32(rand); - uint32_t lsb = sc_rand_u32(rand); - return ((uint64_t) msb << 32) | lsb; -} diff --git a/app/src/util/rand.h b/app/src/util/rand.h deleted file mode 100644 index 262b0b9b..00000000 --- a/app/src/util/rand.h +++ /dev/null @@ -1,16 +0,0 @@ -#ifndef SC_RAND_H -#define SC_RAND_H - -#include "common.h" - -#include - -struct sc_rand { - unsigned short xsubi[3]; -}; - -void sc_rand_init(struct sc_rand *rand); -uint32_t sc_rand_u32(struct sc_rand *rand); -uint64_t sc_rand_u64(struct sc_rand *rand); - -#endif diff --git a/app/src/util/str.c b/app/src/util/str.c deleted file mode 100644 index 83d19c4d..00000000 --- a/app/src/util/str.c +++ /dev/null @@ -1,375 +0,0 @@ -#include "str.h" - -#include -#include -#include -#include -#include -#include - -#ifdef _WIN32 -# include -# include -#endif - -#include "util/log.h" -#include "util/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; -} - -char * -sc_str_concat(const char *start, const char *end) { - assert(start); - assert(end); - - size_t start_len = strlen(start); - size_t end_len = strlen(end); - - char *result = malloc(start_len + end_len + 1); - if (!result) { - LOG_OOM(); - return NULL; - } - - memcpy(result, start, start_len); - memcpy(result + start_len, end, end_len + 1); - - return result; -} - -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; -} - -char * -sc_str_to_hex_string(const uint8_t *data, size_t size) { - size_t buffer_size = size * 3 + 1; - char *buffer = malloc(buffer_size); - if (!buffer) { - LOG_OOM(); - return NULL; - } - - for (size_t i = 0; i < size; ++i) { - snprintf(buffer + i * 3, 4, "%02X ", data[i]); - } - - // Remove the final space - buffer[size * 3] = '\0'; - - return buffer; -} diff --git a/app/src/util/str.h b/app/src/util/str.h deleted file mode 100644 index b386b48d..00000000 --- a/app/src/util/str.h +++ /dev/null @@ -1,158 +0,0 @@ -#ifndef SC_STR_H -#define SC_STR_H - -#include "common.h" - -#include -#include -#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); - -/** - * Concat two strings - * - * Return a new allocated string, contanining the concatenation of the two - * input strings. - */ -char * -sc_str_concat(const char *start, const char *end); - -/** - * Parse `s` as an integer into `out` - * - * Return true if the conversion succeeded, false otherwise. - */ -bool -sc_str_parse_integer(const char *s, long *out); - -/** - * Parse `s` as integers separated by `sep` (for example `1234:2000`) into `out` - * - * Returns the number of integers on success, 0 on failure. - */ -size_t -sc_str_parse_integers(const char *s, const char sep, size_t max_items, - long *out); - -/** - * Parse `s` as an integer into `out` - * - * Like `sc_str_parse_integer()`, but accept 'k'/'K' (x1000) and 'm'/'M' - * (x1000000) as suffixes. - * - * Return true if the conversion succeeded, false otherwise. - */ -bool -sc_str_parse_integer_with_suffix(const char *s, long *out); - -/** - * Search `s` in the list separated by `sep` - * - * For example, sc_str_list_contains("a,bc,def", ',', "bc") returns true. - */ -bool -sc_str_list_contains(const char *list, char sep, const char *s); - -/** - * Return the index to truncate a UTF-8 string at a valid position - */ -size_t -sc_str_utf8_truncation_index(const char *utf8, size_t max_len); - -#ifdef _WIN32 -/** - * Convert a UTF-8 string to a wchar_t string - * - * Return the new allocated string, to be freed by the caller. - */ -wchar_t * -sc_str_to_wchars(const char *utf8); - -/** - * Convert a wchar_t string to a UTF-8 string - * - * Return the new allocated string, to be freed by the caller. - */ -char * -sc_str_from_wchars(const wchar_t *s); -#endif - -/** - * Wrap input lines to fit in `columns` columns - * - * Break input lines at word boundaries (spaces) so that they fit in `columns` - * columns, left-indented by `indent` spaces. - */ -char * -sc_str_wrap_lines(const char *input, unsigned columns, unsigned indent); - -/** - * Find the start of a column in a string - * - * A string may represent several columns, separated by some "spaces" - * (separators). This function aims to find the start of the column number - * `col`. - * - * For example, to find the 4th column (column number 3): - * - * // here - * // v - * const char *s = "abc def ghi jk"; - * ssize_t index = sc_str_index_of_column(s, 3, " "); - * assert(index == 16); // points to "jk" - * - * Return -1 if no such column exists. - */ -ssize_t -sc_str_index_of_column(const char *s, unsigned col, const char *seps); - -/** - * Remove all `\r` at the end of the line - * - * The line length is provided by `len` (this avoids a call to `strlen()` when - * the caller already knows the length). - * - * Return the new length. - */ -size_t -sc_str_remove_trailing_cr(char *s, size_t len); - -/** - * Convert binary data to hexadecimal string - */ -char * -sc_str_to_hex_string(const uint8_t *data, size_t len); - -#endif diff --git a/app/src/util/strbuf.c b/app/src/util/strbuf.c deleted file mode 100644 index 6196d746..00000000 --- a/app/src/util/strbuf.c +++ /dev/null @@ -1,89 +0,0 @@ -#include "strbuf.h" - -#include -#include -#include - -#include "util/log.h" - -bool -sc_strbuf_init(struct sc_strbuf *buf, size_t init_cap) { - buf->s = malloc(init_cap + 1); // +1 for '\0' - if (!buf->s) { - LOG_OOM(); - return false; - } - - buf->len = 0; - buf->cap = init_cap; - return true; -} - -static bool -sc_strbuf_reserve(struct sc_strbuf *buf, size_t len) { - if (buf->len + len > buf->cap) { - size_t new_cap = buf->cap * 3 / 2 + len; - char *s = realloc(buf->s, new_cap + 1); // +1 for '\0' - if (!s) { - // Leave the old buf->s - LOG_OOM(); - return false; - } - buf->s = s; - buf->cap = new_cap; - } - return true; -} - -bool -sc_strbuf_append(struct sc_strbuf *buf, const char *s, size_t len) { - assert(s); - assert(*s); - assert(strlen(s) >= len); - if (!sc_strbuf_reserve(buf, len)) { - return false; - } - - memcpy(&buf->s[buf->len], s, len); - buf->len += len; - buf->s[buf->len] = '\0'; - - return true; -} - -bool -sc_strbuf_append_char(struct sc_strbuf *buf, const char c) { - if (!sc_strbuf_reserve(buf, 1)) { - return false; - } - - buf->s[buf->len] = c; - buf->len ++; - buf->s[buf->len] = '\0'; - - return true; -} - -bool -sc_strbuf_append_n(struct sc_strbuf *buf, const char c, size_t n) { - if (!sc_strbuf_reserve(buf, n)) { - return false; - } - - memset(&buf->s[buf->len], c, n); - buf->len += n; - buf->s[buf->len] = '\0'; - - return true; -} - -void -sc_strbuf_shrink(struct sc_strbuf *buf) { - assert(buf->len <= buf->cap); - if (buf->len != buf->cap) { - char *s = realloc(buf->s, buf->len + 1); // +1 for '\0' - assert(s); // decreasing the size may not fail - buf->s = s; - buf->cap = buf->len; - } -} diff --git a/app/src/util/strbuf.h b/app/src/util/strbuf.h deleted file mode 100644 index 1878df2f..00000000 --- a/app/src/util/strbuf.h +++ /dev/null @@ -1,73 +0,0 @@ -#ifndef SC_STRBUF_H -#define SC_STRBUF_H - -#include "common.h" - -#include -#include -#include - -struct sc_strbuf { - char *s; - size_t len; - size_t cap; -}; - -/** - * Initialize the string buffer - * - * `buf->s` must be manually freed by the caller. - */ -bool -sc_strbuf_init(struct sc_strbuf *buf, size_t init_cap); - -/** - * Append a string - * - * Append `len` characters from `s` to the buffer. - */ -bool -sc_strbuf_append(struct sc_strbuf *buf, const char *s, size_t len); - -/** - * Append a char - * - * Append a single character to the buffer. - */ -bool -sc_strbuf_append_char(struct sc_strbuf *buf, const char c); - -/** - * Append a char `n` times - * - * Append the same characters `n` times to the buffer. - */ -bool -sc_strbuf_append_n(struct sc_strbuf *buf, const char c, size_t n); - -/** - * Append a NUL-terminated string - */ -static inline bool -sc_strbuf_append_str(struct sc_strbuf *buf, const char *s) { - return sc_strbuf_append(buf, s, strlen(s)); -} - -/** - * Append a static string - * - * Append a string whose size is known at compile time (for - * example a string literal). - */ -#define sc_strbuf_append_staticstr(BUF, S) \ - sc_strbuf_append(BUF, S, sizeof(S) - 1) - -/** - * Shrink the buffer capacity to its current length - * - * This resizes `buf->s` to fit the content. - */ -void -sc_strbuf_shrink(struct sc_strbuf *buf); - -#endif diff --git a/app/src/util/term.c b/app/src/util/term.c deleted file mode 100644 index ff6bc4b1..00000000 --- a/app/src/util/term.c +++ /dev/null @@ -1,51 +0,0 @@ -#include "term.h" - -#include - -#ifdef _WIN32 -# include -#else -# include -# include -#endif - -bool -sc_term_get_size(unsigned *rows, unsigned *cols) { -#ifdef _WIN32 - CONSOLE_SCREEN_BUFFER_INFO csbi; - - bool ok = - GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi); - if (!ok) { - return false; - } - - if (rows) { - assert(csbi.srWindow.Bottom >= csbi.srWindow.Top); - *rows = csbi.srWindow.Bottom - csbi.srWindow.Top + 1; - } - - if (cols) { - assert(csbi.srWindow.Right >= csbi.srWindow.Left); - *cols = csbi.srWindow.Right - csbi.srWindow.Left + 1; - } - - return true; -#else - struct winsize ws; - int r = ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws); - if (r == -1) { - return false; - } - - if (rows) { - *rows = ws.ws_row; - } - - if (cols) { - *cols = ws.ws_col; - } - - return true; -#endif -} diff --git a/app/src/util/term.h b/app/src/util/term.h deleted file mode 100644 index 0211bcb4..00000000 --- a/app/src/util/term.h +++ /dev/null @@ -1,21 +0,0 @@ -#ifndef SC_TERM_H -#define SC_TERM_H - -#include "common.h" - -#include - -/** - * Return the terminal dimensions - * - * Return false if the dimensions could not be retrieved. - * - * Otherwise, return true, and: - * - if `rows` is not NULL, then the number of rows is written to `*rows`. - * - if `columns` is not NULL, then the number of columns is written to - * `*columns`. - */ -bool -sc_term_get_size(unsigned *rows, unsigned *cols); - -#endif diff --git a/app/src/util/thread.c b/app/src/util/thread.c deleted file mode 100644 index 2a5253f7..00000000 --- a/app/src/util/thread.c +++ /dev/null @@ -1,220 +0,0 @@ -#include "thread.h" - -#include -#include -#include -#include -#include - -#include "util/log.h" - -sc_thread_id SC_MAIN_THREAD_ID; - -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; -} - -static SDL_ThreadPriority -to_sdl_thread_priority(enum sc_thread_priority priority) { - switch (priority) { - case SC_THREAD_PRIORITY_TIME_CRITICAL: -#ifdef SCRCPY_SDL_HAS_THREAD_PRIORITY_TIME_CRITICAL - return SDL_THREAD_PRIORITY_TIME_CRITICAL; -#else - // fall through -#endif - case SC_THREAD_PRIORITY_HIGH: - return SDL_THREAD_PRIORITY_HIGH; - case SC_THREAD_PRIORITY_NORMAL: - return SDL_THREAD_PRIORITY_NORMAL; - case SC_THREAD_PRIORITY_LOW: - return SDL_THREAD_PRIORITY_LOW; - default: - assert(!"Unknown thread priority"); - return 0; - } -} - -bool -sc_thread_set_priority(enum sc_thread_priority priority) { - SDL_ThreadPriority sdl_priority = to_sdl_thread_priority(priority); - int r = SDL_SetThreadPriority(sdl_priority); - if (r) { - LOGD("Could not set thread priority: %s", SDL_GetError()); - return false; - } - - return true; -} - -void -sc_thread_join(sc_thread *thread, int *status) { - SDL_WaitThread(thread->thread, status); -} - -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 -} diff --git a/app/src/util/thread.h b/app/src/util/thread.h deleted file mode 100644 index 3d544046..00000000 --- a/app/src/util/thread.h +++ /dev/null @@ -1,96 +0,0 @@ -#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; - -enum sc_thread_priority { - SC_THREAD_PRIORITY_LOW, - SC_THREAD_PRIORITY_NORMAL, - SC_THREAD_PRIORITY_HIGH, - SC_THREAD_PRIORITY_TIME_CRITICAL, -}; - -typedef struct sc_mutex { - SDL_mutex *mutex; -#ifndef NDEBUG - sc_atomic_thread_id locker; -#endif -} sc_mutex; - -typedef struct sc_cond { - SDL_cond *cond; -} sc_cond; - -extern sc_thread_id SC_MAIN_THREAD_ID; - -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_thread_set_priority(enum sc_thread_priority priority); - -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 diff --git a/app/src/util/tick.c b/app/src/util/tick.c deleted file mode 100644 index edef1070..00000000 --- a/app/src/util/tick.c +++ /dev/null @@ -1,56 +0,0 @@ -#include "tick.h" - -#include -#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 -} diff --git a/app/src/util/tick.h b/app/src/util/tick.h deleted file mode 100644 index b037734b..00000000 --- a/app/src/util/tick.h +++ /dev/null @@ -1,25 +0,0 @@ -#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) ((sc_tick) (tick) * 1000) -#define SC_TICK_TO_US(tick) ((sc_tick) tick) -#define SC_TICK_TO_MS(tick) ((sc_tick) (tick) / 1000) -#define SC_TICK_TO_SEC(tick) ((sc_tick) (tick) / 1000000) -#define SC_TICK_FROM_NS(ns) ((sc_tick) (ns) / 1000) -#define SC_TICK_FROM_US(us) ((sc_tick) us) -#define SC_TICK_FROM_MS(ms) ((sc_tick) (ms) * 1000) -#define SC_TICK_FROM_SEC(sec) ((sc_tick) (sec) * 1000000) - -sc_tick -sc_tick_now(void); - -#endif diff --git a/app/src/util/timeout.c b/app/src/util/timeout.c deleted file mode 100644 index 21bc3a53..00000000 --- a/app/src/util/timeout.c +++ /dev/null @@ -1,79 +0,0 @@ -#include "timeout.h" - -#include -#include - -#include "util/log.h" - -bool -sc_timeout_init(struct sc_timeout *timeout) { - bool ok = sc_mutex_init(&timeout->mutex); - if (!ok) { - return false; - } - - ok = sc_cond_init(&timeout->cond); - if (!ok) { - return false; - } - - timeout->stopped = false; - - return true; -} - -static int -run_timeout(void *data) { - struct sc_timeout *timeout = data; - sc_tick deadline = timeout->deadline; - - sc_mutex_lock(&timeout->mutex); - bool timed_out = false; - while (!timeout->stopped && !timed_out) { - timed_out = !sc_cond_timedwait(&timeout->cond, &timeout->mutex, - deadline); - } - sc_mutex_unlock(&timeout->mutex); - - timeout->cbs->on_timeout(timeout, timeout->cbs_userdata); - - return 0; -} - -bool -sc_timeout_start(struct sc_timeout *timeout, sc_tick deadline, - const struct sc_timeout_callbacks *cbs, void *cbs_userdata) { - bool ok = sc_thread_create(&timeout->thread, run_timeout, "scrcpy-timeout", - timeout); - if (!ok) { - LOGE("Timeout: could not start thread"); - return false; - } - - timeout->deadline = deadline; - - assert(cbs && cbs->on_timeout); - timeout->cbs = cbs; - timeout->cbs_userdata = cbs_userdata; - - return true; -} - -void -sc_timeout_stop(struct sc_timeout *timeout) { - sc_mutex_lock(&timeout->mutex); - timeout->stopped = true; - sc_cond_signal(&timeout->cond); - sc_mutex_unlock(&timeout->mutex); -} - -void -sc_timeout_join(struct sc_timeout *timeout) { - sc_thread_join(&timeout->thread, NULL); -} - -void -sc_timeout_destroy(struct sc_timeout *timeout) { - sc_mutex_destroy(&timeout->mutex); - sc_cond_destroy(&timeout->cond); -} diff --git a/app/src/util/timeout.h b/app/src/util/timeout.h deleted file mode 100644 index a45ae2ae..00000000 --- a/app/src/util/timeout.h +++ /dev/null @@ -1,43 +0,0 @@ -#ifndef SC_TIMEOUT_H -#define SC_TIMEOUT_H - -#include "common.h" - -#include - -#include "util/thread.h" -#include "util/tick.h" - -struct sc_timeout { - sc_thread thread; - sc_tick deadline; - - sc_mutex mutex; - sc_cond cond; - bool stopped; - - const struct sc_timeout_callbacks *cbs; - void *cbs_userdata; -}; - -struct sc_timeout_callbacks { - void (*on_timeout)(struct sc_timeout *timeout, void *userdata); -}; - -bool -sc_timeout_init(struct sc_timeout *timeout); - -void -sc_timeout_destroy(struct sc_timeout *timeout); - -bool -sc_timeout_start(struct sc_timeout *timeout, sc_tick deadline, - const struct sc_timeout_callbacks *cbs, void *cbs_userdata); - -void -sc_timeout_stop(struct sc_timeout *timeout); - -void -sc_timeout_join(struct sc_timeout *timeout); - -#endif diff --git a/app/src/util/vecdeque.h b/app/src/util/vecdeque.h deleted file mode 100644 index e31724e2..00000000 --- a/app/src/util/vecdeque.h +++ /dev/null @@ -1,380 +0,0 @@ -#ifndef SC_VECDEQUE_H -#define SC_VECDEQUE_H - -#include "common.h" - -#include -#include -#include -#include -#include -#include - -#include "util/memory.h" - -/** - * A double-ended queue implemented with a growable ring buffer. - * - * Inspired from the Rust VecDeque type: - * - */ - -/** - * VecDeque struct body - * - * A VecDeque is a dynamic ring-buffer, managed by the sc_vecdeque_* helpers. - * - * It is generic over the type of its items, so it is implemented via macros. - * - * To use a VecDeque, a new type must be defined: - * - * struct vecdeque_int SC_VECDEQUE(int); - * - * The struct may be anonymous: - * - * struct SC_VECDEQUE(const char *) names; - * - * Functions and macros having name ending with '_' are private. - */ -#define SC_VECDEQUE(type) { \ - size_t cap; \ - size_t origin; \ - size_t size; \ - type *data; \ -} - -/** - * Static initializer for a VecDeque - */ -#define SC_VECDEQUE_INITIALIZER { 0, 0, 0, NULL } - -/** - * Initialize an empty VecDeque - */ -#define sc_vecdeque_init(pv) \ -({ \ - (pv)->cap = 0; \ - (pv)->origin = 0; \ - (pv)->size = 0; \ - (pv)->data = NULL; \ -}) - -/** - * Destroy a VecDeque - */ -#define sc_vecdeque_destroy(pv) \ - free((pv)->data) - -/** - * Clear a VecDeque - * - * Remove all items. - */ -#define sc_vecdeque_clear(pv) \ -(void) ({ \ - sc_vecdeque_destroy(pv); \ - sc_vecdeque_init(pv); \ -}) - -/** - * Returns the content size - */ -#define sc_vecdeque_size(pv) \ - (pv)->size - -/** - * Return whether the VecDeque is empty (i.e. its size is 0) - */ -#define sc_vecdeque_is_empty(pv) \ - ((pv)->size == 0) - -/** - * Return whether the VecDeque is full - * - * A VecDeque is full when its size equals its current capacity. However, it - * does not prevent to push a new item (with sc_vecdeque_push()), since this - * will increase its capacity. - */ -#define sc_vecdeque_is_full(pv) \ - ((pv)->size == (pv)->cap) - -/** - * The minimal allocation size, in number of items - * - * Private. - */ -#define SC_VECDEQUE_MINCAP_ ((size_t) 10) - -/** - * The maximal allocation size, in number of items - * - * Use SIZE_MAX/2 to fit in ssize_t, and so that cap*1.5 does not overflow. - * - * Private. - */ -#define sc_vecdeque_max_cap_(pv) (SIZE_MAX / 2 / sizeof(*(pv)->data)) - -/** - * Realloc the internal array to a specific capacity - * - * On reallocation success, update the VecDeque capacity (`*pcap`) and origin - * (`*porigin`), and return the reallocated data. - * - * On reallocation failure, return NULL without any change. - * - * Private. - * - * \param ptr the current `data` field of the SC_VECDEQUE to realloc - * \param newcap the requested capacity, in number of items - * \param item_size the size of one item (the generic type is unknown from this - * function) - * \param pcap a pointer to the `cap` field of the SC_VECDEQUE [IN/OUT] - * \param porigin a pointer to pv->origin [IN/OUT] - * \param size the `size` field of the SC_VECDEQUE - * \return the new array to assign to the `data` field of the SC_VECDEQUE (if - * not NULL) - */ -static inline void * -sc_vecdeque_reallocdata_(void *ptr, size_t newcap, size_t item_size, - size_t *pcap, size_t *porigin, size_t size) { - - size_t oldcap = *pcap; - size_t oldorigin = *porigin; - - assert(newcap > oldcap); // Could only grow - - if (oldorigin + size <= oldcap) { - // The current content will stay in place, just realloc - // - // As an example, here is the content of a ring-buffer (oldcap=10) - // before the realloc: - // - // _ _ 2 3 4 5 6 7 _ _ - // ^ - // origin - // - // It is resized (newcap=15), e.g. with sc_vecdeque_reserve(): - // - // _ _ 2 3 4 5 6 7 _ _ _ _ _ _ _ - // ^ - // origin - - void *newptr = reallocarray(ptr, newcap, item_size); - if (!newptr) { - return NULL; - } - - *pcap = newcap; - return newptr; - } - - // Copy the current content to the new array - // - // As an example, here is the content of a ring-buffer (oldcap=10) before - // the realloc: - // - // 5 6 7 _ _ 0 1 2 3 4 - // ^ - // origin - // - // It is resized (newcap=15), e.g. with sc_vecdeque_reserve(): - // - // 0 1 2 3 4 5 6 7 _ _ _ _ _ _ _ - // ^ - // origin - - assert(size); - void *newptr = sc_allocarray(newcap, item_size); - if (!newptr) { - return NULL; - } - - size_t right_len = MIN(size, oldcap - oldorigin); - assert(right_len); - memcpy(newptr, (char *) ptr + (oldorigin * item_size), right_len * item_size); - - if (size > right_len) { - memcpy((char *) newptr + (right_len * item_size), ptr, - (size - right_len) * item_size); - } - - free(ptr); - - *pcap = newcap; - *porigin = 0; - return newptr; -} - -/** - * Macro to realloc the internal data to a new capacity - * - * Private. - * - * \retval true on success - * \retval false on allocation failure (the VecDeque is left untouched) - */ -#define sc_vecdeque_realloc_(pv, newcap) \ -({ \ - void *p = sc_vecdeque_reallocdata_((pv)->data, newcap, \ - sizeof(*(pv)->data), &(pv)->cap, \ - &(pv)->origin, (pv)->size); \ - if (p) { \ - (pv)->data = p; \ - } \ - (bool) p; \ -}); - -static inline size_t -sc_vecdeque_growsize_(size_t value) -{ - /* integer multiplication by 1.5 */ - return value + (value >> 1); -} - -/** - * Increase the capacity of the VecDeque to at least `mincap` - * - * \param pv a pointer to the VecDeque - * \param mincap (`size_t`) the requested capacity - * \retval true on success - * \retval false on allocation failure (the VecDeque is left untouched) - */ -#define sc_vecdeque_reserve(pv, mincap) \ -({ \ - assert(mincap <= sc_vecdeque_max_cap_(pv)); \ - bool ok; \ - /* avoid to allocate tiny arrays (< SC_VECDEQUE_MINCAP_) */ \ - size_t mincap_ = MAX(mincap, SC_VECDEQUE_MINCAP_); \ - if (mincap_ <= (pv)->cap) { \ - /* nothing to do */ \ - ok = true; \ - } else if (mincap_ <= sc_vecdeque_max_cap_(pv)) { \ - /* not too big */ \ - size_t newsize = sc_vecdeque_growsize_((pv)->cap); \ - newsize = CLAMP(newsize, mincap_, sc_vecdeque_max_cap_(pv)); \ - ok = sc_vecdeque_realloc_(pv, newsize); \ - } else { \ - ok = false; \ - } \ - ok; \ -}) - -/** - * Automatically grow the VecDeque capacity - * - * Private. - * - * \retval true on success - * \retval false on allocation failure (the VecDeque is left untouched) - */ -#define sc_vecdeque_grow_(pv) \ -({ \ - bool ok; \ - if ((pv)->cap < sc_vecdeque_max_cap_(pv)) { \ - size_t newsize = sc_vecdeque_growsize_((pv)->cap); \ - newsize = CLAMP(newsize, SC_VECDEQUE_MINCAP_, \ - sc_vecdeque_max_cap_(pv)); \ - ok = sc_vecdeque_realloc_(pv, newsize); \ - } else { \ - ok = false; \ - } \ - ok; \ -}) - -/** - * Grow the VecDeque capacity if it is full - * - * Private. - * - * \retval true on success - * \retval false on allocation failure (the VecDeque is left untouched) - */ -#define sc_vecdeque_grow_if_needed_(pv) \ - (!sc_vecdeque_is_full(pv) || sc_vecdeque_grow_(pv)) - -/** - * Push an uninitialized item, and return a pointer to it - * - * It does not attempt to resize the VecDeque. It is an error to this function - * if the VecDeque is full. - * - * This function may not fail. It returns a valid non-NULL pointer to the - * uninitialized item just pushed. - */ -#define sc_vecdeque_push_hole_noresize(pv) \ -({ \ - assert(!sc_vecdeque_is_full(pv)); \ - ++(pv)->size; \ - &(pv)->data[((pv)->origin + (pv)->size - 1) % (pv)->cap]; \ -}) - -/** - * Push an uninitialized item, and return a pointer to it - * - * If the VecDeque is full, it is resized. - * - * This function returns either a valid non-NULL pointer to the uninitialized - * item just pushed, or NULL on reallocation failure. - */ -#define sc_vecdeque_push_hole(pv) \ - (sc_vecdeque_grow_if_needed_(pv) ? \ - sc_vecdeque_push_hole_noresize(pv) : NULL) - -/** - * Push an item - * - * It does not attempt to resize the VecDeque. It is an error to this function - * if the VecDeque is full. - * - * This function may not fail. - */ -#define sc_vecdeque_push_noresize(pv, item) \ -(void) ({ \ - assert(!sc_vecdeque_is_full(pv)); \ - ++(pv)->size; \ - (pv)->data[((pv)->origin + (pv)->size - 1) % (pv)->cap] = item; \ -}) - -/** - * Push an item - * - * If the VecDeque is full, it is resized. - * - * \retval true on success - * \retval false on allocation failure (the VecDeque is left untouched) - */ -#define sc_vecdeque_push(pv, item) \ -({ \ - bool ok = sc_vecdeque_grow_if_needed_(pv); \ - if (ok) { \ - sc_vecdeque_push_noresize(pv, item); \ - } \ - ok; \ -}) - -/** - * Pop an item and return a pointer to it (still in the VecDeque) - * - * Returning a pointer allows the caller to destroy it in place without copy - * (especially if the item type is big). - * - * It is an error to call this function if the VecDeque is empty. - */ -#define sc_vecdeque_popref(pv) \ -({ \ - assert(!sc_vecdeque_is_empty(pv)); \ - size_t pos = (pv)->origin; \ - (pv)->origin = ((pv)->origin + 1) % (pv)->cap; \ - --(pv)->size; \ - &(pv)->data[pos]; \ -}) - -/** - * Pop an item and return it - * - * It is an error to call this function if the VecDeque is empty. - */ -#define sc_vecdeque_pop(pv) \ - (*sc_vecdeque_popref(pv)) - -#endif diff --git a/app/src/util/vector.h b/app/src/util/vector.h deleted file mode 100644 index 5b399d56..00000000 --- a/app/src/util/vector.h +++ /dev/null @@ -1,541 +0,0 @@ -#ifndef SC_VECTOR_H -#define SC_VECTOR_H - -#include "common.h" - -#include -#include -#include -#include - -// Adapted from vlc_vector: -// - -/** - * Vector struct body - * - * A vector is a dynamic array, managed by the sc_vector_* helpers. - * - * It is generic over the type of its items, so it is implemented as macros. - * - * To use a vector, a new type must be defined: - * - * struct vec_int SC_VECTOR(int); - * - * The struct may be anonymous: - * - * struct SC_VECTOR(const char *) names; - * - * Vector size is accessible via `vec.size`, and items are intended to be - * accessed directly, via `vec.data[i]`. - * - * Functions and macros having name ending with '_' are private. - */ -#define SC_VECTOR(type) \ -{ \ - size_t cap; \ - size_t size; \ - type *data; \ -} - -/** - * Static initializer for a vector - */ -#define SC_VECTOR_INITIALIZER { 0, 0, NULL } - -/** - * Initialize an empty vector - */ -#define sc_vector_init(pv) \ -({ \ - (pv)->cap = 0; \ - (pv)->size = 0; \ - (pv)->data = NULL; \ -}) - -/** - * Destroy a vector - * - * The vector may not be used anymore unless sc_vector_init() is called. - */ -#define sc_vector_destroy(pv) \ - free((pv)->data) - -/** - * Clear a vector - * - * Remove all items from the vector. - */ -#define sc_vector_clear(pv) \ -({ \ - sc_vector_destroy(pv); \ - sc_vector_init(pv);\ -}) - -/** - * The minimal allocation size, in number of items - * - * Private. - */ -#define SC_VECTOR_MINCAP_ ((size_t) 10) - -static inline size_t -sc_vector_min_(size_t a, size_t b) -{ - return a < b ? a : b; -} - -static inline size_t -sc_vector_max_(size_t a, size_t b) -{ - return a > b ? a : b; -} - -static inline size_t -sc_vector_clamp_(size_t x, size_t min, size_t max) -{ - return sc_vector_max_(min, sc_vector_min_(max, x)); -} - -/** - * Realloc data and update vector fields - * - * On reallocation success, update the vector capacity (*pcap) and size - * (*psize), and return the reallocated data. - * - * On reallocation failure, return NULL without any change. - * - * Private. - * - * \param ptr the current `data` field of the vector to realloc - * \param count the requested capacity, in number of items - * \param size the size of one item - * \param pcap a pointer to the `cap` field of the vector [IN/OUT] - * \param psize a pointer to the `size` field of the vector [IN/OUT] - * \return the new ptr on success, NULL on error - */ -static inline void * -sc_vector_reallocdata_(void *ptr, size_t count, size_t size, - size_t *restrict pcap, size_t *restrict psize) -{ - void *p = reallocarray(ptr, count, size); - if (!p) { - return NULL; - } - - *pcap = count; - *psize = sc_vector_min_(*psize, count); - return p; -} - -#define sc_vector_realloc_(pv, newcap) \ -({ \ - void *p = sc_vector_reallocdata_((pv)->data, newcap, sizeof(*(pv)->data), \ - &(pv)->cap, &(pv)->size); \ - if (p) { \ - (pv)->data = p; \ - } \ - (bool) p; \ -}); - -#define sc_vector_resize_(pv, newcap) \ -({ \ - bool ok; \ - if ((pv)->cap == (newcap)) { \ - ok = true; \ - } else if ((newcap) > 0) { \ - ok = sc_vector_realloc_(pv, (newcap)); \ - } else { \ - sc_vector_clear(pv); \ - ok = true; \ - } \ - ok; \ -}) - -static inline size_t -sc_vector_growsize_(size_t value) -{ - /* integer multiplication by 1.5 */ - return value + (value >> 1); -} - -/* SIZE_MAX/2 to fit in ssize_t, and so that cap*1.5 does not overflow. */ -#define sc_vector_max_cap_(pv) (SIZE_MAX / 2 / sizeof(*(pv)->data)) - -/** - * Increase the capacity of the vector to at least `mincap` - * - * \param pv a pointer to the vector - * \param mincap (size_t) the requested capacity - * \retval true if no allocation failed - * \retval false on allocation failure (the vector is left untouched) - */ -#define sc_vector_reserve(pv, mincap) \ -({ \ - bool ok; \ - /* avoid to allocate tiny arrays (< SC_VECTOR_MINCAP_) */ \ - size_t mincap_ = sc_vector_max_(mincap, SC_VECTOR_MINCAP_); \ - if (mincap_ <= (pv)->cap) { \ - /* nothing to do */ \ - ok = true; \ - } else if (mincap_ <= sc_vector_max_cap_(pv)) { \ - /* not too big */ \ - size_t newsize = sc_vector_growsize_((pv)->cap); \ - newsize = sc_vector_clamp_(newsize, mincap_, sc_vector_max_cap_(pv)); \ - ok = sc_vector_realloc_(pv, newsize); \ - } else { \ - ok = false; \ - } \ - ok; \ -}) - -#define sc_vector_shrink_to_fit(pv) \ - /* decreasing the size may not fail */ \ - (void) sc_vector_resize_(pv, (pv)->size) - -/** - * Resize the vector down automatically - * - * Shrink only when necessary (in practice when cap > (size+5)*1.5) - * - * \param pv a pointer to the vector - */ -#define sc_vector_autoshrink(pv) \ -({ \ - bool must_shrink = \ - /* do not shrink to tiny size */ \ - (pv)->cap > SC_VECTOR_MINCAP_ && \ - /* no need to shrink */ \ - (pv)->cap >= sc_vector_growsize_((pv)->size + 5); \ - if (must_shrink) { \ - size_t newsize = sc_vector_max_((pv)->size + 5, SC_VECTOR_MINCAP_); \ - sc_vector_resize_(pv, newsize); \ - } \ -}) - -#define sc_vector_check_same_ptr_type_(a, b) \ - (void) ((a) == (b)) /* warn on type mismatch */ - -/** - * Push an item at the end of the vector - * - * The amortized complexity is O(1). - * - * \param pv a pointer to the vector - * \param item the item to append - * \retval true if no allocation failed - * \retval false on allocation failure (the vector is left untouched) - */ -#define sc_vector_push(pv, item) \ -({ \ - bool ok = sc_vector_reserve(pv, (pv)->size + 1); \ - if (ok) { \ - (pv)->data[(pv)->size++] = (item); \ - } \ - ok; \ -}) - -/** - * Append `count` items at the end of the vector - * - * \param pv a pointer to the vector - * \param items the items array to append - * \param count the number of items in the array - * \retval true if no allocation failed - * \retval false on allocation failure (the vector is left untouched) - */ -#define sc_vector_push_all(pv, items, count) \ - sc_vector_push_all_(pv, items, (size_t) count) - -#define sc_vector_push_all_(pv, items, count) \ -({ \ - sc_vector_check_same_ptr_type_((pv)->data, items); \ - bool ok = sc_vector_reserve(pv, (pv)->size + (count)); \ - if (ok) { \ - memcpy(&(pv)->data[(pv)->size], items, (count) * sizeof(*(pv)->data)); \ - (pv)->size += count; \ - } \ - ok; \ -}) - -/** - * Insert an hole of size `count` to the given index - * - * The items in range [index; size-1] will be moved. The items in the hole are - * left uninitialized. - * - * \param pv a pointer to the vector - * \param index the index where the hole is to be inserted - * \param count the number of items in the hole - * \retval true if no allocation failed - * \retval false on allocation failure (the vector is left untouched) - */ -#define sc_vector_insert_hole(pv, index, count) \ - sc_vector_insert_hole_(pv, (size_t) index, (size_t) count); - -#define sc_vector_insert_hole_(pv, index, count) \ -({ \ - bool ok = sc_vector_reserve(pv, (pv)->size + (count)); \ - if (ok) { \ - if ((index) < (pv)->size) { \ - memmove(&(pv)->data[(index) + (count)], \ - &(pv)->data[(index)], \ - ((pv)->size - (index)) * sizeof(*(pv)->data)); \ - } \ - (pv)->size += count; \ - } \ - ok; \ -}) - -/** - * Insert an item at the given index - * - * The items in range [index; size-1] will be moved. - * - * \param pv a pointer to the vector - * \param index the index where the item is to be inserted - * \param item the item to append - * \retval true if no allocation failed - * \retval false on allocation failure (the vector is left untouched) - */ -#define sc_vector_insert(pv, index, item) \ - sc_vector_insert_(pv, (size_t) index, (size_t) item); - -#define sc_vector_insert_(pv, index, item) \ -({ \ - bool ok = sc_vector_insert_hole_(pv, index, 1); \ - if (ok) { \ - (pv)->data[index] = (item); \ - } \ - ok; \ -}) - -/** - * Insert `count` items at the given index - * - * The items in range [index; size-1] will be moved. - * - * \param pv a pointer to the vector - * \param index the index where the items are to be inserted - * \param items the items array to append - * \param count the number of items in the array - * \retval true if no allocation failed - * \retval false on allocation failure (the vector is left untouched) - */ -#define sc_vector_insert_all(pv, index, items, count) \ - sc_vector_insert_all_(pv, (size_t) index, items, (size_t) count) - -#define sc_vector_insert_all_(pv, index, items, count) \ -({ \ - sc_vector_check_same_ptr_type_((pv)->data, items); \ - bool ok = sc_vector_insert_hole_(pv, index, count); \ - if (ok) { \ - memcpy(&(pv)->data[index], items, count * sizeof(*(pv)->data)); \ - } \ - ok; \ -}) - -/** Reverse a char array in place */ -static inline void -sc_char_array_reverse(char *array, size_t len) -{ - for (size_t i = 0; i < len / 2; ++i) - { - char c = array[i]; - array[i] = array[len - i - 1]; - array[len - i - 1] = c; - } -} - -/** - * Right-rotate a (char) array in place - * - * For example, left-rotating a char array containing {1, 2, 3, 4, 5, 6} with - * distance 4 will result in {5, 6, 1, 2, 3, 4}. - * - * Private. - */ -static inline void -sc_char_array_rotate_left(char *array, size_t len, size_t distance) -{ - sc_char_array_reverse(array, distance); - sc_char_array_reverse(&array[distance], len - distance); - sc_char_array_reverse(array, len); -} - -/** - * Right-rotate a (char) array in place - * - * For example, left-rotating a char array containing {1, 2, 3, 4, 5, 6} with - * distance 2 will result in {5, 6, 1, 2, 3, 4}. - * - * Private. - */ -static inline void -sc_char_array_rotate_right(char *array, size_t len, size_t distance) -{ - sc_char_array_rotate_left(array, len, len - distance); -} - -/** - * Move items in a (char) array in place - * - * Move slice [index, count] to target. - */ -static inline void -sc_char_array_move(char *array, size_t idx, size_t count, size_t target) -{ - if (idx < target) { - sc_char_array_rotate_left(&array[idx], target - idx + count, count); - } else { - sc_char_array_rotate_right(&array[target], idx - target + count, count); - } -} - -/** - * Move a slice of items to a given target index - * - * The items in range [index; count] will be moved so that the *new* position - * of the first item is `target`. - * - * \param pv a pointer to the vector - * \param index the index of the first item to move - * \param count the number of items to move - * \param target the new index of the moved slice - */ -#define sc_vector_move_slice(pv, index, count, target) \ - sc_vector_move_slice_(pv, (size_t) index, count, (size_t) target); - -#define sc_vector_move_slice_(pv, index, count, target) \ -({ \ - sc_char_array_move((char *) (pv)->data, \ - (index) * sizeof(*(pv)->data), \ - (count) * sizeof(*(pv)->data), \ - (target) * sizeof(*(pv)->data)); \ -}) - -/** - * Move an item to a given target index - * - * The items will be moved so that its *new* position is `target`. - * - * \param pv a pointer to the vector - * \param index the index of the item to move - * \param target the new index of the moved item - */ -#define sc_vector_move(pv, index, target) \ - sc_vector_move_slice(pv, index, 1, target) - -/** - * Remove a slice of items, without shrinking the array - * - * If you have no good reason to use the _noshrink() version, use - * sc_vector_remove_slice() instead. - * - * The items in range [index+count; size-1] will be moved. - * - * \param pv a pointer to the vector - * \param index the index of the first item to remove - * \param count the number of items to remove - */ -#define sc_vector_remove_slice_noshrink(pv, index, count) \ - sc_vector_remove_slice_noshrink_(pv, (size_t) index, (size_t) count) - -#define sc_vector_remove_slice_noshrink_(pv, index, count) \ -({ \ - if ((index) + (count) < (pv)->size) { \ - memmove(&(pv)->data[index], \ - &(pv)->data[(index) + (count)], \ - ((pv)->size - (index) - (count)) * sizeof(*(pv)->data)); \ - } \ - (pv)->size -= count; \ -}) - -/** - * Remove a slice of items - * - * The items in range [index+count; size-1] will be moved. - * - * \param pv a pointer to the vector - * \param index the index of the first item to remove - * \param count the number of items to remove - */ -#define sc_vector_remove_slice(pv, index, count) \ -({ \ - sc_vector_remove_slice_noshrink(pv, index, count); \ - sc_vector_autoshrink(pv); \ -}) - -/** - * Remove an item, without shrinking the array - * - * If you have no good reason to use the _noshrink() version, use - * sc_vector_remove() instead. - * - * The items in range [index+1; size-1] will be moved. - * - * \param pv a pointer to the vector - * \param index the index of item to remove - */ -#define sc_vector_remove_noshrink(pv, index) \ - sc_vector_remove_slice_noshrink(pv, index, 1) - -/** - * Remove an item - * - * The items in range [index+1; size-1] will be moved. - * - * \param pv a pointer to the vector - * \param index the index of item to remove - */ -#define sc_vector_remove(pv, index) \ -({ \ - sc_vector_remove_noshrink(pv, index); \ - sc_vector_autoshrink(pv); \ -}) - -/** - * Remove an item - * - * The removed item is replaced by the last item of the vector. - * - * This does not preserve ordering, but is O(1). This is useful when the order - * of items is not meaningful. - * - * \param pv a pointer to the vector - * \param index the index of item to remove - */ -#define sc_vector_swap_remove(pv, index) \ - sc_vector_swap_remove_(pv, (size_t) index); - -#define sc_vector_swap_remove_(pv, index) \ -({ \ - (pv)->data[index] = (pv)->data[(pv)->size-1]; \ - (pv)->size--; \ -}); - -/** - * Return the index of an item - * - * Iterate over all items to find a given item. - * - * Use only for vectors of primitive types or pointers. - * - * Return the index, or -1 if not found. - * - * \param pv a pointer to the vector - * \param item the item to find (compared with ==) - */ -#define sc_vector_index_of(pv, item) \ -({ \ - ssize_t idx = -1; \ - for (size_t i = 0; i < (pv)->size; ++i) { \ - if ((pv)->data[i] == (item)) { \ - idx = (ssize_t) i; \ - break; \ - } \ - } \ - idx; \ -}) - -#endif diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c deleted file mode 100644 index da9e02ef..00000000 --- a/app/src/v4l2_sink.c +++ /dev/null @@ -1,368 +0,0 @@ -#include "v4l2_sink.h" - -#include -#include -#include -#include -#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_frame_buffer_consume(&vs->fb, vs->frame); - - bool ok = encode_and_write_frame(vs, vs->frame); - av_frame_unref(vs->frame); - if (!ok) { - LOGE("Could not send frame to v4l2 sink"); - break; - } - } - - LOGD("V4l2 thread ended"); - - return 0; -} - -static bool -sc_v4l2_sink_open(struct sc_v4l2_sink *vs, const AVCodecContext *ctx) { - assert(ctx->pix_fmt == AV_PIX_FMT_YUV420P); - (void) ctx; - - bool ok = sc_frame_buffer_init(&vs->fb); - if (!ok) { - return false; - } - - ok = sc_mutex_init(&vs->mutex); - if (!ok) { - goto error_frame_buffer_destroy; - } - - 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; - } - - int r = avcodec_parameters_from_context(ostream->codecpar, ctx); - if (r < 0) { - goto error_avformat_free_context; - } - - // The codec is from the v4l2 encoder, not from the decoder - ostream->codecpar->codec_id = encoder->id; - - 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 = ctx->width; - vs->encoder_ctx->height = ctx->height; - vs->encoder_ctx->pix_fmt = AV_PIX_FMT_YUV420P; - vs->encoder_ctx->time_base.num = 1; - vs->encoder_ctx->time_base.den = 1; - - 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_free_context; - } - - 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_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_frame_buffer_destroy: - sc_frame_buffer_destroy(&vs->fb); - - return false; -} - -static void -sc_v4l2_sink_close(struct sc_v4l2_sink *vs) { - sc_mutex_lock(&vs->mutex); - vs->stopped = true; - sc_cond_signal(&vs->cond); - sc_mutex_unlock(&vs->mutex); - - sc_thread_join(&vs->thread, NULL); - - av_packet_free(&vs->packet); - av_frame_free(&vs->frame); - avcodec_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_frame_buffer_destroy(&vs->fb); -} - -static bool -sc_v4l2_sink_push(struct sc_v4l2_sink *vs, const AVFrame *frame) { - bool previous_skipped; - bool ok = sc_frame_buffer_push(&vs->fb, frame, &previous_skipped); - if (!ok) { - return false; - } - - if (!previous_skipped) { - sc_mutex_lock(&vs->mutex); - vs->has_frame = true; - sc_cond_signal(&vs->cond); - sc_mutex_unlock(&vs->mutex); - } - - return true; -} - -static bool -sc_v4l2_frame_sink_open(struct sc_frame_sink *sink, const AVCodecContext *ctx) { - struct sc_v4l2_sink *vs = DOWNCAST(sink); - return sc_v4l2_sink_open(vs, ctx); -} - -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) { - vs->device_name = strdup(device_name); - if (!vs->device_name) { - LOGE("Could not strdup v4l2 device name"); - return false; - } - - static const struct sc_frame_sink_ops ops = { - .open = sc_v4l2_frame_sink_open, - .close = sc_v4l2_frame_sink_close, - .push = sc_v4l2_frame_sink_push, - }; - - vs->frame_sink.ops = &ops; - - return true; -} - -void -sc_v4l2_sink_destroy(struct sc_v4l2_sink *vs) { - free(vs->device_name); -} diff --git a/app/src/v4l2_sink.h b/app/src/v4l2_sink.h deleted file mode 100644 index 2b7c5b50..00000000 --- a/app/src/v4l2_sink.h +++ /dev/null @@ -1,40 +0,0 @@ -#ifndef SC_V4L2_SINK_H -#define SC_V4L2_SINK_H - -#include "common.h" - -#include -#include -#include - -#include "frame_buffer.h" -#include "trait/frame_sink.h" -#include "util/thread.h" - -struct sc_v4l2_sink { - struct sc_frame_sink frame_sink; // frame sink trait - - struct sc_frame_buffer fb; - AVFormatContext *format_ctx; - AVCodecContext *encoder_ctx; - - char *device_name; - - 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); - -void -sc_v4l2_sink_destroy(struct sc_v4l2_sink *vs); - -#endif diff --git a/app/src/version.c b/app/src/version.c deleted file mode 100644 index f8610714..00000000 --- a/app/src/version.c +++ /dev/null @@ -1,69 +0,0 @@ -#include "version.h" - -#include -#include -#include -#include -#ifdef HAVE_V4L2 -# include -#endif -#ifdef HAVE_USB -# include -#endif -#include - -void -scrcpy_print_version(void) { - printf("\nDependencies (compiled / linked):\n"); - - SDL_version sdl; - SDL_GetVersion(&sdl); - printf(" - SDL: %u.%u.%u / %u.%u.%u\n", - SDL_MAJOR_VERSION, SDL_MINOR_VERSION, SDL_PATCHLEVEL, - (unsigned) sdl.major, (unsigned) sdl.minor, (unsigned) sdl.patch); - - unsigned avcodec = avcodec_version(); - printf(" - libavcodec: %u.%u.%u / %u.%u.%u\n", - LIBAVCODEC_VERSION_MAJOR, - LIBAVCODEC_VERSION_MINOR, - LIBAVCODEC_VERSION_MICRO, - AV_VERSION_MAJOR(avcodec), - AV_VERSION_MINOR(avcodec), - AV_VERSION_MICRO(avcodec)); - - unsigned avformat = avformat_version(); - printf(" - libavformat: %u.%u.%u / %u.%u.%u\n", - LIBAVFORMAT_VERSION_MAJOR, - LIBAVFORMAT_VERSION_MINOR, - LIBAVFORMAT_VERSION_MICRO, - AV_VERSION_MAJOR(avformat), - AV_VERSION_MINOR(avformat), - AV_VERSION_MICRO(avformat)); - - unsigned avutil = avutil_version(); - printf(" - libavutil: %u.%u.%u / %u.%u.%u\n", - LIBAVUTIL_VERSION_MAJOR, - LIBAVUTIL_VERSION_MINOR, - LIBAVUTIL_VERSION_MICRO, - AV_VERSION_MAJOR(avutil), - AV_VERSION_MINOR(avutil), - AV_VERSION_MICRO(avutil)); - -#ifdef HAVE_V4L2 - unsigned avdevice = avdevice_version(); - printf(" - libavdevice: %u.%u.%u / %u.%u.%u\n", - LIBAVDEVICE_VERSION_MAJOR, - LIBAVDEVICE_VERSION_MINOR, - LIBAVDEVICE_VERSION_MICRO, - AV_VERSION_MAJOR(avdevice), - AV_VERSION_MINOR(avdevice), - AV_VERSION_MICRO(avdevice)); -#endif - -#ifdef HAVE_USB - const struct libusb_version *usb = libusb_get_version(); - // The compiled version may not be known - printf(" - libusb: - / %u.%u.%u\n", - (unsigned) usb->major, (unsigned) usb->minor, (unsigned) usb->micro); -#endif -} diff --git a/app/src/version.h b/app/src/version.h deleted file mode 100644 index 920360e8..00000000 --- a/app/src/version.h +++ /dev/null @@ -1,9 +0,0 @@ -#ifndef SC_VERSION_H -#define SC_VERSION_H - -#include "common.h" - -void -scrcpy_print_version(void); - -#endif diff --git a/app/src/video_buffer.c b/app/src/video_buffer.c new file mode 100644 index 00000000..2b5f1c2f --- /dev/null +++ b/app/src/video_buffer.c @@ -0,0 +1,113 @@ +#include "video_buffer.h" + +#include +#include +#include +#include + +#include "config.h" +#include "lock_util.h" +#include "log.h" + +bool +video_buffer_init(struct video_buffer *vb, struct fps_counter *fps_counter, + bool render_expired_frames) { + vb->fps_counter = fps_counter; + + if (!(vb->decoding_frame = av_frame_alloc())) { + goto error_0; + } + + if (!(vb->rendering_frame = av_frame_alloc())) { + goto error_1; + } + + if (!(vb->mutex = SDL_CreateMutex())) { + goto error_2; + } + + vb->render_expired_frames = render_expired_frames; + if (render_expired_frames) { + if (!(vb->rendering_frame_consumed_cond = SDL_CreateCond())) { + SDL_DestroyMutex(vb->mutex); + goto error_2; + } + // interrupted is not used if expired frames are not rendered + // since offering a frame will never block + vb->interrupted = false; + } + + // there is initially no rendering frame, so consider it has already been + // consumed + vb->rendering_frame_consumed = true; + + return true; + +error_2: + av_frame_free(&vb->rendering_frame); +error_1: + av_frame_free(&vb->decoding_frame); +error_0: + return false; +} + +void +video_buffer_destroy(struct video_buffer *vb) { + if (vb->render_expired_frames) { + SDL_DestroyCond(vb->rendering_frame_consumed_cond); + } + SDL_DestroyMutex(vb->mutex); + av_frame_free(&vb->rendering_frame); + av_frame_free(&vb->decoding_frame); +} + +static void +video_buffer_swap_frames(struct video_buffer *vb) { + AVFrame *tmp = vb->decoding_frame; + vb->decoding_frame = vb->rendering_frame; + vb->rendering_frame = tmp; +} + +void +video_buffer_offer_decoded_frame(struct video_buffer *vb, + bool *previous_frame_skipped) { + mutex_lock(vb->mutex); + if (vb->render_expired_frames) { + // wait for the current (expired) frame to be consumed + while (!vb->rendering_frame_consumed && !vb->interrupted) { + cond_wait(vb->rendering_frame_consumed_cond, vb->mutex); + } + } else if (!vb->rendering_frame_consumed) { + fps_counter_add_skipped_frame(vb->fps_counter); + } + + video_buffer_swap_frames(vb); + + *previous_frame_skipped = !vb->rendering_frame_consumed; + vb->rendering_frame_consumed = false; + + mutex_unlock(vb->mutex); +} + +const AVFrame * +video_buffer_consume_rendered_frame(struct video_buffer *vb) { + SDL_assert(!vb->rendering_frame_consumed); + vb->rendering_frame_consumed = true; + fps_counter_add_rendered_frame(vb->fps_counter); + if (vb->render_expired_frames) { + // unblock video_buffer_offer_decoded_frame() + cond_signal(vb->rendering_frame_consumed_cond); + } + return vb->rendering_frame; +} + +void +video_buffer_interrupt(struct video_buffer *vb) { + if (vb->render_expired_frames) { + mutex_lock(vb->mutex); + vb->interrupted = true; + mutex_unlock(vb->mutex); + // wake up blocking wait + cond_signal(vb->rendering_frame_consumed_cond); + } +} diff --git a/app/src/video_buffer.h b/app/src/video_buffer.h new file mode 100644 index 00000000..26a6fa1f --- /dev/null +++ b/app/src/video_buffer.h @@ -0,0 +1,48 @@ +#ifndef VIDEO_BUFFER_H +#define VIDEO_BUFFER_H + +#include +#include + +#include "fps_counter.h" + +// forward declarations +typedef struct AVFrame AVFrame; + +struct video_buffer { + AVFrame *decoding_frame; + AVFrame *rendering_frame; + SDL_mutex *mutex; + bool render_expired_frames; + bool interrupted; + SDL_cond *rendering_frame_consumed_cond; + bool rendering_frame_consumed; + struct fps_counter *fps_counter; +}; + +bool +video_buffer_init(struct video_buffer *vb, struct fps_counter *fps_counter, + bool render_expired_frames); + +void +video_buffer_destroy(struct video_buffer *vb); + +// set the decoded frame as ready for rendering +// this function locks frames->mutex during its execution +// the output flag is set to report whether the previous frame has been skipped +void +video_buffer_offer_decoded_frame(struct video_buffer *vb, + bool *previous_frame_skipped); + +// mark the rendering frame as consumed and return it +// MUST be called with frames->mutex locked!!! +// the caller is expected to render the returned frame to some texture before +// unlocking frames->mutex +const AVFrame * +video_buffer_consume_rendered_frame(struct video_buffer *vb); + +// wake up and avoid any blocking call +void +video_buffer_interrupt(struct video_buffer *vb); + +#endif diff --git a/app/tests/test_adb_parser.c b/app/tests/test_adb_parser.c deleted file mode 100644 index 362b254f..00000000 --- a/app/tests/test_adb_parser.c +++ /dev/null @@ -1,280 +0,0 @@ -#include "common.h" - -#include - -#include "adb/adb_device.h" -#include "adb/adb_parser.h" - -static void test_adb_devices(void) { - char output[] = - "List of devices attached\n" - "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " - "device:MyDevice transport_id:1\n" - "192.168.1.1:5555 device product:MyWifiProduct model:MyWifiModel " - "device:MyWifiDevice trandport_id:2\n"; - - struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER; - bool ok = sc_adb_parse_devices(output, &vec); - assert(ok); - assert(vec.size == 2); - - struct sc_adb_device *device = &vec.data[0]; - assert(!strcmp("0123456789abcdef", device->serial)); - assert(!strcmp("device", device->state)); - assert(!strcmp("MyModel", device->model)); - - device = &vec.data[1]; - assert(!strcmp("192.168.1.1:5555", device->serial)); - assert(!strcmp("device", device->state)); - assert(!strcmp("MyWifiModel", device->model)); - - sc_adb_devices_destroy(&vec); -} - -static void test_adb_devices_cr(void) { - char output[] = - "List of devices attached\r\n" - "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " - "device:MyDevice transport_id:1\r\n" - "192.168.1.1:5555 device product:MyWifiProduct model:MyWifiModel " - "device:MyWifiDevice trandport_id:2\r\n"; - - struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER; - bool ok = sc_adb_parse_devices(output, &vec); - assert(ok); - assert(vec.size == 2); - - struct sc_adb_device *device = &vec.data[0]; - assert(!strcmp("0123456789abcdef", device->serial)); - assert(!strcmp("device", device->state)); - assert(!strcmp("MyModel", device->model)); - - device = &vec.data[1]; - assert(!strcmp("192.168.1.1:5555", device->serial)); - assert(!strcmp("device", device->state)); - assert(!strcmp("MyWifiModel", device->model)); - - sc_adb_devices_destroy(&vec); -} - -static void test_adb_devices_daemon_start(void) { - char output[] = - "* daemon not running; starting now at tcp:5037\n" - "* daemon started successfully\n" - "List of devices attached\n" - "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " - "device:MyDevice transport_id:1\n"; - - struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER; - bool ok = sc_adb_parse_devices(output, &vec); - assert(ok); - assert(vec.size == 1); - - struct sc_adb_device *device = &vec.data[0]; - assert(!strcmp("0123456789abcdef", device->serial)); - assert(!strcmp("device", device->state)); - assert(!strcmp("MyModel", device->model)); - - sc_adb_devices_destroy(&vec); -} - -static void test_adb_devices_daemon_start_mixed(void) { - char output[] = - "List of devices attached\n" - "adb server version (41) doesn't match this client (39); killing...\n" - "* daemon started successfully *\n" - "0123456789abcdef unauthorized usb:1-1\n" - "87654321 device usb:2-1 product:MyProduct model:MyModel " - "device:MyDevice\n"; - - struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER; - bool ok = sc_adb_parse_devices(output, &vec); - assert(ok); - assert(vec.size == 2); - - struct sc_adb_device *device = &vec.data[0]; - assert(!strcmp("0123456789abcdef", device->serial)); - assert(!strcmp("unauthorized", device->state)); - assert(!device->model); - - device = &vec.data[1]; - assert(!strcmp("87654321", device->serial)); - assert(!strcmp("device", device->state)); - assert(!strcmp("MyModel", device->model)); - - sc_adb_devices_destroy(&vec); -} - -static void test_adb_devices_without_eol(void) { - char output[] = - "List of devices attached\n" - "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " - "device:MyDevice transport_id:1"; - - struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER; - bool ok = sc_adb_parse_devices(output, &vec); - assert(ok); - assert(vec.size == 1); - - struct sc_adb_device *device = &vec.data[0]; - assert(!strcmp("0123456789abcdef", device->serial)); - assert(!strcmp("device", device->state)); - assert(!strcmp("MyModel", device->model)); - - sc_adb_devices_destroy(&vec); -} - -static void test_adb_devices_without_header(void) { - char output[] = - "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " - "device:MyDevice transport_id:1\n"; - - struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER; - bool ok = sc_adb_parse_devices(output, &vec); - assert(!ok); -} - -static void test_adb_devices_corrupted(void) { - char output[] = - "List of devices attached\n" - "corrupted_garbage\n"; - - struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER; - bool ok = sc_adb_parse_devices(output, &vec); - assert(ok); - assert(vec.size == 0); -} - -static void test_adb_devices_spaces(void) { - char output[] = - "List of devices attached\n" - "0123456789abcdef unauthorized usb:1-4 transport_id:3\n"; - - struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER; - bool ok = sc_adb_parse_devices(output, &vec); - assert(ok); - assert(vec.size == 1); - - struct sc_adb_device *device = &vec.data[0]; - assert(!strcmp("0123456789abcdef", device->serial)); - assert(!strcmp("unauthorized", device->state)); - assert(!device->model); - - sc_adb_devices_destroy(&vec); -} - -static void test_get_ip_single_line(void) { - char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src " - "192.168.12.34\r\r\n"; - - char *ip = sc_adb_parse_device_ip(ip_route); - assert(ip); - assert(!strcmp(ip, "192.168.12.34")); - free(ip); -} - -static void test_get_ip_single_line_without_eol(void) { - char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src " - "192.168.12.34"; - - char *ip = sc_adb_parse_device_ip(ip_route); - assert(ip); - assert(!strcmp(ip, "192.168.12.34")); - free(ip); -} - -static void test_get_ip_single_line_with_trailing_space(void) { - char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src " - "192.168.12.34 \n"; - - char *ip = sc_adb_parse_device_ip(ip_route); - assert(ip); - assert(!strcmp(ip, "192.168.12.34")); - free(ip); -} - -static void test_get_ip_multiline_first_ok(void) { - char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src " - "192.168.1.2\r\n" - "10.0.0.0/24 dev rmnet proto kernel scope link src " - "10.0.0.2\r\n"; - - char *ip = sc_adb_parse_device_ip(ip_route); - assert(ip); - assert(!strcmp(ip, "192.168.1.2")); - free(ip); -} - -static void test_get_ip_multiline_second_ok(void) { - char ip_route[] = "10.0.0.0/24 dev rmnet proto kernel scope link src " - "10.0.0.3\r\n" - "192.168.1.0/24 dev wlan0 proto kernel scope link src " - "192.168.1.3\r\n"; - - char *ip = sc_adb_parse_device_ip(ip_route); - assert(ip); - assert(!strcmp(ip, "192.168.1.3")); - free(ip); -} - -static void test_get_ip_multiline_second_ok_without_cr(void) { - char ip_route[] = "10.0.0.0/24 dev rmnet proto kernel scope link src " - "10.0.0.3\n" - "192.168.1.0/24 dev wlan0 proto kernel scope link src " - "192.168.1.3\n"; - - char *ip = sc_adb_parse_device_ip(ip_route); - assert(ip); - assert(!strcmp(ip, "192.168.1.3")); - free(ip); -} - -static void test_get_ip_no_wlan(void) { - char ip_route[] = "192.168.1.0/24 dev rmnet proto kernel scope link src " - "192.168.12.34\r\r\n"; - - char *ip = sc_adb_parse_device_ip(ip_route); - assert(!ip); -} - -static void test_get_ip_no_wlan_without_eol(void) { - char ip_route[] = "192.168.1.0/24 dev rmnet proto kernel scope link src " - "192.168.12.34"; - - char *ip = sc_adb_parse_device_ip(ip_route); - assert(!ip); -} - -static void test_get_ip_truncated(void) { - char ip_route[] = "192.168.1.0/24 dev rmnet proto kernel scope link src " - "\n"; - - char *ip = sc_adb_parse_device_ip(ip_route); - assert(!ip); -} - -int main(int argc, char *argv[]) { - (void) argc; - (void) argv; - - test_adb_devices(); - test_adb_devices_cr(); - test_adb_devices_daemon_start(); - test_adb_devices_daemon_start_mixed(); - test_adb_devices_without_eol(); - test_adb_devices_without_header(); - test_adb_devices_corrupted(); - test_adb_devices_spaces(); - - test_get_ip_single_line(); - test_get_ip_single_line_without_eol(); - test_get_ip_single_line_with_trailing_space(); - test_get_ip_multiline_first_ok(); - test_get_ip_multiline_second_ok(); - test_get_ip_multiline_second_ok_without_cr(); - test_get_ip_no_wlan(); - test_get_ip_no_wlan_without_eol(); - test_get_ip_truncated(); - - return 0; -} diff --git a/app/tests/test_audiobuf.c b/app/tests/test_audiobuf.c deleted file mode 100644 index 539ee238..00000000 --- a/app/tests/test_audiobuf.c +++ /dev/null @@ -1,136 +0,0 @@ -#include "common.h" - -#include -#include - -#include "util/audiobuf.h" - -static void test_audiobuf_simple(void) { - struct sc_audiobuf buf; - uint32_t data[20]; - - bool ok = sc_audiobuf_init(&buf, 4, 20); - assert(ok); - - uint32_t samples[] = {1, 2, 3, 4, 5}; - uint32_t w = sc_audiobuf_write(&buf, samples, 5); - assert(w == 5); - - uint32_t r = sc_audiobuf_read(&buf, data, 4); - assert(r == 4); - assert(!memcmp(data, samples, 16)); - - uint32_t samples2[] = {6, 7, 8}; - w = sc_audiobuf_write(&buf, samples2, 3); - assert(w == 3); - - uint32_t single = 9; - w = sc_audiobuf_write(&buf, &single, 1); - assert(w == 1); - - r = sc_audiobuf_read(&buf, &data[4], 8); - assert(r == 5); - - uint32_t expected[] = {1, 2, 3, 4, 5, 6, 7, 8, 9}; - assert(!memcmp(data, expected, 36)); - - sc_audiobuf_destroy(&buf); -} - -static void test_audiobuf_boundaries(void) { - struct sc_audiobuf buf; - uint32_t data[20]; - - bool ok = sc_audiobuf_init(&buf, 4, 20); - assert(ok); - - uint32_t samples[] = {1, 2, 3, 4, 5, 6}; - uint32_t w = sc_audiobuf_write(&buf, samples, 6); - assert(w == 6); - - w = sc_audiobuf_write(&buf, samples, 6); - assert(w == 6); - - w = sc_audiobuf_write(&buf, samples, 6); - assert(w == 6); - - uint32_t r = sc_audiobuf_read(&buf, data, 9); - assert(r == 9); - - uint32_t expected[] = {1, 2, 3, 4, 5, 6, 1, 2, 3}; - assert(!memcmp(data, expected, 36)); - - uint32_t samples2[] = {7, 8, 9, 10, 11}; - w = sc_audiobuf_write(&buf, samples2, 5); - assert(w == 5); - - uint32_t single = 12; - w = sc_audiobuf_write(&buf, &single, 1); - assert(w == 1); - - w = sc_audiobuf_read(&buf, NULL, 3); - assert(w == 3); - - assert(sc_audiobuf_can_read(&buf) == 12); - - r = sc_audiobuf_read(&buf, data, 12); - assert(r == 12); - - uint32_t expected2[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; - assert(!memcmp(data, expected2, 48)); - - sc_audiobuf_destroy(&buf); -} - -static void test_audiobuf_partial_read_write(void) { - struct sc_audiobuf buf; - uint32_t data[15]; - - bool ok = sc_audiobuf_init(&buf, 4, 10); - assert(ok); - - uint32_t samples[] = {1, 2, 3, 4, 5, 6}; - uint32_t w = sc_audiobuf_write(&buf, samples, 6); - assert(w == 6); - - w = sc_audiobuf_write(&buf, samples, 6); - assert(w == 4); - - w = sc_audiobuf_write(&buf, samples, 6); - assert(w == 0); - - uint32_t r = sc_audiobuf_read(&buf, data, 3); - assert(r == 3); - - uint32_t expected[] = {1, 2, 3}; - assert(!memcmp(data, expected, 12)); - - w = sc_audiobuf_write(&buf, samples, 6); - assert(w == 3); - - r = sc_audiobuf_read(&buf, data, 15); - assert(r == 10); - uint32_t expected2[] = {4, 5, 6, 1, 2, 3, 4, 1, 2, 3}; - assert(!memcmp(data, expected2, 12)); - - w = sc_audiobuf_write_silence(&buf, 4); - assert(w == 4); - - r = sc_audiobuf_read(&buf, data, 4); - assert(r == 4); - uint32_t expected3[] = {0, 0, 0, 0}; - assert(!memcmp(data, expected3, 4)); - - sc_audiobuf_destroy(&buf); -} - -int main(int argc, char *argv[]) { - (void) argc; - (void) argv; - - test_audiobuf_simple(); - test_audiobuf_boundaries(); - test_audiobuf_partial_read_write(); - - return 0; -} diff --git a/app/tests/test_binary.c b/app/tests/test_binary.c deleted file mode 100644 index bce74ce2..00000000 --- a/app/tests/test_binary.c +++ /dev/null @@ -1,156 +0,0 @@ -#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_write16le(void) { - uint16_t val = 0xABCD; - uint8_t buf[2]; - - sc_write16le(buf, val); - - assert(buf[0] == 0xCD); - assert(buf[1] == 0xAB); -} - -static void test_write32le(void) { - uint32_t val = 0xABCD1234; - uint8_t buf[4]; - - sc_write32le(buf, val); - - assert(buf[0] == 0x34); - assert(buf[1] == 0x12); - assert(buf[2] == 0xCD); - assert(buf[3] == 0xAB); -} - -static void test_write64le(void) { - uint64_t val = 0xABCD1234567890EF; - uint8_t buf[8]; - - sc_write64le(buf, val); - - assert(buf[0] == 0xEF); - assert(buf[1] == 0x90); - assert(buf[2] == 0x78); - assert(buf[3] == 0x56); - assert(buf[4] == 0x34); - assert(buf[5] == 0x12); - assert(buf[6] == 0xCD); - assert(buf[7] == 0xAB); -} - -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_write16le(); - test_write32le(); - test_write64le(); - - test_float_to_u16fp(); - test_float_to_i16fp(); - return 0; -} diff --git a/app/tests/test_cbuf.c b/app/tests/test_cbuf.c new file mode 100644 index 00000000..9d5fdc27 --- /dev/null +++ b/app/tests/test_cbuf.c @@ -0,0 +1,73 @@ +#include +#include + +#include "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(void) { + test_cbuf_empty(); + test_cbuf_full(); + test_cbuf_push_take(); + return 0; +} diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c deleted file mode 100644 index de605cb9..00000000 --- a/app/tests/test_cli.c +++ /dev/null @@ -1,162 +0,0 @@ -#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", - "--video-bit-rate", "5M", - "--crop", "100:200:300:400", - "--fullscreen", - "--max-fps", "30", - "--max-size", "1024", - // "--no-control" is not compatible with "--turn-screen-off" - // "--no-playback" 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->video_bit_rate == 5000000); - assert(!strcmp(opts->crop, "100:200:300:400")); - assert(opts->fullscreen); - assert(!strcmp(opts->max_fps, "30")); - assert(opts->max_size == 1024); - 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-playback", - "--record", "file.mp4", // cannot enable --no-playback 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->video_playback); - assert(!opts->audio_playback); - assert(!strcmp(opts->record_filename, "file.mp4")); - assert(opts->record_format == SC_RECORD_FORMAT_MP4); -} - -static void test_parse_shortcut_mods(void) { - uint8_t mods; - bool ok; - - ok = sc_parse_shortcut_mods("lctrl", &mods); - assert(ok); - assert(mods == SC_SHORTCUT_MOD_LCTRL); - - ok = sc_parse_shortcut_mods("rctrl,lalt", &mods); - assert(ok); - assert(mods == (SC_SHORTCUT_MOD_RCTRL | SC_SHORTCUT_MOD_LALT)); - - ok = sc_parse_shortcut_mods("lsuper,rsuper,lctrl", &mods); - assert(ok); - assert(mods == (SC_SHORTCUT_MOD_LSUPER - | SC_SHORTCUT_MOD_RSUPER - | SC_SHORTCUT_MOD_LCTRL)); - - ok = sc_parse_shortcut_mods("", &mods); - assert(!ok); - - ok = sc_parse_shortcut_mods("lctrl+", &mods); - assert(!ok); - - ok = sc_parse_shortcut_mods("lctrl,", &mods); - assert(!ok); -} - -int main(int argc, char *argv[]) { - (void) argc; - (void) argv; - - test_flag_version(); - test_flag_help(); - test_options(); - test_options2(); - test_parse_shortcut_mods(); - return 0; -} diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index 0d19919e..c0c501f2 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -1,121 +1,108 @@ -#include "common.h" - #include -#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, + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_INJECT_KEYCODE, .inject_keycode = { .action = AKEY_EVENT_ACTION_UP, .keycode = AKEYCODE_ENTER, - .repeat = 5, .metastate = AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON, }, }; - uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; - size_t size = sc_control_msg_serialize(&msg, buf); - assert(size == 14); + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); + assert(size == 10); - const uint8_t expected[] = { - SC_CONTROL_MSG_TYPE_INJECT_KEYCODE, + const unsigned char expected[] = { + CONTROL_MSG_TYPE_INJECT_KEYCODE, 0x01, // AKEY_EVENT_ACTION_UP 0x00, 0x00, 0x00, 0x42, // AKEYCODE_ENTER - 0x00, 0x00, 0x00, 0X05, // repeat 0x00, 0x00, 0x00, 0x41, // AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON }; assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_inject_text(void) { - struct sc_control_msg msg = { - .type = SC_CONTROL_MSG_TYPE_INJECT_TEXT, + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_INJECT_TEXT, .inject_text = { .text = "hello, world!", }, }; - uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; - size_t size = sc_control_msg_serialize(&msg, buf); - assert(size == 18); + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); + assert(size == 16); - const uint8_t expected[] = { - SC_CONTROL_MSG_TYPE_INJECT_TEXT, - 0x00, 0x00, 0x00, 0x0d, // text length + const unsigned char expected[] = { + CONTROL_MSG_TYPE_INJECT_TEXT, + 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'; + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; + char text[CONTROL_MSG_TEXT_MAX_LENGTH + 1]; + memset(text, 'a', sizeof(text)); + text[CONTROL_MSG_TEXT_MAX_LENGTH] = '\0'; msg.inject_text.text = text; - uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; - size_t size = sc_control_msg_serialize(&msg, buf); - assert(size == 5 + SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); + assert(size == 3 + CONTROL_MSG_TEXT_MAX_LENGTH); - uint8_t expected[5 + SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH]; - expected[0] = SC_CONTROL_MSG_TYPE_INJECT_TEXT; - expected[1] = 0x00; - expected[2] = 0x00; - expected[3] = 0x01; - expected[4] = 0x2c; // text length (32 bits) - memset(&expected[5], 'a', SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); + unsigned char expected[3 + CONTROL_MSG_TEXT_MAX_LENGTH]; + expected[0] = CONTROL_MSG_TYPE_INJECT_TEXT; + expected[1] = 0x01; + expected[2] = 0x2c; // text length (16 bits) + memset(&expected[3], 'a', CONTROL_MSG_TEXT_MAX_LENGTH); 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 = { +static void test_serialize_inject_mouse_event(void) { + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT, + .inject_mouse_event = { .action = AMOTION_EVENT_ACTION_DOWN, - .pointer_id = UINT64_C(0x1234567887654321), + .buttons = AMOTION_EVENT_BUTTON_PRIMARY, .position = { .point = { - .x = 100, - .y = 200, + .x = 260, + .y = 1026, }, .screen_size = { .width = 1080, .height = 1920, }, }, - .pressure = 1.0f, - .action_button = AMOTION_EVENT_BUTTON_PRIMARY, - .buttons = AMOTION_EVENT_BUTTON_PRIMARY, }, }; - uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; - size_t size = sc_control_msg_serialize(&msg, buf); - assert(size == 32); + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); + assert(size == 18); - const uint8_t expected[] = { - SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, + const unsigned char expected[] = { + CONTROL_MSG_TYPE_INJECT_MOUSE_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 + 0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY + 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026 0x04, 0x38, 0x07, 0x80, // 1080 1920 - 0xff, 0xff, // pressure - 0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY (action button) - 0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY (buttons) }; assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_inject_scroll_event(void) { - struct sc_control_msg msg = { - .type = SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, .inject_scroll_event = { .position = { .point = { @@ -127,327 +114,135 @@ static void test_serialize_inject_scroll_event(void) { .height = 1920, }, }, - .hscroll = 16, - .vscroll = -16, - .buttons = 1, + .hscroll = 1, + .vscroll = -1, }, }; - uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; - size_t size = sc_control_msg_serialize(&msg, buf); + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); assert(size == 21); - const uint8_t expected[] = { - SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, + const unsigned char expected[] = { + CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026 0x04, 0x38, 0x07, 0x80, // 1080 1920 - 0x7F, 0xFF, // 16 (float encoded as i16 in the range [-16, 16]) - 0x80, 0x00, // -16 (float encoded as i16 in the range [-16, 16]) 0x00, 0x00, 0x00, 0x01, // 1 + 0xFF, 0xFF, 0xFF, 0xFF, // -1 }; assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_back_or_screen_on(void) { - struct sc_control_msg msg = { - .type = SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, - .back_or_screen_on = { - .action = AKEY_EVENT_ACTION_UP, - }, + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, }; - uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; - size_t size = sc_control_msg_serialize(&msg, buf); - assert(size == 2); + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); + assert(size == 1); - const uint8_t expected[] = { - SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, - 0x01, // AKEY_EVENT_ACTION_UP + const unsigned char expected[] = { + CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, }; 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, + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, }; - uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; - size_t size = sc_control_msg_serialize(&msg, buf); + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); assert(size == 1); - const uint8_t expected[] = { - SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, + const unsigned char expected[] = { + CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, }; assert(!memcmp(buf, expected, sizeof(expected))); } -static void test_serialize_expand_settings_panel(void) { - struct sc_control_msg msg = { - .type = SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL, +static void test_serialize_collapse_notification_panel(void) { + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL, }; - uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; - size_t size = sc_control_msg_serialize(&msg, buf); + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); assert(size == 1); - const uint8_t expected[] = { - SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL, - }; - assert(!memcmp(buf, expected, sizeof(expected))); -} - -static void test_serialize_collapse_panels(void) { - struct sc_control_msg msg = { - .type = SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS, - }; - - uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; - size_t size = sc_control_msg_serialize(&msg, buf); - assert(size == 1); - - const uint8_t expected[] = { - SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS, + const unsigned char expected[] = { + CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL, }; 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, - }, + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_GET_CLIPBOARD, }; - uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; - size_t size = sc_control_msg_serialize(&msg, buf); - assert(size == 2); + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); + assert(size == 1); - const uint8_t expected[] = { - SC_CONTROL_MSG_TYPE_GET_CLIPBOARD, - SC_COPY_KEY_COPY, + const unsigned char expected[] = { + CONTROL_MSG_TYPE_GET_CLIPBOARD, }; 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, + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_SET_CLIPBOARD, + .inject_text = { .text = "hello, world!", }, }; - uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; - size_t size = sc_control_msg_serialize(&msg, buf); - assert(size == 27); + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); + assert(size == 16); - const uint8_t expected[] = { - SC_CONTROL_MSG_TYPE_SET_CLIPBOARD, - 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // sequence - 1, // paste - 0x00, 0x00, 0x00, 0x0d, // text length + const unsigned char expected[] = { + CONTROL_MSG_TYPE_SET_CLIPBOARD, + 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, +static void test_serialize_set_screen_power_mode(void) { + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, + .set_screen_power_mode = { + .mode = SCREEN_POWER_MODE_NORMAL, }, }; - char text[SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH + 1]; - memset(text, 'a', SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH); - text[SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH] = '\0'; - msg.set_clipboard.text = text; - - uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; - size_t size = sc_control_msg_serialize(&msg, buf); - assert(size == SC_CONTROL_MSG_MAX_SIZE); - - uint8_t expected[SC_CONTROL_MSG_MAX_SIZE] = { - SC_CONTROL_MSG_TYPE_SET_CLIPBOARD, - 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // sequence - 1, // paste - // text length - SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH >> 24, - (SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH >> 16) & 0xff, - (SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH >> 8) & 0xff, - SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH & 0xff, - }; - memset(expected + 14, 'a', SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH); - - assert(!memcmp(buf, expected, sizeof(expected))); -} - -static void test_serialize_set_display_power(void) { - struct sc_control_msg msg = { - .type = SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER, - .set_display_power = { - .on = true, - }, - }; - - uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; - size_t size = sc_control_msg_serialize(&msg, buf); + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); assert(size == 2); - const uint8_t expected[] = { - SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER, - 0x01, // true + const unsigned char expected[] = { + CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, + 0x02, // SCREEN_POWER_MODE_NORMAL }; assert(!memcmp(buf, expected, sizeof(expected))); } -static void test_serialize_rotate_device(void) { - struct sc_control_msg msg = { - .type = SC_CONTROL_MSG_TYPE_ROTATE_DEVICE, - }; - - uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; - size_t size = sc_control_msg_serialize(&msg, buf); - assert(size == 1); - - const uint8_t expected[] = { - SC_CONTROL_MSG_TYPE_ROTATE_DEVICE, - }; - assert(!memcmp(buf, expected, sizeof(expected))); -} - -static void test_serialize_uhid_create(void) { - const uint8_t report_desc[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; - struct sc_control_msg msg = { - .type = SC_CONTROL_MSG_TYPE_UHID_CREATE, - .uhid_create = { - .id = 42, - .vendor_id = 0x1234, - .product_id = 0x5678, - .name = "ABC", - .report_desc_size = sizeof(report_desc), - .report_desc = report_desc, - }, - }; - - uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; - size_t size = sc_control_msg_serialize(&msg, buf); - assert(size == 24); - - const uint8_t expected[] = { - SC_CONTROL_MSG_TYPE_UHID_CREATE, - 0, 42, // id - 0x12, 0x34, // vendor id - 0x56, 0x78, // product id - 3, // name size - 65, 66, 67, // "ABC" - 0, 11, // report desc size - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, - }; - assert(!memcmp(buf, expected, sizeof(expected))); -} - -static void test_serialize_uhid_input(void) { - struct sc_control_msg msg = { - .type = SC_CONTROL_MSG_TYPE_UHID_INPUT, - .uhid_input = { - .id = 42, - .size = 5, - .data = {1, 2, 3, 4, 5}, - }, - }; - - uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; - size_t size = sc_control_msg_serialize(&msg, buf); - assert(size == 10); - - const uint8_t expected[] = { - SC_CONTROL_MSG_TYPE_UHID_INPUT, - 0, 42, // id - 0, 5, // size - 1, 2, 3, 4, 5, - }; - assert(!memcmp(buf, expected, sizeof(expected))); -} - -static void test_serialize_uhid_destroy(void) { - struct sc_control_msg msg = { - .type = SC_CONTROL_MSG_TYPE_UHID_DESTROY, - .uhid_destroy = { - .id = 42, - }, - }; - - uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; - size_t size = sc_control_msg_serialize(&msg, buf); - assert(size == 3); - - const uint8_t expected[] = { - SC_CONTROL_MSG_TYPE_UHID_DESTROY, - 0, 42, // id - }; - assert(!memcmp(buf, expected, sizeof(expected))); -} - -static void test_serialize_open_hard_keyboard(void) { - struct sc_control_msg msg = { - .type = SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS, - }; - - uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; - size_t size = sc_control_msg_serialize(&msg, buf); - assert(size == 1); - - const uint8_t expected[] = { - SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS, - }; - assert(!memcmp(buf, expected, sizeof(expected))); -} - -static void test_serialize_reset_video(void) { - struct sc_control_msg msg = { - .type = SC_CONTROL_MSG_TYPE_RESET_VIDEO, - }; - - uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; - size_t size = sc_control_msg_serialize(&msg, buf); - assert(size == 1); - - const uint8_t expected[] = { - SC_CONTROL_MSG_TYPE_RESET_VIDEO, - }; - assert(!memcmp(buf, expected, sizeof(expected))); -} - -int main(int argc, char *argv[]) { - (void) argc; - (void) argv; - +int main(void) { test_serialize_inject_keycode(); test_serialize_inject_text(); test_serialize_inject_text_long(); - test_serialize_inject_touch_event(); + test_serialize_inject_mouse_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_collapse_notification_panel(); test_serialize_get_clipboard(); test_serialize_set_clipboard(); - test_serialize_set_clipboard_long(); - test_serialize_set_display_power(); - test_serialize_rotate_device(); - test_serialize_uhid_create(); - test_serialize_uhid_input(); - test_serialize_uhid_destroy(); - test_serialize_open_hard_keyboard(); - test_serialize_reset_video(); + test_serialize_set_screen_power_mode(); return 0; } diff --git a/app/tests/test_device_msg_deserialize.c b/app/tests/test_device_msg_deserialize.c index a64a3eb7..e163ad72 100644 --- a/app/tests/test_device_msg_deserialize.c +++ b/app/tests/test_device_msg_deserialize.c @@ -1,95 +1,28 @@ -#include "common.h" - #include -#include -#include #include #include "device_msg.h" +#include static void test_deserialize_clipboard(void) { - const uint8_t input[] = { + const unsigned char input[] = { DEVICE_MSG_TYPE_CLIPBOARD, - 0x00, 0x00, 0x00, 0x03, // text length + 0x00, 0x03, // text length 0x41, 0x42, 0x43, // "ABC" }; - struct sc_device_msg msg; - ssize_t r = sc_device_msg_deserialize(input, sizeof(input), &msg); - assert(r == 8); + struct device_msg msg; + ssize_t r = device_msg_deserialize(input, sizeof(input), &msg); + assert(r == 6); assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD); assert(msg.clipboard.text); assert(!strcmp("ABC", msg.clipboard.text)); - sc_device_msg_destroy(&msg); + device_msg_destroy(&msg); } -static void test_deserialize_clipboard_big(void) { - uint8_t input[DEVICE_MSG_MAX_SIZE]; - input[0] = DEVICE_MSG_TYPE_CLIPBOARD; - input[1] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0xff000000u) >> 24; - input[2] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0x00ff0000u) >> 16; - 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 sc_device_msg msg; - ssize_t r = sc_device_msg_deserialize(input, sizeof(input), &msg); - assert(r == DEVICE_MSG_MAX_SIZE); - - assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD); - assert(msg.clipboard.text); - assert(strlen(msg.clipboard.text) == DEVICE_MSG_TEXT_MAX_LENGTH); - assert(msg.clipboard.text[0] == 'a'); - - sc_device_msg_destroy(&msg); -} - -static void test_deserialize_ack_set_clipboard(void) { - const uint8_t input[] = { - DEVICE_MSG_TYPE_ACK_CLIPBOARD, - 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // sequence - }; - - struct sc_device_msg msg; - ssize_t r = sc_device_msg_deserialize(input, sizeof(input), &msg); - assert(r == 9); - - assert(msg.type == DEVICE_MSG_TYPE_ACK_CLIPBOARD); - assert(msg.ack_clipboard.sequence == UINT64_C(0x0102030405060708)); -} - -static void test_deserialize_uhid_output(void) { - const uint8_t input[] = { - DEVICE_MSG_TYPE_UHID_OUTPUT, - 0, 42, // id - 0, 5, // size - 0x01, 0x02, 0x03, 0x04, 0x05, // data - }; - - struct sc_device_msg msg; - ssize_t r = sc_device_msg_deserialize(input, sizeof(input), &msg); - assert(r == 10); - - assert(msg.type == DEVICE_MSG_TYPE_UHID_OUTPUT); - assert(msg.uhid_output.id == 42); - assert(msg.uhid_output.size == 5); - - uint8_t expected[] = {1, 2, 3, 4, 5}; - assert(!memcmp(msg.uhid_output.data, expected, sizeof(expected))); - - sc_device_msg_destroy(&msg); -} - -int main(int argc, char *argv[]) { - (void) argc; - (void) argv; - +int main(void) { test_deserialize_clipboard(); - test_deserialize_clipboard_big(); - test_deserialize_ack_set_clipboard(); - test_deserialize_uhid_output(); return 0; } diff --git a/app/tests/test_orientation.c b/app/tests/test_orientation.c deleted file mode 100644 index 153211fa..00000000 --- a/app/tests/test_orientation.c +++ /dev/null @@ -1,91 +0,0 @@ -#include "common.h" - -#include - -#include "options.h" - -static void test_transforms(void) { - #define O(X) SC_ORIENTATION_ ## X - #define ASSERT_TRANSFORM(SRC, TR, RES) \ - assert(sc_orientation_apply(O(SRC), O(TR)) == O(RES)); - - ASSERT_TRANSFORM(0, 0, 0); - ASSERT_TRANSFORM(0, 90, 90); - ASSERT_TRANSFORM(0, 180, 180); - ASSERT_TRANSFORM(0, 270, 270); - ASSERT_TRANSFORM(0, FLIP_0, FLIP_0); - ASSERT_TRANSFORM(0, FLIP_90, FLIP_90); - ASSERT_TRANSFORM(0, FLIP_180, FLIP_180); - ASSERT_TRANSFORM(0, FLIP_270, FLIP_270); - - ASSERT_TRANSFORM(90, 0, 90); - ASSERT_TRANSFORM(90, 90, 180); - ASSERT_TRANSFORM(90, 180, 270); - ASSERT_TRANSFORM(90, 270, 0); - ASSERT_TRANSFORM(90, FLIP_0, FLIP_270); - ASSERT_TRANSFORM(90, FLIP_90, FLIP_0); - ASSERT_TRANSFORM(90, FLIP_180, FLIP_90); - ASSERT_TRANSFORM(90, FLIP_270, FLIP_180); - - ASSERT_TRANSFORM(180, 0, 180); - ASSERT_TRANSFORM(180, 90, 270); - ASSERT_TRANSFORM(180, 180, 0); - ASSERT_TRANSFORM(180, 270, 90); - ASSERT_TRANSFORM(180, FLIP_0, FLIP_180); - ASSERT_TRANSFORM(180, FLIP_90, FLIP_270); - ASSERT_TRANSFORM(180, FLIP_180, FLIP_0); - ASSERT_TRANSFORM(180, FLIP_270, FLIP_90); - - ASSERT_TRANSFORM(270, 0, 270); - ASSERT_TRANSFORM(270, 90, 0); - ASSERT_TRANSFORM(270, 180, 90); - ASSERT_TRANSFORM(270, 270, 180); - ASSERT_TRANSFORM(270, FLIP_0, FLIP_90); - ASSERT_TRANSFORM(270, FLIP_90, FLIP_180); - ASSERT_TRANSFORM(270, FLIP_180, FLIP_270); - ASSERT_TRANSFORM(270, FLIP_270, FLIP_0); - - ASSERT_TRANSFORM(FLIP_0, 0, FLIP_0); - ASSERT_TRANSFORM(FLIP_0, 90, FLIP_90); - ASSERT_TRANSFORM(FLIP_0, 180, FLIP_180); - ASSERT_TRANSFORM(FLIP_0, 270, FLIP_270); - ASSERT_TRANSFORM(FLIP_0, FLIP_0, 0); - ASSERT_TRANSFORM(FLIP_0, FLIP_90, 90); - ASSERT_TRANSFORM(FLIP_0, FLIP_180, 180); - ASSERT_TRANSFORM(FLIP_0, FLIP_270, 270); - - ASSERT_TRANSFORM(FLIP_90, 0, FLIP_90); - ASSERT_TRANSFORM(FLIP_90, 90, FLIP_180); - ASSERT_TRANSFORM(FLIP_90, 180, FLIP_270); - ASSERT_TRANSFORM(FLIP_90, 270, FLIP_0); - ASSERT_TRANSFORM(FLIP_90, FLIP_0, 270); - ASSERT_TRANSFORM(FLIP_90, FLIP_90, 0); - ASSERT_TRANSFORM(FLIP_90, FLIP_180, 90); - ASSERT_TRANSFORM(FLIP_90, FLIP_270, 180); - - ASSERT_TRANSFORM(FLIP_180, 0, FLIP_180); - ASSERT_TRANSFORM(FLIP_180, 90, FLIP_270); - ASSERT_TRANSFORM(FLIP_180, 180, FLIP_0); - ASSERT_TRANSFORM(FLIP_180, 270, FLIP_90); - ASSERT_TRANSFORM(FLIP_180, FLIP_0, 180); - ASSERT_TRANSFORM(FLIP_180, FLIP_90, 270); - ASSERT_TRANSFORM(FLIP_180, FLIP_180, 0); - ASSERT_TRANSFORM(FLIP_180, FLIP_270, 90); - - ASSERT_TRANSFORM(FLIP_270, 0, FLIP_270); - ASSERT_TRANSFORM(FLIP_270, 90, FLIP_0); - ASSERT_TRANSFORM(FLIP_270, 180, FLIP_90); - ASSERT_TRANSFORM(FLIP_270, 270, FLIP_180); - ASSERT_TRANSFORM(FLIP_270, FLIP_0, 90); - ASSERT_TRANSFORM(FLIP_270, FLIP_90, 180); - ASSERT_TRANSFORM(FLIP_270, FLIP_180, 270); - ASSERT_TRANSFORM(FLIP_270, FLIP_270, 0); -} - -int main(int argc, char *argv[]) { - (void) argc; - (void) argv; - - test_transforms(); - return 0; -} diff --git a/app/tests/test_str.c b/app/tests/test_str.c deleted file mode 100644 index 4a906d92..00000000 --- a/app/tests/test_str.c +++ /dev/null @@ -1,412 +0,0 @@ -#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_concat(void) { - const char *s = "2024:11"; - char *out = sc_str_concat("my-prefix:", s); - - // contains the concat - assert(!strcmp("my-prefix:2024:11", out)); - - free(out); -} - -static void test_utf8_truncate(void) { - const char *s = "aÉbÔc"; - assert(strlen(s) == 7); // É and Ô are 2 bytes-wide - - size_t count; - - count = sc_str_utf8_truncation_index(s, 1); - assert(count == 1); - - count = sc_str_utf8_truncation_index(s, 2); - assert(count == 1); // É is 2 bytes-wide - - count = sc_str_utf8_truncation_index(s, 3); - assert(count == 3); - - count = sc_str_utf8_truncation_index(s, 4); - assert(count == 4); - - count = sc_str_utf8_truncation_index(s, 5); - assert(count == 4); // Ô is 2 bytes-wide - - count = sc_str_utf8_truncation_index(s, 6); - assert(count == 6); - - count = sc_str_utf8_truncation_index(s, 7); - assert(count == 7); - - count = sc_str_utf8_truncation_index(s, 8); - assert(count == 7); // no more chars -} - -static void test_parse_integer(void) { - long value; - bool ok = sc_str_parse_integer("1234", &value); - assert(ok); - assert(value == 1234); - - ok = sc_str_parse_integer("-1234", &value); - assert(ok); - assert(value == -1234); - - ok = sc_str_parse_integer("1234k", &value); - assert(!ok); - - ok = sc_str_parse_integer("123456789876543212345678987654321", &value); - assert(!ok); // out-of-range -} - -static void test_parse_integers(void) { - long values[5]; - - size_t count = sc_str_parse_integers("1234", ':', 5, values); - assert(count == 1); - assert(values[0] == 1234); - - count = sc_str_parse_integers("1234:5678", ':', 5, values); - assert(count == 2); - assert(values[0] == 1234); - assert(values[1] == 5678); - - count = sc_str_parse_integers("1234:5678", ':', 2, values); - assert(count == 2); - assert(values[0] == 1234); - assert(values[1] == 5678); - - count = sc_str_parse_integers("1234:-5678", ':', 2, values); - assert(count == 2); - assert(values[0] == 1234); - assert(values[1] == -5678); - - count = sc_str_parse_integers("1:2:3:4:5", ':', 5, values); - assert(count == 5); - assert(values[0] == 1); - assert(values[1] == 2); - assert(values[2] == 3); - assert(values[3] == 4); - assert(values[4] == 5); - - count = sc_str_parse_integers("1234:5678", ':', 1, values); - assert(count == 0); // max_items == 1 - - count = sc_str_parse_integers("1:2:3:4:5", ':', 3, values); - assert(count == 0); // max_items == 3 - - count = sc_str_parse_integers(":1234", ':', 5, values); - assert(count == 0); // invalid - - count = sc_str_parse_integers("1234:", ':', 5, values); - assert(count == 0); // invalid - - count = sc_str_parse_integers("1234:", ':', 1, values); - assert(count == 0); // invalid, even when max_items == 1 - - count = sc_str_parse_integers("1234::5678", ':', 5, values); - assert(count == 0); // invalid -} - -static void test_parse_integer_with_suffix(void) { - long value; - bool ok = sc_str_parse_integer_with_suffix("1234", &value); - assert(ok); - assert(value == 1234); - - ok = sc_str_parse_integer_with_suffix("-1234", &value); - assert(ok); - assert(value == -1234); - - ok = sc_str_parse_integer_with_suffix("1234k", &value); - assert(ok); - assert(value == 1234000); - - ok = sc_str_parse_integer_with_suffix("1234m", &value); - assert(ok); - assert(value == 1234000000); - - ok = sc_str_parse_integer_with_suffix("-1234k", &value); - assert(ok); - assert(value == -1234000); - - ok = sc_str_parse_integer_with_suffix("-1234m", &value); - assert(ok); - assert(value == -1234000000); - - ok = sc_str_parse_integer_with_suffix("123456789876543212345678987654321", &value); - assert(!ok); // out-of-range - - char buf[32]; - - int r = snprintf(buf, sizeof(buf), "%ldk", LONG_MAX / 2000); - assert(r >= 0 && (size_t) r < sizeof(buf)); - ok = sc_str_parse_integer_with_suffix(buf, &value); - assert(ok); - assert(value == LONG_MAX / 2000 * 1000); - - r = snprintf(buf, sizeof(buf), "%ldm", LONG_MAX / 2000); - assert(r >= 0 && (size_t) r < sizeof(buf)); - ok = sc_str_parse_integer_with_suffix(buf, &value); - assert(!ok); - - r = snprintf(buf, sizeof(buf), "%ldk", LONG_MIN / 2000); - assert(r >= 0 && (size_t) r < sizeof(buf)); - ok = sc_str_parse_integer_with_suffix(buf, &value); - assert(ok); - assert(value == LONG_MIN / 2000 * 1000); - - r = snprintf(buf, sizeof(buf), "%ldm", LONG_MIN / 2000); - assert(r >= 0 && (size_t) r < sizeof(buf)); - ok = sc_str_parse_integer_with_suffix(buf, &value); - assert(!ok); -} - -static void test_strlist_contains(void) { - assert(sc_str_list_contains("a,bc,def", ',', "bc")); - assert(!sc_str_list_contains("a,bc,def", ',', "b")); - assert(sc_str_list_contains("", ',', "")); - assert(sc_str_list_contains("abc,", ',', "")); - assert(sc_str_list_contains(",abc", ',', "")); - assert(sc_str_list_contains("abc,,def", ',', "")); - assert(!sc_str_list_contains("abc", ',', "")); - assert(sc_str_list_contains(",,|x", '|', ",,")); - assert(sc_str_list_contains("xyz", '\0', "xyz")); -} - -static void test_wrap_lines(void) { - const char *s = "This is a text to test line wrapping. The lines must be " - "wrapped at a space or a line break.\n" - "\n" - "This rectangle must remains a rectangle because it is " - "drawn in lines having lengths lower than the specified " - "number of columns:\n" - " +----+\n" - " | |\n" - " +----+\n"; - - // |---- 1 1 2 2| - // |0 5 0 5 0 3| <-- 24 columns - const char *expected = " This is a text to\n" - " test line wrapping.\n" - " The lines must be\n" - " wrapped at a space\n" - " or a line break.\n" - " \n" - " This rectangle must\n" - " remains a rectangle\n" - " because it is drawn\n" - " in lines having\n" - " lengths lower than\n" - " the specified number\n" - " of columns:\n" - " +----+\n" - " | |\n" - " +----+\n"; - - char *formatted = sc_str_wrap_lines(s, 24, 4); - assert(formatted); - - assert(!strcmp(formatted, expected)); - - free(formatted); -} - -static void test_index_of_column(void) { - assert(sc_str_index_of_column("a bc d", 0, " ") == 0); - assert(sc_str_index_of_column("a bc d", 1, " ") == 2); - assert(sc_str_index_of_column("a bc d", 2, " ") == 6); - assert(sc_str_index_of_column("a bc d", 3, " ") == -1); - - assert(sc_str_index_of_column("a ", 0, " ") == 0); - assert(sc_str_index_of_column("a ", 1, " ") == -1); - - assert(sc_str_index_of_column("", 0, " ") == 0); - assert(sc_str_index_of_column("", 1, " ") == -1); - - assert(sc_str_index_of_column("a \t \t bc \t d\t", 0, " \t") == 0); - assert(sc_str_index_of_column("a \t \t bc \t d\t", 1, " \t") == 8); - assert(sc_str_index_of_column("a \t \t bc \t d\t", 2, " \t") == 15); - assert(sc_str_index_of_column("a \t \t bc \t d\t", 3, " \t") == -1); - - assert(sc_str_index_of_column(" a bc d", 1, " ") == 2); -} - -static void test_remove_trailing_cr(void) { - char s[] = "abc\r"; - sc_str_remove_trailing_cr(s, sizeof(s) - 1); - assert(!strcmp(s, "abc")); - - char s2[] = "def\r\r\r\r"; - sc_str_remove_trailing_cr(s2, sizeof(s2) - 1); - assert(!strcmp(s2, "def")); - - char s3[] = "adb\rdef\r"; - sc_str_remove_trailing_cr(s3, sizeof(s3) - 1); - assert(!strcmp(s3, "adb\rdef")); -} - -int main(int argc, char *argv[]) { - (void) argc; - (void) argv; - - test_strncpy_simple(); - test_strncpy_just_fit(); - test_strncpy_truncated(); - test_join_simple(); - test_join_just_fit(); - test_join_truncated_in_token(); - test_join_truncated_before_sep(); - test_join_truncated_after_sep(); - test_quote(); - test_concat(); - test_utf8_truncate(); - test_parse_integer(); - test_parse_integers(); - test_parse_integer_with_suffix(); - test_strlist_contains(); - test_wrap_lines(); - test_index_of_column(); - test_remove_trailing_cr(); - return 0; -} diff --git a/app/tests/test_strbuf.c b/app/tests/test_strbuf.c deleted file mode 100644 index 58562522..00000000 --- a/app/tests/test_strbuf.c +++ /dev/null @@ -1,48 +0,0 @@ -#include "common.h" - -#include -#include -#include -#include - -#include "util/strbuf.h" - -static void test_strbuf_simple(void) { - struct sc_strbuf buf; - bool ok = sc_strbuf_init(&buf, 10); - assert(ok); - - ok = sc_strbuf_append_staticstr(&buf, "Hello"); - assert(ok); - - ok = sc_strbuf_append_char(&buf, ' '); - assert(ok); - - ok = sc_strbuf_append_staticstr(&buf, "world"); - assert(ok); - - ok = sc_strbuf_append_staticstr(&buf, "!\n"); - assert(ok); - - ok = sc_strbuf_append_staticstr(&buf, "This is a test"); - assert(ok); - - ok = sc_strbuf_append_n(&buf, '.', 3); - assert(ok); - - assert(!strcmp(buf.s, "Hello world!\nThis is a test...")); - - sc_strbuf_shrink(&buf); - assert(buf.len == buf.cap); - assert(!strcmp(buf.s, "Hello world!\nThis is a test...")); - - free(buf.s); -} - -int main(int argc, char *argv[]) { - (void) argc; - (void) argv; - - test_strbuf_simple(); - return 0; -} diff --git a/app/tests/test_strutil.c b/app/tests/test_strutil.c new file mode 100644 index 00000000..18ac4a7d --- /dev/null +++ b/app/tests/test_strutil.c @@ -0,0 +1,171 @@ +#include +#include + +#include "str_util.h" + +static void test_xstrncpy_simple(void) { + char s[] = "xxxxxxxxxx"; + size_t w = xstrncpy(s, "abcdef", sizeof(s)); + + // returns strlen of copied string + assert(w == 6); + + // is nul-terminated + assert(s[6] == '\0'); + + // does not write useless bytes + assert(s[7] == 'x'); + + // copies the content as expected + assert(!strcmp("abcdef", s)); +} + +static void test_xstrncpy_just_fit(void) { + char s[] = "xxxxxx"; + size_t w = xstrncpy(s, "abcdef", sizeof(s)); + + // returns strlen of copied string + assert(w == 6); + + // is nul-terminated + assert(s[6] == '\0'); + + // copies the content as expected + assert(!strcmp("abcdef", s)); +} + +static void test_xstrncpy_truncated(void) { + char s[] = "xxx"; + size_t w = xstrncpy(s, "abcdef", sizeof(s)); + + // returns 'n' (sizeof(s)) + assert(w == 4); + + // is nul-terminated + assert(s[3] == '\0'); + + // copies the content as expected + assert(!strncmp("abcdef", s, 3)); +} + +static void test_xstrjoin_simple(void) { + const char *const tokens[] = { "abc", "de", "fghi", NULL }; + char s[] = "xxxxxxxxxxxxxx"; + size_t w = xstrjoin(s, tokens, ' ', sizeof(s)); + + // returns strlen of concatenation + assert(w == 11); + + // is nul-terminated + assert(s[11] == '\0'); + + // does not write useless bytes + assert(s[12] == 'x'); + + // copies the content as expected + assert(!strcmp("abc de fghi", s)); +} + +static void test_xstrjoin_just_fit(void) { + const char *const tokens[] = { "abc", "de", "fghi", NULL }; + char s[] = "xxxxxxxxxxx"; + size_t w = xstrjoin(s, tokens, ' ', sizeof(s)); + + // returns strlen of concatenation + assert(w == 11); + + // is nul-terminated + assert(s[11] == '\0'); + + // copies the content as expected + assert(!strcmp("abc de fghi", s)); +} + +static void test_xstrjoin_truncated_in_token(void) { + const char *const tokens[] = { "abc", "de", "fghi", NULL }; + char s[] = "xxxxx"; + size_t w = xstrjoin(s, tokens, ' ', sizeof(s)); + + // returns 'n' (sizeof(s)) + assert(w == 6); + + // is nul-terminated + assert(s[5] == '\0'); + + // copies the content as expected + assert(!strcmp("abc d", s)); +} + +static void test_xstrjoin_truncated_before_sep(void) { + const char *const tokens[] = { "abc", "de", "fghi", NULL }; + char s[] = "xxxxxx"; + size_t w = xstrjoin(s, tokens, ' ', sizeof(s)); + + // returns 'n' (sizeof(s)) + assert(w == 7); + + // is nul-terminated + assert(s[6] == '\0'); + + // copies the content as expected + assert(!strcmp("abc de", s)); +} + +static void test_xstrjoin_truncated_after_sep(void) { + const char *const tokens[] = { "abc", "de", "fghi", NULL }; + char s[] = "xxxxxxx"; + size_t w = xstrjoin(s, tokens, ' ', sizeof(s)); + + // returns 'n' (sizeof(s)) + assert(w == 8); + + // is nul-terminated + assert(s[7] == '\0'); + + // copies the content as expected + assert(!strcmp("abc de ", s)); +} + +static void test_utf8_truncate(void) { + const char *s = "aÉbÔc"; + assert(strlen(s) == 7); // É and Ô are 2 bytes-wide + + size_t count; + + count = utf8_truncation_index(s, 1); + assert(count == 1); + + count = utf8_truncation_index(s, 2); + assert(count == 1); // É is 2 bytes-wide + + count = utf8_truncation_index(s, 3); + assert(count == 3); + + count = utf8_truncation_index(s, 4); + assert(count == 4); + + count = utf8_truncation_index(s, 5); + assert(count == 4); // Ô is 2 bytes-wide + + count = utf8_truncation_index(s, 6); + assert(count == 6); + + count = utf8_truncation_index(s, 7); + assert(count == 7); + + count = utf8_truncation_index(s, 8); + assert(count == 7); // no more chars +} + +int main(void) { + test_xstrncpy_simple(); + test_xstrncpy_just_fit(); + test_xstrncpy_truncated(); + test_xstrjoin_simple(); + test_xstrjoin_just_fit(); + test_xstrjoin_truncated_in_token(); + test_xstrjoin_truncated_before_sep(); + test_xstrjoin_truncated_after_sep(); + test_utf8_truncate(); + return 0; +} diff --git a/app/tests/test_vecdeque.c b/app/tests/test_vecdeque.c deleted file mode 100644 index 44d33560..00000000 --- a/app/tests/test_vecdeque.c +++ /dev/null @@ -1,197 +0,0 @@ -#include "common.h" - -#include - -#include "util/vecdeque.h" - -#define pr(pv) \ -({ \ - fprintf(stderr, "cap=%lu origin=%lu size=%lu\n", (pv)->cap, (pv)->origin, (pv)->size); \ - for (size_t i = 0; i < (pv)->cap; ++i) \ - fprintf(stderr, "%d ", (pv)->data[i]); \ - fprintf(stderr, "\n"); \ -}) - -static void test_vecdeque_push_pop(void) { - struct SC_VECDEQUE(int) vdq = SC_VECDEQUE_INITIALIZER; - - assert(sc_vecdeque_is_empty(&vdq)); - assert(sc_vecdeque_size(&vdq) == 0); - - bool ok = sc_vecdeque_push(&vdq, 5); - assert(ok); - assert(sc_vecdeque_size(&vdq) == 1); - - ok = sc_vecdeque_push(&vdq, 12); - assert(ok); - assert(sc_vecdeque_size(&vdq) == 2); - - int v = sc_vecdeque_pop(&vdq); - assert(v == 5); - assert(sc_vecdeque_size(&vdq) == 1); - - ok = sc_vecdeque_push(&vdq, 7); - assert(ok); - assert(sc_vecdeque_size(&vdq) == 2); - - int *p = sc_vecdeque_popref(&vdq); - assert(p); - assert(*p == 12); - assert(sc_vecdeque_size(&vdq) == 1); - - v = sc_vecdeque_pop(&vdq); - assert(v == 7); - assert(sc_vecdeque_size(&vdq) == 0); - assert(sc_vecdeque_is_empty(&vdq)); - - sc_vecdeque_destroy(&vdq); -} - -static void test_vecdeque_reserve(void) { - struct SC_VECDEQUE(int) vdq = SC_VECDEQUE_INITIALIZER; - - bool ok = sc_vecdeque_reserve(&vdq, 20); - assert(ok); - assert(vdq.cap == 20); - - assert(sc_vecdeque_size(&vdq) == 0); - - for (size_t i = 0; i < 20; ++i) { - ok = sc_vecdeque_push(&vdq, i); - assert(ok); - } - - assert(sc_vecdeque_size(&vdq) == 20); - - // It is now full - - for (int i = 0; i < 5; ++i) { - int v = sc_vecdeque_pop(&vdq); - assert(v == i); - } - assert(sc_vecdeque_size(&vdq) == 15); - - for (int i = 20; i < 25; ++i) { - ok = sc_vecdeque_push(&vdq, i); - assert(ok); - } - - assert(sc_vecdeque_size(&vdq) == 20); - assert(vdq.cap == 20); - - // Now, the content wraps around the ring buffer: - // 20 21 22 23 24 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 - // ^ - // origin - - // It is now full, let's reserve some space - ok = sc_vecdeque_reserve(&vdq, 30); - assert(ok); - assert(vdq.cap == 30); - - assert(sc_vecdeque_size(&vdq) == 20); - - for (int i = 0; i < 20; ++i) { - // We should retrieve the items we inserted in order - int v = sc_vecdeque_pop(&vdq); - assert(v == i + 5); - } - - assert(sc_vecdeque_size(&vdq) == 0); - - sc_vecdeque_destroy(&vdq); -} - -static void test_vecdeque_grow(void) { - struct SC_VECDEQUE(int) vdq = SC_VECDEQUE_INITIALIZER; - - bool ok = sc_vecdeque_reserve(&vdq, 20); - assert(ok); - assert(vdq.cap == 20); - - assert(sc_vecdeque_size(&vdq) == 0); - - for (int i = 0; i < 500; ++i) { - ok = sc_vecdeque_push(&vdq, i); - assert(ok); - } - - assert(sc_vecdeque_size(&vdq) == 500); - - for (int i = 0; i < 100; ++i) { - int v = sc_vecdeque_pop(&vdq); - assert(v == i); - } - - assert(sc_vecdeque_size(&vdq) == 400); - - for (int i = 500; i < 1000; ++i) { - ok = sc_vecdeque_push(&vdq, i); - assert(ok); - } - - assert(sc_vecdeque_size(&vdq) == 900); - - for (int i = 100; i < 1000; ++i) { - int v = sc_vecdeque_pop(&vdq); - assert(v == i); - } - - assert(sc_vecdeque_size(&vdq) == 0); - - sc_vecdeque_destroy(&vdq); -} - -static void test_vecdeque_push_hole(void) { - struct SC_VECDEQUE(int) vdq = SC_VECDEQUE_INITIALIZER; - - bool ok = sc_vecdeque_reserve(&vdq, 20); - assert(ok); - assert(vdq.cap == 20); - - assert(sc_vecdeque_size(&vdq) == 0); - - for (int i = 0; i < 20; ++i) { - int *p = sc_vecdeque_push_hole(&vdq); - assert(p); - *p = i * 10; - } - - assert(sc_vecdeque_size(&vdq) == 20); - - for (int i = 0; i < 10; ++i) { - int v = sc_vecdeque_pop(&vdq); - assert(v == i * 10); - } - - assert(sc_vecdeque_size(&vdq) == 10); - - for (int i = 20; i < 30; ++i) { - int *p = sc_vecdeque_push_hole(&vdq); - assert(p); - *p = i * 10; - } - - assert(sc_vecdeque_size(&vdq) == 20); - - for (int i = 10; i < 30; ++i) { - int v = sc_vecdeque_pop(&vdq); - assert(v == i * 10); - } - - assert(sc_vecdeque_size(&vdq) == 0); - - sc_vecdeque_destroy(&vdq); -} - -int main(int argc, char *argv[]) { - (void) argc; - (void) argv; - - test_vecdeque_push_pop(); - test_vecdeque_reserve(); - test_vecdeque_grow(); - test_vecdeque_push_hole(); - - return 0; -} diff --git a/app/tests/test_vector.c b/app/tests/test_vector.c deleted file mode 100644 index 459b4e0f..00000000 --- a/app/tests/test_vector.c +++ /dev/null @@ -1,421 +0,0 @@ -#include "common.h" - -#include - -#include "util/vector.h" - -static void test_vector_insert_remove(void) { - struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; - - bool ok; - - ok = sc_vector_push(&vec, 42); - assert(ok); - assert(vec.data[0] == 42); - assert(vec.size == 1); - - ok = sc_vector_push(&vec, 37); - assert(ok); - assert(vec.size == 2); - assert(vec.data[0] == 42); - assert(vec.data[1] == 37); - - ok = sc_vector_insert(&vec, 1, 100); - assert(ok); - assert(vec.size == 3); - assert(vec.data[0] == 42); - assert(vec.data[1] == 100); - assert(vec.data[2] == 37); - - ok = sc_vector_push(&vec, 77); - assert(ok); - assert(vec.size == 4); - assert(vec.data[0] == 42); - assert(vec.data[1] == 100); - assert(vec.data[2] == 37); - assert(vec.data[3] == 77); - - sc_vector_remove(&vec, 1); - assert(vec.size == 3); - assert(vec.data[0] == 42); - assert(vec.data[1] == 37); - assert(vec.data[2] == 77); - - sc_vector_clear(&vec); - assert(vec.size == 0); - - sc_vector_destroy(&vec); -} - -static void test_vector_push_array(void) { - struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; - bool ok; - - ok = sc_vector_push(&vec, 3); assert(ok); - ok = sc_vector_push(&vec, 14); assert(ok); - ok = sc_vector_push(&vec, 15); assert(ok); - ok = sc_vector_push(&vec, 92); assert(ok); - ok = sc_vector_push(&vec, 65); assert(ok); - assert(vec.size == 5); - - int items[] = { 1, 2, 3, 4, 5, 6, 7, 8 }; - ok = sc_vector_push_all(&vec, items, 8); - - assert(ok); - assert(vec.size == 13); - assert(vec.data[0] == 3); - assert(vec.data[1] == 14); - assert(vec.data[2] == 15); - assert(vec.data[3] == 92); - assert(vec.data[4] == 65); - assert(vec.data[5] == 1); - assert(vec.data[6] == 2); - assert(vec.data[7] == 3); - assert(vec.data[8] == 4); - assert(vec.data[9] == 5); - assert(vec.data[10] == 6); - assert(vec.data[11] == 7); - assert(vec.data[12] == 8); - - sc_vector_destroy(&vec); -} - -static void test_vector_insert_array(void) { - struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; - bool ok; - - ok = sc_vector_push(&vec, 3); assert(ok); - ok = sc_vector_push(&vec, 14); assert(ok); - ok = sc_vector_push(&vec, 15); assert(ok); - ok = sc_vector_push(&vec, 92); assert(ok); - ok = sc_vector_push(&vec, 65); assert(ok); - assert(vec.size == 5); - - int items[] = { 1, 2, 3, 4, 5, 6, 7, 8 }; - ok = sc_vector_insert_all(&vec, 3, items, 8); - assert(ok); - assert(vec.size == 13); - assert(vec.data[0] == 3); - assert(vec.data[1] == 14); - assert(vec.data[2] == 15); - assert(vec.data[3] == 1); - assert(vec.data[4] == 2); - assert(vec.data[5] == 3); - assert(vec.data[6] == 4); - assert(vec.data[7] == 5); - assert(vec.data[8] == 6); - assert(vec.data[9] == 7); - assert(vec.data[10] == 8); - assert(vec.data[11] == 92); - assert(vec.data[12] == 65); - - sc_vector_destroy(&vec); -} - -static void test_vector_remove_slice(void) { - struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; - - bool ok; - - for (int i = 0; i < 100; ++i) - { - ok = sc_vector_push(&vec, i); - assert(ok); - } - - assert(vec.size == 100); - - sc_vector_remove_slice(&vec, 32, 60); - assert(vec.size == 40); - assert(vec.data[31] == 31); - assert(vec.data[32] == 92); - - sc_vector_destroy(&vec); -} - -static void test_vector_swap_remove(void) { - struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; - - bool ok; - - ok = sc_vector_push(&vec, 3); assert(ok); - ok = sc_vector_push(&vec, 14); assert(ok); - ok = sc_vector_push(&vec, 15); assert(ok); - ok = sc_vector_push(&vec, 92); assert(ok); - ok = sc_vector_push(&vec, 65); assert(ok); - assert(vec.size == 5); - - sc_vector_swap_remove(&vec, 1); - assert(vec.size == 4); - assert(vec.data[0] == 3); - assert(vec.data[1] == 65); - assert(vec.data[2] == 15); - assert(vec.data[3] == 92); - - sc_vector_destroy(&vec); -} - -static void test_vector_index_of(void) { - struct SC_VECTOR(int) vec; - sc_vector_init(&vec); - - bool ok; - - for (int i = 0; i < 10; ++i) - { - ok = sc_vector_push(&vec, i); - assert(ok); - } - - ssize_t idx; - - idx = sc_vector_index_of(&vec, 0); - assert(idx == 0); - - idx = sc_vector_index_of(&vec, 1); - assert(idx == 1); - - idx = sc_vector_index_of(&vec, 4); - assert(idx == 4); - - idx = sc_vector_index_of(&vec, 9); - assert(idx == 9); - - idx = sc_vector_index_of(&vec, 12); - assert(idx == -1); - - sc_vector_destroy(&vec); -} - -static void test_vector_grow(void) { - struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; - - bool ok; - - for (int i = 0; i < 50; ++i) - { - ok = sc_vector_push(&vec, i); /* append */ - assert(ok); - } - - assert(vec.cap >= 50); - assert(vec.size == 50); - - for (int i = 0; i < 25; ++i) - { - ok = sc_vector_insert(&vec, 20, i); /* insert in the middle */ - assert(ok); - } - - assert(vec.cap >= 75); - assert(vec.size == 75); - - for (int i = 0; i < 25; ++i) - { - ok = sc_vector_insert(&vec, 0, i); /* prepend */ - assert(ok); - } - - assert(vec.cap >= 100); - assert(vec.size == 100); - - for (int i = 0; i < 50; ++i) - sc_vector_remove(&vec, 20); /* remove from the middle */ - - assert(vec.cap >= 50); - assert(vec.size == 50); - - for (int i = 0; i < 25; ++i) - sc_vector_remove(&vec, 0); /* remove from the head */ - - assert(vec.cap >= 25); - assert(vec.size == 25); - - for (int i = 24; i >=0; --i) - sc_vector_remove(&vec, i); /* remove from the tail */ - - assert(vec.size == 0); - - sc_vector_destroy(&vec); -} - -static void test_vector_exp_growth(void) { - struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; - - size_t oldcap = vec.cap; - int realloc_count = 0; - bool ok; - for (int i = 0; i < 10000; ++i) - { - ok = sc_vector_push(&vec, i); - assert(ok); - if (vec.cap != oldcap) - { - realloc_count++; - oldcap = vec.cap; - } - } - - /* Test speciically for an expected growth factor of 1.5. In practice, the - * result is even lower (19) due to the first alloc of size 10 */ - assert(realloc_count <= 23); /* ln(10000) / ln(1.5) ~= 23 */ - - realloc_count = 0; - for (int i = 9999; i >= 0; --i) - { - sc_vector_remove(&vec, i); - if (vec.cap != oldcap) - { - realloc_count++; - oldcap = vec.cap; - } - } - - assert(realloc_count <= 23); /* same expectations for removals */ - assert(realloc_count > 0); /* sc_vector_remove() must autoshrink */ - - sc_vector_destroy(&vec); -} - -static void test_vector_reserve(void) { - struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; - - bool ok; - - ok = sc_vector_reserve(&vec, 800); - assert(ok); - assert(vec.cap >= 800); - assert(vec.size == 0); - - size_t initial_cap = vec.cap; - - for (int i = 0; i < 800; ++i) - { - ok = sc_vector_push(&vec, i); - assert(ok); - assert(vec.cap == initial_cap); /* no realloc */ - } - - sc_vector_destroy(&vec); -} - -static void test_vector_shrink_to_fit(void) { - struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; - - bool ok; - - ok = sc_vector_reserve(&vec, 800); - assert(ok); - for (int i = 0; i < 250; ++i) - { - ok = sc_vector_push(&vec, i); - assert(ok); - } - - assert(vec.cap >= 800); - assert(vec.size == 250); - - sc_vector_shrink_to_fit(&vec); - assert(vec.cap == 250); - assert(vec.size == 250); - - sc_vector_destroy(&vec); -} - -static void test_vector_move(void) { - struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; - - for (int i = 0; i < 7; ++i) - { - bool ok = sc_vector_push(&vec, i); - assert(ok); - } - - /* move item at 1 so that its new position is 4 */ - sc_vector_move(&vec, 1, 4); - - assert(vec.size == 7); - assert(vec.data[0] == 0); - assert(vec.data[1] == 2); - assert(vec.data[2] == 3); - assert(vec.data[3] == 4); - assert(vec.data[4] == 1); - assert(vec.data[5] == 5); - assert(vec.data[6] == 6); - - sc_vector_destroy(&vec); -} - -static void test_vector_move_slice_forward(void) { - struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; - - for (int i = 0; i < 10; ++i) - { - bool ok = sc_vector_push(&vec, i); - assert(ok); - } - - /* move slice {2, 3, 4, 5} so that its new position is 5 */ - sc_vector_move_slice(&vec, 2, 4, 5); - - assert(vec.size == 10); - assert(vec.data[0] == 0); - assert(vec.data[1] == 1); - assert(vec.data[2] == 6); - assert(vec.data[3] == 7); - assert(vec.data[4] == 8); - assert(vec.data[5] == 2); - assert(vec.data[6] == 3); - assert(vec.data[7] == 4); - assert(vec.data[8] == 5); - assert(vec.data[9] == 9); - - sc_vector_destroy(&vec); -} - -static void test_vector_move_slice_backward(void) { - struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; - - for (int i = 0; i < 10; ++i) - { - bool ok = sc_vector_push(&vec, i); - assert(ok); - } - - /* move slice {5, 6, 7} so that its new position is 2 */ - sc_vector_move_slice(&vec, 5, 3, 2); - - assert(vec.size == 10); - assert(vec.data[0] == 0); - assert(vec.data[1] == 1); - assert(vec.data[2] == 5); - assert(vec.data[3] == 6); - assert(vec.data[4] == 7); - assert(vec.data[5] == 2); - assert(vec.data[6] == 3); - assert(vec.data[7] == 4); - assert(vec.data[8] == 8); - assert(vec.data[9] == 9); - - sc_vector_destroy(&vec); -} - -int main(int argc, char *argv[]) { - (void) argc; - (void) argv; - - test_vector_insert_remove(); - test_vector_push_array(); - test_vector_insert_array(); - test_vector_remove_slice(); - test_vector_swap_remove(); - test_vector_move(); - test_vector_move_slice_forward(); - test_vector_move_slice_backward(); - test_vector_index_of(); - test_vector_grow(); - test_vector_exp_growth(); - test_vector_reserve(); - test_vector_shrink_to_fit(); - return 0; -} diff --git a/build.gradle b/build.gradle index 81c91d37..1b6f5aef 100644 --- a/build.gradle +++ b/build.gradle @@ -4,10 +4,10 @@ buildscript { repositories { google() - mavenCentral() + jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:8.7.1' + classpath 'com.android.tools.build:gradle:3.3.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -17,9 +17,10 @@ buildscript { allprojects { repositories { google() - mavenCentral() - } - tasks.withType(JavaCompile) { - options.compilerArgs << "-Xlint:deprecation" + jcenter() } } + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/bump_version b/bump_version deleted file mode 100755 index a0963666..00000000 --- a/bump_version +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash -# -# This script bump scrcpy version by editing all the necessary files. -# -# Usage: -# -# ./bump_version 1.23.4 -# -# Then check the diff manually to confirm that everything is ok. - -set -e - -if [[ $# != 1 ]] -then - echo "Syntax: $0 " >&2 - exit 1 -fi - -VERSION="$1" - -a=( ${VERSION//./ } ) -MAJOR="${a[0]:-0}" -MINOR="${a[1]:-0}" -PATCH="${a[2]:-0}" - -# If VERSION is 1.23.4, then VERSION_CODE is 12304 -VERSION_CODE="$(( $MAJOR * 10000 + $MINOR * 100 + "$PATCH" ))" - -echo "$VERSION: major=$MAJOR minor=$MINOR patch=$PATCH [versionCode=$VERSION_CODE]" -sed -i "s/^\(\s*version: \)'[^']*'/\1'$VERSION'/" meson.build -sed -i "s/^\(\s*versionCode \).*/\1$VERSION_CODE/;s/^\(\s*versionName \).*/\1\"$VERSION\"/" server/build.gradle -sed -i "s/^\(SCRCPY_VERSION_NAME=\).*/\1$VERSION/" server/build_without_gradle.sh -sed -i "s/^\(\s*VALUE \"ProductVersion\", \)\"[^\"]*\"/\1\"$VERSION\"/" app/scrcpy-windows.rc -echo done diff --git a/config/android-checkstyle.gradle b/config/android-checkstyle.gradle index 1e5ce3ba..f998530e 100644 --- a/config/android-checkstyle.gradle +++ b/config/android-checkstyle.gradle @@ -2,7 +2,7 @@ apply plugin: 'checkstyle' check.dependsOn 'checkstyle' checkstyle { - toolVersion = '10.12.5' + toolVersion = '6.19' } task checkstyle(type: Checkstyle) { diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index edda3919..63ee315a 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -37,14 +37,6 @@ page at http://checkstyle.sourceforge.net/config.html --> - - - - - - - - @@ -62,7 +54,7 @@ page at http://checkstyle.sourceforge.net/config.html --> - + @@ -80,6 +72,13 @@ page at http://checkstyle.sourceforge.net/config.html --> + + + + + + + @@ -100,7 +99,7 @@ page at http://checkstyle.sourceforge.net/config.html --> - + @@ -130,6 +129,11 @@ page at http://checkstyle.sourceforge.net/config.html --> + + + + + @@ -153,6 +157,26 @@ page at http://checkstyle.sourceforge.net/config.html --> + + + + + + + + + + + + + + + + + + + + diff --git a/cross_win32.txt b/cross_win32.txt index ddbc65f3..2db35fe0 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -2,17 +2,19 @@ [binaries] name = 'mingw' -c = 'i686-w64-mingw32-gcc' -cpp = 'i686-w64-mingw32-g++' -ar = 'i686-w64-mingw32-ar' -strip = 'i686-w64-mingw32-strip' -pkg-config = 'i686-w64-mingw32-pkg-config' -# backward compatibility -pkgconfig = 'i686-w64-mingw32-pkg-config' -windres = 'i686-w64-mingw32-windres' +c = '/usr/bin/i686-w64-mingw32-gcc' +cpp = '/usr/bin/i686-w64-mingw32-g++' +ar = '/usr/bin/i686-w64-mingw32-ar' +strip = '/usr/bin/i686-w64-mingw32-strip' +pkgconfig = '/usr/bin/i686-w64-mingw32-pkg-config' [host_machine] system = 'windows' cpu_family = 'x86' cpu = 'i686' endian = 'little' + +[properties] +prebuilt_ffmpeg_shared = 'ffmpeg-4.1.3-win32-shared' +prebuilt_ffmpeg_dev = 'ffmpeg-4.1.3-win32-dev' +prebuilt_sdl2 = 'SDL2-2.0.8/i686-w64-mingw32' diff --git a/cross_win64.txt b/cross_win64.txt index a6f16e16..79181653 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -2,17 +2,19 @@ [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' -pkg-config = 'x86_64-w64-mingw32-pkg-config' -# backward compatibility -pkgconfig = 'x86_64-w64-mingw32-pkg-config' -windres = 'x86_64-w64-mingw32-windres' +c = '/usr/bin/x86_64-w64-mingw32-gcc' +cpp = '/usr/bin/x86_64-w64-mingw32-g++' +ar = '/usr/bin/x86_64-w64-mingw32-ar' +strip = '/usr/bin/x86_64-w64-mingw32-strip' +pkgconfig = '/usr/bin/x86_64-w64-mingw32-pkg-config' [host_machine] system = 'windows' cpu_family = 'x86' cpu = 'x86_64' endian = 'little' + +[properties] +prebuilt_ffmpeg_shared = 'ffmpeg-4.1.3-win64-shared' +prebuilt_ffmpeg_dev = 'ffmpeg-4.1.3-win64-dev' +prebuilt_sdl2 = 'SDL2-2.0.8/x86_64-w64-mingw32' diff --git a/doc/audio.md b/doc/audio.md deleted file mode 100644 index 142626f5..00000000 --- a/doc/audio.md +++ /dev/null @@ -1,199 +0,0 @@ -# Audio - -Audio forwarding is supported for devices with Android 11 or higher, and it is -enabled by default: - - - For **Android 12 or newer**, it works out-of-the-box. - - For **Android 11**, you'll need to ensure that the device screen is unlocked - when starting scrcpy. A fake popup will briefly appear to make the system - think that the shell app is in the foreground. Without this, audio capture - will fail. - - For **Android 10 or earlier**, audio cannot be captured and is automatically - disabled. - -If audio capture fails, then mirroring continues with video only (since audio is -enabled by default, it is not acceptable to make scrcpy fail if it is not -available), unless `--require-audio` is set. - - -## No audio - -To disable audio: - -``` -scrcpy --no-audio -``` - -To disable only the audio playback, see [no playback](video.md#no-playback). - -## Audio only - -To play audio only, disable video and control: - -```bash -scrcpy --no-video --no-control -``` - -To play audio without a window: - -```bash -# --no-video and --no-control are implied by --no-window -scrcpy --no-window -# interrupt with Ctrl+C -``` - -Without video, the audio latency is typically not critical, so it might be -interesting to add [buffering](#buffering) to minimize glitches: - -``` -scrcpy --no-video --audio-buffer=200 -``` - -## Source - -By default, the device audio output is forwarded. - -It is possible to capture the device microphone instead: - -``` -scrcpy --audio-source=mic -``` - -For example, to use the device as a dictaphone and record a capture directly on -the computer: - -``` -scrcpy --audio-source=mic --no-video --no-playback --record=file.opus -``` - -Many sources are available: - - - `output` (default): forwards the whole audio output, and disables playback on the device (mapped to [`REMOTE_SUBMIX`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#REMOTE_SUBMIX)). - - `playback`: captures the audio playback (Android apps can opt-out, so the whole output is not necessarily captured). - - `mic`: captures the microphone (mapped to [`MIC`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#MIC)). - - `mic-unprocessed`: captures the microphone unprocessed (raw) sound (mapped to [`UNPROCESSED`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#UNPROCESSED)). - - `mic-camcorder`: captures the microphone tuned for video recording, with the same orientation as the camera if available (mapped to [`CAMCORDER`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#CAMCORDER)). - - `mic-voice-recognition`: captures the microphone tuned for voice recognition (mapped to [`VOICE_RECOGNITION`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_RECOGNITION)). - - `mic-voice-communication`: captures the microphone tuned for voice communications (it will for instance take advantage of echo cancellation or automatic gain control if available) (mapped to [`VOICE_COMMUNICATION`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_COMMUNICATION)). - - `voice-call`: captures voice call (mapped to [`VOICE_CALL`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_CALL)). - - `voice-call-uplink`: captures voice call uplink only (mapped to [`VOICE_UPLINK`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_UPLINK)). - - `voice-call-downlink`: captures voice call downlink only (mapped to [`VOICE_DOWNLINK`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_DOWNLINK)). - - `voice-performance`: captures audio meant to be processed for live performance (karaoke), includes both the microphone and the device playback (mapped to [`VOICE_PERFORMANCE`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_PERFORMANCE)). - -### Duplication - -An alternative device audio capture method is also available (only for Android -13 and above): - -``` -scrcpy --audio-source=playback -``` - -This audio source supports keeping the audio playing on the device while -mirroring, with `--audio-dup`: - -```bash -scrcpy --audio-source=playback --audio-dup -# or simply: -scrcpy --audio-dup # --audio-source=playback is implied -``` - -However, it requires Android 13, and Android apps can opt-out (so they are not -captured). - - -See [#4380](https://github.com/Genymobile/scrcpy/issues/4380). - - -## Codec - -The audio codec can be selected. The possible values are `opus` (default), -`aac`, `flac` and `raw` (uncompressed PCM 16-bit LE): - -```bash -scrcpy --audio-codec=opus # default -scrcpy --audio-codec=aac -scrcpy --audio-codec=flac -scrcpy --audio-codec=raw -``` - -In particular, if you get the following error: - -> Failed to initialize audio/opus, error 0xfffffffe - -then your device has no Opus encoder: try `scrcpy --audio-codec=aac`. - -For advanced usage, to pass arbitrary parameters to the [`MediaFormat`], -check `--audio-codec-options` in the manpage or in `scrcpy --help`. - -For example, to change the [FLAC compression level]: - -```bash -scrcpy --audio-codec=flac --audio-codec-options=flac-compression-level=8 -``` - -[`MediaFormat`]: https://developer.android.com/reference/android/media/MediaFormat -[FLAC compression level]: https://developer.android.com/reference/android/media/MediaFormat#KEY_FLAC_COMPRESSION_LEVEL - - -## Encoder - -Several encoders may be available on the device. They can be listed by: - -```bash -scrcpy --list-encoders -``` - -To select a specific encoder: - -```bash -scrcpy --audio-codec=opus --audio-encoder='c2.android.opus.encoder' -``` - - -## Bit rate - -The default audio bit rate is 128Kbps. To change it: - -```bash -scrcpy --audio-bit-rate=64K -scrcpy --audio-bit-rate=64000 # equivalent -``` - -_This parameter does not apply to RAW audio codec (`--audio-codec=raw`)._ - - -## Buffering - -Audio buffering is unavoidable. It must be kept small enough so that the latency -is acceptable, but large enough to minimize buffer underrun (causing audio -glitches). - -The default buffer size is set to 50ms. It can be adjusted: - -```bash -scrcpy --audio-buffer=40 # smaller than default -scrcpy --audio-buffer=100 # higher than default -``` - -Note that this option changes the _target_ buffering. It is possible that this -target buffering might not be reached (on frequent buffer underflow typically). - -If you don't interact with the device (to watch a video for example), a higher -latency (for both [video](video.md#buffering) and audio) might be preferable to -avoid glitches and smooth the playback: - -``` -scrcpy --video-buffer=200 --audio-buffer=200 -``` - -It is also possible to configure another audio buffer (the audio output buffer), -by default set to 5ms. Don't change it, unless you get some [robotic and glitchy -sound][#3793]: - -```bash -# Only if absolutely necessary -scrcpy --audio-output-buffer=10 -``` - -[#3793]: https://github.com/Genymobile/scrcpy/issues/3793 diff --git a/doc/camera.md b/doc/camera.md deleted file mode 100644 index 32417694..00000000 --- a/doc/camera.md +++ /dev/null @@ -1,171 +0,0 @@ -# Camera - -Camera mirroring is supported for devices with Android 12 or higher. - -To capture the camera instead of the device screen: - -``` -scrcpy --video-source=camera -``` - -By default, it automatically switches [audio source](audio.md#source) to -microphone (as if `--audio-source=mic` were also passed). - -```bash -scrcpy --video-source=display # default is --audio-source=output -scrcpy --video-source=camera # default is --audio-source=mic -scrcpy --video-source=display --audio-source=mic # force display AND microphone -scrcpy --video-source=camera --audio-source=output # force camera AND device audio output -``` - -Audio can be disabled: - -```bash -# audio not captured at all -scrcpy --video-source=camera --no-audio -scrcpy --video-source=camera --no-audio --record=file.mp4 - -# audio captured and recorded, but not played -scrcpy --video-source=camera --no-audio-playback --record=file.mp4 -``` - - -## List - -To list the cameras available (with their declared valid sizes and frame rates): - -``` -scrcpy --list-cameras -scrcpy --list-camera-sizes -``` - -_Note that the sizes and frame rates are declarative. They are not accurate on -all devices: some of them are declared but not supported, while some others are -not declared but supported._ - - -## Selection - -It is possible to pass an explicit camera id (as listed by `--list-cameras`): - -``` -scrcpy --video-source=camera --camera-id=0 -``` - -Alternatively, the camera may be selected automatically: - -```bash -scrcpy --video-source=camera # use the first camera -scrcpy --video-source=camera --camera-facing=front # use the first front camera -scrcpy --video-source=camera --camera-facing=back # use the first back camera -scrcpy --video-source=camera --camera-facing=external # use the first external camera -``` - -If `--camera-id` is specified, then `--camera-facing` is forbidden (the id -already determines the camera): - -```bash -scrcpy --video-source=camera --camera-id=0 --camera-facing=front # error -``` - - -### Size selection - -It is possible to pass an explicit camera size: - -``` -scrcpy --video-source=camera --camera-size=1920x1080 -``` - -The given size may be listed among the declared valid sizes -(`--list-camera-sizes`), but may also be anything else (some devices support -arbitrary sizes): - -``` -scrcpy --video-source=camera --camera-size=1840x444 -``` - -Alternatively, a declared valid size (among the ones listed by -`list-camera-sizes`) may be selected automatically. - -Two constraints are supported: - - `-m`/`--max-size` (already used for display mirroring), for example `-m1920`; - - `--camera-ar` to specify an aspect ratio (`:`, `` or - `sensor`). - -Some examples: - -```bash -scrcpy --video-source=camera # use the greatest width and the greatest associated height -scrcpy --video-source=camera -m1920 # use the greatest width not above 1920 and the greatest associated height -scrcpy --video-source=camera --camera-ar=4:3 # use the greatest size with an aspect ratio of 4:3 (+/- 10%) -scrcpy --video-source=camera --camera-ar=1.6 # use the greatest size with an aspect ratio of 1.6 (+/- 10%) -scrcpy --video-source=camera --camera-ar=sensor # use the greatest size with the aspect ratio of the camera sensor (+/- 10%) -scrcpy --video-source=camera -m1920 --camera-ar=16:9 # use the greatest width not above 1920 and the closest to 16:9 aspect ratio -``` - -If `--camera-size` is specified, then `-m`/`--max-size` and `--camera-ar` are -forbidden (the size is determined by the value given explicitly): - -```bash -scrcpy --video-source=camera --camera-size=1920x1080 -m3000 # error -``` - - -## Rotation - -To rotate the captured video, use the [video orientation](video.md#orientation) -option: - -``` -scrcpy --video-source=camera --camera-size=1920x1080 --orientation=90 -``` - - -## Frame rate - -By default, camera is captured at Android's default frame rate (30 fps). - -To configure a different frame rate: - -``` -scrcpy --video-source=camera --camera-fps=60 -``` - - -## High speed capture - -The Android camera API also supports a [high speed capture mode][high speed]. - -This mode is restricted to specific resolutions and frame rates, listed by -`--list-camera-sizes`. - -``` -scrcpy --video-source=camera --camera-size=1920x1080 --camera-fps=240 -``` - -[high speed]: https://developer.android.com/reference/android/hardware/camera2/CameraConstrainedHighSpeedCaptureSession - - -## Brace expansion tip - -All camera options start with `--camera-`, so if your shell supports it, you can -benefit from [brace expansion] (for example, it is supported _bash_ and _zsh_): - -```bash -scrcpy --video-source=camera --camera-{facing=back,ar=16:9,high-speed,fps=120} -``` - -This will be expanded as: - -```bash -scrcpy --video-source=camera --camera-facing=back --camera-ar=16:9 --camera-high-speed --camera-fps=120 -``` - -[brace expansion]: https://www.gnu.org/software/bash/manual/html_node/Brace-Expansion.html - - -## Webcam - -Combined with the [V4L2](v4l2.md) feature on Linux, the Android device camera -may be used as a webcam on the computer. diff --git a/doc/connection.md b/doc/connection.md deleted file mode 100644 index dcf00147..00000000 --- a/doc/connection.md +++ /dev/null @@ -1,132 +0,0 @@ -# Connection - -## Selection - -If exactly one device is connected (i.e. listed by `adb devices`), then it is -automatically selected. - -However, if there are multiple devices connected, you must specify the one to -use in one of 4 ways: - - by its serial: - ```bash - scrcpy --serial=0123456789abcdef - scrcpy -s 0123456789abcdef # short version - - # the serial is the ip:port if connected over TCP/IP (same behavior as adb) - scrcpy --serial=192.168.1.1:5555 - ``` - - the one connected over USB (if there is exactly one): - ```bash - scrcpy --select-usb - scrcpy -d # short version - ``` - - the one connected over TCP/IP (if there is exactly one): - ```bash - scrcpy --select-tcpip - scrcpy -e # short version - ``` - - a device already listening on TCP/IP (see [below](#tcpip-wireless)): - ```bash - scrcpy --tcpip=192.168.1.1:5555 - scrcpy --tcpip=192.168.1.1 # default port is 5555 - ``` - -The serial may also be provided via the environment variable `ANDROID_SERIAL` -(also used by `adb`): - -```bash -# in bash -export ANDROID_SERIAL=0123456789abcdef -scrcpy -``` - -```cmd -:: in cmd -set ANDROID_SERIAL=0123456789abcdef -scrcpy -``` - -```powershell -# in PowerShell -$env:ANDROID_SERIAL = '0123456789abcdef' -scrcpy -``` - - -## TCP/IP (wireless) - -_Scrcpy_ uses `adb` to communicate with the device, and `adb` can [connect] to a -device over TCP/IP. The device must be connected on the same network as the -computer. - -[connect]: https://developer.android.com/studio/command-line/adb.html#wireless - - -### Automatic - -An option `--tcpip` allows to configure the connection automatically. There are -two variants. - -If _adb_ TCP/IP mode is disabled on the device (or if you don't know the IP -address), connect the device over USB, then run: - -```bash -scrcpy --tcpip # without arguments -``` - -It will automatically find the device IP address and adb port, enable TCP/IP -mode if necessary, then connect to the device before starting. - -If the device (accessible at 192.168.1.1 in this example) already listens on a -port (typically 5555) for incoming _adb_ connections, then run: - -```bash -scrcpy --tcpip=192.168.1.1 # default port is 5555 -scrcpy --tcpip=192.168.1.1:5555 -``` - -Prefix the address with a '+' to force a reconnection: - -```bash -scrcpy --tcpip=+192.168.1.1 -``` - - -### Manual - -Alternatively, it is possible to enable the TCP/IP connection manually using -`adb`: - -1. Plug the device into a USB port on your computer. -2. Connect the device to the same Wi-Fi network as your computer. -3. Get your device IP address, in Settings → About phone → Status, or by - executing this command: - - ```bash - adb shell ip route | awk '{print $9}' - ``` - -4. Enable `adb` over TCP/IP on your device: `adb tcpip 5555`. -5. Unplug your device. -6. Connect to your device: `adb connect DEVICE_IP:5555` _(replace `DEVICE_IP` -with the device IP address you found)_. -7. Run `scrcpy` as usual. -8. Run `adb disconnect` once you're done. - -Since Android 11, a [wireless debugging option][adb-wireless] allows you to -bypass having to physically connect your device to your computer. - -[adb-wireless]: https://developer.android.com/studio/command-line/adb#wireless-android11-command-line - - -## Autostart - -A small tool (by the scrcpy author) allows you to run arbitrary commands -whenever a new Android device is connected: [AutoAdb]. It can be used to start -scrcpy: - -```bash -autoadb scrcpy -s '{}' -``` - -[AutoAdb]: https://github.com/rom1v/autoadb diff --git a/doc/control.md b/doc/control.md deleted file mode 100644 index 86c0efe6..00000000 --- a/doc/control.md +++ /dev/null @@ -1,140 +0,0 @@ -# Control - -## Read-only - -To disable controls (everything which can interact with the device: input keys, -mouse events, drag&drop files): - -```bash -scrcpy --no-control -scrcpy -n # short version -``` - -## Keyboard and mouse - -Read [keyboard](keyboard.md) and [mouse](mouse.md). - - -## Control only - -To control the device without mirroring: - -```bash -scrcpy --no-video --no-audio -``` - -By default, the mouse is disabled when video playback is turned off. - -To control the device using a relative mouse, enable UHID mouse mode: - -```bash -scrcpy --no-video --no-audio --mouse=uhid -scrcpy --no-video --no-audio -M # short version -``` - -To also use a UHID keyboard, set it explicitly: - -```bash -scrcpy --no-video --no-audio --mouse=uhid --keyboard=uhid -scrcpy --no-video --no-audio -MK # short version -``` - -To use AOA instead (over USB only): - -```bash -scrcpy --no-video --no-audio --keyboard=aoa --mouse=aoa -``` - - -## Copy-paste - -Any time the Android clipboard changes, it is automatically synchronized to the -computer clipboard. - -Any Ctrl shortcut is forwarded to the device. In particular: - - Ctrl+c typically copies - - Ctrl+x typically cuts - - Ctrl+v typically pastes (after computer-to-device - clipboard synchronization) - -This typically works as you expect. - -The actual behavior depends on the active application though. For example, -_Termux_ sends SIGINT on Ctrl+c instead, and _K-9 Mail_ -composes a new message. - -To copy, cut and paste in such cases (but only supported on Android >= 7): - - MOD+c injects `COPY` - - MOD+x injects `CUT` - - MOD+v injects `PASTE` (after computer-to-device - clipboard synchronization) - -In addition, MOD+Shift+v injects the computer -clipboard text as a sequence of key events. This is useful when the component -does not accept text pasting (for example in _Termux_), but it can break -non-ASCII content. - -**WARNING:** Pasting the computer clipboard to the device (either via -Ctrl+v or MOD+v) copies the content -into the Android clipboard. As a consequence, any Android application could read -its content. You should avoid pasting sensitive content (like passwords) that -way. - -Some Android devices do not behave as expected when setting the device clipboard -programmatically. An option `--legacy-paste` is provided to change the behavior -of Ctrl+v and MOD+v so that they -also inject the computer clipboard text as a sequence of key events (the same -way as MOD+Shift+v). - -To disable automatic clipboard synchronization, use -`--no-clipboard-autosync`. - - -## Pinch-to-zoom, rotate and tilt simulation - -To simulate "pinch-to-zoom": Ctrl+_click-and-move_. - -More precisely, hold down Ctrl while pressing the left-click button. -Until the left-click button is released, all mouse movements scale and rotate -the content (if supported by the app) relative to the center of the screen. - -https://github.com/Genymobile/scrcpy/assets/543275/26c4a920-9805-43f1-8d4c-608752d04767 - -To simulate a vertical tilt gesture: Shift+_click-and-move-up-or-down_. - -https://github.com/Genymobile/scrcpy/assets/543275/1e252341-4a90-4b29-9d11-9153b324669f - -Similarly, to simulate a horizontal tilt gesture: -Ctrl+Shift+_click-and-move-left-or-right_. - -Technically, _scrcpy_ generates additional touch events from a "virtual finger" -at a location inverted through the center of the screen. When pressing -Ctrl the _x_ and _y_ coordinates are inverted. Using Shift -only inverts _x_, whereas using Ctrl+Shift only inverts -_y_. - -This only works for the default mouse mode (`--mouse=sdk`). - - -## File drop - -### Install APK - -To install an APK, drag & drop an APK file (ending with `.apk`) to the _scrcpy_ -window. - -There is no visual feedback, a log is printed to the console. - - -### Push file to device - -To push a file to `/sdcard/Download/` on the device, drag & drop a (non-APK) -file to the _scrcpy_ window. - -There is no visual feedback, a log is printed to the console. - -The target directory can be changed on start: - -```bash -scrcpy --push-target=/sdcard/Movies/ -``` diff --git a/doc/develop.md b/doc/develop.md deleted file mode 100644 index 21949ea6..00000000 --- a/doc/develop.md +++ /dev/null @@ -1,492 +0,0 @@ -# scrcpy for developers - -## Overview - -This application is composed of two parts: - - the server (`scrcpy-server`), to be executed on the device, - - the client (the `scrcpy` binary), executed on the host computer. - -The client is responsible to push the server to the device and start its -execution. - -The client and the server establish communication using separate sockets for -video, audio and controls. Any of them may be disabled (but not all), so -there are 1, 2 or 3 socket(s). - -The server initially sends the device name on the first socket (it is used for -the scrcpy window title), then each socket is used for its own purpose. All -reads and writes are performed from a dedicated thread for each socket, both on -the client and on the server. - -If video is enabled, then the server sends a raw video stream (H.264 by default) -of the device screen, with some additional headers for each packet. The client -decodes the video frames, and displays them as soon as possible, without -buffering (unless `--video-buffer=delay` is specified) to minimize latency. The -client is not aware of the device rotation (which is handled by the server), it -just knows the dimensions of the video frames it receives. - -Similarly, if audio is enabled, then the server sends a raw audio stream (OPUS -by default) of the device audio output (or the microphone if -`--audio-source=mic` is specified), with some additional headers for each -packet. The client decodes the stream, attempts to keep a minimal latency by -maintaining an average buffering. The [blog post][scrcpy2] of the scrcpy v2.0 -release gives more details about the audio feature. - -If control is enabled, then the client captures relevant keyboard and mouse -events, that it transmits to the server, which injects them to the device. This -is the only socket which is used in both direction: input events are sent from -the client to the device, and when the device clipboard changes, the new content -is sent from the device to the client to support seamless copy-paste. - -[scrcpy2]: https://blog.rom1v.com/2023/03/scrcpy-2-0-with-audio/ - -Note that the client-server roles are expressed at the application level: - - - the server _serves_ video and audio streams, and handle requests from the - client, - - the client _controls_ the device through the server. - -However, by default (when `--force-adb-forward` is not set), the roles are -reversed at the network level: - - - the client opens a server socket and listen on a port before starting the - server, - - the server connects to the client. - -This role inversion guarantees that the connection will not fail due to race -conditions without polling. - - -## Server - - -### Privileges - -Capturing the screen requires some privileges, which are granted to `shell`. - -The server is a Java application (with a [`public static void main(String... -args)`][main] method), compiled against the Android framework, and executed as -`shell` on the Android device. - -[main]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/Server.java#L193 - -To run such a Java application, the classes must be [_dexed_][dex] (typically, -to `classes.dex`). If `my.package.MainClass` is the main class, compiled to -`classes.dex`, pushed to the device in `/data/local/tmp`, then it can be run -with: - - adb shell CLASSPATH=/data/local/tmp/classes.dex app_process / my.package.MainClass - -_The path `/data/local/tmp` is a good candidate to push the server, since it's -readable and writable by `shell`, but not world-writable, so a malicious -application may not replace the server just before the client executes it._ - -Instead of a raw _dex_ file, `app_process` accepts a _jar_ containing -`classes.dex` (e.g. an [APK]). For simplicity, and to benefit from the gradle -build system, the server is built to an (unsigned) APK (renamed to -`scrcpy-server.jar`). - -[dex]: https://en.wikipedia.org/wiki/Dalvik_(software) -[apk]: https://en.wikipedia.org/wiki/Android_application_package - - -### Hidden methods - -Although compiled against the Android framework, [hidden] methods and classes are -not directly accessible (and they may differ from one Android version to -another). - -They can be called using reflection though. The communication with hidden -components is provided by [_wrappers_ classes][wrappers] and [aidl]. - -[hidden]: https://stackoverflow.com/a/31908373/1987178 -[wrappers]: https://github.com/Genymobile/scrcpy/tree/master/server/src/main/java/com/genymobile/scrcpy/wrappers -[aidl]: https://github.com/Genymobile/scrcpy/tree/master/server/src/main/aidl - - - -### Execution - -The server is started by the client basically by executing the following -commands: - -```bash -adb push scrcpy-server /data/local/tmp/scrcpy-server.jar -adb forward tcp:27183 localabstract:scrcpy -adb shell CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process / com.genymobile.scrcpy.Server 2.1 -``` - -The first argument (`2.1` in the example) is the client scrcpy version. The -server fails if the client and the server do not have the exact same version. -The protocol between the client and the server may change from version to -version (see [protocol](#protocol) below), and there is no backward or forward -compatibility (there is no point to use different client and server versions). -This check allows to detect misconfiguration (running an older or newer server -by mistake). - -It is followed by any number of arguments, in the form of `key=value` pairs. -Their order is irrelevant. The possible keys and associated value types can be -found in the [server][server-options] and [client][client-options] code. - -[server-options]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/Options.java#L181 -[client-options]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/app/src/server.c#L226 - -For example, if we execute `scrcpy -m1920 --no-audio`, then the server -execution will look like this: - -```bash -# scid is a random number to identify different clients running on the same device -adb shell CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process / com.genymobile.scrcpy.Server 2.1 scid=12345678 log_level=info audio=false max_size=1920 -``` - -### Components - -When executed, its [`main()`][main] method is executed (on the "main" thread). -It parses the arguments, establishes the connection with the client and starts -the other "components": - - the **video** streamer: it captures the video screen and send encoded video - packets on the _video_ socket (from the _video_ thread). - - the **audio** streamer: it uses several threads to capture raw packets, - submits them to encoding and retrieve encoded packets, which it sends on the - _audio_ socket. - - the **controller**: it receives _control messages_ (typically input events) - on the _control_ socket from one thread, and sends _device messages_ (e.g. to - transmit the device clipboard content to the client) on the same _control - socket_ from another thread. Thus, the _control_ socket is used in both - directions (contrary to the _video_ and _audio_ sockets). - - -### Screen video encoding - -The encoding is managed by [`ScreenEncoder`]. - -The video is encoded using the [`MediaCodec`] API. The codec encodes the content -of a `Surface` associated to the display, and writes the encoding packets to the -client (on the _video_ socket). - -[`ScreenEncoder`]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java -[`MediaCodec`]: https://developer.android.com/reference/android/media/MediaCodec.html - -On device rotation (or folding), the encoding session is [reset] and restarted. - -New frames are produced only when changes occur on the surface. This avoids to -send unnecessary frames, but by default there might be drawbacks: - - - it does not send any frame on start if the device screen does not change, - - after fast motion changes, the last frame may have poor quality. - -Both problems are [solved][repeat] by the flag -[`KEY_REPEAT_PREVIOUS_FRAME_AFTER`][repeat-flag]. - -[reset]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L179 -[rotation]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L90 -[repeat]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L246-L247 -[repeat-flag]: https://developer.android.com/reference/android/media/MediaFormat.html#KEY_REPEAT_PREVIOUS_FRAME_AFTER - - -### Audio encoding - -Similarly, the audio is [captured] using an [`AudioRecord`], and [encoded] using -the [`MediaCodec`] asynchronous API. - -More details are available on the [blog post][scrcpy2] introducing the audio feature. - -[captured]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java -[encoded]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java -[`AudioRecord`]: https://developer.android.com/reference/android/media/AudioRecord - - -### Input events injection - -_Control messages_ are received from the client by the [`Controller`] (run in a -separate thread). There are several types of input events: - - keycode (cf [`KeyEvent`]), - - text (special characters may not be handled by keycodes directly), - - mouse motion/click, - - mouse scroll, - - other commands (e.g. to switch the screen on or to copy the clipboard). - -Some of them need to inject input events to the system. To do so, they use the -_hidden_ method [`InputManager.injectInputEvent()`] (exposed by the -[`InputManager` wrapper][inject-wrapper]). - -[`Controller`]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/Controller.java -[`KeyEvent`]: https://developer.android.com/reference/android/view/KeyEvent.html -[`MotionEvent`]: https://developer.android.com/reference/android/view/MotionEvent.html -[`InputManager.injectInputEvent()`]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L34 -[inject-wrapper]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L27 - - - -## Client - -The client relies on [SDL], which provides cross-platform API for UI, input -events, threading, etc. - -The video and audio streams are decoded by [FFmpeg]. - -[SDL]: https://www.libsdl.org -[ffmpeg]: https://ffmpeg.org/ - - -### Initialization - -The client parses the command line arguments, then [runs one of two code -paths][run]: - - scrcpy in "normal" mode ([`scrcpy.c`]) - - scrcpy in [OTG mode](otg.md) ([`scrcpy_otg.c`]) - -[run]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/app/src/main.c#L81-L82 -[`scrcpy.c`]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/app/src/scrcpy.c#L292-L293 -[`scrcpy_otg.c`]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/app/src/usb/scrcpy_otg.c#L51-L52 - -In the remaining of this document, we assume that the "normal" mode is used -(read the code for the OTG mode). - -On startup, the client: - - opens the _video_, _audio_ and _control_ sockets; - - pushes and starts the server on the device; - - initializes its components (demuxers, decoders, recorder…). - - -### Video and audio streams - -Depending on the arguments passed to `scrcpy`, several components may be used. -Here is an overview of the video and audio components: - -``` - V4L2 sink - / - decoder - / \ - VIDEO -------------> demuxer display - \ - recorder - / - AUDIO -------------> demuxer - \ - decoder --- audio player -``` - -The _demuxer_ is responsible to extract video and audio packets (read some -header, split the video stream into packets at correct boundaries, etc.). - -The demuxed packets may be sent to a _decoder_ (one per stream, to produce -frames) and to a recorder (receiving both video and audio stream to record a -single file). The packets are encoded on the device (by `MediaCodec`), but when -recording, they are _muxed_ (asynchronously) into a container (MKV or MP4) on -the client side. - -Video frames are sent to the screen/display to be rendered in the scrcpy window. -They may also be sent to a [V4L2 sink](v4l2.md). - -Audio "frames" (an array of decoded samples) are sent to the audio player. - - -### Controller - -The _controller_ is responsible to send _control messages_ to the device. It -runs in a separate thread, to avoid I/O on the main thread. - -On SDL event, received on the main thread, the _input manager_ creates -appropriate _control messages_. It is responsible to convert SDL events to -Android events. It then pushes the _control messages_ to a queue hold by the -controller. On its own thread, the controller takes messages from the queue, -that it serializes and sends to the client. - - -## Protocol - -The protocol between the client and the server must be considered _internal_: it -may (and will) change at any time for any reason. Everything may change (the -number of sockets, the order in which the sockets must be opened, the data -format on the wire…) from version to version. A client must always be run with a -matching server version. - -This section documents the current protocol in scrcpy v2.1. - -### Connection - -Firstly, the client sets up an adb tunnel: - -```bash -# By default, a reverse redirection: the computer listens, the device connects -adb reverse localabstract:scrcpy_ tcp:27183 - -# As a fallback (or if --force-adb forward is set), a forward redirection: -# the device listens, the computer connects -adb forward tcp:27183 localabstract:scrcpy_ -``` - -(`` is a 31-bit random number, so that it does not fail when several -scrcpy instances start "at the same time" for the same device.) - -Then, up to 3 sockets are opened, in that order: - - a _video_ socket - - an _audio_ socket - - a _control_ socket - -Each one may be disabled (respectively by `--no-video`, `--no-audio` and -`--no-control`, directly or indirectly). For example, if `--no-audio` is set, -then the _video_ socket is opened first, then the _control_ socket. - -On the _first_ socket opened (whichever it is), if the tunnel is _forward_, then -a [dummy byte] is sent from the device to the client. This allows to detect a -connection error (the client connection does not fail as long as there is an adb -forward redirection, even if nothing is listening on the device side). - -Still on this _first_ socket, the device sends some [metadata][device meta] to -the client (currently only the device name, used as the window title, but there -might be other fields in the future). - -[dummy byte]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java#L93 -[device meta]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java#L151 - -You can read the [client][client-connection] and [server][server-connection] -code for more details. - -[client-connection]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/app/src/server.c#L465-L466 -[server-connection]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java#L63 - -Then each socket is used for its intended purpose. - -### Video and audio - -On the _video_ and _audio_ sockets, the device first sends some [codec -metadata]: - - On the _video_ socket, 12 bytes: - - the codec id (`u32`) (H264, H265 or AV1) - - the initial video width (`u32`) - - the initial video height (`u32`) - - On the _audio_ socket, 4 bytes: - - the codec id (`u32`) (OPUS, AAC or RAW) - -[codec metadata]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/Streamer.java#L33-L51 - -Then each packet produced by `MediaCodec` is sent, prefixed by a 12-byte [frame -header]: - - config packet flag (`u1`) - - key frame flag (`u1`) - - PTS (`u62`) - - packet size (`u32`) - -Here is a schema describing the frame header: - -``` - [. . . . . . . .|. . . .]. . . . . . . . . . . . . . . ... - <-------------> <-----> <-----------------------------... - PTS packet raw packet - size - <---------------------> - frame header - -The most significant bits of the PTS are used for packet flags: - - byte 7 byte 6 byte 5 byte 4 byte 3 byte 2 byte 1 byte 0 - CK...... ........ ........ ........ ........ ........ ........ ........ - ^^<-------------------------------------------------------------------> - || PTS - | `- key frame - `-- config packet -``` - -[frame header]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/Streamer.java#L83 - - -### Controls - -Controls messages are sent via a custom binary protocol. - -The only documentation for this protocol is the set of unit tests on both sides: - - `ControlMessage` (from client to device): [serialization](https://github.com/Genymobile/scrcpy/blob/master/app/tests/test_control_msg_serialize.c) | [deserialization](https://github.com/Genymobile/scrcpy/blob/master/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java) - - `DeviceMessage` (from device to client) [serialization](https://github.com/Genymobile/scrcpy/blob/master/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java) | [deserialization](https://github.com/Genymobile/scrcpy/blob/master/app/tests/test_device_msg_deserialize.c) - - -## Standalone server - -Although the server is designed to work for the scrcpy client, it can be used -with any client which uses the same protocol. - -For simplicity, some [server-specific options] have been added to produce raw -streams easily: - - `send_device_meta=false`: disable the device metata (in practice, the device - name) sent on the _first_ socket - - `send_frame_meta=false`: disable the 12-byte header for each packet - - `send_dummy_byte`: disable the dummy byte sent on forward connections - - `send_codec_meta`: disable the codec information (and initial device size for - video) - - `raw_stream`: disable all the above - -[server-specific options]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/Options.java#L309-L329 - -Concretely, here is how to expose a raw H.264 stream on a TCP socket: - -```bash -adb push scrcpy-server-v2.1 /data/local/tmp/scrcpy-server-manual.jar -adb forward tcp:1234 localabstract:scrcpy -adb shell CLASSPATH=/data/local/tmp/scrcpy-server-manual.jar \ - app_process / com.genymobile.scrcpy.Server 2.1 \ - tunnel_forward=true audio=false control=false cleanup=false \ - raw_stream=true max_size=1920 -``` - -As soon as a client connects over TCP on port 1234, the device will start -streaming the video. For example, VLC can play the video (although you will -experience a very high latency, more details [here][vlc-0latency]): - -``` -vlc -Idummy --demux=h264 --network-caching=0 tcp://localhost:1234 -``` - -[vlc-0latency]: https://code.videolan.org/rom1v/vlc/-/merge_requests/20 - - -## Hack - -For more details, go read the code! - -If you find a bug, or have an awesome idea to implement, please discuss and -contribute ;-) - - -### Debug the server - -The server is pushed to the device by the client on startup. - -To debug it, enable the server debugger during configuration: - -```bash -meson setup x -Dserver_debugger=true -# or, if x is already configured -meson configure x -Dserver_debugger=true -``` - -Then recompile, and run scrcpy. - -For Android < 11, it will start a debugger on port 5005 on the device and wait: -Redirect that port to the computer: - -```bash -adb forward tcp:5005 tcp:5005 -``` - -For Android >= 11, first find the listening port: - -```bash -adb jdwp -# press Ctrl+C to interrupt -``` - -Then redirect the resulting PID: - -```bash -adb forward tcp:5005 jdwp:XXXX # replace XXXX -``` - -In Android Studio, _Run_ > _Debug_ > _Edit configurations..._ On the left, click -on `+`, _Remote_, and fill the form: - - - Host: `localhost` - - Port: `5005` - -Then click on _Debug_. diff --git a/doc/device.md b/doc/device.md deleted file mode 100644 index ab1e6ba4..00000000 --- a/doc/device.md +++ /dev/null @@ -1,184 +0,0 @@ -# Device - -Some command line arguments perform actions on the device itself while scrcpy is -running. - -## Stay awake - -To prevent the device from sleeping after a delay **when the device is plugged -in**: - -```bash -scrcpy --stay-awake -scrcpy -w -``` - -The initial state is restored when _scrcpy_ is closed. - -If the device is not plugged in (i.e. only connected over TCP/IP), -`--stay-awake` has no effect (this is the Android behavior). - -This changes the value of [`stay_on_while_plugged_in`], setting which can be -changed manually: - -[`stay_on_while_plugged_in`]: https://developer.android.com/reference/android/provider/Settings.Global#STAY_ON_WHILE_PLUGGED_IN - - -```bash -# get the current show_touches value -adb shell settings get global stay_on_while_plugged_in -# enable for AC/USB/wireless chargers -adb shell settings put global stay_on_while_plugged_in 7 -# disable -adb shell settings put global stay_on_while_plugged_in 0 -``` - - -## Screen off timeout - -The Android screen automatically turns off after some delay. - -To change this delay while scrcpy is running: - -```bash -scrcpy --screen-off-timeout=300 # 300 seconds (5 minutes) -``` - -The initial value is restored on exit. - -It is possible to change this setting manually: - -```bash -# get the current screen_off_timeout value -adb shell settings get system screen_off_timeout -# set a new value (in milliseconds) -adb shell settings put system screen_off_timeout 30000 -``` - -Note that the Android value is in milliseconds, but the scrcpy command line -argument is in seconds. - - -## Turn screen off - -It is possible to turn the device screen off while mirroring on start with a -command-line option: - -```bash -scrcpy --turn-screen-off -scrcpy -S # short version -``` - -Or by pressing MOD+o at any time (see -[shortcuts](shortcuts.md)). - -To turn it back on, press MOD+Shift+o. - -On Android, the `POWER` button always turns the screen on. For convenience, if -`POWER` is sent via _scrcpy_ (via right-click or MOD+p), -it will force to turn the screen off after a small delay (on a best effort -basis). The physical `POWER` button will still cause the screen to be turned on. - -It can also be useful to prevent the device from sleeping: - -```bash -scrcpy --turn-screen-off --stay-awake -scrcpy -Sw # short version -``` - -Since Android 15, it is possible to change this setting manually: - -``` -# turn screen off (0 for main display) -adb shell cmd display power-off 0 -# turn screen on -adb shell cmd display power-on 0 -``` - - -## Show touches - -For presentations, it may be useful to show physical touches (on the physical -device). Android exposes this feature in _Developers options_. - -_Scrcpy_ provides an option to enable this feature on start and restore the -initial value on exit: - -```bash -scrcpy --show-touches -scrcpy -t # short version -``` - -Note that it only shows _physical_ touches (by a finger on the device). - -It is possible to change this setting manually: - -```bash -# get the current show_touches value -adb shell settings get system show_touches -# enable show_touches -adb shell settings put system show_touches 1 -# disable show_touches -adb shell settings put system show_touches 0 -``` - -## 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 -``` - - -## Start Android app - -To list the Android apps installed on the device: - -```bash -scrcpy --list-apps -``` - -An app, selected by its package name, can be launched on start: - -``` -scrcpy --start-app=org.mozilla.firefox -``` - -This feature can be used to run an app in a [virtual -display](virtual_display.md): - -``` -scrcpy --new-display=1920x1080 --start-app=org.videolan.vlc -``` - -The app can be optionally forced-stop before being started, by adding a `+` -prefix: - -``` -scrcpy --start-app=+org.mozilla.firefox -``` - -For convenience, it is also possible to select an app by its name, by adding a -`?` prefix: - -``` -scrcpy --start-app=?firefox -``` - -But retrieving app names may take some time (sometimes several seconds), so -passing the package name is recommended. - -The `+` and `?` prefixes can be combined (in that order): - -``` -scrcpy --start-app=+?firefox -``` diff --git a/doc/gamepad.md b/doc/gamepad.md deleted file mode 100644 index d3d27b51..00000000 --- a/doc/gamepad.md +++ /dev/null @@ -1,58 +0,0 @@ -# Gamepad - -Several gamepad input modes are available: - - - `--gamepad=disabled` (default) - - `--gamepad=uhid` (or `-G`): simulates physical HID gamepads using the UHID - kernel module on the device - - `--gamepad=aoa`: simulates physical HID gamepads using the AOAv2 protocol - - -## Physical gamepad simulation - -Two modes allow to simulate physical HID gamepads on the device, one for each -physical gamepad plugged into the computer. - - -### UHID - -This mode simulates physical HID gamepads using the [UHID] kernel module on the -device. - -[UHID]: https://kernel.org/doc/Documentation/hid/uhid.txt - -To enable UHID gamepads, use: - -```bash -scrcpy --gamepad=uhid -scrcpy -G # short version -``` - -Note: UHID may not work on old Android versions due to permission errors. - - -### AOA - -This mode simulates physical HID gamepads using the [AOAv2] protocol. - -[AOAv2]: https://source.android.com/devices/accessories/aoa2#hid-support - -To enable AOA gamepads, use: - -```bash -scrcpy --gamepad=aoa -``` - -Contrary to the other mode, it works at the USB level directly (so it only works -over USB). - -It does not use the scrcpy server, and does not require `adb` (USB debugging). -Therefore, it is possible to control the device (but not mirror) even with USB -debugging disabled (see [OTG](otg.md)). - -Note: For some reason, in this mode, Android detects multiple physical gamepads -as a single misbehaving one. Use UHID if you need multiple gamepads. - -Note: On Windows, it may only work in [OTG mode](otg.md), not while mirroring -(it is not possible to open a USB device if it is already open by another -process like the _adb daemon_). diff --git a/doc/keyboard.md b/doc/keyboard.md deleted file mode 100644 index 80dfe070..00000000 --- a/doc/keyboard.md +++ /dev/null @@ -1,136 +0,0 @@ -# Keyboard - -Several keyboard input modes are available: - - - `--keyboard=sdk` (default) - - `--keyboard=uhid` (or `-K`): simulates a physical HID keyboard using the UHID - kernel module on the device - - `--keyboard=aoa`: simulates a physical HID keyboard using the AOAv2 protocol - - `--keyboard=disabled` - -By default, `sdk` is used, but if you use scrcpy regularly, it is recommended to -use [`uhid`](#uhid) and configure the keyboard layout once and for all. - - -## SDK keyboard - -In this mode (`--keyboard=sdk`, or if the parameter is omitted), keyboard input -events are injected at the Android API level. It works everywhere, but it is -limited to ASCII and some other characters. - -Note that on some devices, an additional option must be enabled in developer -options for this keyboard mode to work. See -[prerequisites](/README.md#prerequisites). - -Additional parameters (specific to `--keyboard=sdk`) described below allow to -customize the behavior. - - -### Text injection preference - -Two kinds of [events][textevents] are generated when typing text: - - _key events_, signaling that a key is pressed or released; - - _text events_, signaling that a text has been entered. - -By default, numbers and "special characters" are inserted using text events, but -letters are injected using key events, so that the keyboard behaves as expected -in games (typically for WASD keys). - -But this may [cause issues][prefertext]. If you encounter such a problem, you -can inject letters as text (or just switch to [UHID](#uhid)): - -```bash -scrcpy --prefer-text -``` - -(but this will break keyboard behavior in games) - -On the contrary, you could force to always inject raw key events: - -```bash -scrcpy --raw-key-events -``` - -[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input -[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 - - -### Key repeat - -By default, holding a key down generates repeated key events. Ths can cause -performance problems in some games, where these events are useless anyway. - -To avoid forwarding repeated key events: - -```bash -scrcpy --no-key-repeat -``` - - -## Physical keyboard simulation - -Two modes allow to simulate a physical HID keyboard on the device. - -To work properly, it is necessary to configure (once and for all) the keyboard -layout on the device to match that of the computer. - -The configuration page can be opened in one of the following ways: - - from the scrcpy window (when `uhid` or `aoa` is used), by pressing - MOD+k (see [shortcuts](shortcuts.md)) - - from the device, in Settings → System → Languages and input → Physical - devices - - from a terminal on the computer, by executing `adb shell am start -a - android.settings.HARD_KEYBOARD_SETTINGS` - -From this configuration page, it is also possible to enable or disable on-screen -keyboard. - - -### UHID - -This mode simulates a physical HID keyboard using the [UHID] kernel module on the -device. - -[UHID]: https://kernel.org/doc/Documentation/hid/uhid.txt - -To enable UHID keyboard, use: - -```bash -scrcpy --keyboard=uhid -scrcpy -K # short version -``` - -Once the keyboard layout is configured (see above), it is the best mode for -using the keyboard while mirroring: - - - it works for all characters and IME (contrary to `--keyboard=sdk`) - - the on-screen keyboard can be disabled (contrary to `--keyboard=sdk`) - - it works over TCP/IP (wirelessly) (contrary to `--keyboard=aoa`) - - there are no issues on Windows (contrary to `--keyboard=aoa`) - -One drawback is that it may not work on old Android versions due to permission -errors. - - -### AOA - -This mode simulates a physical HID keyboard using the [AOAv2] protocol. - -[AOAv2]: https://source.android.com/devices/accessories/aoa2#hid-support - -To enable AOA keyboard, use: - -```bash -scrcpy --keyboard=aoa -``` - -Contrary to the other modes, it works at the USB level directly (so it only -works over USB). - -It does not use the scrcpy server, and does not require `adb` (USB debugging). -Therefore, it is possible to control the device (but not mirror) even with USB -debugging disabled (see [OTG](otg.md)). - -Note: On Windows, it may only work in [OTG mode](otg.md), not while mirroring -(it is not possible to open a USB device if it is already open by another -process like the _adb daemon_). diff --git a/doc/linux.md b/doc/linux.md deleted file mode 100644 index be433df4..00000000 --- a/doc/linux.md +++ /dev/null @@ -1,96 +0,0 @@ -# On Linux - -## Install - -### From the official release - -Download a static build of the [latest release]: - - - [`scrcpy-linux-x86_64-v3.3.1.tar.gz`][direct-linux-x86_64] (x86_64) - SHA-256: `bbfe54c6b178adafeaffbbfbbc1548a74486553170c63e63bdd41863ad123422` - -[latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-linux-x86_64-v3.3.1.tar.gz - -and extract it. - -_Static builds of scrcpy for Linux are still experimental._ - - -### From your package manager - -Packaging status - -Scrcpy is packaged in several distributions and package managers: - - - Debian/Ubuntu: ~~`apt install scrcpy`~~ _(obsolete version)_ - - Arch Linux: `pacman -S scrcpy` - - Fedora: `dnf copr enable zeno/scrcpy && dnf install scrcpy` - - Gentoo: `emerge scrcpy` - - Snap: ~~`snap install scrcpy`~~ _(obsolete version)_ - - … (see [repology](https://repology.org/project/scrcpy/versions)) - - -### From an install script - -To install the latest release from `master`, follow this simplified process. - -First, you need to install the required packages: - -```bash -# for Debian/Ubuntu -sudo apt install ffmpeg libsdl2-2.0-0 adb wget \ - gcc git pkg-config meson ninja-build libsdl2-dev \ - libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \ - libswresample-dev libusb-1.0-0 libusb-1.0-0-dev -``` - -Then clone the repo and execute the installation script -([source](/install_release.sh)): - -```bash -git clone https://github.com/Genymobile/scrcpy -cd scrcpy -./install_release.sh -``` - -When a new release is out, update the repo and reinstall: - -```bash -git pull -./install_release.sh -``` - -To uninstall: - -```bash -sudo ninja -Cbuild-auto uninstall -``` - -_Note that this simplified process only works for released versions (it -downloads a prebuilt server binary), so for example you can't use it for testing -the development branch (`dev`)._ - -_See [build.md](build.md) to build and install the app manually._ - - -## Run - -_Make sure that your device meets the [prerequisites](/README.md#prerequisites)._ - -Once installed, run from a terminal: - -```bash -scrcpy -``` - -or with arguments (here to disable audio and record to `file.mkv`): - -```bash -scrcpy --no-audio --record=file.mkv -``` - -Documentation for command line arguments is available: - - `man scrcpy` - - `scrcpy --help` - - on [github](/README.md) diff --git a/doc/macos.md b/doc/macos.md deleted file mode 100644 index f6b01c30..00000000 --- a/doc/macos.md +++ /dev/null @@ -1,70 +0,0 @@ -# On macOS - -## Install - -### From the official release - -Download a static build of the [latest release]: - - - [`scrcpy-macos-aarch64-v3.3.1.tar.gz`][direct-macos-aarch64] (aarch64) - SHA-256: `907b925900ebd8499c1e47acc9689a95bd3a6f9930eb1d7bdfbca8375ae4f139` - - - [`scrcpy-macos-x86_64-v3.3.1.tar.gz`][direct-macos-x86_64] (x86_64) - SHA-256: `69772491dad718eea82fc65c8e89febff7d41c4ce6faff02f4789a588d10fd7d` - -[latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-macos-aarch64-v3.3.1.tar.gz -[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-macos-x86_64-v3.3.1.tar.gz - -and extract it. - -_Static builds of scrcpy for macOS are still experimental._ - - -### From a package manager - -Scrcpy is available in [Homebrew]: - -```bash -brew install scrcpy -``` - -[Homebrew]: https://brew.sh/ - -You need `adb`, accessible from your `PATH`. If you don't have it yet: - -```bash -brew install --cask android-platform-tools -``` - -Alternatively, Scrcpy is also available in [MacPorts], which sets up `adb` for you: - -```bash -sudo port install scrcpy -``` - -[MacPorts]: https://www.macports.org/ - -_See [build.md](build.md) to build and install the app manually._ - - -## Run - -_Make sure that your device meets the [prerequisites](/README.md#prerequisites)._ - -Once installed, run from a terminal: - -```bash -scrcpy -``` - -or with arguments (here to disable audio and record to `file.mkv`): - -```bash -scrcpy --no-audio --record=file.mkv -``` - -Documentation for command line arguments is available: - - `man scrcpy` - - `scrcpy --help` - - on [github](/README.md) diff --git a/doc/mouse.md b/doc/mouse.md deleted file mode 100644 index 0bea4aea..00000000 --- a/doc/mouse.md +++ /dev/null @@ -1,146 +0,0 @@ -# Mouse - -Several mouse input modes are available: - - - `--mouse=sdk` (default) - - `--mouse=uhid` (or `-M`): simulates a physical HID mouse using the UHID - kernel module on the device - - `--mouse=aoa`: simulates a physical HID mouse using the AOAv2 protocol - - `--mouse=disabled` - - -## SDK mouse - -In this mode (`--mouse=sdk`, or if the parameter is omitted), mouse input events -are injected at the Android API level with absolute coordinates. - -Note that on some devices, an additional option must be enabled in developer -options for this mouse mode to work. See -[prerequisites](/README.md#prerequisites). - -### Mouse hover - -By default, mouse hover (mouse motion without any clicks) events are forwarded -to the device. This can be disabled with: - -``` -scrcpy --no-mouse-hover -``` - -## Physical mouse simulation - -Two modes allow to simulate a physical HID mouse on the device. - -In these modes, the computer mouse is "captured": the mouse pointer disappears -from the computer and appears on the Android device instead. - -The [shortcut mod](shortcuts.md) (either Alt or Super by -default) toggle (disable or enable) the mouse capture. Use one of them to give -the control of the mouse back to the computer. - - -### UHID - -This mode simulates a physical HID mouse using the [UHID] kernel module on the -device. - -[UHID]: https://kernel.org/doc/Documentation/hid/uhid.txt - -To enable UHID mouse, use: - -```bash -scrcpy --mouse=uhid -scrcpy -M # short version -``` - -Note: UHID may not work on old Android versions due to permission errors. - - -### AOA - -This mode simulates a physical HID mouse using the [AOAv2] protocol. - -[AOAv2]: https://source.android.com/devices/accessories/aoa2#hid-support - -To enable AOA mouse, use: - -```bash -scrcpy --mouse=aoa -``` - -Contrary to the other modes, it works at the USB level directly (so it only -works over USB). - -It does not use the scrcpy server, and does not require `adb` (USB debugging). -Therefore, it is possible to control the device (but not mirror) even with USB -debugging disabled (see [OTG](otg.md)). - -Note: On Windows, it may only work in [OTG mode](otg.md), not while mirroring -(it is not possible to open a USB device if it is already open by another -process like the _adb daemon_). - - -## Mouse bindings - -By default, with SDK mouse: - - right-click triggers `BACK` (or `POWER` on) - - middle-click triggers `HOME` - - the 4th click triggers `APP_SWITCH` - - the 5th click expands the notification panel - -The secondary clicks may be forwarded to the device instead by pressing the -Shift key (e.g. Shift+right-click injects a right click to -the device). - -In AOA and UHID mouse modes, the default bindings are reversed: all clicks are -forwarded by default, and pressing Shift gives access to the -shortcuts (since the cursor is handled on the device side, it makes more sense -to forward all mouse buttons by default in these modes). - -The shortcuts can be configured using `--mouse-bind=xxxx:xxxx` for any mouse -mode. The argument must be one or two sequences (separated by `:`) of exactly 4 -characters, one for each secondary click: - -``` - .---- Shift + right click - SECONDARY |.--- Shift + middle click - BINDINGS ||.-- Shift + 4th click - |||.- Shift + 5th click - |||| - vvvv ---mouse-bind=xxxx:xxxx - ^^^^ - |||| - PRIMARY ||| `- 5th click - BINDINGS || `-- 4th click - | `--- middle click - `---- right click -``` - -Each character must be one of the following: - - - `+`: forward the click to the device - - `-`: ignore the click - - `b`: trigger shortcut `BACK` (or turn screen on if off) - - `h`: trigger shortcut `HOME` - - `s`: trigger shortcut `APP_SWITCH` - - `n`: trigger shortcut "expand notification panel" - -For example: - -```bash -scrcpy --mouse-bind=bhsn:++++ # the default mode for SDK mouse -scrcpy --mouse-bind=++++:bhsn # the default mode for AOA and UHID -scrcpy --mouse-bind=++bh:++sn # forward right and middle clicks, - # use 4th and 5th for BACK and HOME, - # use Shift+4th and Shift+5th for APP_SWITCH - # and expand notification panel -``` - -The second sequence of bindings may be omitted. In that case, it is the same as -the first one: - -```bash -scrcpy --mouse-bind=bhsn -scrcpy --mouse-bind=bhsn:bhsn # equivalent -``` diff --git a/doc/otg.md b/doc/otg.md deleted file mode 100644 index 7d31c0a7..00000000 --- a/doc/otg.md +++ /dev/null @@ -1,67 +0,0 @@ -# OTG - -By default, _scrcpy_ injects input events at the Android API level. As an -alternative, it is possible to send HID events, so that scrcpy behaves as if it -was a [physical keyboard] and/or a [physical mouse] connected to the Android -device (see [keyboard](keyboard.md) and [mouse](mouse.md)). - -[physical keyboard]: keyboard.md#physical-keyboard-simulation -[physical mouse]: mouse.md#physical-mouse-simulation - -A special mode (OTG) allows to control the device using AOA -[keyboard](keyboard.md#aoa), [mouse](mouse.md#aoa) and -[gamepad](gamepad.md#aoa), without using _adb_ at all (so USB debugging is not -necessary). In this mode, video and audio are disabled, and `--keyboard=aoa` and -`--mouse=aoa` are implicitly set. However, gamepads are disabled by default, so -`--gamepad=aoa` (or `-G` in OTG mode) must be explicitly set. - -Therefore, it is possible to run _scrcpy_ with only physical keyboard, mouse and -gamepad simulation, as if the computer keyboard, mouse and gamepads were plugged -directly to the device via an OTG cable. - -To enable OTG mode: - -```bash -scrcpy --otg -# Pass the serial if several USB devices are available -scrcpy --otg -s 0123456789abcdef -``` - -It is possible to disable keyboard or mouse: - -```bash -scrcpy --otg --keyboard=disabled -scrcpy --otg --mouse=disabled -``` - -and to enable gamepads: - -```bash -scrcpy --otg --gamepad=aoa -scrcpy --otg -G # short version -``` - -It only works if the device is connected over USB. - -## OTG issues on Windows - -See [FAQ](/FAQ.md#otg-issues-on-windows). - - -## Control only - -Note that the purpose of OTG is to control the device without USB debugging -(adb). - -If you want to solely control the device without mirroring while USB debugging -is enabled, then OTG mode is not necessary. - -Instead, disable video and audio, and select UHID (or AOA): - -```bash -scrcpy --no-video --no-audio --keyboard=uhid --mouse=uhid --gamepad=uhid -scrcpy --no-video --no-audio -KMG # short version -scrcpy --no-video --no-audio --keyboard=aoa --mouse=aoa --gamepad=aoa -``` - -One benefit of UHID is that it also works wirelessly. diff --git a/doc/recording.md b/doc/recording.md deleted file mode 100644 index f1a5a6e7..00000000 --- a/doc/recording.md +++ /dev/null @@ -1,94 +0,0 @@ -# Recording - -To record video and audio streams while mirroring: - -```bash -scrcpy --record=file.mp4 -scrcpy -r file.mkv -``` - -To record only the video: - -```bash -scrcpy --no-audio --record=file.mp4 -``` - -To record only the audio: - -```bash -scrcpy --no-video --record=file.opus -scrcpy --no-video --audio-codec=aac --record=file.aac -scrcpy --no-video --audio-codec=flac --record=file.flac -scrcpy --no-video --audio-codec=raw --record=file.wav -# .m4a/.mp4 and .mka/.mkv are also supported for opus, aac and flac -``` - -Timestamps are captured on the device, so [packet delay variation] does not -impact the recorded file, which is always clean (only if you use `--record` of -course, not if you capture your scrcpy window and audio output on the computer). - -[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation - - -## Format - -The video and audio streams are encoded on the device, but are muxed on the -client side. Several formats (containers) are supported: - - MP4 (`.mp4`, `.m4a`, `.aac`) - - Matroska (`.mkv`, `.mka`) - - OPUS (`.opus`) - - FLAC (`.flac`) - - WAV (`.wav`) - -The container is automatically selected based on the filename. - -It is also possible to explicitly select a container (in that case the filename -needs not end with a known extension): - -``` -scrcpy --record=file --record-format=mkv -``` - - -## Rotation - -The video can be recorded rotated. See [video -orientation](video.md#orientation). - - -## No playback - -To disable playback and control while recording: - -```bash -scrcpy --no-playback --no-control --record=file.mp4 -``` - -It is also possible to disable video and audio playback separately: - -```bash -# Record both video and audio, but only play video -scrcpy --record=file.mkv --no-audio-playback -``` - -To also disable the window: - -```bash -scrcpy --no-playback --no-window --record=file.mp4 -# interrupt recording with Ctrl+C -``` - -## Time limit - -To limit the recording time: - -```bash -scrcpy --record=file.mkv --time-limit=20 # in seconds -``` - -The `--time-limit` option is not limited to recording, it also impacts simple -mirroring: - -``` -scrcpy --time-limit=20 -``` diff --git a/doc/shortcuts.md b/doc/shortcuts.md deleted file mode 100644 index d22eb473..00000000 --- a/doc/shortcuts.md +++ /dev/null @@ -1,76 +0,0 @@ -# Shortcuts - -Actions can be performed on the scrcpy window using keyboard and mouse -shortcuts. - -In the following list, MOD is the shortcut modifier. By default, it's -(left) Alt or (left) Super. - -It can be changed using `--shortcut-mod`. Possible keys are `lctrl`, `rctrl`, -`lalt`, `ralt`, `lsuper` and `rsuper`. For example: - -```bash -# use RCtrl for shortcuts -scrcpy --shortcut-mod=rctrl - -# use either LCtrl or LSuper for shortcuts -scrcpy --shortcut-mod=lctrl,lsuper -``` - -_[Super] is typically the Windows or Cmd key._ - -[Super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button) - - | Action | Shortcut - | ------------------------------------------- |:----------------------------- - | Switch fullscreen mode | MOD+f - | Rotate display left | MOD+ _(left)_ - | Rotate display right | MOD+ _(right)_ - | Flip display horizontally | MOD+Shift+ _(left)_ \| MOD+Shift+ _(right)_ - | Flip display vertically | MOD+Shift+ _(up)_ \| MOD+Shift+ _(down)_ - | Pause or re-pause display | MOD+z - | Unpause display | MOD+Shift+z - | Reset video capture/encoding | MOD+Shift+r - | Resize window to 1:1 (pixel-perfect) | MOD+g - | Resize window to remove black borders | MOD+w \| _Double-left-click¹_ - | Click on `HOME` | MOD+h \| _Middle-click_ - | Click on `BACK` | MOD+b \| MOD+Backspace \| _Right-click²_ - | Click on `APP_SWITCH` | MOD+s \| _4th-click³_ - | Click on `MENU` (unlock screen)⁴ | MOD+m - | Click on `VOLUME_UP` | MOD+ _(up)_ - | Click on `VOLUME_DOWN` | MOD+ _(down)_ - | Click on `POWER` | MOD+p - | Power on | _Right-click²_ - | Turn device screen off (keep mirroring) | MOD+o - | Turn device screen on | MOD+Shift+o - | Rotate device screen | MOD+r - | Expand notification panel | MOD+n \| _5th-click³_ - | Expand settings panel | MOD+n+n \| _Double-5th-click³_ - | Collapse panels | MOD+Shift+n - | Copy to clipboard⁵ | MOD+c - | Cut to clipboard⁵ | MOD+x - | Synchronize clipboards and paste⁵ | MOD+v - | Inject computer clipboard text | MOD+Shift+v - | Open keyboard settings (HID keyboard only) | MOD+k - | Enable/disable FPS counter (on stdout) | MOD+i - | Pinch-to-zoom/rotate | Ctrl+_click-and-move_ - | Tilt vertically (slide with 2 fingers) | Shift+_click-and-move_ - | Tilt horizontally (slide with 2 fingers) | Ctrl+Shift+_click-and-move_ - | Drag & drop APK file | Install APK from computer - | Drag & drop non-APK file | [Push file to device](control.md#push-file-to-device) - -_¹Double-click on black borders to remove them._ -_²Right-click turns the screen on if it was off, presses BACK otherwise._ -_³4th and 5th mouse buttons, if your mouse has them._ -_⁴For react-native apps in development, `MENU` triggers development menu._ -_⁵Only on Android >= 7._ - -Shortcuts with repeated keys are executed by releasing and pressing the key a -second time. For example, to execute "Expand settings panel": - - 1. Press and keep pressing MOD. - 2. Then double-press n. - 3. Finally, release MOD. - -All Ctrl+_key_ shortcuts are forwarded to the device, so they are -handled by the active application. diff --git a/doc/tunnels.md b/doc/tunnels.md deleted file mode 100644 index 987a0293..00000000 --- a/doc/tunnels.md +++ /dev/null @@ -1,123 +0,0 @@ -# Tunnels - -Scrcpy is designed to mirror local Android devices. Tunnels allow to connect to -a remote device (e.g. over the Internet). - -To connect to a remote device, it is possible to connect a local `adb` client to -a remote `adb` server (provided they use the same version of the _adb_ -protocol). - - -## Remote ADB server - -To connect to a remote _adb server_, make the server listen on all interfaces: - -```bash -adb kill-server -adb -a nodaemon server start -# keep this open -``` - -**Warning: all communications between clients and the _adb server_ are -unencrypted.** - -Suppose that this server is accessible at 192.168.1.2. Then, from another -terminal, run `scrcpy`: - -```bash -# in bash -export ADB_SERVER_SOCKET=tcp:192.168.1.2:5037 -scrcpy --tunnel-host=192.168.1.2 -``` - -```cmd -:: in cmd -set ADB_SERVER_SOCKET=tcp:192.168.1.2:5037 -scrcpy --tunnel-host=192.168.1.2 -``` - -```powershell -# in PowerShell -$env:ADB_SERVER_SOCKET = 'tcp:192.168.1.2:5037' -scrcpy --tunnel-host=192.168.1.2 -``` - -By default, `scrcpy` uses the local port used for `adb forward` tunnel -establishment (typically `27183`, see `--port`). It is also possible to force a -different tunnel port (it may be useful in more complex situations, when more -redirections are involved): - -``` -scrcpy --tunnel-port=1234 -``` - - -## SSH tunnel - -To communicate with a remote _adb server_ securely, it is preferable to use an -SSH tunnel. - -First, make sure the _adb server_ is running on the remote computer: - -```bash -adb start-server -``` - -Then, establish an SSH tunnel: - -```bash -# local 5038 --> remote 5037 -# local 27183 <-- remote 27183 -ssh -CN -L5038:localhost:5037 -R27183:localhost:27183 your_remote_computer -# keep this open -``` - -From another terminal, run `scrcpy`: - -```bash -# in bash -export ADB_SERVER_SOCKET=tcp:localhost:5038 -scrcpy -``` - -```cmd -:: in cmd -set ADB_SERVER_SOCKET=tcp:localhost:5038 -scrcpy -``` - -```powershell -# in PowerShell -$env:ADB_SERVER_SOCKET = 'tcp:localhost:5038' -scrcpy -``` - -To avoid enabling remote port forwarding, you could force a forward connection -instead (notice the `-L` instead of `-R`): - -```bash -# local 5038 --> remote 5037 -# local 27183 --> remote 27183 -ssh -CN -L5038:localhost:5037 -L27183:localhost:27183 your_remote_computer -# keep this open -``` - -From another terminal, run `scrcpy`: - -```bash -# in bash -export ADB_SERVER_SOCKET=tcp:localhost:5038 -scrcpy --force-adb-forward -``` - -```cmd -:: in cmd -set ADB_SERVER_SOCKET=tcp:localhost:5038 -scrcpy --force-adb-forward -``` - -```powershell -# in PowerShell -$env:ADB_SERVER_SOCKET = 'tcp:localhost:5038' -scrcpy --force-adb-forward -``` diff --git a/doc/v4l2.md b/doc/v4l2.md deleted file mode 100644 index 54272b2b..00000000 --- a/doc/v4l2.md +++ /dev/null @@ -1,72 +0,0 @@ -# Video4Linux - -On Linux, it is possible to send the video stream to a [v4l2] loopback device, -so that the Android device can be opened like a webcam by any v4l2-capable tool. - -[v4l2]: https://en.wikipedia.org/wiki/Video4Linux - -The module `v4l2loopback` must be installed: - -```bash -sudo apt install v4l2loopback-dkms -``` - -To create a v4l2 device: - -```bash -sudo modprobe v4l2loopback -``` - -This will create a new video device in `/dev/videoN`, where `N` is an integer -(more [options](https://github.com/umlaeute/v4l2loopback#options) are available -to create several devices or devices with specific IDs). - -If you encounter problems detecting your device with Chrome/WebRTC, you can try -`exclusive_caps` mode: - -``` -sudo modprobe v4l2loopback exclusive_caps=1 -``` - -To list the enabled devices: - -```bash -# requires v4l-utils package -v4l2-ctl --list-devices - -# simple but might be sufficient -ls /dev/video* -``` - -To start `scrcpy` using a v4l2 sink: - -```bash -scrcpy --v4l2-sink=/dev/videoN -scrcpy --v4l2-sink=/dev/videoN --no-video-playback # disable playback window -``` - -(replace `N` with the device ID, check with `ls /dev/video*`) - -Once enabled, you can open your video stream with a v4l2-capable tool: - -```bash -ffplay -i /dev/videoN -vlc v4l2:///dev/videoN # VLC might add some buffering delay -``` - -For example, you could capture the video within [OBS] or within your video -conference tool. - -[OBS]: https://obsproject.com/ - - -## Buffering - -By default, there is no video buffering, to get the lowest possible latency. - -As for the [video display](video.md#buffering), it is possible to add -buffering to delay the v4l2 stream: - -```bash -scrcpy --v4l2-buffer=300 # add 300ms buffering for v4l2 sink -``` diff --git a/doc/video.md b/doc/video.md deleted file mode 100644 index 4de6814a..00000000 --- a/doc/video.md +++ /dev/null @@ -1,281 +0,0 @@ -# Video - -## Source - -By default, scrcpy mirrors the device screen. - -It is possible to capture the device camera instead. - -See the dedicated [camera](camera.md) page. - - -## Size - -By default, scrcpy attempts to mirror at the Android device resolution. - -It might be useful to mirror at a lower definition to increase performance. To -limit both width and height to some maximum value (here 1024): - -```bash -scrcpy --max-size=1024 -scrcpy -m 1024 # short version -``` - -The other dimension is computed so that the Android device aspect ratio is -preserved. That way, a device in 1920×1080 will be mirrored at 1024×576. - -If encoding fails, scrcpy automatically tries again with a lower definition -(unless `--no-downsize-on-error` is enabled). - -For camera mirroring, the `--max-size` value is used to select the camera source -size instead (among the available resolutions). - - -## Bit rate - -The default video bit rate is 8 Mbps. To change it: - -```bash -scrcpy --video-bit-rate=2M -scrcpy --video-bit-rate=2000000 # equivalent -scrcpy -b 2M # short version -``` - - -## Frame rate - -The capture frame rate can be limited: - -```bash -scrcpy --max-fps=15 -``` - -The actual capture frame rate may be printed to the console: - -``` -scrcpy --print-fps -``` - -It may also be enabled or disabled at anytime with MOD+i -(see [shortcuts](shortcuts.md)). - -The frame rate is intrinsically variable: a new frame is produced only when the -screen content changes. For example, if you play a fullscreen video at 24fps on -your device, you should not get more than 24 frames per second in scrcpy. - - -## Codec - -The video codec can be selected. The possible values are `h264` (default), -`h265` and `av1`: - -```bash -scrcpy --video-codec=h264 # default -scrcpy --video-codec=h265 -scrcpy --video-codec=av1 -``` - -H265 may provide better quality, but H264 should provide lower latency. -AV1 encoders are not common on current Android devices. - -For advanced usage, to pass arbitrary parameters to the [`MediaFormat`], -check `--video-codec-options` in the manpage or in `scrcpy --help`. - -[`MediaFormat`]: https://developer.android.com/reference/android/media/MediaFormat - - -## Encoder - -Several encoders may be available on the device. They can be listed by: - -```bash -scrcpy --list-encoders -``` - -Sometimes, the default encoder may have issues or even crash, so it is useful to -try another one: - -```bash -scrcpy --video-codec=h264 --video-encoder=OMX.qcom.video.encoder.avc -``` - - -## Orientation - -The orientation may be applied at 3 different levels: - - The [shortcut](shortcuts.md) MOD+r requests the - device to switch between portrait and landscape (the current running app may - refuse, if it does not support the requested orientation). - - `--capture-orientation` changes the mirroring orientation (the orientation - of the video sent from the device to the computer). This affects the - recording. - - `--orientation` is applied on the client side, and affects display and - recording. For the display, it can be changed dynamically using - [shortcuts](shortcuts.md). - -To capture the video with a specific orientation: - -```bash -scrcpy --capture-orientation=0 -scrcpy --capture-orientation=90 # 90° clockwise -scrcpy --capture-orientation=180 # 180° -scrcpy --capture-orientation=270 # 270° clockwise -scrcpy --capture-orientation=flip0 # hflip -scrcpy --capture-orientation=flip90 # hflip + 90° clockwise -scrcpy --capture-orientation=flip180 # hflip + 180° -scrcpy --capture-orientation=flip270 # hflip + 270° clockwise -``` - -The capture orientation can be locked by using `@`, so that a physical device -rotation does not change the captured video orientation: - -```bash -scrcpy --capture-orientation=@ # locked to the initial orientation -scrcpy --capture-orientation=@0 # locked to 0° -scrcpy --capture-orientation=@90 # locked to 90° clockwise -scrcpy --capture-orientation=@180 # locked to 180° -scrcpy --capture-orientation=@270 # locked to 270° clockwise -scrcpy --capture-orientation=@flip0 # locked to hflip -scrcpy --capture-orientation=@flip90 # locked to hflip + 90° clockwise -scrcpy --capture-orientation=@flip180 # locked to hflip + 180° -scrcpy --capture-orientation=@flip270 # locked to hflip + 270° clockwise -``` - -The capture orientation transform is applied after `--crop`, but before -`--angle`. - -To orient the video (on the client side): - -```bash -scrcpy --orientation=0 -scrcpy --orientation=90 # 90° clockwise -scrcpy --orientation=180 # 180° -scrcpy --orientation=270 # 270° clockwise -scrcpy --orientation=flip0 # hflip -scrcpy --orientation=flip90 # hflip + 90° clockwise -scrcpy --orientation=flip180 # vflip (hflip + 180°) -scrcpy --orientation=flip270 # hflip + 270° clockwise -``` - -The orientation can be set separately for display and record if necessary, via -`--display-orientation` and `--record-orientation`. - -The rotation is applied to a recorded file by writing a display transformation -to the MP4 or MKV target file. Flipping is not supported, so only the 4 first -values are allowed when recording. - - -## Angle - -To rotate the video content by a custom angle (in degrees, clockwise): - -``` -scrcpy --angle=23 -``` - -The center of rotation is the center of the visible area. - -This transformation is applied after `--crop` and `--capture-orientation`. - - -## Crop - -The device screen may be cropped to mirror only part of the screen. - -This is useful, for example, to mirror only one eye of the Oculus Go: - -```bash -scrcpy --crop=1224:1440:0:0 # 1224x1440 at offset (0,0) -``` - -The values are expressed in the device natural orientation (portrait for a -phone, landscape for a tablet). - -Cropping is performed before `--capture-orientation` and `--angle`. - -For display mirroring, `--max-size` is applied after cropping. For camera, -`--max-size` is applied first (because it selects the source size rather than -resizing the content). - - -## Display - -If several displays are available on the Android device, it is possible to -select the display to mirror: - -```bash -scrcpy --display-id=1 -``` - -The list of display ids can be retrieved by: - -```bash -scrcpy --list-displays -``` - -A secondary display may only be controlled if the device runs at least Android -10 (otherwise it is mirrored as read-only). - -It is also possible to create a [virtual display](virtual_display.md). - - -## Buffering - -By default, there is no video buffering, to get the lowest possible latency. - -Buffering can be added to delay the video stream and compensate for jitter to -get a smoother playback (see [#2464]). - -[#2464]: https://github.com/Genymobile/scrcpy/issues/2464 - -The configuration is available independently for the display, -[v4l2 sinks](video.md#video4linux) and [audio](audio.md#buffering) playback. - -```bash -scrcpy --video-buffer=50 # add 50ms buffering for video playback -scrcpy --audio-buffer=200 # set 200ms buffering for audio playback -scrcpy --v4l2-buffer=300 # add 300ms buffering for v4l2 sink -``` - -They can be applied simultaneously: - -```bash -scrcpy --video-buffer=50 --v4l2-buffer=300 -``` - - -## No playback - -It is possible to capture an Android device without playing video or audio on -the computer. This option is useful when [recording](recording.md) or when -[v4l2](#video4linux) is enabled: - -```bash -scrcpy --v4l2-sink=/dev/video2 --no-playback -scrcpy --record=file.mkv --no-playback -# interrupt with Ctrl+C -``` - -It is also possible to disable video and audio playback separately: - -```bash -# Send video to V4L2 sink without playing it, but keep audio playback -scrcpy --v4l2-sink=/dev/video2 --no-video-playback - -# Record both video and audio, but only play video -scrcpy --record=file.mkv --no-audio-playback -``` - - -## No video - -To disable video forwarding completely, so that only audio is forwarded: - -``` -scrcpy --no-video -``` - - -## Video4Linux - -See the dedicated [Video4Linux](v4l2.md) page. diff --git a/doc/virtual_display.md b/doc/virtual_display.md deleted file mode 100644 index 9f962127..00000000 --- a/doc/virtual_display.md +++ /dev/null @@ -1,77 +0,0 @@ -# Virtual display - -## New display - -To mirror a new virtual display instead of the device screen: - -```bash -scrcpy --new-display=1920x1080 -scrcpy --new-display=1920x1080/420 # force 420 dpi -scrcpy --new-display # use the main display size and density -scrcpy --new-display=/240 # use the main display size and 240 dpi -``` - -The new virtual display is destroyed on exit. - -## Start app - -On some devices, a launcher is available in the virtual display. - -When no launcher is available (or if is explicitly disabled by -[`--no-vd-system-decorations`](#system-decorations)), the virtual display is -empty. In that case, you must [start an Android -app](device.md#start-android-app). - -For example: - -```bash -scrcpy --new-display=1920x1080 --start-app=org.videolan.vlc -``` - -The app may itself be a launcher. For example, to run the open source [Fossify -Launcher]: - -```bash -scrcpy --new-display=1920x1080 --no-vd-system-decorations --start-app=org.fossify.home -``` - -[Fossify Launcher]: https://f-droid.org/en/packages/org.fossify.home/ - - -## System decorations - -By default, virtual display system decorations are enabled. To disable them, use -`--no-vd-system-decorations`: - -``` -scrcpy --new-display --no-vd-system-decorations -``` - -This is useful for some devices which might display a broken UI, or to disable -any default launcher UI available in virtual displays. - -Note that if no app is started, no content will be rendered, so no video frame -will be produced at all. - - -## Destroy on close - -By default, when the virtual display is closed, the running apps are destroyed. - -To move them to the main display instead, use: - -``` -scrcpy --new-display --no-vd-destroy-content -``` - - -## Display IME policy - -By default, the virtual display IME appears on the default display. - -To make it appear on the local display, use `--display-ime-policy=local`: - -```bash -scrcpy --display-id=1 --display-ime-policy=local -scrcpy --new-display --display-ime-policy=local -``` diff --git a/doc/window.md b/doc/window.md deleted file mode 100644 index b72c716c..00000000 --- a/doc/window.md +++ /dev/null @@ -1,64 +0,0 @@ -# Window - -## Disable window - -To disable window (may be useful for recording or for playing audio only): - -```bash -scrcpy --no-window --record=file.mp4 -# Ctrl+C to interrupt -``` - -## Title - -By default, the window title is the device model. It can be changed: - -```bash -scrcpy --window-title='My device' -``` - -## Position and size - -The initial window position and size may be specified: - -```bash -scrcpy --window-x=100 --window-y=100 --window-width=800 --window-height=600 -``` - -## Borderless - -To disable window decorations: - -```bash -scrcpy --window-borderless -``` - -## Always on top - -To keep the window always on top: - -```bash -scrcpy --always-on-top -``` - -## Fullscreen - -The app may be started directly in fullscreen: - -```bash -scrcpy --fullscreen -scrcpy -f # short version -``` - -Fullscreen mode can then be toggled dynamically with MOD+f -(see [shortcuts](shortcuts.md)). - - -## Disable screensaver - -By default, _scrcpy_ does not prevent the screensaver from running on the -computer. To disable it: - -```bash -scrcpy --disable-screensaver -``` diff --git a/doc/windows.md b/doc/windows.md deleted file mode 100644 index 8fa1921f..00000000 --- a/doc/windows.md +++ /dev/null @@ -1,100 +0,0 @@ -# On Windows - -## Install - -### From the official release - -Download the [latest release]: - - - [`scrcpy-win64-v3.3.1.zip`][direct-win64] (64-bit) - SHA-256: `4fcad494772a3ae5de9a133149f8856d2fc429b41795f7cf7c754e0c1bb6fbc0` - - [`scrcpy-win32-v3.3.1.zip`][direct-win32] (32-bit) - SHA-256: `ccdf1b4f5d19dfe760446a107e55b0a010a00e097d46533a161499c9333a20a6` - -[latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-win64-v3.3.1.zip -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-win32-v3.3.1.zip - -and extract it. - - -### From a package manager - -From [WinGet] (ADB and other dependencies will be installed alongside scrcpy): - -```bash -winget install --exact Genymobile.scrcpy -``` - -From [Chocolatey]: - -```bash -choco install scrcpy -choco install adb # if you don't have it yet -``` - -From [Scoop]: - -```bash -scoop install scrcpy -scoop install adb # if you don't have it yet -``` - -[WinGet]: https://github.com/microsoft/winget-cli -[Chocolatey]: https://chocolatey.org/ -[Scoop]: https://scoop.sh - -_See [build.md](build.md) to build and install the app manually._ - - -## Run - -_Make sure that your device meets the [prerequisites](/README.md#prerequisites)._ - -Scrcpy is a command line application: it is mainly intended to be executed from -a terminal with command line arguments. - -To open a terminal at the expected location, double-click on -`open_a_terminal_here.bat` in your scrcpy directory, then type your command. For -example, without arguments: - -```bash -scrcpy -``` - -or with arguments (here to disable audio and record to `file.mkv`): - -``` -scrcpy --no-audio --record=file.mkv -``` - -Documentation for command line arguments is available: - - `scrcpy --help` - - on [github](/README.md) - -To start scrcpy directly without opening a terminal, double-click on one of -these files: - - `scrcpy-console.bat`: start with a terminal open (it will close when scrcpy - terminates, unless an error occurs); - - `scrcpy-noconsole.vbs`: start without a terminal (but you won't see any error - message). - -_Avoid double-clicking on `scrcpy.exe` directly: on error, the terminal would -close immediately and you won't have time to read any error message (this -executable is intended to be run from the terminal). Use `scrcpy-console.bat` -instead._ - -If you plan to always use the same arguments, create a file `myscrcpy.bat` -(enable [show file extensions] to avoid confusion) containing your command, For -example: - -```bash -scrcpy --prefer-text --turn-screen-off --stay-awake -``` - -[show file extensions]: https://www.howtogeek.com/205086/beginner-how-to-make-windows-show-file-extensions/ - -Then just double-click on that file. - -You could also edit (a copy of) `scrcpy-console.bat` or `scrcpy-noconsole.vbs` -to add some arguments. diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 490fda85..13372aef 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b34b7096..33997651 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,6 @@ +#Thu Apr 18 11:45:59 CEST 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip -# https://gradle.org/release-checksums/ -distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip diff --git a/gradlew b/gradlew index 2fe81a7d..9d82f789 100755 --- a/gradlew +++ b/gradlew @@ -1,20 +1,4 @@ -#!/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. -# +#!/usr/bin/env bash ############################################################################## ## @@ -22,6 +6,42 @@ ## ############################################################################## +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# 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 +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" @@ -40,46 +60,6 @@ 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. @@ -105,7 +85,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then @@ -125,8 +105,8 @@ 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 +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` @@ -154,30 +134,27 @@ if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then else eval `echo args$i`="\"$arg\"" fi - i=`expr $i + 1` + i=$((i+1)) done case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") } -APP_ARGS=`save "$@"` +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" -# 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" "$@" +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat index 9109989e..aec99730 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,19 +1,3 @@ -@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 @@ -24,17 +8,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal +@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= + 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 @@ -65,9 +46,10 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windows variants +@rem Get command-line arguments, handling Windowz variants if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. @@ -78,6 +60,11 @@ set _SKIP=2 if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ :execute @rem Setup the command line diff --git a/install_release.sh b/install_release.sh deleted file mode 100755 index d960932b..00000000 --- a/install_release.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env bash -set -e - -BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-server-v3.3.1 -PREBUILT_SERVER_SHA256=a0f70b20aa4998fbf658c94118cd6c8dab6abbb0647a3bdab344d70bc1ebcbb8 - -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 setup "$BUILDDIR" --buildtype=release --strip -Db_lto=true \ - -Dprebuilt_server=scrcpy-server -cd "$BUILDDIR" -ninja - -echo "[scrcpy] Installing (sudo)..." -sudo ninja install diff --git a/meson.build b/meson.build index d991d672..053d8c94 100644 --- a/meson.build +++ b/meson.build @@ -1,18 +1,14 @@ project('scrcpy', 'c', - version: '3.3.1', - meson_version: '>= 0.49', - default_options: [ - 'c_std=c11', - 'warning_level=2', - 'b_ndebug=if-release', - ]) + version: '1.9', + meson_version: '>= 0.37', + default_options: 'c_std=c11') -add_project_arguments('-Wmissing-prototypes', language: 'c') - -if get_option('compile_app') +if get_option('build_app') subdir('app') endif -if get_option('compile_server') +if get_option('build_server') subdir('server') endif + +run_target('run', command: ['scripts/run-scrcpy.sh']) diff --git a/meson_options.txt b/meson_options.txt index fd347734..a443ccb2 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,8 +1,8 @@ -option('compile_app', type: 'boolean', value: true, description: 'Build the client') -option('compile_server', type: 'boolean', value: true, description: 'Build the server') +option('build_app', type: 'boolean', value: true, description: 'Build the client') +option('build_server', type: 'boolean', value: true, description: 'Build the server') +option('crossbuild_windows', type: 'boolean', value: false, description: 'Build for Windows from Linux') +option('windows_noconsole', type: 'boolean', value: false, description: 'Disable console on Windows (pass -mwindows flag)') option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server') -option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server from the same directory as the scrcpy executable') -option('static', type: 'boolean', value: false, description: 'Use static dependencies') -option('server_debugger', type: 'boolean', value: false, description: 'Run a server debugger and wait for a client to be attached') -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') +option('portable', type: 'boolean', description: 'Use scrcpy-server.jar from the same directory as the scrcpy executable') +option('skip_frames', type: 'boolean', value: true, description: 'Always display the most recent frame') +option('hidpi_support', type: 'boolean', value: true, description: 'Enable High DPI support') diff --git a/prebuilt-deps/.gitignore b/prebuilt-deps/.gitignore new file mode 100644 index 00000000..934bc04c --- /dev/null +++ b/prebuilt-deps/.gitignore @@ -0,0 +1,4 @@ +* +!/.gitignore +!/Makefile +!/prepare-dep diff --git a/prebuilt-deps/Makefile b/prebuilt-deps/Makefile new file mode 100644 index 00000000..04f8b779 --- /dev/null +++ b/prebuilt-deps/Makefile @@ -0,0 +1,40 @@ +.PHONY: prepare-win32 prepare-win64 \ + prepare-ffmpeg-shared-win32 \ + prepare-ffmpeg-dev-win32 \ + prepare-ffmpeg-shared-win64 \ + prepare-ffmpeg-dev-win64 \ + prepare-sdl2 \ + prepare-adb + +prepare-win32: prepare-sdl2 prepare-ffmpeg-shared-win32 prepare-ffmpeg-dev-win32 prepare-adb +prepare-win64: prepare-sdl2 prepare-ffmpeg-shared-win64 prepare-ffmpeg-dev-win64 prepare-adb + +prepare-ffmpeg-shared-win32: + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.1.3-win32-shared.zip \ + 8ea472d673370d5e87517a75587abfa6f189ee4f82e8da21fdbc49d0db0c1a89 \ + ffmpeg-4.1.3-win32-shared + +prepare-ffmpeg-dev-win32: + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.1.3-win32-dev.zip \ + e16d3150b6ccf0b71908f5b964cb8c051d79053c8f5cd6d777d617ab4f03613a \ + ffmpeg-4.1.3-win32-dev + +prepare-ffmpeg-shared-win64: + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.1.3-win64-shared.zip \ + 0b974578e07d974c4bafb36c7ed0b46e46b001d38b149455089c13b57ddefe5d \ + ffmpeg-4.1.3-win64-shared + +prepare-ffmpeg-dev-win64: + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.1.3-win64-dev.zip \ + 334b473467db096a5b74242743592a73e120a137232794508e4fc55593696a5b \ + ffmpeg-4.1.3-win64-dev + +prepare-sdl2: + @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.8-mingw.tar.gz \ + ffff7305d634aff5e1df5b7bb935435c3a02c8b03ad94a1a2be9169a558a7961 \ + SDL2-2.0.8 + +prepare-adb: + @./prepare-dep https://dl.google.com/android/repository/platform-tools_r29.0.1-windows.zip \ + 2334f92cf571fd2d9bf6ff7c637765bee5d8323e0bd8e051e15927d87b54b4e8 \ + platform-tools diff --git a/prebuilt-deps/prepare-dep b/prebuilt-deps/prepare-dep new file mode 100755 index 00000000..34ddcbf5 --- /dev/null +++ b/prebuilt-deps/prepare-dep @@ -0,0 +1,58 @@ +#!/bin/bash +set -e +url="$1" +sum="$2" +dir="$3" + +checksum() { + local file="$1" + local sum="$2" + echo "$file: verifying checksum..." + echo "$sum $file" | sha256sum -c +} + +get_file() { + local url="$1" + local file="$2" + local sum="$3" + if [[ -f "$file" ]] + then + echo "$file: found" + else + echo "$file: not found, downloading..." + wget "$url" -O "$file" + fi + checksum "$file" "$sum" +} + +extract() { + local file="$1" + echo "Extracting $file..." + if [[ "$file" == *.zip ]] + then + unzip -q "$file" + elif [[ "$file" == *.tar.gz ]] + then + tar xf "$file" + else + echo "Unsupported file: $file" + return 1 + fi +} + +get_dep() { + local url="$1" + local sum="$2" + local dir="$3" + local file="${url##*/}" + if [[ -d "$dir" ]] + then + echo "$dir: found" + else + echo "$dir: not found" + get_file "$url" "$file" "$sum" + extract "$file" + fi +} + +get_dep "$url" "$sum" "$dir" diff --git a/release.sh b/release.sh new file mode 100755 index 00000000..fbd1eb54 --- /dev/null +++ b/release.sh @@ -0,0 +1,44 @@ +#!/bin/bash +set -e + +# test locally +TESTDIR=build_test +rm -rf "$TESTDIR" +# run client tests with ASAN enabled +meson "$TESTDIR" -Db_sanitize=address +ninja -C"$TESTDIR" test + +# test server +GRADLE=${GRADLE:-./gradlew} +$GRADLE -p server check + +BUILDDIR=build_release +rm -rf "$BUILDDIR" +meson "$BUILDDIR" --buildtype release --strip -Db_lto=true +cd "$BUILDDIR" +ninja +cd - + +# build Windows releases +make -f Makefile.CrossWindows + +# the generated server must be the same everywhere +cmp "$BUILDDIR/server/scrcpy-server.jar" dist/scrcpy-win32/scrcpy-server.jar +cmp "$BUILDDIR/server/scrcpy-server.jar" dist/scrcpy-win64/scrcpy-server.jar + +# get version name +TAG=$(git describe --tags --always) + +# create release directory +mkdir -p "release-$TAG" +cp "$BUILDDIR/server/scrcpy-server.jar" "release-$TAG/scrcpy-server-$TAG.jar" +cp "dist/scrcpy-win32-$TAG.zip" "release-$TAG/" +cp "dist/scrcpy-win64-$TAG.zip" "release-$TAG/" + +# generate checksums +cd "release-$TAG" +sha256sum "scrcpy-server-$TAG.jar" \ + "scrcpy-win32-$TAG.zip" \ + "scrcpy-win64-$TAG.zip" > SHA256SUMS.txt + +echo "Release generated in release-$TAG/" diff --git a/release/.gitignore b/release/.gitignore deleted file mode 100644 index ed363cdf..00000000 --- a/release/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/work -/output diff --git a/release/build_common b/release/build_common deleted file mode 100644 index 199a80b6..00000000 --- a/release/build_common +++ /dev/null @@ -1,5 +0,0 @@ -# This file must be sourced from the release scripts directory -WORK_DIR="$PWD/work" -OUTPUT_DIR="$PWD/output" - -VERSION="${VERSION:-$(git describe --tags --always)}" diff --git a/release/build_linux.sh b/release/build_linux.sh deleted file mode 100755 index 6bca6979..00000000 --- a/release/build_linux.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash -set -ex -cd "$(dirname ${BASH_SOURCE[0]})" -. build_common -cd .. # root project dir - -if [[ $# != 1 ]] -then - echo "Syntax: $0 " >&2 - exit 1 -fi - -ARCH="$1" -LINUX_BUILD_DIR="$WORK_DIR/build-linux-$ARCH" - -app/deps/adb_linux.sh -app/deps/sdl.sh linux native static -app/deps/dav1d.sh linux native static -app/deps/ffmpeg.sh linux native static -app/deps/libusb.sh linux native static - -DEPS_INSTALL_DIR="$PWD/app/deps/work/install/linux-native-static" -ADB_INSTALL_DIR="$PWD/app/deps/work/install/adb-linux" - -rm -rf "$LINUX_BUILD_DIR" -meson setup "$LINUX_BUILD_DIR" \ - --pkg-config-path="$DEPS_INSTALL_DIR/lib/pkgconfig" \ - -Dc_args="-I$DEPS_INSTALL_DIR/include" \ - -Dc_link_args="-L$DEPS_INSTALL_DIR/lib" \ - --buildtype=release \ - --strip \ - -Db_lto=true \ - -Dcompile_server=false \ - -Dportable=true \ - -Dstatic=true -ninja -C "$LINUX_BUILD_DIR" - -# Group intermediate outputs into a 'dist' directory -mkdir -p "$LINUX_BUILD_DIR/dist" -cp "$LINUX_BUILD_DIR"/app/scrcpy "$LINUX_BUILD_DIR/dist/" -cp app/data/icon.png "$LINUX_BUILD_DIR/dist/" -cp app/scrcpy.1 "$LINUX_BUILD_DIR/dist/" -cp -r "$ADB_INSTALL_DIR"/. "$LINUX_BUILD_DIR/dist/" diff --git a/release/build_macos.sh b/release/build_macos.sh deleted file mode 100755 index 8f4beb9b..00000000 --- a/release/build_macos.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash -set -ex -cd "$(dirname ${BASH_SOURCE[0]})" -. build_common -cd .. # root project dir - -if [[ $# != 1 ]] -then - echo "Syntax: $0 " >&2 - exit 1 -fi - -ARCH="$1" -MACOS_BUILD_DIR="$WORK_DIR/build-macos-$ARCH" - -app/deps/adb_macos.sh -app/deps/sdl.sh macos native static -app/deps/dav1d.sh macos native static -app/deps/ffmpeg.sh macos native static -app/deps/libusb.sh macos native static - -DEPS_INSTALL_DIR="$PWD/app/deps/work/install/macos-native-static" -ADB_INSTALL_DIR="$PWD/app/deps/work/install/adb-macos" - -rm -rf "$MACOS_BUILD_DIR" -meson setup "$MACOS_BUILD_DIR" \ - --pkg-config-path="$DEPS_INSTALL_DIR/lib/pkgconfig" \ - -Dc_args="-I$DEPS_INSTALL_DIR/include" \ - -Dc_link_args="-L$DEPS_INSTALL_DIR/lib" \ - --buildtype=release \ - --strip \ - -Db_lto=true \ - -Dcompile_server=false \ - -Dportable=true \ - -Dstatic=true -ninja -C "$MACOS_BUILD_DIR" - -# Group intermediate outputs into a 'dist' directory -mkdir -p "$MACOS_BUILD_DIR/dist" -cp "$MACOS_BUILD_DIR"/app/scrcpy "$MACOS_BUILD_DIR/dist/" -cp app/data/icon.png "$MACOS_BUILD_DIR/dist/" -cp app/scrcpy.1 "$MACOS_BUILD_DIR/dist/" -cp -r "$ADB_INSTALL_DIR"/. "$MACOS_BUILD_DIR/dist/" diff --git a/release/build_server.sh b/release/build_server.sh deleted file mode 100755 index f52672de..00000000 --- a/release/build_server.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -set -ex -cd "$(dirname ${BASH_SOURCE[0]})" -. build_common -cd .. # root project dir - -GRADLE="${GRADLE:-./gradlew}" -SERVER_BUILD_DIR="$WORK_DIR/build-server" - -rm -rf "$SERVER_BUILD_DIR" -"$GRADLE" -p server assembleRelease -mkdir -p "$SERVER_BUILD_DIR/server" -cp server/build/outputs/apk/release/server-release-unsigned.apk \ - "$SERVER_BUILD_DIR/server/scrcpy-server" diff --git a/release/build_windows.sh b/release/build_windows.sh deleted file mode 100755 index c83d2e31..00000000 --- a/release/build_windows.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/bin/bash -set -ex - -case "$1" in - 32) - WINXX=win32 - ;; - 64) - WINXX=win64 - ;; - *) - echo "ERROR: $0 must be called with one argument: 32 or 64" >&2 - exit 1 - ;; -esac - -cd "$(dirname ${BASH_SOURCE[0]})" -. build_common -cd .. # root project dir - -WINXX_BUILD_DIR="$WORK_DIR/build-$WINXX" - -app/deps/adb_windows.sh -app/deps/sdl.sh $WINXX cross shared -app/deps/dav1d.sh $WINXX cross shared -app/deps/ffmpeg.sh $WINXX cross shared -app/deps/libusb.sh $WINXX cross shared - -DEPS_INSTALL_DIR="$PWD/app/deps/work/install/$WINXX-cross-shared" -ADB_INSTALL_DIR="$PWD/app/deps/work/install/adb-windows" - -rm -rf "$WINXX_BUILD_DIR" -meson setup "$WINXX_BUILD_DIR" \ - --pkg-config-path="$DEPS_INSTALL_DIR/lib/pkgconfig" \ - -Dc_args="-I$DEPS_INSTALL_DIR/include" \ - -Dc_link_args="-L$DEPS_INSTALL_DIR/lib" \ - --cross-file=cross_$WINXX.txt \ - --buildtype=release \ - --strip \ - -Db_lto=true \ - -Dcompile_server=false \ - -Dportable=true -ninja -C "$WINXX_BUILD_DIR" - -# Group intermediate outputs into a 'dist' directory -mkdir -p "$WINXX_BUILD_DIR/dist" -cp "$WINXX_BUILD_DIR"/app/scrcpy.exe "$WINXX_BUILD_DIR/dist/" -cp app/data/scrcpy-console.bat "$WINXX_BUILD_DIR/dist/" -cp app/data/scrcpy-noconsole.vbs "$WINXX_BUILD_DIR/dist/" -cp app/data/icon.png "$WINXX_BUILD_DIR/dist/" -cp app/data/open_a_terminal_here.bat "$WINXX_BUILD_DIR/dist/" -cp "$DEPS_INSTALL_DIR"/bin/*.dll "$WINXX_BUILD_DIR/dist/" -cp -r "$ADB_INSTALL_DIR"/. "$WINXX_BUILD_DIR/dist/" diff --git a/release/generate_checksums.sh b/release/generate_checksums.sh deleted file mode 100755 index 2785c6c3..00000000 --- a/release/generate_checksums.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -set -ex -cd "$(dirname ${BASH_SOURCE[0]})" -. build_common - -cd "$OUTPUT_DIR" -sha256sum "scrcpy-server-$VERSION" \ - "scrcpy-linux-x86_64-$VERSION.tar.gz" \ - "scrcpy-win32-$VERSION.zip" \ - "scrcpy-win64-$VERSION.zip" \ - "scrcpy-macos-aarch64-$VERSION.tar.gz" \ - "scrcpy-macos-x86_64-$VERSION.tar.gz" \ - | tee SHA256SUMS.txt -echo "Release checksums generated in $PWD/SHA256SUMS.txt" diff --git a/release/package_client.sh b/release/package_client.sh deleted file mode 100755 index 51997e75..00000000 --- a/release/package_client.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/bin/bash -set -ex -cd "$(dirname ${BASH_SOURCE[0]})" -. build_common -cd .. # root project dir - -if [[ $# != 2 ]] -then - # : for example win64 - # : zip or tar.gz - echo "Syntax: $0 " >&2 - exit 1 -fi - -FORMAT=$2 - -if [[ "$FORMAT" != zip && "$FORMAT" != tar.gz ]] -then - echo "Invalid format (expected zip or tar.gz): $FORMAT" >&2 - exit 1 -fi - -BUILD_DIR="$WORK_DIR/build-$1" -ARCHIVE_DIR="$BUILD_DIR/release-archive" -TARGET_DIRNAME="scrcpy-$1-$VERSION" - -rm -rf "$ARCHIVE_DIR/$TARGET_DIRNAME" -mkdir -p "$ARCHIVE_DIR/$TARGET_DIRNAME" - -cp -r "$BUILD_DIR/dist/." "$ARCHIVE_DIR/$TARGET_DIRNAME/" -cp "$WORK_DIR/build-server/server/scrcpy-server" "$ARCHIVE_DIR/$TARGET_DIRNAME/" - -mkdir -p "$OUTPUT_DIR" - -cd "$ARCHIVE_DIR" -rm -f "$OUTPUT_DIR/$TARGET_DIRNAME.$FORMAT" - -case "$FORMAT" in - zip) - zip -r "$OUTPUT_DIR/$TARGET_DIRNAME.zip" "$TARGET_DIRNAME" - ;; - tar.gz) - tar cvzf "$OUTPUT_DIR/$TARGET_DIRNAME.tar.gz" "$TARGET_DIRNAME" - ;; - *) - echo "Invalid format (expected zip or tar.gz): $FORMAT" >&2 - exit 1 -esac - -rm -rf "$TARGET_DIRNAME" -cd - -echo "Generated '$OUTPUT_DIR/$TARGET_DIRNAME.$FORMAT'" diff --git a/release/package_server.sh b/release/package_server.sh deleted file mode 100755 index a856cebb..00000000 --- a/release/package_server.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -set -ex -cd "$(dirname ${BASH_SOURCE[0]})" -OUTPUT_DIR="$PWD/output" -. build_common -cd .. # root project dir - -mkdir -p "$OUTPUT_DIR" -cp "$WORK_DIR/build-server/server/scrcpy-server" "$OUTPUT_DIR/scrcpy-server-$VERSION" -echo "Generated '$OUTPUT_DIR/scrcpy-server-$VERSION'" diff --git a/release/release.sh b/release/release.sh deleted file mode 100755 index ddba585b..00000000 --- a/release/release.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash -# To customize the version name: -# VERSION=myversion ./release.sh -set -e - -cd "$(dirname ${BASH_SOURCE[0]})" -rm -rf output - -./test_server.sh -./test_client.sh - -./build_server.sh -./build_windows.sh 32 -./build_windows.sh 64 -./build_linux.sh x86_64 - -./package_server.sh -./package_client.sh win32 zip -./package_client.sh win64 zip -./package_client.sh linux-x86_64 tar.gz - -./generate_checksums.sh - -echo "Release generated in $PWD/output" diff --git a/release/test_client.sh b/release/test_client.sh deleted file mode 100755 index 6059541d..00000000 --- a/release/test_client.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -set -ex -cd "$(dirname ${BASH_SOURCE[0]})" -. build_common -cd .. # root project dir - -TEST_BUILD_DIR="$WORK_DIR/build-test" - -rm -rf "$TEST_BUILD_DIR" -meson setup "$TEST_BUILD_DIR" -Dcompile_server=false \ - -Db_sanitize=address,undefined -ninja -C "$TEST_BUILD_DIR" test diff --git a/release/test_server.sh b/release/test_server.sh deleted file mode 100755 index 940e8c1a..00000000 --- a/release/test_server.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -set -ex -cd "$(dirname ${BASH_SOURCE[0]})" -. build_common -cd .. # root project dir - -GRADLE="${GRADLE:-./gradlew}" - -"$GRADLE" -p server check diff --git a/run b/run index 56f0a4e1..7abeca05 100755 --- a/run +++ b/run @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/bash # Run scrcpy generated in the specified BUILDDIR. # # This provides the same feature as "ninja run", except that it is possible to @@ -20,6 +20,4 @@ then exit 1 fi -SCRCPY_ICON_PATH="app/data/icon.png" \ -SCRCPY_SERVER_PATH="$BUILDDIR/server/scrcpy-server" \ -"$BUILDDIR/app/scrcpy" "$@" +SCRCPY_SERVER_PATH="$BUILDDIR/server/scrcpy-server.jar" "$BUILDDIR/app/scrcpy" "$@" diff --git a/scripts/run-scrcpy.sh b/scripts/run-scrcpy.sh new file mode 100755 index 00000000..fa6d7c8f --- /dev/null +++ b/scripts/run-scrcpy.sh @@ -0,0 +1,2 @@ +#!/bin/bash +SCRCPY_SERVER_PATH="$MESON_BUILD_ROOT/server/scrcpy-server.jar" "$MESON_BUILD_ROOT/app/scrcpy" diff --git a/server/build.gradle b/server/build.gradle index 31092b12..d5c1fb00 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -1,14 +1,13 @@ apply plugin: 'com.android.application' android { - namespace 'com.genymobile.scrcpy' - compileSdk 35 + compileSdkVersion 29 defaultConfig { applicationId "com.genymobile.scrcpy" minSdkVersion 21 - targetSdkVersion 35 - versionCode 30301 - versionName "3.3.1" + targetSdkVersion 29 + versionCode 10 + versionName "1.9" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { @@ -17,14 +16,11 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } - buildFeatures { - buildConfig true - aidl true - } } dependencies { - testImplementation 'junit:junit:4.13.2' + implementation fileTree(dir: 'libs', include: ['*.jar']) + testImplementation 'junit:junit:4.12' } apply from: "$project.rootDir/config/android-checkstyle.gradle" diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh deleted file mode 100755 index 193a9902..00000000 --- a/server/build_without_gradle.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/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=3.3.1 - -PLATFORM=${ANDROID_PLATFORM:-35} -BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.0.0} -PLATFORM_TOOLS="$ANDROID_HOME/platforms/android-$PLATFORM" -BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS" - -BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})" -CLASSES_DIR="$BUILD_DIR/classes" -GEN_DIR="$BUILD_DIR/gen" -SERVER_DIR=$(dirname "$0") -SERVER_BINARY=scrcpy-server -ANDROID_JAR="$PLATFORM_TOOLS/android.jar" -ANDROID_AIDL="$PLATFORM_TOOLS/framework.aidl" -LAMBDA_JAR="$BUILD_TOOLS_DIR/core-lambda-stubs.jar" - -echo "Platform: android-$PLATFORM" -echo "Build-tools: $BUILD_TOOLS" -echo "Build dir: $BUILD_DIR" - -rm -rf "$CLASSES_DIR" "$GEN_DIR" "$BUILD_DIR/$SERVER_BINARY" classes.dex -mkdir -p "$CLASSES_DIR" -mkdir -p "$GEN_DIR/com/genymobile/scrcpy" - -<< EOF cat > "$GEN_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"$GEN_DIR" -I. \ - android/content/IOnPrimaryClipChangedListener.aidl -"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. -p "$ANDROID_AIDL" \ - android/view/IDisplayWindowListener.aidl - -# Fake sources to expose hidden Android types to the project -FAKE_SRC=( \ - android/content/*java \ -) - -SRC=( \ - com/genymobile/scrcpy/*.java \ - com/genymobile/scrcpy/audio/*.java \ - com/genymobile/scrcpy/control/*.java \ - com/genymobile/scrcpy/device/*.java \ - com/genymobile/scrcpy/opengl/*.java \ - com/genymobile/scrcpy/util/*.java \ - com/genymobile/scrcpy/video/*.java \ - com/genymobile/scrcpy/wrappers/*.java \ -) - -CLASSES=() -for src in "${SRC[@]}" -do - CLASSES+=("${src%.java}.class") -done - -echo "Compiling java sources..." -cd ../java -javac -encoding UTF-8 -bootclasspath "$ANDROID_JAR" \ - -cp "$LAMBDA_JAR:$GEN_DIR" \ - -d "$CLASSES_DIR" \ - -source 1.8 -target 1.8 \ - ${FAKE_SRC[@]} \ - ${SRC[@]} - -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 \ - ${CLASSES[@]} - - echo "Archiving..." - cd "$BUILD_DIR" - jar cvf "$SERVER_BINARY" classes.dex - rm -rf classes.dex -else - # use d8 - "$BUILD_TOOLS_DIR/d8" --classpath "$ANDROID_JAR" \ - --output "$BUILD_DIR/classes.zip" \ - android/view/*.class \ - android/content/*.class \ - ${CLASSES[@]} - - cd "$BUILD_DIR" - mv classes.zip "$SERVER_BINARY" -fi - -rm -rf "$GEN_DIR" "$CLASSES_DIR" - -echo "Server generated in $BUILD_DIR/$SERVER_BINARY" diff --git a/server/meson.build b/server/meson.build index 55828e2d..d96373a0 100644 --- a/server/meson.build +++ b/server/meson.build @@ -3,29 +3,20 @@ 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', + build_always: true, # gradle is responsible for tracking source changes + output: 'scrcpy-server.jar', 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 + # relative path needs some trick + prebuilt_server = meson.source_root() + '/' + prebuilt_server endif custom_target('scrcpy-server-prebuilt', input: prebuilt_server, - output: 'scrcpy-server', + output: 'scrcpy-server.jar', command: ['cp', '@INPUT@', '@OUTPUT@'], install: true, install_dir: 'share/scrcpy') endif - -if meson.version().version_compare('>= 0.58.0') - devenv = environment() - devenv.set('SCRCPY_SERVER_PATH', meson.current_build_dir() / 'scrcpy-server') - meson.add_devenv(devenv) -endif diff --git a/server/scripts/build-wrapper.sh b/server/scripts/build-wrapper.sh index 7e16dc94..f55e1ea4 100755 --- a/server/scripts/build-wrapper.sh +++ b/server/scripts/build-wrapper.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/bash # Wrapper script to invoke gradle from meson set -e diff --git a/server/src/main/AndroidManifest.xml b/server/src/main/AndroidManifest.xml index a94ad86b..ccd69d2f 100644 --- a/server/src/main/AndroidManifest.xml +++ b/server/src/main/AndroidManifest.xml @@ -1,2 +1,2 @@ - + diff --git a/server/src/main/aidl/android/content/IOnPrimaryClipChangedListener.aidl b/server/src/main/aidl/android/content/IOnPrimaryClipChangedListener.aidl deleted file mode 100644 index 46d7f7ca..00000000 --- a/server/src/main/aidl/android/content/IOnPrimaryClipChangedListener.aidl +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) 2008, The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.content; - -/** - * {@hide} - */ -oneway interface IOnPrimaryClipChangedListener { - void dispatchPrimaryClipChanged(); -} diff --git a/server/src/main/aidl/android/view/IDisplayWindowListener.aidl b/server/src/main/aidl/android/view/IDisplayWindowListener.aidl deleted file mode 100644 index 2b331175..00000000 --- a/server/src/main/aidl/android/view/IDisplayWindowListener.aidl +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.view; - -import android.graphics.Rect; -import android.content.res.Configuration; - -import java.util.List; - -/** - * Interface to listen for changes to display window-containers. - * - * This differs from DisplayManager's DisplayListener in a couple ways: - * - onDisplayAdded is always called after the display is actually added to the WM hierarchy. - * This corresponds to the DisplayContent and not the raw Dislay from DisplayManager. - * - onDisplayConfigurationChanged is called for all configuration changes, not just changes - * to displayinfo (eg. windowing-mode). - * - */ -oneway interface IDisplayWindowListener { - - /** - * Called when a new display is added to the WM hierarchy. The existing display ids are returned - * when this listener is registered with WM via {@link #registerDisplayWindowListener}. - */ - void onDisplayAdded(int displayId); - - /** - * Called when a display's window-container configuration has changed. - */ - void onDisplayConfigurationChanged(int displayId, in Configuration newConfig); - - /** - * Called when a display is removed from the hierarchy. - */ - void onDisplayRemoved(int displayId); - - /** - * Called when fixed rotation is started on a display. - */ - void onFixedRotationStarted(int displayId, int newRotation); - - /** - * Called when the previous fixed rotation on a display is finished. - */ - void onFixedRotationFinished(int displayId); - - /** - * Called when the keep clear ares on a display have changed. - */ - void onKeepClearAreasChanged(int displayId, in List restricted, in List unrestricted); -} diff --git a/server/src/main/aidl/android/view/IRotationWatcher.aidl b/server/src/main/aidl/android/view/IRotationWatcher.aidl new file mode 100644 index 00000000..2cc5e44a --- /dev/null +++ b/server/src/main/aidl/android/view/IRotationWatcher.aidl @@ -0,0 +1,25 @@ +/* //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); +} diff --git a/server/src/main/java/android/content/IContentProvider.java b/server/src/main/java/android/content/IContentProvider.java deleted file mode 100644 index bb907dd3..00000000 --- a/server/src/main/java/android/content/IContentProvider.java +++ /dev/null @@ -1,5 +0,0 @@ -package android.content; - -public interface IContentProvider { - // android.content.IContentProvider is hidden, this is a fake one to expose the type to the project -} diff --git a/server/src/main/java/com/genymobile/scrcpy/AndroidVersions.java b/server/src/main/java/com/genymobile/scrcpy/AndroidVersions.java deleted file mode 100644 index 5303924a..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/AndroidVersions.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.genymobile.scrcpy; - -import android.os.Build; - -/** - * Android version code constants, done right. - *

- * API levels - */ -public final class AndroidVersions { - - private AndroidVersions() { - // not instantiable - } - - public static final int API_21_ANDROID_5_0 = Build.VERSION_CODES.LOLLIPOP; - public static final int API_22_ANDROID_5_1 = Build.VERSION_CODES.LOLLIPOP_MR1; - public static final int API_23_ANDROID_6_0 = Build.VERSION_CODES.M; - public static final int API_24_ANDROID_7_0 = Build.VERSION_CODES.N; - public static final int API_25_ANDROID_7_1 = Build.VERSION_CODES.N_MR1; - public static final int API_26_ANDROID_8_0 = Build.VERSION_CODES.O; - public static final int API_27_ANDROID_8_1 = Build.VERSION_CODES.O_MR1; - public static final int API_28_ANDROID_9 = Build.VERSION_CODES.P; - public static final int API_29_ANDROID_10 = Build.VERSION_CODES.Q; - public static final int API_30_ANDROID_11 = Build.VERSION_CODES.R; - public static final int API_31_ANDROID_12 = Build.VERSION_CODES.S; - public static final int API_32_ANDROID_12L = Build.VERSION_CODES.S_V2; - public static final int API_33_ANDROID_13 = Build.VERSION_CODES.TIRAMISU; - public static final int API_34_ANDROID_14 = Build.VERSION_CODES.UPSIDE_DOWN_CAKE; - public static final int API_35_ANDROID_15 = Build.VERSION_CODES.VANILLA_ICE_CREAM; - -} diff --git a/server/src/main/java/com/genymobile/scrcpy/AsyncProcessor.java b/server/src/main/java/com/genymobile/scrcpy/AsyncProcessor.java deleted file mode 100644 index d5da6a90..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/AsyncProcessor.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.genymobile.scrcpy; - -public interface AsyncProcessor { - interface TerminationListener { - /** - * Notify processor termination - * - * @param fatalError {@code true} if this must cause the termination of the whole scrcpy-server. - */ - void onTerminated(boolean fatalError); - } - - void start(TerminationListener listener); - - void stop(); - - void join() throws InterruptedException; -} diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java deleted file mode 100644 index 77018afa..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ /dev/null @@ -1,270 +0,0 @@ -package com.genymobile.scrcpy; - -import com.genymobile.scrcpy.device.Device; -import com.genymobile.scrcpy.util.Ln; -import com.genymobile.scrcpy.util.Settings; -import com.genymobile.scrcpy.util.SettingsException; -import com.genymobile.scrcpy.wrappers.ServiceManager; - -import android.os.BatteryManager; -import android.os.Looper; -import android.system.ErrnoException; -import android.system.Os; - -import java.io.File; -import java.io.IOException; -import java.io.OutputStream; - -/** - * 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 { - - // Dynamic options - private static final int PENDING_CHANGE_DISPLAY_POWER = 1 << 0; - private int pendingChanges; - private boolean pendingRestoreDisplayPower; - - private Thread thread; - private boolean interrupted; - - private CleanUp(Options options) { - thread = new Thread(() -> runCleanUp(options), "cleanup"); - thread.start(); - } - - public static CleanUp start(Options options) { - return new CleanUp(options); - } - - public synchronized void interrupt() { - // Do not use thread.interrupt() because only the wait() call must be interrupted, not Command.exec() - interrupted = true; - notify(); - } - - public void join() throws InterruptedException { - thread.join(); - } - - private void runCleanUp(Options options) { - boolean disableShowTouches = false; - 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 - disableShowTouches = !"1".equals(oldValue); - } catch (SettingsException e) { - Ln.e("Could not change \"show_touches\"", e); - } - } - - int restoreStayOn = -1; - 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 { - int currentStayOn = Integer.parseInt(oldValue); - // Restore only if the current value is different - if (currentStayOn != stayOn) { - restoreStayOn = currentStayOn; - } - } catch (NumberFormatException e) { - // ignore - } - } catch (SettingsException e) { - Ln.e("Could not change \"stay_on_while_plugged_in\"", e); - } - } - - int restoreScreenOffTimeout = -1; - int screenOffTimeout = options.getScreenOffTimeout(); - if (screenOffTimeout != -1) { - try { - String oldValue = Settings.getAndPutValue(Settings.TABLE_SYSTEM, "screen_off_timeout", String.valueOf(screenOffTimeout)); - try { - int currentScreenOffTimeout = Integer.parseInt(oldValue); - // Restore only if the current value is different - if (currentScreenOffTimeout != screenOffTimeout) { - restoreScreenOffTimeout = currentScreenOffTimeout; - } - } catch (NumberFormatException e) { - // ignore - } - } catch (SettingsException e) { - Ln.e("Could not change \"screen_off_timeout\"", e); - } - } - - int displayId = options.getDisplayId(); - - int restoreDisplayImePolicy = -1; - if (displayId > 0) { - int displayImePolicy = options.getDisplayImePolicy(); - if (displayImePolicy != -1) { - int currentDisplayImePolicy = ServiceManager.getWindowManager().getDisplayImePolicy(displayId); - if (currentDisplayImePolicy != displayImePolicy) { - ServiceManager.getWindowManager().setDisplayImePolicy(displayId, displayImePolicy); - restoreDisplayImePolicy = currentDisplayImePolicy; - } - } - } - - boolean powerOffScreen = options.getPowerOffScreenOnClose(); - - try { - run(displayId, restoreStayOn, disableShowTouches, powerOffScreen, restoreScreenOffTimeout, restoreDisplayImePolicy); - } catch (IOException e) { - Ln.e("Clean up I/O exception", e); - } - } - - private void run(int displayId, int restoreStayOn, boolean disableShowTouches, boolean powerOffScreen, int restoreScreenOffTimeout, - int restoreDisplayImePolicy) throws IOException { - String[] cmd = { - "app_process", - "/", - CleanUp.class.getName(), - String.valueOf(displayId), - String.valueOf(restoreStayOn), - String.valueOf(disableShowTouches), - String.valueOf(powerOffScreen), - String.valueOf(restoreScreenOffTimeout), - String.valueOf(restoreDisplayImePolicy), - }; - - ProcessBuilder builder = new ProcessBuilder(cmd); - builder.environment().put("CLASSPATH", Server.SERVER_PATH); - Process process = builder.start(); - OutputStream out = process.getOutputStream(); - - while (true) { - int localPendingChanges; - boolean localPendingRestoreDisplayPower; - synchronized (this) { - while (!interrupted && pendingChanges == 0) { - try { - wait(); - } catch (InterruptedException e) { - throw new AssertionError("Clean up thread MUST NOT be interrupted"); - } - } - if (interrupted) { - break; - } - localPendingChanges = pendingChanges; - localPendingRestoreDisplayPower = pendingRestoreDisplayPower; - pendingChanges = 0; - } - if ((localPendingChanges & PENDING_CHANGE_DISPLAY_POWER) != 0) { - out.write(localPendingRestoreDisplayPower ? 1 : 0); - out.flush(); - } - } - } - - public synchronized void setRestoreDisplayPower(boolean restoreDisplayPower) { - pendingRestoreDisplayPower = restoreDisplayPower; - pendingChanges |= PENDING_CHANGE_DISPLAY_POWER; - notify(); - } - - public static void unlinkSelf() { - try { - new File(Server.SERVER_PATH).delete(); - } catch (Exception e) { - Ln.e("Could not unlink server", e); - } - } - - @SuppressWarnings("deprecation") - private static void prepareMainLooper() { - Looper.prepareMainLooper(); - } - - public static void main(String... args) { - try { - // Start a new session to avoid being terminated along with the server process on some devices - Os.setsid(); - } catch (ErrnoException e) { - Ln.e("setsid() failed", e); - } - unlinkSelf(); - - // Needed for workarounds - prepareMainLooper(); - - int displayId = Integer.parseInt(args[0]); - int restoreStayOn = Integer.parseInt(args[1]); - boolean disableShowTouches = Boolean.parseBoolean(args[2]); - boolean powerOffScreen = Boolean.parseBoolean(args[3]); - int restoreScreenOffTimeout = Integer.parseInt(args[4]); - int restoreDisplayImePolicy = Integer.parseInt(args[5]); - - // Dynamic option - boolean restoreDisplayPower = false; - - try { - // Wait for the server to die - int msg; - while ((msg = System.in.read()) != -1) { - // Only restore display power - assert msg == 0 || msg == 1; - restoreDisplayPower = msg != 0; - } - } catch (IOException e) { - // Expected when the server is dead - } - - Ln.i("Cleaning up"); - - if (disableShowTouches) { - Ln.i("Disabling \"show touches\""); - try { - Settings.putValue(Settings.TABLE_SYSTEM, "show_touches", "0"); - } catch (SettingsException e) { - Ln.e("Could not restore \"show_touches\"", e); - } - } - - if (restoreStayOn != -1) { - Ln.i("Restoring \"stay awake\""); - try { - Settings.putValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(restoreStayOn)); - } catch (SettingsException e) { - Ln.e("Could not restore \"stay_on_while_plugged_in\"", e); - } - } - - if (restoreScreenOffTimeout != -1) { - Ln.i("Restoring \"screen off timeout\""); - try { - Settings.putValue(Settings.TABLE_SYSTEM, "screen_off_timeout", String.valueOf(restoreScreenOffTimeout)); - } catch (SettingsException e) { - Ln.e("Could not restore \"screen_off_timeout\"", e); - } - } - - if (restoreDisplayImePolicy != -1) { - Ln.i("Restoring \"display IME policy\""); - ServiceManager.getWindowManager().setDisplayImePolicy(displayId, restoreDisplayImePolicy); - } - - // Change the power of the main display when mirroring a virtual display - int targetDisplayId = displayId != Device.DISPLAY_ID_NONE ? displayId : 0; - if (Device.isScreenOn(targetDisplayId)) { - if (powerOffScreen) { - Ln.i("Power off screen"); - Device.powerOffScreen(targetDisplayId); - } else if (restoreDisplayPower) { - Ln.i("Restoring display power"); - Device.setDisplayPower(targetDisplayId, true); - } - } - - System.exit(0); - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java new file mode 100644 index 00000000..0de4bc3c --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -0,0 +1,124 @@ +package 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_MOUSE_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_COLLAPSE_NOTIFICATION_PANEL = 6; + public static final int TYPE_GET_CLIPBOARD = 7; + public static final int TYPE_SET_CLIPBOARD = 8; + public static final int TYPE_SET_SCREEN_POWER_MODE = 9; + + 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 Position position; + private int hScroll; + private int vScroll; + + private ControlMessage() { + } + + public static ControlMessage createInjectKeycode(int action, int keycode, int metaState) { + ControlMessage event = new ControlMessage(); + event.type = TYPE_INJECT_KEYCODE; + event.action = action; + event.keycode = keycode; + event.metaState = metaState; + return event; + } + + public static ControlMessage createInjectText(String text) { + ControlMessage event = new ControlMessage(); + event.type = TYPE_INJECT_TEXT; + event.text = text; + return event; + } + + public static ControlMessage createInjectMouseEvent(int action, int buttons, Position position) { + ControlMessage event = new ControlMessage(); + event.type = TYPE_INJECT_MOUSE_EVENT; + event.action = action; + event.buttons = buttons; + event.position = position; + return event; + } + + public static ControlMessage createInjectScrollEvent(Position position, int hScroll, int vScroll) { + ControlMessage event = new ControlMessage(); + event.type = TYPE_INJECT_SCROLL_EVENT; + event.position = position; + event.hScroll = hScroll; + event.vScroll = vScroll; + return event; + } + + public static ControlMessage createSetClipboard(String text) { + ControlMessage event = new ControlMessage(); + event.type = TYPE_SET_CLIPBOARD; + event.text = text; + return event; + } + + /** + * @param mode one of the {@code Device.SCREEN_POWER_MODE_*} constants + */ + public static ControlMessage createSetScreenPowerMode(int mode) { + ControlMessage event = new ControlMessage(); + event.type = TYPE_SET_SCREEN_POWER_MODE; + event.action = mode; + return event; + } + + public static ControlMessage createEmpty(int type) { + ControlMessage event = new ControlMessage(); + event.type = type; + return event; + } + + 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 Position getPosition() { + return position; + } + + public int getHScroll() { + return hScroll; + } + + public int getVScroll() { + return vScroll; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java new file mode 100644 index 00000000..8ced049d --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -0,0 +1,176 @@ +package 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 { + + private static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 9; + private static final int INJECT_MOUSE_EVENT_PAYLOAD_LENGTH = 17; + private static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; + private static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; + + public static final int TEXT_MAX_LENGTH = 300; + public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093; + private static final int RAW_BUFFER_SIZE = 1024; + + private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE]; + private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); + private final byte[] textBuffer = new byte[CLIPBOARD_TEXT_MAX_LENGTH]; + + public ControlMessageReader() { + // invariant: the buffer is always in "get" mode + 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_MOUSE_EVENT: + msg = parseInjectMouseEvent(); + break; + case ControlMessage.TYPE_INJECT_SCROLL_EVENT: + msg = parseInjectScrollEvent(); + break; + case ControlMessage.TYPE_SET_CLIPBOARD: + msg = parseSetClipboard(); + break; + case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: + msg = parseSetScreenPowerMode(); + break; + case ControlMessage.TYPE_BACK_OR_SCREEN_ON: + case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: + case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL: + case ControlMessage.TYPE_GET_CLIPBOARD: + msg = ControlMessage.createEmpty(type); + break; + default: + Ln.w("Unknown event type: " + type); + msg = null; + break; + } + + if (msg == null) { + // failure, reset savedPosition + buffer.position(savedPosition); + } + return msg; + } + + private ControlMessage parseInjectKeycode() { + if (buffer.remaining() < INJECT_KEYCODE_PAYLOAD_LENGTH) { + return null; + } + int action = toUnsigned(buffer.get()); + int keycode = buffer.getInt(); + int metaState = buffer.getInt(); + return ControlMessage.createInjectKeycode(action, keycode, metaState); + } + + private String parseString() { + if (buffer.remaining() < 2) { + return null; + } + int len = toUnsigned(buffer.getShort()); + if (buffer.remaining() < len) { + return null; + } + buffer.get(textBuffer, 0, len); + return new String(textBuffer, 0, len, StandardCharsets.UTF_8); + } + + private ControlMessage parseInjectText() { + String text = parseString(); + if (text == null) { + return null; + } + return ControlMessage.createInjectText(text); + } + + private ControlMessage parseInjectMouseEvent() { + if (buffer.remaining() < INJECT_MOUSE_EVENT_PAYLOAD_LENGTH) { + return null; + } + int action = toUnsigned(buffer.get()); + int buttons = buffer.getInt(); + Position position = readPosition(buffer); + return ControlMessage.createInjectMouseEvent(action, buttons, position); + } + + private ControlMessage parseInjectScrollEvent() { + if (buffer.remaining() < INJECT_SCROLL_EVENT_PAYLOAD_LENGTH) { + return null; + } + Position position = readPosition(buffer); + int hScroll = buffer.getInt(); + int vScroll = buffer.getInt(); + return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll); + } + + private ControlMessage parseSetClipboard() { + String text = parseString(); + if (text == null) { + return null; + } + return ControlMessage.createSetClipboard(text); + } + + private ControlMessage parseSetScreenPowerMode() { + if (buffer.remaining() < SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH) { + return null; + } + int mode = buffer.get(); + return ControlMessage.createSetScreenPowerMode(mode); + } + + private static Position readPosition(ByteBuffer buffer) { + int x = buffer.getInt(); + int y = buffer.getInt(); + int screenWidth = toUnsigned(buffer.getShort()); + int screenHeight = toUnsigned(buffer.getShort()); + return new Position(x, y, screenWidth, screenHeight); + } + + @SuppressWarnings("checkstyle:MagicNumber") + private static int toUnsigned(short value) { + return value & 0xffff; + } + + @SuppressWarnings("checkstyle:MagicNumber") + private static int toUnsigned(byte value) { + return value & 0xff; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java new file mode 100644 index 00000000..263fc2fc --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -0,0 +1,201 @@ +package com.genymobile.scrcpy; + +import com.genymobile.scrcpy.wrappers.InputManager; + +import android.os.SystemClock; +import android.view.InputDevice; +import android.view.InputEvent; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.MotionEvent; + +import java.io.IOException; + +public class Controller { + + private final Device device; + private final DesktopConnection connection; + private final DeviceMessageSender sender; + + private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); + + private long lastMouseDown; + private final MotionEvent.PointerProperties[] pointerProperties = {new MotionEvent.PointerProperties()}; + private final MotionEvent.PointerCoords[] pointerCoords = {new MotionEvent.PointerCoords()}; + + public Controller(Device device, DesktopConnection connection) { + this.device = device; + this.connection = connection; + initPointer(); + sender = new DeviceMessageSender(connection); + } + + private void initPointer() { + MotionEvent.PointerProperties props = pointerProperties[0]; + props.id = 0; + props.toolType = MotionEvent.TOOL_TYPE_FINGER; + + MotionEvent.PointerCoords coords = pointerCoords[0]; + coords.orientation = 0; + coords.pressure = 1; + coords.size = 1; + } + + private void setPointerCoords(Point point) { + MotionEvent.PointerCoords coords = pointerCoords[0]; + coords.x = point.getX(); + coords.y = point.getY(); + } + + private void setScroll(int hScroll, int vScroll) { + MotionEvent.PointerCoords coords = pointerCoords[0]; + coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll); + coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll); + } + + @SuppressWarnings("checkstyle:MagicNumber") + public void control() throws IOException { + // on start, power on the device + if (!device.isScreenOn()) { + injectKeycode(KeyEvent.KEYCODE_POWER); + + // 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: + injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState()); + break; + case ControlMessage.TYPE_INJECT_TEXT: + injectText(msg.getText()); + break; + case ControlMessage.TYPE_INJECT_MOUSE_EVENT: + injectMouse(msg.getAction(), msg.getButtons(), msg.getPosition()); + break; + case ControlMessage.TYPE_INJECT_SCROLL_EVENT: + injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll()); + break; + case ControlMessage.TYPE_BACK_OR_SCREEN_ON: + pressBackOrTurnScreenOn(); + break; + case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: + device.expandNotificationPanel(); + break; + case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL: + device.collapsePanels(); + break; + case ControlMessage.TYPE_GET_CLIPBOARD: + String clipboardText = device.getClipboardText(); + sender.pushClipboardText(clipboardText); + break; + case ControlMessage.TYPE_SET_CLIPBOARD: + device.setClipboardText(msg.getText()); + break; + case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: + device.setScreenPowerMode(msg.getAction()); + break; + default: + // do nothing + } + } + + private boolean injectKeycode(int action, int keycode, int metaState) { + return injectKeyEvent(action, keycode, 0, metaState); + } + + 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 (!injectEvent(event)) { + 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 injectMouse(int action, int buttons, Position position) { + long now = SystemClock.uptimeMillis(); + if (action == MotionEvent.ACTION_DOWN) { + lastMouseDown = now; + } + Point point = device.getPhysicalPoint(position); + if (point == null) { + // ignore event + return false; + } + setPointerCoords(point); + MotionEvent event = MotionEvent.obtain(lastMouseDown, now, action, 1, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, 0, 0, + InputDevice.SOURCE_TOUCHSCREEN, 0); + return injectEvent(event); + } + + private boolean injectScroll(Position position, int hScroll, int vScroll) { + long now = SystemClock.uptimeMillis(); + Point point = device.getPhysicalPoint(position); + if (point == null) { + // ignore event + return false; + } + setPointerCoords(point); + setScroll(hScroll, vScroll); + MotionEvent event = MotionEvent.obtain(lastMouseDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, 0, + 0, InputDevice.SOURCE_MOUSE, 0); + return injectEvent(event); + } + + private boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) { + long now = SystemClock.uptimeMillis(); + KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, + InputDevice.SOURCE_KEYBOARD); + return injectEvent(event); + } + + private boolean injectKeycode(int keyCode) { + return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) + && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0); + } + + private boolean injectEvent(InputEvent event) { + return device.injectInputEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); + } + + private boolean pressBackOrTurnScreenOn() { + int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER; + return injectKeycode(keycode); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java new file mode 100644 index 00000000..a725d83d --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java @@ -0,0 +1,119 @@ +package com.genymobile.scrcpy; + +import android.net.LocalServerSocket; +import android.net.LocalSocket; +import android.net.LocalSocketAddress; + +import java.io.Closeable; +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +public final class DesktopConnection implements Closeable { + + private static final int DEVICE_NAME_FIELD_LENGTH = 64; + + private static final String SOCKET_NAME = "scrcpy"; + + private final LocalSocket videoSocket; + private final FileDescriptor videoFd; + + private final LocalSocket controlSocket; + private final InputStream controlInputStream; + private final OutputStream controlOutputStream; + + private final ControlMessageReader reader = new ControlMessageReader(); + private final DeviceMessageWriter writer = new DeviceMessageWriter(); + + private DesktopConnection(LocalSocket videoSocket, LocalSocket controlSocket) throws IOException { + this.videoSocket = videoSocket; + this.controlSocket = controlSocket; + controlInputStream = controlSocket.getInputStream(); + controlOutputStream = controlSocket.getOutputStream(); + videoFd = videoSocket.getFileDescriptor(); + } + + private static LocalSocket connect(String abstractName) throws IOException { + LocalSocket localSocket = new LocalSocket(); + localSocket.connect(new LocalSocketAddress(abstractName)); + return localSocket; + } + + public static DesktopConnection open(Device device, boolean tunnelForward) throws IOException { + LocalSocket videoSocket; + LocalSocket controlSocket; + if (tunnelForward) { + LocalServerSocket localServerSocket = new LocalServerSocket(SOCKET_NAME); + try { + videoSocket = localServerSocket.accept(); + // send one byte so the client may read() to detect a connection error + videoSocket.getOutputStream().write(0); + try { + controlSocket = localServerSocket.accept(); + } catch (IOException | RuntimeException e) { + videoSocket.close(); + throw e; + } + } finally { + localServerSocket.close(); + } + } else { + videoSocket = connect(SOCKET_NAME); + try { + controlSocket = connect(SOCKET_NAME); + } catch (IOException | RuntimeException e) { + videoSocket.close(); + throw e; + } + } + + DesktopConnection connection = new DesktopConnection(videoSocket, controlSocket); + Size videoSize = device.getScreenInfo().getVideoSize(); + connection.send(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight()); + return connection; + } + + public void close() throws IOException { + videoSocket.shutdownInput(); + videoSocket.shutdownOutput(); + videoSocket.close(); + controlSocket.shutdownInput(); + controlSocket.shutdownOutput(); + controlSocket.close(); + } + + @SuppressWarnings("checkstyle:MagicNumber") + private void send(String deviceName, int width, int height) throws IOException { + byte[] buffer = new byte[DEVICE_NAME_FIELD_LENGTH + 4]; + + byte[] deviceNameBytes = deviceName.getBytes(StandardCharsets.UTF_8); + int len = StringUtils.getUtf8TruncationIndex(deviceNameBytes, DEVICE_NAME_FIELD_LENGTH - 1); + System.arraycopy(deviceNameBytes, 0, buffer, 0, len); + // byte[] are always 0-initialized in java, no need to set '\0' explicitly + + buffer[DEVICE_NAME_FIELD_LENGTH] = (byte) (width >> 8); + buffer[DEVICE_NAME_FIELD_LENGTH + 1] = (byte) width; + buffer[DEVICE_NAME_FIELD_LENGTH + 2] = (byte) (height >> 8); + buffer[DEVICE_NAME_FIELD_LENGTH + 3] = (byte) height; + IO.writeFully(videoFd, buffer, 0, buffer.length); + } + + public FileDescriptor getVideoFd() { + return videoFd; + } + + public ControlMessage receiveControlMessage() throws IOException { + ControlMessage msg = reader.next(); + while (msg == null) { + reader.readFrom(controlInputStream); + msg = reader.next(); + } + return msg; + } + + public void sendDeviceMessage(DeviceMessage msg) throws IOException { + writer.writeTo(msg, controlOutputStream); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java new file mode 100644 index 00000000..538135d4 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -0,0 +1,172 @@ +package com.genymobile.scrcpy; + +import com.genymobile.scrcpy.wrappers.ServiceManager; +import com.genymobile.scrcpy.wrappers.SurfaceControl; + +import android.graphics.Rect; +import android.os.Build; +import android.os.IBinder; +import android.os.RemoteException; +import android.view.IRotationWatcher; +import android.view.InputEvent; + +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 interface RotationListener { + void onRotationChanged(int rotation); + } + + private final ServiceManager serviceManager = new ServiceManager(); + + private ScreenInfo screenInfo; + private RotationListener rotationListener; + + public Device(Options options) { + screenInfo = computeScreenInfo(options.getCrop(), options.getMaxSize()); + registerRotationWatcher(new IRotationWatcher.Stub() { + @Override + public void onRotationChanged(int rotation) throws RemoteException { + synchronized (Device.this) { + screenInfo = screenInfo.withRotation(rotation); + + // notify + if (rotationListener != null) { + rotationListener.onRotationChanged(rotation); + } + } + } + }); + } + + public synchronized ScreenInfo getScreenInfo() { + return screenInfo; + } + + private ScreenInfo computeScreenInfo(Rect crop, int maxSize) { + DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(); + boolean rotated = (displayInfo.getRotation() & 1) != 0; + Size deviceSize = displayInfo.getSize(); + Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight()); + if (crop != null) { + if (rotated) { + // the crop (provided by the user) is expressed in the natural orientation + crop = flipRect(crop); + } + if (!contentRect.intersect(crop)) { + // intersect() changes contentRect so that it is intersected with crop + Ln.w("Crop rectangle (" + formatCrop(crop) + ") does not intersect device screen (" + formatCrop(deviceSize.toRect()) + ")"); + contentRect = new Rect(); // empty + } + } + + Size videoSize = computeVideoSize(contentRect.width(), contentRect.height(), maxSize); + return new ScreenInfo(contentRect, videoSize, rotated); + } + + private static String formatCrop(Rect rect) { + return rect.width() + ":" + rect.height() + ":" + rect.left + ":" + rect.top; + } + + @SuppressWarnings("checkstyle:MagicNumber") + private static Size computeVideoSize(int w, int h, int maxSize) { + // Compute the video size and the padding of the content inside this video. + // Principle: + // - scale down the great side of the screen to maxSize (if necessary); + // - scale down the other side so that the aspect ratio is preserved; + // - round this value to the nearest multiple of 8 (H.264 only accepts multiples of 8) + w &= ~7; // in case it's not a multiple of 8 + h &= ~7; + if (maxSize > 0) { + if (BuildConfig.DEBUG && maxSize % 8 != 0) { + throw new AssertionError("Max size must be a multiple of 8"); + } + boolean portrait = h > w; + int major = portrait ? h : w; + int minor = portrait ? w : h; + if (major > maxSize) { + int minorExact = minor * maxSize / major; + // +4 to round the value to the nearest multiple of 8 + minor = (minorExact + 4) & ~7; + major = maxSize; + } + w = portrait ? minor : major; + h = portrait ? major : minor; + } + return new Size(w, h); + } + + public Point getPhysicalPoint(Position position) { + // it hides the field on purpose, to read it with a lock + @SuppressWarnings("checkstyle:HiddenField") + ScreenInfo screenInfo = getScreenInfo(); // read with synchronization + Size videoSize = screenInfo.getVideoSize(); + Size clientVideoSize = position.getScreenSize(); + if (!videoSize.equals(clientVideoSize)) { + // The client sends a click relative to a video with wrong dimensions, + // the device may have been rotated since the event was generated, so ignore the event + return null; + } + Rect contentRect = screenInfo.getContentRect(); + Point point = position.getPoint(); + int scaledX = contentRect.left + point.getX() * contentRect.width() / videoSize.getWidth(); + int scaledY = contentRect.top + point.getY() * contentRect.height() / videoSize.getHeight(); + return new Point(scaledX, scaledY); + } + + public static String getDeviceName() { + return Build.MODEL; + } + + public boolean injectInputEvent(InputEvent inputEvent, int mode) { + return serviceManager.getInputManager().injectInputEvent(inputEvent, mode); + } + + public boolean isScreenOn() { + return serviceManager.getPowerManager().isScreenOn(); + } + + public void registerRotationWatcher(IRotationWatcher rotationWatcher) { + serviceManager.getWindowManager().registerRotationWatcher(rotationWatcher); + } + + public synchronized void setRotationListener(RotationListener rotationListener) { + this.rotationListener = rotationListener; + } + + public void expandNotificationPanel() { + serviceManager.getStatusBarManager().expandNotificationsPanel(); + } + + public void collapsePanels() { + serviceManager.getStatusBarManager().collapsePanels(); + } + + public String getClipboardText() { + CharSequence s = serviceManager.getClipboardManager().getText(); + if (s == null) { + return null; + } + return s.toString(); + } + + public void setClipboardText(String text) { + serviceManager.getClipboardManager().setText(text); + Ln.i("Device clipboard set"); + } + + /** + * @param mode one of the {@code SCREEN_POWER_MODE_*} constants + */ + public void setScreenPowerMode(int mode) { + IBinder d = SurfaceControl.getBuiltInDisplay(0); + SurfaceControl.setDisplayPowerMode(d, mode); + Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on")); + } + + static Rect flipRect(Rect crop) { + return new Rect(crop.top, crop.left, crop.bottom, crop.right); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java new file mode 100644 index 00000000..c6eebd38 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java @@ -0,0 +1,27 @@ +package com.genymobile.scrcpy; + +public final class DeviceMessage { + + public static final int TYPE_CLIPBOARD = 0; + + private int type; + private String text; + + private DeviceMessage() { + } + + public static DeviceMessage createClipboard(String text) { + DeviceMessage event = new DeviceMessage(); + event.type = TYPE_CLIPBOARD; + event.text = text; + return event; + } + + public int getType() { + return type; + } + + public String getText() { + return text; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java new file mode 100644 index 00000000..bbf4dd2e --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java @@ -0,0 +1,34 @@ +package com.genymobile.scrcpy; + +import java.io.IOException; + +public final class DeviceMessageSender { + + private final DesktopConnection connection; + + private String clipboardText; + + public DeviceMessageSender(DesktopConnection connection) { + this.connection = connection; + } + + public synchronized void pushClipboardText(String text) { + clipboardText = text; + notify(); + } + + public void loop() throws IOException, InterruptedException { + while (true) { + String text; + synchronized (this) { + while (clipboardText == null) { + wait(); + } + text = clipboardText; + clipboardText = null; + } + DeviceMessage event = DeviceMessage.createClipboard(text); + connection.sendDeviceMessage(event); + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java new file mode 100644 index 00000000..e2a3a1a2 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java @@ -0,0 +1,34 @@ +package com.genymobile.scrcpy; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +public class DeviceMessageWriter { + + public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093; + private static final int MAX_EVENT_SIZE = CLIPBOARD_TEXT_MAX_LENGTH + 3; + + private final byte[] rawBuffer = new byte[MAX_EVENT_SIZE]; + private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); + + @SuppressWarnings("checkstyle:MagicNumber") + public void writeTo(DeviceMessage msg, OutputStream output) throws IOException { + buffer.clear(); + buffer.put((byte) DeviceMessage.TYPE_CLIPBOARD); + 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.putShort((short) len); + buffer.put(raw, 0, len); + output.write(rawBuffer, 0, buffer.position()); + break; + default: + Ln.w("Unknown device message: " + msg.getType()); + break; + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java b/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java new file mode 100644 index 00000000..639869b5 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java @@ -0,0 +1,20 @@ +package com.genymobile.scrcpy; + +public final class DisplayInfo { + private final Size size; + private final int rotation; + + public DisplayInfo(Size size, int rotation) { + this.size = size; + this.rotation = rotation; + } + + public Size getSize() { + return size; + } + + public int getRotation() { + return rotation; + } +} + diff --git a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java deleted file mode 100644 index b43e9e1b..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java +++ /dev/null @@ -1,119 +0,0 @@ -package com.genymobile.scrcpy; - -import com.genymobile.scrcpy.wrappers.ServiceManager; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.content.AttributionSource; -import android.content.ClipboardManager; -import android.content.ContentResolver; -import android.content.Context; -import android.content.ContextWrapper; -import android.content.IContentProvider; -import android.os.Binder; -import android.os.Process; - -import java.lang.reflect.Field; - -public final class FakeContext extends ContextWrapper { - - public static final String PACKAGE_NAME = "com.android.shell"; - public static final int ROOT_UID = 0; // Like android.os.Process.ROOT_UID, but before API 29 - - private static final FakeContext INSTANCE = new FakeContext(); - - public static FakeContext get() { - return INSTANCE; - } - - private final ContentResolver contentResolver = new ContentResolver(this) { - @SuppressWarnings({"unused", "ProtectedMemberInFinalClass"}) - // @Override (but super-class method not visible) - protected IContentProvider acquireProvider(Context c, String name) { - return ServiceManager.getActivityManager().getContentProviderExternal(name, new Binder()); - } - - @SuppressWarnings("unused") - // @Override (but super-class method not visible) - public boolean releaseProvider(IContentProvider icp) { - return false; - } - - @SuppressWarnings({"unused", "ProtectedMemberInFinalClass"}) - // @Override (but super-class method not visible) - protected IContentProvider acquireUnstableProvider(Context c, String name) { - return null; - } - - @SuppressWarnings("unused") - // @Override (but super-class method not visible) - public boolean releaseUnstableProvider(IContentProvider icp) { - return false; - } - - @SuppressWarnings("unused") - // @Override (but super-class method not visible) - public void unstableProviderDied(IContentProvider icp) { - // ignore - } - }; - - private FakeContext() { - super(Workarounds.getSystemContext()); - } - - @Override - public String getPackageName() { - return PACKAGE_NAME; - } - - @Override - public String getOpPackageName() { - return PACKAGE_NAME; - } - - @TargetApi(AndroidVersions.API_31_ANDROID_12) - @Override - public AttributionSource getAttributionSource() { - AttributionSource.Builder builder = new AttributionSource.Builder(Process.SHELL_UID); - builder.setPackageName(PACKAGE_NAME); - return builder.build(); - } - - // @Override to be added on SDK upgrade for Android 14 - @SuppressWarnings("unused") - public int getDeviceId() { - return 0; - } - - @Override - public Context getApplicationContext() { - return this; - } - - @Override - public ContentResolver getContentResolver() { - return contentResolver; - } - - @SuppressLint("SoonBlockedPrivateApi") - @Override - public Object getSystemService(String name) { - Object service = super.getSystemService(name); - if (service == null) { - return null; - } - - if (Context.CLIPBOARD_SERVICE.equals(name)) { - try { - Field field = ClipboardManager.class.getDeclaredField("mContext"); - field.setAccessible(true); - field.set(service, this); - } catch (ReflectiveOperationException e) { - throw new RuntimeException(e); - } - } - - return service; - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/IO.java b/server/src/main/java/com/genymobile/scrcpy/IO.java new file mode 100644 index 00000000..57c017db --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/IO.java @@ -0,0 +1,40 @@ +package com.genymobile.scrcpy; + +import android.system.ErrnoException; +import android.system.Os; +import android.system.OsConstants; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.nio.ByteBuffer; + +public final class IO { + private IO() { + // not instantiable + } + + public static void writeFully(FileDescriptor fd, ByteBuffer from) throws IOException { + // ByteBuffer position is not updated as expected by Os.write() on old Android versions, so + // count the remaining bytes manually. + // See . + int remaining = from.remaining(); + while (remaining > 0) { + try { + int w = Os.write(fd, from); + if (BuildConfig.DEBUG && w < 0) { + // w should not be negative, since an exception is thrown on error + throw new AssertionError("Os.write() returned a negative value (" + w + ")"); + } + remaining -= w; + } catch (ErrnoException e) { + if (e.errno != OsConstants.EINTR) { + throw new IOException(e); + } + } + } + } + + public static void writeFully(FileDescriptor fd, byte[] buffer, int offset, int len) throws IOException { + writeFully(fd, ByteBuffer.wrap(buffer, offset, len)); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/control/KeyComposition.java b/server/src/main/java/com/genymobile/scrcpy/KeyComposition.java similarity index 99% rename from server/src/main/java/com/genymobile/scrcpy/control/KeyComposition.java rename to server/src/main/java/com/genymobile/scrcpy/KeyComposition.java index 5b988f53..2f2835c9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/KeyComposition.java +++ b/server/src/main/java/com/genymobile/scrcpy/KeyComposition.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy.control; +package com.genymobile.scrcpy; import java.util.HashMap; import java.util.Map; diff --git a/server/src/main/java/com/genymobile/scrcpy/Ln.java b/server/src/main/java/com/genymobile/scrcpy/Ln.java new file mode 100644 index 00000000..bb741225 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/Ln.java @@ -0,0 +1,65 @@ +package 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 { + DEBUG, + INFO, + WARN, + ERROR; + } + + private static final Level THRESHOLD = BuildConfig.DEBUG ? Level.DEBUG : Level.INFO; + + private Ln() { + // not instantiable + } + + public static boolean isEnabled(Level level) { + return level.ordinal() >= THRESHOLD.ordinal(); + } + + 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) { + if (isEnabled(Level.WARN)) { + Log.w(TAG, message); + System.out.println(PREFIX + "WARN: " + message); + } + } + + 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); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 66bb68e8..af6b2ee1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -1,651 +1,60 @@ package com.genymobile.scrcpy; -import com.genymobile.scrcpy.audio.AudioCodec; -import com.genymobile.scrcpy.audio.AudioSource; -import com.genymobile.scrcpy.device.Device; -import com.genymobile.scrcpy.device.NewDisplay; -import com.genymobile.scrcpy.device.Orientation; -import com.genymobile.scrcpy.device.Size; -import com.genymobile.scrcpy.util.CodecOption; -import com.genymobile.scrcpy.util.Ln; -import com.genymobile.scrcpy.video.CameraAspectRatio; -import com.genymobile.scrcpy.video.CameraFacing; -import com.genymobile.scrcpy.video.VideoCodec; -import com.genymobile.scrcpy.video.VideoSource; -import com.genymobile.scrcpy.wrappers.WindowManager; - import android.graphics.Rect; -import android.util.Pair; - -import java.util.List; -import java.util.Locale; public class Options { - - private Ln.Level logLevel = Ln.Level.DEBUG; - private int scid = -1; // 31-bit non-negative value, or -1 - private boolean video = true; - private boolean audio = true; private int maxSize; - private VideoCodec videoCodec = VideoCodec.H264; - private AudioCodec audioCodec = AudioCodec.OPUS; - private VideoSource videoSource = VideoSource.DISPLAY; - private AudioSource audioSource = AudioSource.OUTPUT; - private boolean audioDup; - private int videoBitRate = 8000000; - private int audioBitRate = 128000; - private float maxFps; - private float angle; + private int bitRate; private boolean tunnelForward; private Rect crop; - private boolean control = true; - private int displayId; - private String cameraId; - private Size cameraSize; - private CameraFacing cameraFacing; - private CameraAspectRatio cameraAspectRatio; - private int cameraFps; - private boolean cameraHighSpeed; - private boolean showTouches; - private boolean stayAwake; - private int screenOffTimeout = -1; - private int displayImePolicy = -1; - private List videoCodecOptions; - private List audioCodecOptions; - - private String videoEncoder; - private String audioEncoder; - private boolean powerOffScreenOnClose; - private boolean clipboardAutosync = true; - private boolean downsizeOnError = true; - private boolean cleanup = true; - private boolean powerOn = true; - - private NewDisplay newDisplay; - private boolean vdDestroyContent = true; - private boolean vdSystemDecorations = true; - - private Orientation.Lock captureOrientationLock = Orientation.Lock.Unlocked; - private Orientation captureOrientation = Orientation.Orient0; - - private boolean listEncoders; - private boolean listDisplays; - private boolean listCameras; - private boolean listCameraSizes; - private boolean listApps; - - // Options not used by the scrcpy client, but useful to use scrcpy-server directly - private boolean sendDeviceMeta = true; // send device name and size - private boolean sendFrameMeta = true; // send PTS so that the client may record properly - private boolean sendDummyByte = true; // write a byte on start to detect connection issues - private boolean sendCodecMeta = true; // write the codec metadata before the stream - - public Ln.Level getLogLevel() { - return logLevel; - } - - public int getScid() { - return scid; - } - - public boolean getVideo() { - return video; - } - - public boolean getAudio() { - return audio; - } + private boolean sendFrameMeta; // send PTS so that the client may record properly + private boolean control; public int getMaxSize() { return maxSize; } - public VideoCodec getVideoCodec() { - return videoCodec; + public void setMaxSize(int maxSize) { + this.maxSize = maxSize; } - public AudioCodec getAudioCodec() { - return audioCodec; + public int getBitRate() { + return bitRate; } - public VideoSource getVideoSource() { - return videoSource; - } - - public AudioSource getAudioSource() { - return audioSource; - } - - public boolean getAudioDup() { - return audioDup; - } - - public int getVideoBitRate() { - return videoBitRate; - } - - public int getAudioBitRate() { - return audioBitRate; - } - - public float getMaxFps() { - return maxFps; - } - - public float getAngle() { - return angle; + public void setBitRate(int bitRate) { + this.bitRate = bitRate; } public boolean isTunnelForward() { return tunnelForward; } + public void setTunnelForward(boolean tunnelForward) { + this.tunnelForward = tunnelForward; + } + public Rect getCrop() { return crop; } - public boolean getControl() { - return control; - } - - public int getDisplayId() { - return displayId; - } - - public String getCameraId() { - return cameraId; - } - - public Size getCameraSize() { - return cameraSize; - } - - public CameraFacing getCameraFacing() { - return cameraFacing; - } - - public CameraAspectRatio getCameraAspectRatio() { - return cameraAspectRatio; - } - - public int getCameraFps() { - return cameraFps; - } - - public boolean getCameraHighSpeed() { - return cameraHighSpeed; - } - - public boolean getShowTouches() { - return showTouches; - } - - public boolean getStayAwake() { - return stayAwake; - } - - public int getScreenOffTimeout() { - return screenOffTimeout; - } - - public int getDisplayImePolicy() { - return displayImePolicy; - } - - public List getVideoCodecOptions() { - return videoCodecOptions; - } - - public List getAudioCodecOptions() { - return audioCodecOptions; - } - - public String getVideoEncoder() { - return videoEncoder; - } - - public String getAudioEncoder() { - return audioEncoder; - } - - public boolean getPowerOffScreenOnClose() { - return this.powerOffScreenOnClose; - } - - public boolean getClipboardAutosync() { - return clipboardAutosync; - } - - public boolean getDownsizeOnError() { - return downsizeOnError; - } - - public boolean getCleanup() { - return cleanup; - } - - public boolean getPowerOn() { - return powerOn; - } - - public NewDisplay getNewDisplay() { - return newDisplay; - } - - public Orientation getCaptureOrientation() { - return captureOrientation; - } - - public Orientation.Lock getCaptureOrientationLock() { - return captureOrientationLock; - } - - public boolean getVDDestroyContent() { - return vdDestroyContent; - } - - public boolean getVDSystemDecorations() { - return vdSystemDecorations; - } - - public boolean getList() { - return listEncoders || listDisplays || listCameras || listCameraSizes || listApps; - } - - public boolean getListEncoders() { - return listEncoders; - } - - public boolean getListDisplays() { - return listDisplays; - } - - public boolean getListCameras() { - return listCameras; - } - - public boolean getListCameraSizes() { - return listCameraSizes; - } - - public boolean getListApps() { - return listApps; - } - - public boolean getSendDeviceMeta() { - return sendDeviceMeta; + public void setCrop(Rect crop) { + this.crop = crop; } public boolean getSendFrameMeta() { return sendFrameMeta; } - public boolean getSendDummyByte() { - return sendDummyByte; + public void setSendFrameMeta(boolean sendFrameMeta) { + this.sendFrameMeta = sendFrameMeta; } - public boolean getSendCodecMeta() { - return sendCodecMeta; + public boolean getControl() { + return control; } - @SuppressWarnings("MethodLength") - public static Options parse(String... args) { - if (args.length < 1) { - throw new IllegalArgumentException("Missing client version"); - } - - String clientVersion = args[0]; - if (!clientVersion.equals(BuildConfig.VERSION_NAME)) { - throw new IllegalArgumentException( - "The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")"); - } - - Options options = new Options(); - - for (int i = 1; i < args.length; ++i) { - String arg = args[i]; - int equalIndex = arg.indexOf('='); - if (equalIndex == -1) { - throw new IllegalArgumentException("Invalid key=value pair: \"" + arg + "\""); - } - String key = arg.substring(0, equalIndex); - String value = arg.substring(equalIndex + 1); - switch (key) { - case "scid": - int scid = Integer.parseInt(value, 0x10); - if (scid < -1) { - throw new IllegalArgumentException("scid may not be negative (except -1 for 'none'): " + scid); - } - options.scid = scid; - break; - case "log_level": - options.logLevel = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH)); - break; - case "video": - options.video = Boolean.parseBoolean(value); - break; - case "audio": - options.audio = Boolean.parseBoolean(value); - break; - case "video_codec": - VideoCodec videoCodec = VideoCodec.findByName(value); - if (videoCodec == null) { - throw new IllegalArgumentException("Video codec " + value + " not supported"); - } - options.videoCodec = videoCodec; - break; - case "audio_codec": - AudioCodec audioCodec = AudioCodec.findByName(value); - if (audioCodec == null) { - throw new IllegalArgumentException("Audio codec " + value + " not supported"); - } - options.audioCodec = audioCodec; - break; - case "video_source": - VideoSource videoSource = VideoSource.findByName(value); - if (videoSource == null) { - throw new IllegalArgumentException("Video source " + value + " not supported"); - } - options.videoSource = videoSource; - break; - case "audio_source": - AudioSource audioSource = AudioSource.findByName(value); - if (audioSource == null) { - throw new IllegalArgumentException("Audio source " + value + " not supported"); - } - options.audioSource = audioSource; - break; - case "audio_dup": - options.audioDup = Boolean.parseBoolean(value); - break; - case "max_size": - options.maxSize = Integer.parseInt(value) & ~7; // multiple of 8 - break; - case "video_bit_rate": - options.videoBitRate = Integer.parseInt(value); - break; - case "audio_bit_rate": - options.audioBitRate = Integer.parseInt(value); - break; - case "max_fps": - options.maxFps = parseFloat("max_fps", value); - break; - case "angle": - options.angle = parseFloat("angle", value); - break; - case "tunnel_forward": - options.tunnelForward = Boolean.parseBoolean(value); - break; - case "crop": - if (!value.isEmpty()) { - options.crop = parseCrop(value); - } - break; - case "control": - options.control = Boolean.parseBoolean(value); - break; - case "display_id": - options.displayId = Integer.parseInt(value); - break; - case "show_touches": - options.showTouches = Boolean.parseBoolean(value); - break; - case "stay_awake": - options.stayAwake = Boolean.parseBoolean(value); - break; - case "screen_off_timeout": - options.screenOffTimeout = Integer.parseInt(value); - if (options.screenOffTimeout < -1) { - throw new IllegalArgumentException("Invalid screen off timeout: " + options.screenOffTimeout); - } - break; - case "video_codec_options": - options.videoCodecOptions = CodecOption.parse(value); - break; - case "audio_codec_options": - options.audioCodecOptions = CodecOption.parse(value); - break; - case "video_encoder": - if (!value.isEmpty()) { - options.videoEncoder = value; - } - break; - case "audio_encoder": - if (!value.isEmpty()) { - options.audioEncoder = value; - } - case "power_off_on_close": - options.powerOffScreenOnClose = Boolean.parseBoolean(value); - break; - case "clipboard_autosync": - options.clipboardAutosync = Boolean.parseBoolean(value); - break; - case "downsize_on_error": - options.downsizeOnError = Boolean.parseBoolean(value); - break; - case "cleanup": - options.cleanup = Boolean.parseBoolean(value); - break; - case "power_on": - options.powerOn = Boolean.parseBoolean(value); - break; - case "list_encoders": - options.listEncoders = Boolean.parseBoolean(value); - break; - case "list_displays": - options.listDisplays = Boolean.parseBoolean(value); - break; - case "list_cameras": - options.listCameras = Boolean.parseBoolean(value); - break; - case "list_camera_sizes": - options.listCameraSizes = Boolean.parseBoolean(value); - break; - case "list_apps": - options.listApps = Boolean.parseBoolean(value); - break; - case "camera_id": - if (!value.isEmpty()) { - options.cameraId = value; - } - break; - case "camera_size": - if (!value.isEmpty()) { - options.cameraSize = parseSize(value); - } - break; - case "camera_facing": - if (!value.isEmpty()) { - CameraFacing facing = CameraFacing.findByName(value); - if (facing == null) { - throw new IllegalArgumentException("Camera facing " + value + " not supported"); - } - options.cameraFacing = facing; - } - break; - case "camera_ar": - if (!value.isEmpty()) { - options.cameraAspectRatio = parseCameraAspectRatio(value); - } - break; - case "camera_fps": - options.cameraFps = Integer.parseInt(value); - break; - case "camera_high_speed": - options.cameraHighSpeed = Boolean.parseBoolean(value); - break; - case "new_display": - options.newDisplay = parseNewDisplay(value); - break; - case "vd_destroy_content": - options.vdDestroyContent = Boolean.parseBoolean(value); - break; - case "vd_system_decorations": - options.vdSystemDecorations = Boolean.parseBoolean(value); - break; - case "capture_orientation": - Pair pair = parseCaptureOrientation(value); - options.captureOrientationLock = pair.first; - options.captureOrientation = pair.second; - break; - case "display_ime_policy": - options.displayImePolicy = parseDisplayImePolicy(value); - break; - case "send_device_meta": - options.sendDeviceMeta = Boolean.parseBoolean(value); - break; - case "send_frame_meta": - options.sendFrameMeta = Boolean.parseBoolean(value); - break; - case "send_dummy_byte": - options.sendDummyByte = Boolean.parseBoolean(value); - break; - case "send_codec_meta": - options.sendCodecMeta = Boolean.parseBoolean(value); - break; - case "raw_stream": - boolean rawStream = Boolean.parseBoolean(value); - if (rawStream) { - options.sendDeviceMeta = false; - options.sendFrameMeta = false; - options.sendDummyByte = false; - options.sendCodecMeta = false; - } - break; - default: - Ln.w("Unknown server option: " + key); - break; - } - } - - if (options.newDisplay != null) { - assert options.displayId == 0 : "Must not set both displayId and newDisplay"; - options.displayId = Device.DISPLAY_ID_NONE; - } - - return options; - } - - private static Rect parseCrop(String crop) { - // input format: "width:height:x:y" - String[] tokens = crop.split(":"); - if (tokens.length != 4) { - throw new IllegalArgumentException("Crop must contains 4 values separated by colons: \"" + crop + "\""); - } - int width = Integer.parseInt(tokens[0]); - int height = Integer.parseInt(tokens[1]); - if (width <= 0 || height <= 0) { - throw new IllegalArgumentException("Invalid crop size: " + width + "x" + height); - } - int x = Integer.parseInt(tokens[2]); - int y = Integer.parseInt(tokens[3]); - if (x < 0 || y < 0) { - throw new IllegalArgumentException("Invalid crop offset: " + x + ":" + y); - } - return new Rect(x, y, x + width, y + height); - } - - private static Size parseSize(String size) { - // input format: "x" - String[] tokens = size.split("x"); - if (tokens.length != 2) { - throw new IllegalArgumentException("Invalid size format (expected x): \"" + size + "\""); - } - int width = Integer.parseInt(tokens[0]); - int height = Integer.parseInt(tokens[1]); - if (width <= 0 || height <= 0) { - throw new IllegalArgumentException("Invalid non-positive size dimension: \"" + size + "\""); - } - return new Size(width, height); - } - - private static CameraAspectRatio parseCameraAspectRatio(String ar) { - if ("sensor".equals(ar)) { - return CameraAspectRatio.sensorAspectRatio(); - } - - String[] tokens = ar.split(":"); - if (tokens.length == 2) { - int w = Integer.parseInt(tokens[0]); - int h = Integer.parseInt(tokens[1]); - return CameraAspectRatio.fromFraction(w, h); - } - - float floatAr = Float.parseFloat(tokens[0]); - return CameraAspectRatio.fromFloat(floatAr); - } - - private static float parseFloat(String key, String value) { - try { - return Float.parseFloat(value); - } catch (NumberFormatException e) { - throw new IllegalArgumentException("Invalid float value for " + key + ": \"" + value + "\""); - } - } - - private static NewDisplay parseNewDisplay(String newDisplay) { - // Possible inputs: - // - "" (empty string) - // - "x/" - // - "x" - // - "/" - if (newDisplay.isEmpty()) { - return new NewDisplay(); - } - - String[] tokens = newDisplay.split("/"); - - Size size; - if (!tokens[0].isEmpty()) { - size = parseSize(tokens[0]); - } else { - size = null; - } - - int dpi; - if (tokens.length >= 2) { - dpi = Integer.parseInt(tokens[1]); - if (dpi <= 0) { - throw new IllegalArgumentException("Invalid non-positive dpi: " + tokens[1]); - } - } else { - dpi = 0; - } - - return new NewDisplay(size, dpi); - } - - private static Pair parseCaptureOrientation(String value) { - if (value.isEmpty()) { - throw new IllegalArgumentException("Empty capture orientation string"); - } - - Orientation.Lock lock; - if (value.charAt(0) == '@') { - // Consume '@' - value = value.substring(1); - if (value.isEmpty()) { - // Only '@': lock to the initial orientation (orientation is unused) - return Pair.create(Orientation.Lock.LockedInitial, Orientation.Orient0); - } - lock = Orientation.Lock.LockedValue; - } else { - lock = Orientation.Lock.Unlocked; - } - - return Pair.create(lock, Orientation.getByName(value)); - } - - private static int parseDisplayImePolicy(String value) { - switch (value) { - case "local": - return WindowManager.DISPLAY_IME_POLICY_LOCAL; - case "fallback": - return WindowManager.DISPLAY_IME_POLICY_FALLBACK_DISPLAY; - case "hide": - return WindowManager.DISPLAY_IME_POLICY_HIDE; - default: - throw new IllegalArgumentException("Invalid display IME policy: " + value); - } + public void setControl(boolean control) { + this.control = control; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Point.java b/server/src/main/java/com/genymobile/scrcpy/Point.java similarity index 77% rename from server/src/main/java/com/genymobile/scrcpy/device/Point.java rename to server/src/main/java/com/genymobile/scrcpy/Point.java index 361b9958..9ef2db03 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Point.java +++ b/server/src/main/java/com/genymobile/scrcpy/Point.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy.device; +package com.genymobile.scrcpy; import java.util.Objects; @@ -28,7 +28,8 @@ public class Point { return false; } Point point = (Point) o; - return x == point.x && y == point.y; + return x == point.x + && y == point.y; } @Override @@ -38,6 +39,9 @@ public class Point { @Override public String toString() { - return "Point{" + "x=" + x + ", y=" + y + '}'; + return "Point{" + + "x=" + x + + ", y=" + y + + '}'; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Position.java b/server/src/main/java/com/genymobile/scrcpy/Position.java new file mode 100644 index 00000000..757fa36e --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/Position.java @@ -0,0 +1,52 @@ +package 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; + } + + @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 + + '}'; + } + +} diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java new file mode 100644 index 00000000..8357b061 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -0,0 +1,179 @@ +package com.genymobile.scrcpy; + +import com.genymobile.scrcpy.wrappers.SurfaceControl; + +import android.graphics.Rect; +import android.media.MediaCodec; +import android.media.MediaCodecInfo; +import android.media.MediaFormat; +import android.os.IBinder; +import android.view.Surface; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicBoolean; + +public class ScreenEncoder implements Device.RotationListener { + + private static final int DEFAULT_FRAME_RATE = 60; // fps + private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds + + private static final int REPEAT_FRAME_DELAY = 6; // repeat after 6 frames + + private static final int MICROSECONDS_IN_ONE_SECOND = 1_000_000; + private static final int NO_PTS = -1; + + private final AtomicBoolean rotationChanged = new AtomicBoolean(); + private final ByteBuffer headerBuffer = ByteBuffer.allocate(12); + + private int bitRate; + private int frameRate; + private int iFrameInterval; + private boolean sendFrameMeta; + private long ptsOrigin; + + public ScreenEncoder(boolean sendFrameMeta, int bitRate, int frameRate, int iFrameInterval) { + this.sendFrameMeta = sendFrameMeta; + this.bitRate = bitRate; + this.frameRate = frameRate; + this.iFrameInterval = iFrameInterval; + } + + public ScreenEncoder(boolean sendFrameMeta, int bitRate) { + this(sendFrameMeta, bitRate, DEFAULT_FRAME_RATE, DEFAULT_I_FRAME_INTERVAL); + } + + @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 { + MediaFormat format = createFormat(bitRate, frameRate, iFrameInterval); + device.setRotationListener(this); + boolean alive; + try { + do { + MediaCodec codec = createCodec(); + IBinder display = createDisplay(); + Rect contentRect = device.getScreenInfo().getContentRect(); + Rect videoRect = device.getScreenInfo().getVideoSize().toRect(); + setSize(format, videoRect.width(), videoRect.height()); + configure(codec, format); + Surface surface = codec.createInputSurface(); + setDisplaySurface(display, surface, contentRect, videoRect); + codec.start(); + try { + alive = encode(codec, fd); + // do not call stop() on exception, it would trigger an IllegalStateException + codec.stop(); + } finally { + destroyDisplay(display); + codec.release(); + surface.release(); + } + } while (alive); + } finally { + device.setRotationListener(null); + } + } + + private boolean encode(MediaCodec codec, FileDescriptor fd) throws IOException { + boolean eof = false; + MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); + + while (!consumeRotationChange() && !eof) { + int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1); + eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0; + try { + if (consumeRotationChange()) { + // must restart encoding with new size + break; + } + if (outputBufferId >= 0) { + ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId); + + if (sendFrameMeta) { + writeFrameMeta(fd, bufferInfo, codecBuffer.remaining()); + } + + IO.writeFully(fd, codecBuffer); + } + } finally { + if (outputBufferId >= 0) { + codec.releaseOutputBuffer(outputBufferId, false); + } + } + } + + return !eof; + } + + private void writeFrameMeta(FileDescriptor fd, MediaCodec.BufferInfo bufferInfo, int packetSize) throws IOException { + headerBuffer.clear(); + + long pts; + if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { + pts = NO_PTS; // non-media data packet + } else { + if (ptsOrigin == 0) { + ptsOrigin = bufferInfo.presentationTimeUs; + } + pts = bufferInfo.presentationTimeUs - ptsOrigin; + } + + headerBuffer.putLong(pts); + headerBuffer.putInt(packetSize); + headerBuffer.flip(); + IO.writeFully(fd, headerBuffer); + } + + private static MediaCodec createCodec() throws IOException { + return MediaCodec.createEncoderByType("video/avc"); + } + + private static MediaFormat createFormat(int bitRate, int frameRate, int iFrameInterval) throws IOException { + MediaFormat format = new MediaFormat(); + format.setString(MediaFormat.KEY_MIME, "video/avc"); + format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); + format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate); + format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); + format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval); + // display the very first frame, and recover from bad quality when no new frames + format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, MICROSECONDS_IN_ONE_SECOND * REPEAT_FRAME_DELAY / frameRate); // µs + return format; + } + + private static IBinder createDisplay() { + return SurfaceControl.createDisplay("scrcpy", true); + } + + 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, Rect deviceRect, Rect displayRect) { + SurfaceControl.openTransaction(); + try { + SurfaceControl.setDisplaySurface(display, surface); + SurfaceControl.setDisplayProjection(display, 0, deviceRect, displayRect); + SurfaceControl.setDisplayLayerStack(display, 0); + } finally { + SurfaceControl.closeTransaction(); + } + } + + private static void destroyDisplay(IBinder display) { + SurfaceControl.destroyDisplay(display); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java b/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java new file mode 100644 index 00000000..f2fce1d6 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java @@ -0,0 +1,31 @@ +package com.genymobile.scrcpy; + +import android.graphics.Rect; + +public final class ScreenInfo { + private final Rect contentRect; // device size, possibly cropped + private final Size videoSize; + private final boolean rotated; + + public ScreenInfo(Rect contentRect, Size videoSize, boolean rotated) { + this.contentRect = contentRect; + this.videoSize = videoSize; + this.rotated = rotated; + } + + public Rect getContentRect() { + return contentRect; + } + + public Size getVideoSize() { + return videoSize; + } + + public ScreenInfo withRotation(int rotation) { + boolean newRotated = (rotation & 1) != 0; + if (rotated == newRotated) { + return this; + } + return new ScreenInfo(Device.flipRect(contentRect), videoSize.rotate(), newRotated); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index a08c948c..1e4d10d6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -1,272 +1,135 @@ package com.genymobile.scrcpy; -import com.genymobile.scrcpy.audio.AudioCapture; -import com.genymobile.scrcpy.audio.AudioCodec; -import com.genymobile.scrcpy.audio.AudioDirectCapture; -import com.genymobile.scrcpy.audio.AudioEncoder; -import com.genymobile.scrcpy.audio.AudioPlaybackCapture; -import com.genymobile.scrcpy.audio.AudioRawRecorder; -import com.genymobile.scrcpy.audio.AudioSource; -import com.genymobile.scrcpy.control.ControlChannel; -import com.genymobile.scrcpy.control.Controller; -import com.genymobile.scrcpy.device.ConfigurationException; -import com.genymobile.scrcpy.device.DesktopConnection; -import com.genymobile.scrcpy.device.Device; -import com.genymobile.scrcpy.device.NewDisplay; -import com.genymobile.scrcpy.device.Streamer; -import com.genymobile.scrcpy.opengl.OpenGLRunner; -import com.genymobile.scrcpy.util.Ln; -import com.genymobile.scrcpy.util.LogUtils; -import com.genymobile.scrcpy.video.CameraCapture; -import com.genymobile.scrcpy.video.NewDisplayCapture; -import com.genymobile.scrcpy.video.ScreenCapture; -import com.genymobile.scrcpy.video.SurfaceCapture; -import com.genymobile.scrcpy.video.SurfaceEncoder; -import com.genymobile.scrcpy.video.VideoSource; - -import android.annotation.SuppressLint; -import android.os.Build; -import android.os.Looper; +import android.graphics.Rect; import java.io.File; import java.io.IOException; -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.List; public final class Server { - public static final String SERVER_PATH; - - static { - String[] classPaths = System.getProperty("java.class.path").split(File.pathSeparator); - // By convention, scrcpy is always executed with the absolute path of scrcpy-server.jar as the first item in the classpath - SERVER_PATH = classPaths[0]; - } - - private static class Completion { - private int running; - private boolean fatalError; - - Completion(int running) { - this.running = running; - } - - synchronized void addCompleted(boolean fatalError) { - --running; - if (fatalError) { - this.fatalError = true; - } - if (running == 0 || this.fatalError) { - Looper.getMainLooper().quitSafely(); - } - } - } + private static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar"; private Server() { // not instantiable } - private static void scrcpy(Options options) throws IOException, ConfigurationException { - if (Build.VERSION.SDK_INT < AndroidVersions.API_31_ANDROID_12 && options.getVideoSource() == VideoSource.CAMERA) { - Ln.e("Camera mirroring is not supported before Android 12"); - throw new ConfigurationException("Camera mirroring is not supported"); - } - - if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { - if (options.getNewDisplay() != null) { - Ln.e("New virtual display is not supported before Android 10"); - throw new ConfigurationException("New virtual display is not supported"); - } - if (options.getDisplayImePolicy() != -1) { - Ln.e("Display IME policy is not supported before Android 10"); - throw new ConfigurationException("Display IME policy is not supported"); - } - } - - CleanUp cleanUp = null; - - if (options.getCleanup()) { - cleanUp = CleanUp.start(options); - } - - int scid = options.getScid(); + private static void scrcpy(Options options) throws IOException { + final Device device = new Device(options); boolean tunnelForward = options.isTunnelForward(); - boolean control = options.getControl(); - boolean video = options.getVideo(); - boolean audio = options.getAudio(); - boolean sendDummyByte = options.getSendDummyByte(); + try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) { + ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate()); - Workarounds.apply(); + if (options.getControl()) { + Controller controller = new Controller(device, connection); - List asyncProcessors = new ArrayList<>(); - - DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, video, audio, control, sendDummyByte); - try { - if (options.getSendDeviceMeta()) { - connection.sendDeviceMeta(Device.getDeviceName()); + // asynchronous + startController(controller); + startDeviceMessageSender(controller.getSender()); } - Controller controller = null; - - if (control) { - ControlChannel controlChannel = connection.getControlChannel(); - controller = new Controller(controlChannel, cleanUp, options); - asyncProcessors.add(controller); - } - - if (audio) { - AudioCodec audioCodec = options.getAudioCodec(); - AudioSource audioSource = options.getAudioSource(); - AudioCapture audioCapture; - if (audioSource.isDirect()) { - audioCapture = new AudioDirectCapture(audioSource); - } else { - audioCapture = new AudioPlaybackCapture(options.getAudioDup()); - } - - Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(), options.getSendFrameMeta()); - AsyncProcessor audioRecorder; - if (audioCodec == AudioCodec.RAW) { - audioRecorder = new AudioRawRecorder(audioCapture, audioStreamer); - } else { - audioRecorder = new AudioEncoder(audioCapture, audioStreamer, options); - } - asyncProcessors.add(audioRecorder); - } - - if (video) { - Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendCodecMeta(), - options.getSendFrameMeta()); - SurfaceCapture surfaceCapture; - if (options.getVideoSource() == VideoSource.DISPLAY) { - NewDisplay newDisplay = options.getNewDisplay(); - if (newDisplay != null) { - surfaceCapture = new NewDisplayCapture(controller, options); - } else { - assert options.getDisplayId() != Device.DISPLAY_ID_NONE; - surfaceCapture = new ScreenCapture(controller, options); - } - } else { - surfaceCapture = new CameraCapture(options); - } - SurfaceEncoder surfaceEncoder = new SurfaceEncoder(surfaceCapture, videoStreamer, options); - asyncProcessors.add(surfaceEncoder); - - if (controller != null) { - controller.setSurfaceCapture(surfaceCapture); - } - } - - Completion completion = new Completion(asyncProcessors.size()); - for (AsyncProcessor asyncProcessor : asyncProcessors) { - asyncProcessor.start((fatalError) -> { - completion.addCompleted(fatalError); - }); - } - - Looper.loop(); // interrupted by the Completion implementation - } finally { - if (cleanUp != null) { - cleanUp.interrupt(); - } - for (AsyncProcessor asyncProcessor : asyncProcessors) { - asyncProcessor.stop(); - } - - OpenGLRunner.quit(); // quit the OpenGL thread, if any - - connection.shutdown(); - try { - if (cleanUp != null) { - cleanUp.join(); - } - for (AsyncProcessor asyncProcessor : asyncProcessors) { - asyncProcessor.join(); - } - OpenGLRunner.join(); - } catch (InterruptedException e) { - // ignore - } - - connection.close(); - } - } - - private static void prepareMainLooper() { - // Like Looper.prepareMainLooper(), but with quitAllowed set to true - Looper.prepare(); - synchronized (Looper.class) { - try { - @SuppressLint("DiscouragedPrivateApi") - Field field = Looper.class.getDeclaredField("sMainLooper"); - field.setAccessible(true); - field.set(null, Looper.myLooper()); - } catch (ReflectiveOperationException e) { - throw new AssertionError(e); + // synchronous + screenEncoder.streamScreen(device, connection.getVideoFd()); + } catch (IOException e) { + // this is expected on close + Ln.d("Screen streaming stopped"); } } } - public static void main(String... args) { - int status = 0; + private static void startController(final Controller controller) { + new Thread(new Runnable() { + @Override + public void run() { + try { + controller.control(); + } catch (IOException e) { + // this is expected on close + Ln.d("Controller stopped"); + } + } + }).start(); + } + + private static void startDeviceMessageSender(final DeviceMessageSender sender) { + 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"); + } + } + }).start(); + } + + @SuppressWarnings("checkstyle:MagicNumber") + private static Options createOptions(String... args) { + if (args.length != 6) { + throw new IllegalArgumentException("Expecting 5 parameters"); + } + + Options options = new Options(); + + int maxSize = Integer.parseInt(args[0]) & ~7; // multiple of 8 + options.setMaxSize(maxSize); + + int bitRate = Integer.parseInt(args[1]); + options.setBitRate(bitRate); + + // use "adb forward" instead of "adb tunnel"? (so the server must listen) + boolean tunnelForward = Boolean.parseBoolean(args[2]); + options.setTunnelForward(tunnelForward); + + Rect crop = parseCrop(args[3]); + options.setCrop(crop); + + boolean sendFrameMeta = Boolean.parseBoolean(args[4]); + options.setSendFrameMeta(sendFrameMeta); + + boolean control = Boolean.parseBoolean(args[5]); + options.setControl(control); + + return options; + } + + @SuppressWarnings("checkstyle:MagicNumber") + private static Rect parseCrop(String crop) { + if ("-".equals(crop)) { + 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 unlinkSelf() { try { - internalMain(args); - } catch (Throwable t) { - Ln.e(t.getMessage(), t); - status = 1; - } finally { - // By default, the Java process exits when all non-daemon threads are terminated. - // The Android SDK might start some non-daemon threads internally, preventing the scrcpy server to exit. - // So force the process to exit explicitly. - System.exit(status); + new File(SERVER_PATH).delete(); + } catch (Exception e) { + Ln.e("Cannot unlink server", e); } } - private static void internalMain(String... args) throws Exception { - Thread.setDefaultUncaughtExceptionHandler((t, e) -> { - Ln.e("Exception on thread " + t, e); + 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); + } }); - prepareMainLooper(); - - Options options = Options.parse(args); - - Ln.disableSystemStreams(); - Ln.initLogLevel(options.getLogLevel()); - - Ln.i("Device: [" + Build.MANUFACTURER + "] " + Build.BRAND + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")"); - - if (options.getList()) { - if (options.getCleanup()) { - CleanUp.unlinkSelf(); - } - - if (options.getListEncoders()) { - Ln.i(LogUtils.buildVideoEncoderListMessage()); - Ln.i(LogUtils.buildAudioEncoderListMessage()); - } - if (options.getListDisplays()) { - Ln.i(LogUtils.buildDisplayListMessage()); - } - if (options.getListCameras() || options.getListCameraSizes()) { - Workarounds.apply(); - Ln.i(LogUtils.buildCameraListMessage(options.getListCameraSizes())); - } - if (options.getListApps()) { - Workarounds.apply(); - Ln.i("Processing Android apps... (this may take some time)"); - Ln.i(LogUtils.buildAppListMessage()); - } - // Just print the requested data, do not mirror - return; - } - - try { - scrcpy(options); - } catch (ConfigurationException e) { - // Do not print stack trace, a user-friendly error-message has already been logged - } + unlinkSelf(); + Options options = createOptions(args); + scrcpy(options); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Size.java b/server/src/main/java/com/genymobile/scrcpy/Size.java new file mode 100644 index 00000000..0d546bbd --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/Size.java @@ -0,0 +1,57 @@ +package 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 + + '}'; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/util/StringUtils.java b/server/src/main/java/com/genymobile/scrcpy/StringUtils.java similarity index 89% rename from server/src/main/java/com/genymobile/scrcpy/util/StringUtils.java rename to server/src/main/java/com/genymobile/scrcpy/StringUtils.java index 8b19ca3d..199fc8c1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/StringUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/StringUtils.java @@ -1,10 +1,11 @@ -package com.genymobile.scrcpy.util; +package com.genymobile.scrcpy; public final class StringUtils { private StringUtils() { // not instantiable } + @SuppressWarnings("checkstyle:MagicNumber") public static int getUtf8TruncationIndex(byte[] utf8, int maxLength) { int len = utf8.length; if (len <= maxLength) { diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java deleted file mode 100644 index b89f19ae..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ /dev/null @@ -1,299 +0,0 @@ -package com.genymobile.scrcpy; - -import com.genymobile.scrcpy.audio.AudioCaptureException; -import com.genymobile.scrcpy.util.Ln; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.app.Application; -import android.content.AttributionSource; -import android.content.Context; -import android.content.ContextWrapper; -import android.content.pm.ApplicationInfo; -import android.media.AudioAttributes; -import android.media.AudioManager; -import android.media.AudioRecord; -import android.os.Build; -import android.os.Looper; -import android.os.Parcel; - -import java.lang.ref.WeakReference; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.Method; - -@SuppressLint("PrivateApi,BlockedPrivateApi,SoonBlockedPrivateApi,DiscouragedPrivateApi") -public final class Workarounds { - - private static final Class ACTIVITY_THREAD_CLASS; - private static final Object ACTIVITY_THREAD; - - static { - try { - // ActivityThread activityThread = new ActivityThread(); - ACTIVITY_THREAD_CLASS = Class.forName("android.app.ActivityThread"); - Constructor activityThreadConstructor = ACTIVITY_THREAD_CLASS.getDeclaredConstructor(); - activityThreadConstructor.setAccessible(true); - ACTIVITY_THREAD = activityThreadConstructor.newInstance(); - - // ActivityThread.sCurrentActivityThread = activityThread; - Field sCurrentActivityThreadField = ACTIVITY_THREAD_CLASS.getDeclaredField("sCurrentActivityThread"); - sCurrentActivityThreadField.setAccessible(true); - sCurrentActivityThreadField.set(null, ACTIVITY_THREAD); - - // activityThread.mSystemThread = true; - Field mSystemThreadField = ACTIVITY_THREAD_CLASS.getDeclaredField("mSystemThread"); - mSystemThreadField.setAccessible(true); - mSystemThreadField.setBoolean(ACTIVITY_THREAD, true); - } catch (Exception e) { - throw new AssertionError(e); - } - } - - private Workarounds() { - // not instantiable - } - - public static void apply() { - if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) { - // On some Samsung devices, DisplayManagerGlobal.getDisplayInfoLocked() calls ActivityThread.currentActivityThread().getConfiguration(), - // which requires a non-null ConfigurationController. - // ConfigurationController was introduced in Android 12, so do not attempt to set it on lower versions. - // - // Must be called before fillAppContext() because it is necessary to get a valid system context. - fillConfigurationController(); - } - - // On ONYX devices, fillAppInfo() breaks video mirroring: - // - boolean mustFillAppInfo = !Build.BRAND.equalsIgnoreCase("ONYX"); - - if (mustFillAppInfo) { - fillAppInfo(); - } - - fillAppContext(); - } - - private static void fillAppInfo() { - try { - // 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 = FakeContext.PACKAGE_NAME; - - // appBindData.appInfo = applicationInfo; - Field appInfoField = appBindDataClass.getDeclaredField("appInfo"); - appInfoField.setAccessible(true); - appInfoField.set(appBindData, applicationInfo); - - // activityThread.mBoundApplication = appBindData; - Field mBoundApplicationField = ACTIVITY_THREAD_CLASS.getDeclaredField("mBoundApplication"); - mBoundApplicationField.setAccessible(true); - mBoundApplicationField.set(ACTIVITY_THREAD, appBindData); - } catch (Throwable throwable) { - // this is a workaround, so failing is not an error - Ln.d("Could not fill app info: " + throwable.getMessage()); - } - } - - private static void fillAppContext() { - try { - Application app = new Application(); - Field baseField = ContextWrapper.class.getDeclaredField("mBase"); - baseField.setAccessible(true); - baseField.set(app, FakeContext.get()); - - // activityThread.mInitialApplication = app; - Field mInitialApplicationField = ACTIVITY_THREAD_CLASS.getDeclaredField("mInitialApplication"); - mInitialApplicationField.setAccessible(true); - mInitialApplicationField.set(ACTIVITY_THREAD, app); - } catch (Throwable throwable) { - // this is a workaround, so failing is not an error - Ln.d("Could not fill app context: " + throwable.getMessage()); - } - } - - private static void fillConfigurationController() { - try { - Class configurationControllerClass = Class.forName("android.app.ConfigurationController"); - Class activityThreadInternalClass = Class.forName("android.app.ActivityThreadInternal"); - - // configurationController = new ConfigurationController(ACTIVITY_THREAD); - Constructor configurationControllerConstructor = configurationControllerClass.getDeclaredConstructor(activityThreadInternalClass); - configurationControllerConstructor.setAccessible(true); - Object configurationController = configurationControllerConstructor.newInstance(ACTIVITY_THREAD); - - // ACTIVITY_THREAD.mConfigurationController = configurationController; - Field configurationControllerField = ACTIVITY_THREAD_CLASS.getDeclaredField("mConfigurationController"); - configurationControllerField.setAccessible(true); - configurationControllerField.set(ACTIVITY_THREAD, configurationController); - } catch (Throwable throwable) { - Ln.d("Could not fill configuration: " + throwable.getMessage()); - } - } - - static Context getSystemContext() { - try { - Method getSystemContextMethod = ACTIVITY_THREAD_CLASS.getDeclaredMethod("getSystemContext"); - return (Context) getSystemContextMethod.invoke(ACTIVITY_THREAD); - } catch (Throwable throwable) { - // this is a workaround, so failing is not an error - Ln.d("Could not get system context: " + throwable.getMessage()); - return null; - } - } - - @TargetApi(AndroidVersions.API_30_ANDROID_11) - @SuppressLint("WrongConstant,MissingPermission") - public static AudioRecord createAudioRecord(int source, int sampleRate, int channelConfig, int channels, int channelMask, int encoding) throws - AudioCaptureException { - // Vivo (and maybe some other third-party ROMs) modified `AudioRecord`'s constructor, requiring `Context`s from real App environment. - // - // This method invokes the `AudioRecord(long nativeRecordInJavaObj)` constructor to create an empty `AudioRecord` instance, then uses - // reflections to initialize it like the normal constructor do (or the `AudioRecord.Builder.build()` method do). - // As a result, the modified code was not executed. - try { - // AudioRecord audioRecord = new AudioRecord(0L); - Constructor audioRecordConstructor = AudioRecord.class.getDeclaredConstructor(long.class); - audioRecordConstructor.setAccessible(true); - AudioRecord audioRecord = audioRecordConstructor.newInstance(0L); - - // audioRecord.mRecordingState = RECORDSTATE_STOPPED; - Field mRecordingStateField = AudioRecord.class.getDeclaredField("mRecordingState"); - mRecordingStateField.setAccessible(true); - mRecordingStateField.set(audioRecord, AudioRecord.RECORDSTATE_STOPPED); - - Looper looper = Looper.myLooper(); - if (looper == null) { - looper = Looper.getMainLooper(); - } - - // audioRecord.mInitializationLooper = looper; - Field mInitializationLooperField = AudioRecord.class.getDeclaredField("mInitializationLooper"); - mInitializationLooperField.setAccessible(true); - mInitializationLooperField.set(audioRecord, looper); - - // Create `AudioAttributes` with fixed capture preset - int capturePreset = source; - AudioAttributes.Builder audioAttributesBuilder = new AudioAttributes.Builder(); - Method setInternalCapturePresetMethod = AudioAttributes.Builder.class.getMethod("setInternalCapturePreset", int.class); - setInternalCapturePresetMethod.invoke(audioAttributesBuilder, capturePreset); - AudioAttributes attributes = audioAttributesBuilder.build(); - - // audioRecord.mAudioAttributes = attributes; - Field mAudioAttributesField = AudioRecord.class.getDeclaredField("mAudioAttributes"); - mAudioAttributesField.setAccessible(true); - mAudioAttributesField.set(audioRecord, attributes); - - // audioRecord.audioParamCheck(capturePreset, sampleRate, encoding); - Method audioParamCheckMethod = AudioRecord.class.getDeclaredMethod("audioParamCheck", int.class, int.class, int.class); - audioParamCheckMethod.setAccessible(true); - audioParamCheckMethod.invoke(audioRecord, capturePreset, sampleRate, encoding); - - // audioRecord.mChannelCount = channels - Field mChannelCountField = AudioRecord.class.getDeclaredField("mChannelCount"); - mChannelCountField.setAccessible(true); - mChannelCountField.set(audioRecord, channels); - - // audioRecord.mChannelMask = channelMask - Field mChannelMaskField = AudioRecord.class.getDeclaredField("mChannelMask"); - mChannelMaskField.setAccessible(true); - mChannelMaskField.set(audioRecord, channelMask); - - int minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, encoding); - int bufferSizeInBytes = minBufferSize * 8; - - // audioRecord.audioBuffSizeCheck(bufferSizeInBytes) - Method audioBuffSizeCheckMethod = AudioRecord.class.getDeclaredMethod("audioBuffSizeCheck", int.class); - audioBuffSizeCheckMethod.setAccessible(true); - audioBuffSizeCheckMethod.invoke(audioRecord, bufferSizeInBytes); - - final int channelIndexMask = 0; - - int[] sampleRateArray = new int[]{sampleRate}; - int[] session = new int[]{AudioManager.AUDIO_SESSION_ID_GENERATE}; - - int initResult; - if (Build.VERSION.SDK_INT < AndroidVersions.API_31_ANDROID_12) { - // private native final int native_setup(Object audiorecord_this, - // Object /*AudioAttributes*/ attributes, - // int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat, - // int buffSizeInBytes, int[] sessionId, String opPackageName, - // long nativeRecordInJavaObj); - Method nativeSetupMethod = AudioRecord.class.getDeclaredMethod("native_setup", Object.class, Object.class, int[].class, int.class, - int.class, int.class, int.class, int[].class, String.class, long.class); - nativeSetupMethod.setAccessible(true); - initResult = (int) nativeSetupMethod.invoke(audioRecord, new WeakReference(audioRecord), attributes, sampleRateArray, - channelMask, channelIndexMask, audioRecord.getAudioFormat(), bufferSizeInBytes, session, FakeContext.get().getOpPackageName(), - 0L); - } else { - // Assume `context` is never `null` - AttributionSource attributionSource = FakeContext.get().getAttributionSource(); - - // Assume `attributionSource.getPackageName()` is never null - - // ScopedParcelState attributionSourceState = attributionSource.asScopedParcelState() - Method asScopedParcelStateMethod = AttributionSource.class.getDeclaredMethod("asScopedParcelState"); - asScopedParcelStateMethod.setAccessible(true); - - try (AutoCloseable attributionSourceState = (AutoCloseable) asScopedParcelStateMethod.invoke(attributionSource)) { - Method getParcelMethod = attributionSourceState.getClass().getDeclaredMethod("getParcel"); - Parcel attributionSourceParcel = (Parcel) getParcelMethod.invoke(attributionSourceState); - - if (Build.VERSION.SDK_INT < AndroidVersions.API_34_ANDROID_14) { - // private native int native_setup(Object audiorecordThis, - // Object /*AudioAttributes*/ attributes, - // int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat, - // int buffSizeInBytes, int[] sessionId, @NonNull Parcel attributionSource, - // long nativeRecordInJavaObj, int maxSharedAudioHistoryMs); - Method nativeSetupMethod = AudioRecord.class.getDeclaredMethod("native_setup", Object.class, Object.class, int[].class, - int.class, int.class, int.class, int.class, int[].class, Parcel.class, long.class, int.class); - nativeSetupMethod.setAccessible(true); - initResult = (int) nativeSetupMethod.invoke(audioRecord, new WeakReference(audioRecord), attributes, - sampleRateArray, channelMask, channelIndexMask, audioRecord.getAudioFormat(), bufferSizeInBytes, session, - attributionSourceParcel, 0L, 0); - } else { - // Android 14 added a new int parameter "halInputFlags" - // - Method nativeSetupMethod = AudioRecord.class.getDeclaredMethod("native_setup", Object.class, Object.class, int[].class, - int.class, int.class, int.class, int.class, int[].class, Parcel.class, long.class, int.class, int.class); - nativeSetupMethod.setAccessible(true); - initResult = (int) nativeSetupMethod.invoke(audioRecord, new WeakReference(audioRecord), attributes, - sampleRateArray, channelMask, channelIndexMask, audioRecord.getAudioFormat(), bufferSizeInBytes, session, - attributionSourceParcel, 0L, 0, 0); - } - } - } - - if (initResult != AudioRecord.SUCCESS) { - Ln.e("Error code " + initResult + " when initializing native AudioRecord object."); - throw new RuntimeException("Cannot create AudioRecord"); - } - - // mSampleRate = sampleRate[0] - Field mSampleRateField = AudioRecord.class.getDeclaredField("mSampleRate"); - mSampleRateField.setAccessible(true); - mSampleRateField.set(audioRecord, sampleRateArray[0]); - - // audioRecord.mSessionId = session[0] - Field mSessionIdField = AudioRecord.class.getDeclaredField("mSessionId"); - mSessionIdField.setAccessible(true); - mSessionIdField.set(audioRecord, session[0]); - - // audioRecord.mState = AudioRecord.STATE_INITIALIZED - Field mStateField = AudioRecord.class.getDeclaredField("mState"); - mStateField.setAccessible(true); - mStateField.set(audioRecord, AudioRecord.STATE_INITIALIZED); - - return audioRecord; - } catch (Exception e) { - Ln.e("Cannot create AudioRecord", e); - throw new AudioCaptureException(); - } - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java deleted file mode 100644 index 62903f83..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.genymobile.scrcpy.audio; - -import android.media.MediaCodec; - -import java.nio.ByteBuffer; - -public interface AudioCapture { - void checkCompatibility() throws AudioCaptureException; - void start() throws AudioCaptureException; - void stop(); - - /** - * Read a chunk of {@link AudioConfig#MAX_READ_SIZE} samples. - * - * @param outDirectBuffer The target buffer - * @param outBufferInfo The info to provide to MediaCodec - * @return the number of bytes actually read. - */ - int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo); -} diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCaptureException.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCaptureException.java deleted file mode 100644 index 4b0b7e83..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCaptureException.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.genymobile.scrcpy.audio; - -/** - * Exception for any audio capture issue. - *

- * This includes the case where audio capture failed on Android 11 specifically because the running App (Shell) was not in foreground. - *

- * Its purpose is to disable audio without errors (that's why the exception is empty, any error message must be printed by the caller before - * throwing the exception). - */ -public class AudioCaptureException extends Exception { -} diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCodec.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCodec.java deleted file mode 100644 index 8f9e59b3..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCodec.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.genymobile.scrcpy.audio; - -import com.genymobile.scrcpy.util.Codec; - -import android.media.MediaFormat; - -public enum AudioCodec implements Codec { - OPUS(0x6f_70_75_73, "opus", MediaFormat.MIMETYPE_AUDIO_OPUS), - AAC(0x00_61_61_63, "aac", MediaFormat.MIMETYPE_AUDIO_AAC), - FLAC(0x66_6c_61_63, "flac", MediaFormat.MIMETYPE_AUDIO_FLAC), - RAW(0x00_72_61_77, "raw", MediaFormat.MIMETYPE_AUDIO_RAW); - - private final int id; // 4-byte ASCII representation of the name - private final String name; - private final String mimeType; - - AudioCodec(int id, String name, String mimeType) { - this.id = id; - this.name = name; - this.mimeType = mimeType; - } - - @Override - public Type getType() { - return Type.AUDIO; - } - - @Override - public int getId() { - return id; - } - - @Override - public String getName() { - return name; - } - - @Override - public String getMimeType() { - return mimeType; - } - - public static AudioCodec findByName(String name) { - for (AudioCodec codec : values()) { - if (codec.name.equals(name)) { - return codec; - } - } - return null; - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioConfig.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioConfig.java deleted file mode 100644 index c77165a7..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioConfig.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.genymobile.scrcpy.audio; - -import android.media.AudioFormat; - -public final class AudioConfig { - public static final int SAMPLE_RATE = 48000; - public static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO; - public static final int CHANNELS = 2; - public static final int CHANNEL_MASK = AudioFormat.CHANNEL_IN_LEFT | AudioFormat.CHANNEL_IN_RIGHT; - public static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT; - public static final int BYTES_PER_SAMPLE = 2; - - // Never read more than 1024 samples, even if the buffer is bigger (that would increase latency). - // A lower value is useless, since the system captures audio samples by blocks of 1024 (so for example if we read by blocks of 256 samples, we - // receive 4 successive blocks without waiting, then we wait for the 4 next ones). - public static final int MAX_READ_SIZE = 1024 * CHANNELS * BYTES_PER_SAMPLE; - - private AudioConfig() { - // Not instantiable - } - - public static AudioFormat createAudioFormat() { - AudioFormat.Builder builder = new AudioFormat.Builder(); - builder.setEncoding(ENCODING); - builder.setSampleRate(SAMPLE_RATE); - builder.setChannelMask(CHANNEL_CONFIG); - return builder.build(); - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java deleted file mode 100644 index bf870bee..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java +++ /dev/null @@ -1,142 +0,0 @@ -package com.genymobile.scrcpy.audio; - -import com.genymobile.scrcpy.AndroidVersions; -import com.genymobile.scrcpy.FakeContext; -import com.genymobile.scrcpy.Workarounds; -import com.genymobile.scrcpy.util.Ln; -import com.genymobile.scrcpy.wrappers.ServiceManager; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.content.ComponentName; -import android.content.Intent; -import android.media.AudioRecord; -import android.media.MediaCodec; -import android.os.Build; -import android.os.SystemClock; - -import java.nio.ByteBuffer; - -public class AudioDirectCapture implements AudioCapture { - - private static final int SAMPLE_RATE = AudioConfig.SAMPLE_RATE; - private static final int CHANNEL_CONFIG = AudioConfig.CHANNEL_CONFIG; - private static final int CHANNELS = AudioConfig.CHANNELS; - private static final int CHANNEL_MASK = AudioConfig.CHANNEL_MASK; - private static final int ENCODING = AudioConfig.ENCODING; - - private final int audioSource; - - private AudioRecord recorder; - private AudioRecordReader reader; - - public AudioDirectCapture(AudioSource audioSource) { - this.audioSource = audioSource.getDirectAudioSource(); - } - - @TargetApi(AndroidVersions.API_23_ANDROID_6_0) - @SuppressLint({"WrongConstant", "MissingPermission"}) - private static AudioRecord createAudioRecord(int audioSource) { - AudioRecord.Builder builder = new AudioRecord.Builder(); - if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) { - // On older APIs, Workarounds.fillAppInfo() must be called beforehand - builder.setContext(FakeContext.get()); - } - builder.setAudioSource(audioSource); - builder.setAudioFormat(AudioConfig.createAudioFormat()); - int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, ENCODING); - if (minBufferSize > 0) { - // This buffer size does not impact latency - builder.setBufferSizeInBytes(8 * minBufferSize); - } - - return builder.build(); - } - - private static void startWorkaroundAndroid11() { - // Android 11 requires Apps to be at foreground to record audio. - // Normally, each App has its own user ID, so Android checks whether the requesting App has the user ID that's at the foreground. - // But scrcpy server is NOT an App, it's a Java application started from Android shell, so it has the same user ID (2000) with Android - // shell ("com.android.shell"). - // If there is an Activity from Android shell running at foreground, then the permission system will believe scrcpy is also in the - // foreground. - Intent intent = new Intent(Intent.ACTION_MAIN); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.addCategory(Intent.CATEGORY_LAUNCHER); - intent.setComponent(new ComponentName(FakeContext.PACKAGE_NAME, "com.android.shell.HeapDumpActivity")); - ServiceManager.getActivityManager().startActivity(intent); - } - - private static void stopWorkaroundAndroid11() { - ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME); - } - - private void tryStartRecording(int attempts, int delayMs) throws AudioCaptureException { - while (attempts-- > 0) { - // Wait for activity to start - SystemClock.sleep(delayMs); - try { - startRecording(); - return; // it worked - } catch (UnsupportedOperationException e) { - if (attempts == 0) { - Ln.e("Failed to start audio capture"); - Ln.e("On Android 11, audio capture must be started in the foreground, make sure that the device is unlocked when starting " - + "scrcpy."); - throw new AudioCaptureException(); - } else { - Ln.d("Failed to start audio capture, retrying..."); - } - } - } - } - - private void startRecording() throws AudioCaptureException { - try { - recorder = createAudioRecord(audioSource); - } catch (NullPointerException e) { - // Creating an AudioRecord using an AudioRecord.Builder does not work on Vivo phones: - // - - // - - recorder = Workarounds.createAudioRecord(audioSource, SAMPLE_RATE, CHANNEL_CONFIG, CHANNELS, CHANNEL_MASK, ENCODING); - } - recorder.startRecording(); - reader = new AudioRecordReader(recorder); - } - - @Override - public void checkCompatibility() throws AudioCaptureException { - if (Build.VERSION.SDK_INT < AndroidVersions.API_30_ANDROID_11) { - Ln.w("Audio disabled: it is not supported before Android 11"); - throw new AudioCaptureException(); - } - } - - @Override - public void start() throws AudioCaptureException { - if (Build.VERSION.SDK_INT == AndroidVersions.API_30_ANDROID_11) { - startWorkaroundAndroid11(); - try { - tryStartRecording(5, 100); - } finally { - stopWorkaroundAndroid11(); - } - } else { - startRecording(); - } - } - - @Override - public void stop() { - if (recorder != null) { - // Will call .stop() if necessary, without throwing an IllegalStateException - recorder.release(); - } - } - - @Override - @TargetApi(AndroidVersions.API_24_ANDROID_7_0) - public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) { - return reader.read(outDirectBuffer, outBufferInfo); - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java deleted file mode 100644 index 33177228..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java +++ /dev/null @@ -1,380 +0,0 @@ -package com.genymobile.scrcpy.audio; - -import com.genymobile.scrcpy.AndroidVersions; -import com.genymobile.scrcpy.AsyncProcessor; -import com.genymobile.scrcpy.Options; -import com.genymobile.scrcpy.device.ConfigurationException; -import com.genymobile.scrcpy.device.Streamer; -import com.genymobile.scrcpy.util.Codec; -import com.genymobile.scrcpy.util.CodecOption; -import com.genymobile.scrcpy.util.CodecUtils; -import com.genymobile.scrcpy.util.IO; -import com.genymobile.scrcpy.util.Ln; -import com.genymobile.scrcpy.util.LogUtils; - -import android.annotation.TargetApi; -import android.media.MediaCodec; -import android.media.MediaFormat; -import android.os.Build; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Looper; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.List; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.BlockingQueue; - -public final class AudioEncoder implements AsyncProcessor { - - private static class InputTask { - private final int index; - - InputTask(int index) { - this.index = index; - } - } - - private static class OutputTask { - private final int index; - private final MediaCodec.BufferInfo bufferInfo; - - OutputTask(int index, MediaCodec.BufferInfo bufferInfo) { - this.index = index; - this.bufferInfo = bufferInfo; - } - } - - private static final int SAMPLE_RATE = AudioConfig.SAMPLE_RATE; - private static final int CHANNELS = AudioConfig.CHANNELS; - - private final AudioCapture capture; - private final Streamer streamer; - private final int bitRate; - private final List codecOptions; - private final String encoderName; - - private boolean recreatePts; - private long previousPts; - - // Capacity of 64 is in practice "infinite" (it is limited by the number of available MediaCodec buffers, typically 4). - // So many pending tasks would lead to an unacceptable delay anyway. - private final BlockingQueue inputTasks = new ArrayBlockingQueue<>(64); - private final BlockingQueue outputTasks = new ArrayBlockingQueue<>(64); - - private Thread thread; - private HandlerThread mediaCodecThread; - - private Thread inputThread; - private Thread outputThread; - - private boolean ended; - - public AudioEncoder(AudioCapture capture, Streamer streamer, Options options) { - this.capture = capture; - this.streamer = streamer; - this.bitRate = options.getAudioBitRate(); - this.codecOptions = options.getAudioCodecOptions(); - this.encoderName = options.getAudioEncoder(); - } - - private static MediaFormat createFormat(String mimeType, int bitRate, List codecOptions) { - MediaFormat format = new MediaFormat(); - format.setString(MediaFormat.KEY_MIME, mimeType); - format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); - format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, CHANNELS); - format.setInteger(MediaFormat.KEY_SAMPLE_RATE, SAMPLE_RATE); - - if (codecOptions != null) { - for (CodecOption option : codecOptions) { - String key = option.getKey(); - Object value = option.getValue(); - CodecUtils.setCodecOption(format, key, value); - Ln.d("Audio codec option set: " + key + " (" + value.getClass().getSimpleName() + ") = " + value); - } - } - - return format; - } - - @TargetApi(AndroidVersions.API_24_ANDROID_7_0) - private void inputThread(MediaCodec mediaCodec, AudioCapture capture) throws IOException, InterruptedException { - final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); - - while (!Thread.currentThread().isInterrupted()) { - InputTask task = inputTasks.take(); - ByteBuffer buffer = mediaCodec.getInputBuffer(task.index); - int r = capture.read(buffer, bufferInfo); - if (r <= 0) { - throw new IOException("Could not read audio: " + r); - } - - mediaCodec.queueInputBuffer(task.index, bufferInfo.offset, bufferInfo.size, bufferInfo.presentationTimeUs, bufferInfo.flags); - } - } - - private void outputThread(MediaCodec mediaCodec) throws IOException, InterruptedException { - streamer.writeAudioHeader(); - - while (!Thread.currentThread().isInterrupted()) { - OutputTask task = outputTasks.take(); - ByteBuffer buffer = mediaCodec.getOutputBuffer(task.index); - try { - if (recreatePts) { - fixTimestamp(task.bufferInfo); - } - streamer.writePacket(buffer, task.bufferInfo); - } finally { - mediaCodec.releaseOutputBuffer(task.index, false); - } - } - } - - private void fixTimestamp(MediaCodec.BufferInfo bufferInfo) { - assert recreatePts; - - if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { - // Config packet, nothing to fix - return; - } - - long pts = bufferInfo.presentationTimeUs; - if (previousPts != 0) { - long now = System.nanoTime() / 1000; - // This specific encoder produces PTS matching the exact number of samples - long duration = pts - previousPts; - bufferInfo.presentationTimeUs = now - duration; - } - - previousPts = pts; - } - - @Override - public void start(TerminationListener listener) { - thread = new Thread(() -> { - boolean fatalError = false; - try { - encode(); - } catch (ConfigurationException e) { - // Do not print stack trace, a user-friendly error-message has already been logged - fatalError = true; - } catch (AudioCaptureException e) { - // Do not print stack trace, a user-friendly error-message has already been logged - } catch (IOException e) { - Ln.e("Audio encoding error", e); - fatalError = true; - } finally { - Ln.d("Audio encoder stopped"); - listener.onTerminated(fatalError); - } - }, "audio-encoder"); - thread.start(); - } - - @Override - public void stop() { - if (thread != null) { - // Just wake up the blocking wait from the thread, so that it properly releases all its resources and terminates - end(); - } - } - - @Override - public void join() throws InterruptedException { - if (thread != null) { - thread.join(); - } - } - - private synchronized void end() { - ended = true; - notify(); - } - - private synchronized void waitEnded() { - try { - while (!ended) { - wait(); - } - } catch (InterruptedException e) { - // ignore - } - } - - @TargetApi(AndroidVersions.API_23_ANDROID_6_0) - private void encode() throws IOException, ConfigurationException, AudioCaptureException { - if (Build.VERSION.SDK_INT < AndroidVersions.API_30_ANDROID_11) { - Ln.w("Audio disabled: it is not supported before Android 11"); - streamer.writeDisableStream(false); - return; - } - - MediaCodec mediaCodec = null; - - boolean mediaCodecStarted = false; - try { - capture.checkCompatibility(); // throws an AudioCaptureException on error - - Codec codec = streamer.getCodec(); - mediaCodec = createMediaCodec(codec, encoderName); - - // The default OPUS and FLAC encoders overwrite the input PTS with a value that matches the number of samples. This is not the behavior - // we want: it ignores any audio clock drift and hard silences (packets not produced on silence). To work around this behavior, - // regenerate PTS based on the current time and the packet duration. - String codecName = mediaCodec.getCanonicalName(); - recreatePts = "c2.android.opus.encoder".equals(codecName) || "c2.android.flac.encoder".equals(codecName); - - mediaCodecThread = new HandlerThread("media-codec"); - mediaCodecThread.start(); - - MediaFormat format = createFormat(codec.getMimeType(), bitRate, codecOptions); - mediaCodec.setCallback(new EncoderCallback(), new Handler(mediaCodecThread.getLooper())); - mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); - - capture.start(); - - final MediaCodec mediaCodecRef = mediaCodec; - inputThread = new Thread(() -> { - try { - inputThread(mediaCodecRef, capture); - } catch (IOException | InterruptedException e) { - Ln.e("Audio capture error", e); - } finally { - end(); - } - }, "audio-in"); - - outputThread = new Thread(() -> { - try { - outputThread(mediaCodecRef); - } catch (InterruptedException e) { - // this is expected on close - } catch (IOException e) { - // Broken pipe is expected on close, because the socket is closed by the client - if (!IO.isBrokenPipe(e)) { - Ln.e("Audio encoding error", e); - } - } finally { - end(); - } - }, "audio-out"); - - mediaCodec.start(); - mediaCodecStarted = true; - inputThread.start(); - outputThread.start(); - - waitEnded(); - } catch (ConfigurationException e) { - // Notify the error to make scrcpy exit - streamer.writeDisableStream(true); - throw e; - } catch (Throwable e) { - // Notify the client that the audio could not be captured - streamer.writeDisableStream(false); - throw e; - } finally { - // Cleanup everything (either at the end or on error at any step of the initialization) - if (mediaCodecThread != null) { - Looper looper = mediaCodecThread.getLooper(); - if (looper != null) { - looper.quitSafely(); - } - } - if (inputThread != null) { - inputThread.interrupt(); - } - if (outputThread != null) { - outputThread.interrupt(); - } - - try { - if (mediaCodecThread != null) { - mediaCodecThread.join(); - } - if (inputThread != null) { - inputThread.join(); - } - if (outputThread != null) { - outputThread.join(); - } - } catch (InterruptedException e) { - // Should never happen - throw new AssertionError(e); - } - - if (mediaCodec != null) { - if (mediaCodecStarted) { - mediaCodec.stop(); - } - mediaCodec.release(); - } - if (capture != null) { - capture.stop(); - } - } - } - - private static MediaCodec createMediaCodec(Codec codec, String encoderName) throws IOException, ConfigurationException { - if (encoderName != null) { - Ln.d("Creating audio encoder by name: '" + encoderName + "'"); - try { - MediaCodec mediaCodec = MediaCodec.createByCodecName(encoderName); - String mimeType = Codec.getMimeType(mediaCodec); - if (!codec.getMimeType().equals(mimeType)) { - Ln.e("Audio encoder type for \"" + encoderName + "\" (" + mimeType + ") does not match codec type (" + codec.getMimeType() + ")"); - throw new ConfigurationException("Incorrect encoder type: " + encoderName); - } - return mediaCodec; - } catch (IllegalArgumentException e) { - Ln.e("Audio encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildAudioEncoderListMessage()); - throw new ConfigurationException("Unknown encoder: " + encoderName); - } catch (IOException e) { - Ln.e("Could not create audio encoder '" + encoderName + "' for " + codec.getName() + "\n" + LogUtils.buildAudioEncoderListMessage()); - throw e; - } - } - - try { - MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType()); - Ln.d("Using audio encoder: '" + mediaCodec.getName() + "'"); - return mediaCodec; - } catch (IOException | IllegalArgumentException e) { - Ln.e("Could not create default audio encoder for " + codec.getName() + "\n" + LogUtils.buildAudioEncoderListMessage()); - throw e; - } - } - - private final class EncoderCallback extends MediaCodec.Callback { - @TargetApi(AndroidVersions.API_24_ANDROID_7_0) - @Override - public void onInputBufferAvailable(MediaCodec codec, int index) { - try { - inputTasks.put(new InputTask(index)); - } catch (InterruptedException e) { - end(); - } - } - - @Override - public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo bufferInfo) { - try { - outputTasks.put(new OutputTask(index, bufferInfo)); - } catch (InterruptedException e) { - end(); - } - } - - @Override - public void onError(MediaCodec codec, MediaCodec.CodecException e) { - Ln.e("MediaCodec error", e); - end(); - } - - @Override - public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) { - // ignore - } - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java deleted file mode 100644 index 009a239a..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java +++ /dev/null @@ -1,138 +0,0 @@ -package com.genymobile.scrcpy.audio; - -import com.genymobile.scrcpy.AndroidVersions; -import com.genymobile.scrcpy.FakeContext; -import com.genymobile.scrcpy.util.Ln; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.content.Context; -import android.media.AudioAttributes; -import android.media.AudioFormat; -import android.media.AudioManager; -import android.media.AudioRecord; -import android.media.MediaCodec; -import android.os.Build; - -import java.lang.reflect.Method; -import java.nio.ByteBuffer; - -public final class AudioPlaybackCapture implements AudioCapture { - - private final boolean keepPlayingOnDevice; - - private AudioRecord recorder; - private AudioRecordReader reader; - - public AudioPlaybackCapture(boolean keepPlayingOnDevice) { - this.keepPlayingOnDevice = keepPlayingOnDevice; - } - - @SuppressLint("PrivateApi") - private AudioRecord createAudioRecord() throws AudioCaptureException { - // See - try { - Class audioMixingRuleClass = Class.forName("android.media.audiopolicy.AudioMixingRule"); - Class audioMixingRuleBuilderClass = Class.forName("android.media.audiopolicy.AudioMixingRule$Builder"); - - // AudioMixingRule.Builder audioMixingRuleBuilder = new AudioMixingRule.Builder(); - Object audioMixingRuleBuilder = audioMixingRuleBuilderClass.getConstructor().newInstance(); - - // audioMixingRuleBuilder.setTargetMixRole(AudioMixingRule.MIX_ROLE_PLAYERS); - int mixRolePlayersConstant = audioMixingRuleClass.getField("MIX_ROLE_PLAYERS").getInt(null); - Method setTargetMixRoleMethod = audioMixingRuleBuilderClass.getMethod("setTargetMixRole", int.class); - setTargetMixRoleMethod.invoke(audioMixingRuleBuilder, mixRolePlayersConstant); - - AudioAttributes attributes = new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build(); - - // audioMixingRuleBuilder.addMixRule(AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE, attributes); - int ruleMatchAttributeUsageConstant = audioMixingRuleClass.getField("RULE_MATCH_ATTRIBUTE_USAGE").getInt(null); - Method addMixRuleMethod = audioMixingRuleBuilderClass.getMethod("addMixRule", int.class, Object.class); - addMixRuleMethod.invoke(audioMixingRuleBuilder, ruleMatchAttributeUsageConstant, attributes); - - // AudioMixingRule audioMixingRule = builder.build(); - Object audioMixingRule = audioMixingRuleBuilderClass.getMethod("build").invoke(audioMixingRuleBuilder); - - // audioMixingRuleBuilder.voiceCommunicationCaptureAllowed(true); - Method voiceCommunicationCaptureAllowedMethod = audioMixingRuleBuilderClass.getMethod("voiceCommunicationCaptureAllowed", boolean.class); - voiceCommunicationCaptureAllowedMethod.invoke(audioMixingRuleBuilder, true); - - Class audioMixClass = Class.forName("android.media.audiopolicy.AudioMix"); - Class audioMixBuilderClass = Class.forName("android.media.audiopolicy.AudioMix$Builder"); - - // AudioMix.Builder audioMixBuilder = new AudioMix.Builder(audioMixingRule); - Object audioMixBuilder = audioMixBuilderClass.getConstructor(audioMixingRuleClass).newInstance(audioMixingRule); - - // audioMixBuilder.setFormat(createAudioFormat()); - Method setFormat = audioMixBuilder.getClass().getMethod("setFormat", AudioFormat.class); - setFormat.invoke(audioMixBuilder, AudioConfig.createAudioFormat()); - - String routeFlagName = keepPlayingOnDevice ? "ROUTE_FLAG_LOOP_BACK_RENDER" : "ROUTE_FLAG_LOOP_BACK"; - int routeFlags = audioMixClass.getField(routeFlagName).getInt(null); - - // audioMixBuilder.setRouteFlags(routeFlag); - Method setRouteFlags = audioMixBuilder.getClass().getMethod("setRouteFlags", int.class); - setRouteFlags.invoke(audioMixBuilder, routeFlags); - - // AudioMix audioMix = audioMixBuilder.build(); - Object audioMix = audioMixBuilderClass.getMethod("build").invoke(audioMixBuilder); - - Class audioPolicyClass = Class.forName("android.media.audiopolicy.AudioPolicy"); - Class audioPolicyBuilderClass = Class.forName("android.media.audiopolicy.AudioPolicy$Builder"); - - // AudioPolicy.Builder audioPolicyBuilder = new AudioPolicy.Builder(); - Object audioPolicyBuilder = audioPolicyBuilderClass.getConstructor(Context.class).newInstance(FakeContext.get()); - - // audioPolicyBuilder.addMix(audioMix); - Method addMixMethod = audioPolicyBuilderClass.getMethod("addMix", audioMixClass); - addMixMethod.invoke(audioPolicyBuilder, audioMix); - - // AudioPolicy audioPolicy = audioPolicyBuilder.build(); - Object audioPolicy = audioPolicyBuilderClass.getMethod("build").invoke(audioPolicyBuilder); - - // AudioManager.registerAudioPolicyStatic(audioPolicy); - Method registerAudioPolicyStaticMethod = AudioManager.class.getDeclaredMethod("registerAudioPolicyStatic", audioPolicyClass); - registerAudioPolicyStaticMethod.setAccessible(true); - int result = (int) registerAudioPolicyStaticMethod.invoke(null, audioPolicy); - if (result != 0) { - throw new RuntimeException("registerAudioPolicy() returned " + result); - } - - // audioPolicy.createAudioRecordSink(audioPolicy); - Method createAudioRecordSinkClass = audioPolicyClass.getMethod("createAudioRecordSink", audioMixClass); - return (AudioRecord) createAudioRecordSinkClass.invoke(audioPolicy, audioMix); - } catch (Exception e) { - Ln.e("Could not capture audio playback", e); - throw new AudioCaptureException(); - } - } - - @Override - public void checkCompatibility() throws AudioCaptureException { - if (Build.VERSION.SDK_INT < AndroidVersions.API_33_ANDROID_13) { - Ln.w("Audio disabled: audio playback capture source not supported before Android 13"); - throw new AudioCaptureException(); - } - } - - @Override - public void start() throws AudioCaptureException { - recorder = createAudioRecord(); - recorder.startRecording(); - reader = new AudioRecordReader(recorder); - } - - @Override - public void stop() { - if (recorder != null) { - // Will call .stop() if necessary, without throwing an IllegalStateException - recorder.release(); - } - } - - @Override - @TargetApi(AndroidVersions.API_24_ANDROID_7_0) - public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) { - return reader.read(outDirectBuffer, outBufferInfo); - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java deleted file mode 100644 index 9645bbbd..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java +++ /dev/null @@ -1,99 +0,0 @@ -package com.genymobile.scrcpy.audio; - -import com.genymobile.scrcpy.AndroidVersions; -import com.genymobile.scrcpy.AsyncProcessor; -import com.genymobile.scrcpy.device.Streamer; -import com.genymobile.scrcpy.util.IO; -import com.genymobile.scrcpy.util.Ln; - -import android.media.MediaCodec; -import android.os.Build; - -import java.io.IOException; -import java.nio.ByteBuffer; - -public final class AudioRawRecorder implements AsyncProcessor { - - private final AudioCapture capture; - private final Streamer streamer; - - private Thread thread; - - public AudioRawRecorder(AudioCapture capture, Streamer streamer) { - this.capture = capture; - this.streamer = streamer; - } - - private void record() throws IOException, AudioCaptureException { - if (Build.VERSION.SDK_INT < AndroidVersions.API_30_ANDROID_11) { - Ln.w("Audio disabled: it is not supported before Android 11"); - streamer.writeDisableStream(false); - return; - } - - final ByteBuffer buffer = ByteBuffer.allocateDirect(AudioConfig.MAX_READ_SIZE); - final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); - - try { - try { - capture.start(); - } catch (Throwable t) { - // Notify the client that the audio could not be captured - streamer.writeDisableStream(false); - throw t; - } - - streamer.writeAudioHeader(); - while (!Thread.currentThread().isInterrupted()) { - buffer.position(0); - int r = capture.read(buffer, bufferInfo); - if (r < 0) { - throw new IOException("Could not read audio: " + r); - } - buffer.limit(r); - - streamer.writePacket(buffer, bufferInfo); - } - } catch (IOException e) { - // Broken pipe is expected on close, because the socket is closed by the client - if (!IO.isBrokenPipe(e)) { - Ln.e("Audio capture error", e); - } - } finally { - capture.stop(); - } - } - - @Override - public void start(TerminationListener listener) { - thread = new Thread(() -> { - boolean fatalError = false; - try { - record(); - } catch (AudioCaptureException e) { - // Do not print stack trace, a user-friendly error-message has already been logged - } catch (Throwable t) { - Ln.e("Audio recording error", t); - fatalError = true; - } finally { - Ln.d("Audio recorder stopped"); - listener.onTerminated(fatalError); - } - }, "audio-raw"); - thread.start(); - } - - @Override - public void stop() { - if (thread != null) { - thread.interrupt(); - } - } - - @Override - public void join() throws InterruptedException { - if (thread != null) { - thread.join(); - } - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioRecordReader.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRecordReader.java deleted file mode 100644 index 32b42257..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioRecordReader.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.genymobile.scrcpy.audio; - -import com.genymobile.scrcpy.AndroidVersions; -import com.genymobile.scrcpy.util.Ln; - -import android.annotation.TargetApi; -import android.media.AudioRecord; -import android.media.AudioTimestamp; -import android.media.MediaCodec; - -import java.nio.ByteBuffer; - -public class AudioRecordReader { - - private static final long ONE_SAMPLE_US = - (1000000 + AudioConfig.SAMPLE_RATE - 1) / AudioConfig.SAMPLE_RATE; // 1 sample in microseconds (used for fixing PTS) - - private final AudioRecord recorder; - - private final AudioTimestamp timestamp = new AudioTimestamp(); - private long previousRecorderTimestamp = -1; - private long previousPts = 0; - private long nextPts = 0; - - public AudioRecordReader(AudioRecord recorder) { - this.recorder = recorder; - } - - @TargetApi(AndroidVersions.API_24_ANDROID_7_0) - public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) { - int r = recorder.read(outDirectBuffer, AudioConfig.MAX_READ_SIZE); - if (r <= 0) { - return r; - } - - long pts; - - int ret = recorder.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC); - if (ret == AudioRecord.SUCCESS && timestamp.nanoTime != previousRecorderTimestamp) { - pts = timestamp.nanoTime / 1000; - previousRecorderTimestamp = timestamp.nanoTime; - } else { - if (nextPts == 0) { - Ln.w("Could not get initial audio timestamp"); - nextPts = System.nanoTime() / 1000; - } - // compute from previous timestamp and packet size - pts = nextPts; - } - - long durationUs = r * 1000000L / (AudioConfig.CHANNELS * AudioConfig.BYTES_PER_SAMPLE * AudioConfig.SAMPLE_RATE); - nextPts = pts + durationUs; - - if (previousPts != 0 && pts < previousPts + ONE_SAMPLE_US) { - // Audio PTS may come from two sources: - // - recorder.getTimestamp() if the call works; - // - an estimation from the previous PTS and the packet size as a fallback. - // - // Therefore, the property that PTS are monotonically increasing is no guaranteed in corner cases, so enforce it. - pts = previousPts + ONE_SAMPLE_US; - } - previousPts = pts; - - outBufferInfo.set(0, r, pts, 0); - return r; - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java deleted file mode 100644 index d16b5e38..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.genymobile.scrcpy.audio; - -import android.annotation.SuppressLint; -import android.media.MediaRecorder; - -@SuppressLint("InlinedApi") -public enum AudioSource { - OUTPUT("output", MediaRecorder.AudioSource.REMOTE_SUBMIX), - MIC("mic", MediaRecorder.AudioSource.MIC), - PLAYBACK("playback", -1), - MIC_UNPROCESSED("mic-unprocessed", MediaRecorder.AudioSource.UNPROCESSED), - MIC_CAMCORDER("mic-camcorder", MediaRecorder.AudioSource.CAMCORDER), - MIC_VOICE_RECOGNITION("mic-voice-recognition", MediaRecorder.AudioSource.VOICE_RECOGNITION), - MIC_VOICE_COMMUNICATION("mic-voice-communication", MediaRecorder.AudioSource.VOICE_COMMUNICATION), - VOICE_CALL("voice-call", MediaRecorder.AudioSource.VOICE_CALL), - VOICE_CALL_UPLINK("voice-call-uplink", MediaRecorder.AudioSource.VOICE_UPLINK), - VOICE_CALL_DOWNLINK("voice-call-downlink", MediaRecorder.AudioSource.VOICE_DOWNLINK), - VOICE_PERFORMANCE("voice-performance", MediaRecorder.AudioSource.VOICE_PERFORMANCE); - - private final String name; - private final int directAudioSource; - - AudioSource(String name, int directAudioSource) { - this.name = name; - this.directAudioSource = directAudioSource; - } - - public boolean isDirect() { - return this != PLAYBACK; - } - - public int getDirectAudioSource() { - return directAudioSource; - } - - public static AudioSource findByName(String name) { - for (AudioSource audioSource : AudioSource.values()) { - if (name.equals(audioSource.name)) { - return audioSource; - } - } - - return null; - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlChannel.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlChannel.java deleted file mode 100644 index 2f12cdb3..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlChannel.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.genymobile.scrcpy.control; - -import android.net.LocalSocket; - -import java.io.IOException; - -public final class ControlChannel { - - private final ControlMessageReader reader; - private final DeviceMessageWriter writer; - - public ControlChannel(LocalSocket controlSocket) throws IOException { - reader = new ControlMessageReader(controlSocket.getInputStream()); - writer = new DeviceMessageWriter(controlSocket.getOutputStream()); - } - - public ControlMessage recv() throws IOException { - return reader.read(); - } - - public void send(DeviceMessage msg) throws IOException { - writer.write(msg); - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java deleted file mode 100644 index 0eb96adc..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java +++ /dev/null @@ -1,252 +0,0 @@ -package com.genymobile.scrcpy.control; - -import com.genymobile.scrcpy.device.Position; - -/** - * 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_DISPLAY_POWER = 10; - public static final int TYPE_ROTATE_DEVICE = 11; - public static final int TYPE_UHID_CREATE = 12; - public static final int TYPE_UHID_INPUT = 13; - public static final int TYPE_UHID_DESTROY = 14; - public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 15; - public static final int TYPE_START_APP = 16; - public static final int TYPE_RESET_VIDEO = 17; - - 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_* - private int keycode; // KeyEvent.KEYCODE_* - private int actionButton; // MotionEvent.BUTTON_* - private int buttons; // MotionEvent.BUTTON_* - private long pointerId; - private float pressure; - private Position position; - private float hScroll; - private float vScroll; - private int copyKey; - private boolean paste; - private int repeat; - private long sequence; - private int id; - private byte[] data; - private boolean on; - private int vendorId; - private int productId; - - 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 actionButton, - int buttons) { - ControlMessage msg = new ControlMessage(); - msg.type = TYPE_INJECT_TOUCH_EVENT; - msg.action = action; - msg.pointerId = pointerId; - msg.pressure = pressure; - msg.position = position; - msg.actionButton = actionButton; - msg.buttons = buttons; - return msg; - } - - public static ControlMessage createInjectScrollEvent(Position position, 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; - } - - public static ControlMessage createSetDisplayPower(boolean on) { - ControlMessage msg = new ControlMessage(); - msg.type = TYPE_SET_DISPLAY_POWER; - msg.on = on; - return msg; - } - - public static ControlMessage createEmpty(int type) { - ControlMessage msg = new ControlMessage(); - msg.type = type; - return msg; - } - - public static ControlMessage createUhidCreate(int id, int vendorId, int productId, String name, byte[] reportDesc) { - ControlMessage msg = new ControlMessage(); - msg.type = TYPE_UHID_CREATE; - msg.id = id; - msg.vendorId = vendorId; - msg.productId = productId; - msg.text = name; - msg.data = reportDesc; - return msg; - } - - public static ControlMessage createUhidInput(int id, byte[] data) { - ControlMessage msg = new ControlMessage(); - msg.type = TYPE_UHID_INPUT; - msg.id = id; - msg.data = data; - return msg; - } - - public static ControlMessage createUhidDestroy(int id) { - ControlMessage msg = new ControlMessage(); - msg.type = TYPE_UHID_DESTROY; - msg.id = id; - return msg; - } - - public static ControlMessage createStartApp(String name) { - ControlMessage msg = new ControlMessage(); - msg.type = TYPE_START_APP; - msg.text = name; - 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 getActionButton() { - return actionButton; - } - - 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; - } - - public int getId() { - return id; - } - - public byte[] getData() { - return data; - } - - public boolean getOn() { - return on; - } - - public int getVendorId() { - return vendorId; - } - - public int getProductId() { - return productId; - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java deleted file mode 100644 index 830a7ec7..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java +++ /dev/null @@ -1,176 +0,0 @@ -package com.genymobile.scrcpy.control; - -import com.genymobile.scrcpy.device.Position; -import com.genymobile.scrcpy.util.Binary; - -import java.io.BufferedInputStream; -import java.io.DataInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; - -public class ControlMessageReader { - - 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 DataInputStream dis; - - public ControlMessageReader(InputStream rawInputStream) { - dis = new DataInputStream(new BufferedInputStream(rawInputStream)); - } - - public ControlMessage read() throws IOException { - int type = dis.readUnsignedByte(); - switch (type) { - case ControlMessage.TYPE_INJECT_KEYCODE: - return parseInjectKeycode(); - case ControlMessage.TYPE_INJECT_TEXT: - return parseInjectText(); - case ControlMessage.TYPE_INJECT_TOUCH_EVENT: - return parseInjectTouchEvent(); - case ControlMessage.TYPE_INJECT_SCROLL_EVENT: - return parseInjectScrollEvent(); - case ControlMessage.TYPE_BACK_OR_SCREEN_ON: - return parseBackOrScreenOnEvent(); - case ControlMessage.TYPE_GET_CLIPBOARD: - return parseGetClipboard(); - case ControlMessage.TYPE_SET_CLIPBOARD: - return parseSetClipboard(); - case ControlMessage.TYPE_SET_DISPLAY_POWER: - return parseSetDisplayPower(); - case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: - case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL: - case ControlMessage.TYPE_COLLAPSE_PANELS: - case ControlMessage.TYPE_ROTATE_DEVICE: - case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS: - case ControlMessage.TYPE_RESET_VIDEO: - return ControlMessage.createEmpty(type); - case ControlMessage.TYPE_UHID_CREATE: - return parseUhidCreate(); - case ControlMessage.TYPE_UHID_INPUT: - return parseUhidInput(); - case ControlMessage.TYPE_UHID_DESTROY: - return parseUhidDestroy(); - case ControlMessage.TYPE_START_APP: - return parseStartApp(); - default: - throw new ControlProtocolException("Unknown event type: " + type); - } - } - - private ControlMessage parseInjectKeycode() throws IOException { - int action = dis.readUnsignedByte(); - int keycode = dis.readInt(); - int repeat = dis.readInt(); - int metaState = dis.readInt(); - return ControlMessage.createInjectKeycode(action, keycode, repeat, metaState); - } - - private int parseBufferLength(int sizeBytes) throws IOException { - assert sizeBytes > 0 && sizeBytes <= 4; - int value = 0; - for (int i = 0; i < sizeBytes; ++i) { - value = (value << 8) | dis.readUnsignedByte(); - } - return value; - } - - private String parseString(int sizeBytes) throws IOException { - assert sizeBytes > 0 && sizeBytes <= 4; - byte[] data = parseByteArray(sizeBytes); - return new String(data, StandardCharsets.UTF_8); - } - - private String parseString() throws IOException { - return parseString(4); - } - - private byte[] parseByteArray(int sizeBytes) throws IOException { - int len = parseBufferLength(sizeBytes); - byte[] data = new byte[len]; - dis.readFully(data); - return data; - } - - private ControlMessage parseInjectText() throws IOException { - String text = parseString(); - return ControlMessage.createInjectText(text); - } - - private ControlMessage parseInjectTouchEvent() throws IOException { - int action = dis.readUnsignedByte(); - long pointerId = dis.readLong(); - Position position = parsePosition(); - float pressure = Binary.u16FixedPointToFloat(dis.readShort()); - int actionButton = dis.readInt(); - int buttons = dis.readInt(); - return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, actionButton, buttons); - } - - private ControlMessage parseInjectScrollEvent() throws IOException { - Position position = parsePosition(); - // Binary.i16FixedPointToFloat() decodes values assuming the full range is [-1, 1], but the actual range is [-16, 16]. - float hScroll = Binary.i16FixedPointToFloat(dis.readShort()) * 16; - float vScroll = Binary.i16FixedPointToFloat(dis.readShort()) * 16; - int buttons = dis.readInt(); - return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll, buttons); - } - - private ControlMessage parseBackOrScreenOnEvent() throws IOException { - int action = dis.readUnsignedByte(); - return ControlMessage.createBackOrScreenOn(action); - } - - private ControlMessage parseGetClipboard() throws IOException { - int copyKey = dis.readUnsignedByte(); - return ControlMessage.createGetClipboard(copyKey); - } - - private ControlMessage parseSetClipboard() throws IOException { - long sequence = dis.readLong(); - boolean paste = dis.readByte() != 0; - String text = parseString(); - return ControlMessage.createSetClipboard(sequence, text, paste); - } - - private ControlMessage parseSetDisplayPower() throws IOException { - boolean on = dis.readBoolean(); - return ControlMessage.createSetDisplayPower(on); - } - - private ControlMessage parseUhidCreate() throws IOException { - int id = dis.readUnsignedShort(); - int vendorId = dis.readUnsignedShort(); - int productId = dis.readUnsignedShort(); - String name = parseString(1); - byte[] data = parseByteArray(2); - return ControlMessage.createUhidCreate(id, vendorId, productId, name, data); - } - - private ControlMessage parseUhidInput() throws IOException { - int id = dis.readUnsignedShort(); - byte[] data = parseByteArray(2); - return ControlMessage.createUhidInput(id, data); - } - - private ControlMessage parseUhidDestroy() throws IOException { - int id = dis.readUnsignedShort(); - return ControlMessage.createUhidDestroy(id); - } - - private ControlMessage parseStartApp() throws IOException { - String name = parseString(1); - return ControlMessage.createStartApp(name); - } - - private Position parsePosition() throws IOException { - int x = dis.readInt(); - int y = dis.readInt(); - int screenWidth = dis.readUnsignedShort(); - int screenHeight = dis.readUnsignedShort(); - return new Position(x, y, screenWidth, screenHeight); - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlProtocolException.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlProtocolException.java deleted file mode 100644 index cabf63ee..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlProtocolException.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.genymobile.scrcpy.control; - -import java.io.IOException; - -public class ControlProtocolException extends IOException { - public ControlProtocolException(String message) { - super(message); - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java deleted file mode 100644 index b4a8e3ca..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ /dev/null @@ -1,757 +0,0 @@ -package com.genymobile.scrcpy.control; - -import com.genymobile.scrcpy.AndroidVersions; -import com.genymobile.scrcpy.AsyncProcessor; -import com.genymobile.scrcpy.CleanUp; -import com.genymobile.scrcpy.Options; -import com.genymobile.scrcpy.device.Device; -import com.genymobile.scrcpy.device.DeviceApp; -import com.genymobile.scrcpy.device.DisplayInfo; -import com.genymobile.scrcpy.device.Point; -import com.genymobile.scrcpy.device.Position; -import com.genymobile.scrcpy.device.Size; -import com.genymobile.scrcpy.util.Ln; -import com.genymobile.scrcpy.util.LogUtils; -import com.genymobile.scrcpy.video.SurfaceCapture; -import com.genymobile.scrcpy.video.VirtualDisplayListener; -import com.genymobile.scrcpy.wrappers.ClipboardManager; -import com.genymobile.scrcpy.wrappers.InputManager; -import com.genymobile.scrcpy.wrappers.ServiceManager; - -import android.content.Intent; -import android.os.Build; -import android.os.SystemClock; -import android.util.Pair; -import android.view.InputDevice; -import android.view.KeyCharacterMap; -import android.view.KeyEvent; -import android.view.MotionEvent; - -import java.io.IOException; -import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; - -public class Controller implements AsyncProcessor, VirtualDisplayListener { - - /* - * For event injection, there are two display ids: - * - the displayId passed to the constructor (which comes from --display-id passed by the client, 0 for the main display); - * - the virtualDisplayId used for mirroring, notified by the capture instance via the VirtualDisplayListener interface. - * - * (In case the ScreenCapture uses the "SurfaceControl API", then both ids are equals, but this is an implementation detail.) - * - * In order to make events work correctly in all cases: - * - virtualDisplayId must be used for events relative to the display (mouse and touch events with coordinates); - * - displayId must be used for other events (like key events). - * - * If a new separate virtual display is created (using --new-display), then displayId == Device.DISPLAY_ID_NONE. In that case, all events are - * sent to the virtual display id. - */ - - private static final class DisplayData { - private final int virtualDisplayId; - private final PositionMapper positionMapper; - - private DisplayData(int virtualDisplayId, PositionMapper positionMapper) { - this.virtualDisplayId = virtualDisplayId; - this.positionMapper = positionMapper; - } - } - - 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 ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor(); - private ExecutorService startAppExecutor; - - private Thread thread; - - private UhidManager uhidManager; - - private final int displayId; - private final boolean supportsInputEvents; - private final ControlChannel controlChannel; - private final CleanUp cleanUp; - private final DeviceMessageSender sender; - private final boolean clipboardAutosync; - private final boolean powerOn; - - private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); - - private final AtomicBoolean isSettingClipboard = new AtomicBoolean(); - - private final AtomicReference displayData = new AtomicReference<>(); - private final Object displayDataAvailable = new Object(); // condition variable - - 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 keepDisplayPowerOff; - - // Used for resetting video encoding on RESET_VIDEO message - private SurfaceCapture surfaceCapture; - - public Controller(ControlChannel controlChannel, CleanUp cleanUp, Options options) { - this.displayId = options.getDisplayId(); - this.controlChannel = controlChannel; - this.cleanUp = cleanUp; - this.clipboardAutosync = options.getClipboardAutosync(); - this.powerOn = options.getPowerOn(); - initPointers(); - sender = new DeviceMessageSender(controlChannel); - - supportsInputEvents = Device.supportsInputEvents(displayId); - if (!supportsInputEvents) { - Ln.w("Input events are not supported for secondary displays before Android 10"); - } - - // Make sure the clipboard manager is always created from the main thread (even if clipboardAutosync is disabled) - ClipboardManager clipboardManager = ServiceManager.getClipboardManager(); - if (clipboardAutosync) { - // If control and autosync are enabled, synchronize Android clipboard to the computer automatically - if (clipboardManager != null) { - clipboardManager.addPrimaryClipChangedListener(() -> { - if (isSettingClipboard.get()) { - // This is a notification for the change we are currently applying, ignore it - return; - } - String text = Device.getClipboardText(); - if (text != null) { - DeviceMessage msg = DeviceMessage.createClipboard(text); - sender.send(msg); - } - }); - } else { - Ln.w("No clipboard manager, copy-paste between device and computer will not work"); - } - } - } - - @Override - public void onNewVirtualDisplay(int virtualDisplayId, PositionMapper positionMapper) { - DisplayData data = new DisplayData(virtualDisplayId, positionMapper); - DisplayData old = this.displayData.getAndSet(data); - if (old == null) { - // The very first time the Controller is notified of a new virtual display - synchronized (displayDataAvailable) { - displayDataAvailable.notify(); - } - } - } - - public void setSurfaceCapture(SurfaceCapture surfaceCapture) { - this.surfaceCapture = surfaceCapture; - } - - private UhidManager getUhidManager() { - if (uhidManager == null) { - int uhidDisplayId = displayId; - if (Build.VERSION.SDK_INT >= AndroidVersions.API_35_ANDROID_15) { - if (displayId == Device.DISPLAY_ID_NONE) { - // Mirroring a new virtual display id (using --new-display-id feature) on Android >= 15, where the UHID mouse pointer can be - // associated to the virtual display - try { - // Wait for at most 1 second until a virtual display id is known - DisplayData data = waitDisplayData(1000); - if (data != null) { - uhidDisplayId = data.virtualDisplayId; - } - } catch (InterruptedException e) { - // do nothing - } - } - } - - String displayUniqueId = null; - if (uhidDisplayId > 0) { - // Ignore Device.DISPLAY_ID_NONE and 0 (main display) - DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(uhidDisplayId); - if (displayInfo != null) { - displayUniqueId = displayInfo.getUniqueId(); - } - } - uhidManager = new UhidManager(sender, displayUniqueId); - } - - return uhidManager; - } - - 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; - } - } - - private void control() throws IOException { - // on start, power on the device - if (powerOn && displayId == 0 && !Device.isScreenOn(displayId)) { - Device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC); - - // dirty hack - // After POWER is injected, the device is powered on asynchronously. - // To turn the device screen off while mirroring, the client will send a message that - // would be handled before the device is actually powered on, so its effect would - // be "canceled" once the device is turned back on. - // Adding this delay prevents to handle the message before the device is actually - // powered on. - SystemClock.sleep(500); - } - - boolean alive = true; - while (!Thread.currentThread().isInterrupted() && alive) { - alive = handleEvent(); - } - } - - @Override - public void start(TerminationListener listener) { - thread = new Thread(() -> { - try { - control(); - } catch (IOException e) { - Ln.e("Controller error", e); - } finally { - Ln.d("Controller stopped"); - if (uhidManager != null) { - uhidManager.closeAll(); - } - listener.onTerminated(true); - } - }, "control-recv"); - thread.start(); - sender.start(); - } - - @Override - public void stop() { - if (thread != null) { - thread.interrupt(); - } - sender.stop(); - } - - @Override - public void join() throws InterruptedException { - if (thread != null) { - thread.join(); - } - sender.join(); - } - - private boolean handleEvent() throws IOException { - ControlMessage msg; - try { - msg = controlChannel.recv(); - } catch (IOException e) { - // this is expected on close - return false; - } - - switch (msg.getType()) { - case ControlMessage.TYPE_INJECT_KEYCODE: - if (supportsInputEvents) { - injectKeycode(msg.getAction(), msg.getKeycode(), msg.getRepeat(), msg.getMetaState()); - } - break; - case ControlMessage.TYPE_INJECT_TEXT: - if (supportsInputEvents) { - injectText(msg.getText()); - } - break; - case ControlMessage.TYPE_INJECT_TOUCH_EVENT: - if (supportsInputEvents) { - injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getActionButton(), msg.getButtons()); - } - break; - case ControlMessage.TYPE_INJECT_SCROLL_EVENT: - if (supportsInputEvents) { - injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll(), msg.getButtons()); - } - break; - case ControlMessage.TYPE_BACK_OR_SCREEN_ON: - if (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_DISPLAY_POWER: - if (supportsInputEvents) { - setDisplayPower(msg.getOn()); - } - break; - case ControlMessage.TYPE_ROTATE_DEVICE: - Device.rotateDevice(getActionDisplayId()); - break; - case ControlMessage.TYPE_UHID_CREATE: - getUhidManager().open(msg.getId(), msg.getVendorId(), msg.getProductId(), msg.getText(), msg.getData()); - break; - case ControlMessage.TYPE_UHID_INPUT: - getUhidManager().writeInput(msg.getId(), msg.getData()); - break; - case ControlMessage.TYPE_UHID_DESTROY: - getUhidManager().close(msg.getId()); - break; - case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS: - openHardKeyboardSettings(); - break; - case ControlMessage.TYPE_START_APP: - startAppAsync(msg.getText()); - break; - case ControlMessage.TYPE_RESET_VIDEO: - resetVideo(); - break; - default: - // do nothing - } - - return true; - } - - private boolean injectKeycode(int action, int keycode, int repeat, int metaState) { - if (keepDisplayPowerOff && action == KeyEvent.ACTION_UP && (keycode == KeyEvent.KEYCODE_POWER || keycode == KeyEvent.KEYCODE_WAKEUP)) { - assert displayId != Device.DISPLAY_ID_NONE; - scheduleDisplayPowerOff(displayId); - } - return 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; - } - - int actionDisplayId = getActionDisplayId(); - for (KeyEvent event : events) { - if (!Device.injectEvent(event, actionDisplayId, 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 Pair getEventPointAndDisplayId(Position position) { - // it hides the field on purpose, to read it with atomic access - @SuppressWarnings("checkstyle:HiddenField") - DisplayData displayData = this.displayData.get(); - // In scrcpy, displayData should never be null (a touch event can only be generated from the client when a video frame is present). - // However, it is possible to send events without video playback when using scrcpy-server alone (except for virtual displays). - assert displayData != null || displayId != Device.DISPLAY_ID_NONE : "Cannot receive a positional event without a display"; - - Point point; - int targetDisplayId; - if (displayData != null) { - point = displayData.positionMapper.map(position); - if (point == null) { - if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Size eventSize = position.getScreenSize(); - Size currentSize = displayData.positionMapper.getVideoSize(); - Ln.v("Ignore positional event generated for size " + eventSize + " (current size is " + currentSize + ")"); - } - return null; - } - targetDisplayId = displayData.virtualDisplayId; - } else { - // No display, use the raw coordinates - point = position.getPoint(); - targetDisplayId = displayId; - } - - return Pair.create(point, targetDisplayId); - } - - private boolean injectTouch(int action, long pointerId, Position position, float pressure, int actionButton, int buttons) { - long now = SystemClock.uptimeMillis(); - - Pair pair = getEventPointAndDisplayId(position); - if (pair == null) { - return false; - } - - Point point = pair.first; - int targetDisplayId = pair.second; - - 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); - - int source; - boolean activeSecondaryButtons = ((actionButton | buttons) & ~MotionEvent.BUTTON_PRIMARY) != 0; - if (pointerId == POINTER_ID_MOUSE && (action == MotionEvent.ACTION_HOVER_MOVE || activeSecondaryButtons)) { - // real mouse event, or event incompatible with a finger - pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_MOUSE; - source = InputDevice.SOURCE_MOUSE; - pointer.setUp(buttons == 0); - } else { - // POINTER_ID_GENERIC_FINGER, POINTER_ID_VIRTUAL_FINGER or real touch from device - pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_FINGER; - source = InputDevice.SOURCE_TOUCHSCREEN; - // Buttons must not be set for touch events - buttons = 0; - pointer.setUp(action == MotionEvent.ACTION_UP); - } - - int pointerCount = pointersState.update(pointerProperties, pointerCoords); - if (pointerCount == 1) { - if (action == MotionEvent.ACTION_DOWN) { - lastTouchDown = now; - } - } 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); - } - } - - /* If the input device is a mouse (on API >= 23): - * - the first button pressed must first generate ACTION_DOWN; - * - all button pressed (including the first one) must generate ACTION_BUTTON_PRESS; - * - all button released (including the last one) must generate ACTION_BUTTON_RELEASE; - * - the last button released must in addition generate ACTION_UP. - * - * Otherwise, Chrome does not work properly: - */ - if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0 && source == InputDevice.SOURCE_MOUSE) { - if (action == MotionEvent.ACTION_DOWN) { - if (actionButton == buttons) { - // First button pressed: ACTION_DOWN - MotionEvent downEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_DOWN, pointerCount, pointerProperties, - pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); - if (!Device.injectEvent(downEvent, targetDisplayId, Device.INJECT_MODE_ASYNC)) { - return false; - } - } - - // Any button pressed: ACTION_BUTTON_PRESS - MotionEvent pressEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_BUTTON_PRESS, pointerCount, pointerProperties, - pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); - if (!InputManager.setActionButton(pressEvent, actionButton)) { - return false; - } - if (!Device.injectEvent(pressEvent, targetDisplayId, Device.INJECT_MODE_ASYNC)) { - return false; - } - - return true; - } - - if (action == MotionEvent.ACTION_UP) { - // Any button released: ACTION_BUTTON_RELEASE - MotionEvent releaseEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_BUTTON_RELEASE, pointerCount, pointerProperties, - pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); - if (!InputManager.setActionButton(releaseEvent, actionButton)) { - return false; - } - if (!Device.injectEvent(releaseEvent, targetDisplayId, Device.INJECT_MODE_ASYNC)) { - return false; - } - - if (buttons == 0) { - // Last button released: ACTION_UP - MotionEvent upEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_UP, pointerCount, pointerProperties, - pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); - if (!Device.injectEvent(upEvent, targetDisplayId, Device.INJECT_MODE_ASYNC)) { - return false; - } - } - - return true; - } - } - - MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, - DEFAULT_DEVICE_ID, 0, source, 0); - return Device.injectEvent(event, targetDisplayId, Device.INJECT_MODE_ASYNC); - } - - private boolean injectScroll(Position position, float hScroll, float vScroll, int buttons) { - long now = SystemClock.uptimeMillis(); - - Pair pair = getEventPointAndDisplayId(position); - if (pair == null) { - return false; - } - - Point point = pair.first; - int targetDisplayId = pair.second; - - 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, targetDisplayId, Device.INJECT_MODE_ASYNC); - } - - /** - * Schedule a call to set display power to off after a small delay. - */ - private static void scheduleDisplayPowerOff(int displayId) { - EXECUTOR.schedule(() -> { - Ln.i("Forcing display off"); - Device.setDisplayPower(displayId, false); - }, 200, TimeUnit.MILLISECONDS); - } - - private boolean pressBackOrTurnScreenOn(int action) { - if (displayId == Device.DISPLAY_ID_NONE || Device.isScreenOn(displayId)) { - return 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 (keepDisplayPowerOff) { - assert displayId != Device.DISPLAY_ID_NONE; - scheduleDisplayPowerOff(displayId); - } - return 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 >= AndroidVersions.API_24_ANDROID_7_0 && 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 - pressReleaseKeycode(key, Device.INJECT_MODE_WAIT_FOR_FINISH); - } - - // If clipboard autosync is enabled, then the device clipboard is synchronized to the computer clipboard whenever it changes, in - // particular when COPY or CUT are injected, so it should not be synchronized twice. On Android < 7, do not synchronize at all rather than - // copying an old clipboard content. - if (!clipboardAutosync) { - String clipboardText = Device.getClipboardText(); - if (clipboardText != null) { - DeviceMessage msg = DeviceMessage.createClipboard(clipboardText); - sender.send(msg); - } - } - } - - private boolean setClipboard(String text, boolean paste, long sequence) { - isSettingClipboard.set(true); - boolean ok = Device.setClipboardText(text); - isSettingClipboard.set(false); - if (ok) { - Ln.i("Device clipboard set"); - } - - // On Android >= 7, also press the PASTE key if requested - if (paste && Build.VERSION.SDK_INT >= AndroidVersions.API_24_ANDROID_7_0 && supportsInputEvents) { - pressReleaseKeycode(KeyEvent.KEYCODE_PASTE, Device.INJECT_MODE_ASYNC); - } - - if (sequence != ControlMessage.SEQUENCE_INVALID) { - // Acknowledgement requested - DeviceMessage msg = DeviceMessage.createAckClipboard(sequence); - sender.send(msg); - } - - return ok; - } - - private void openHardKeyboardSettings() { - Intent intent = new Intent("android.settings.HARD_KEYBOARD_SETTINGS"); - ServiceManager.getActivityManager().startActivity(intent); - } - - private boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int injectMode) { - return Device.injectKeyEvent(action, keyCode, repeat, metaState, getActionDisplayId(), injectMode); - } - - private boolean pressReleaseKeycode(int keyCode, int injectMode) { - return Device.pressReleaseKeycode(keyCode, getActionDisplayId(), injectMode); - } - - private int getActionDisplayId() { - if (displayId != Device.DISPLAY_ID_NONE) { - // Real screen mirrored, use the source display id - return displayId; - } - - // Virtual display created by --new-display, use the virtualDisplayId - DisplayData data = displayData.get(); - if (data == null) { - // If no virtual display id is initialized yet, use the main display id - return 0; - } - - return data.virtualDisplayId; - } - - private void startAppAsync(String name) { - if (startAppExecutor == null) { - startAppExecutor = Executors.newSingleThreadExecutor(); - } - - // Listing and selecting the app may take a lot of time - startAppExecutor.submit(() -> startApp(name)); - } - - private void startApp(String name) { - boolean forceStopBeforeStart = name.startsWith("+"); - if (forceStopBeforeStart) { - name = name.substring(1); - } - - DeviceApp app; - boolean searchByName = name.startsWith("?"); - if (searchByName) { - name = name.substring(1); - - Ln.i("Processing Android apps... (this may take some time)"); - List apps = Device.findByName(name); - if (apps.isEmpty()) { - Ln.w("No app found for name \"" + name + "\""); - return; - } - - if (apps.size() > 1) { - String title = "No unique app found for name \"" + name + "\":"; - Ln.w(LogUtils.buildAppListMessage(title, apps)); - return; - } - - app = apps.get(0); - } else { - app = Device.findByPackageName(name); - if (app == null) { - Ln.w("No app found for package \"" + name + "\""); - return; - } - } - - int startAppDisplayId = getStartAppDisplayId(); - if (startAppDisplayId == Device.DISPLAY_ID_NONE) { - Ln.e("No known display id to start app \"" + name + "\""); - return; - } - - Ln.i("Starting app \"" + app.getName() + "\" [" + app.getPackageName() + "] on display " + startAppDisplayId + "..."); - Device.startApp(app.getPackageName(), startAppDisplayId, forceStopBeforeStart); - } - - private int getStartAppDisplayId() { - if (displayId != Device.DISPLAY_ID_NONE) { - return displayId; - } - - // Mirroring a new virtual display id (using --new-display-id feature) - try { - // Wait for at most 1 second until a virtual display id is known - DisplayData data = waitDisplayData(1000); - if (data != null) { - return data.virtualDisplayId; - } - } catch (InterruptedException e) { - // do nothing - } - - // No display id available - return Device.DISPLAY_ID_NONE; - } - - private DisplayData waitDisplayData(long timeoutMillis) throws InterruptedException { - long deadline = System.currentTimeMillis() + timeoutMillis; - - synchronized (displayDataAvailable) { - DisplayData data = displayData.get(); - while (data == null) { - long timeout = deadline - System.currentTimeMillis(); - if (timeout < 0) { - return null; - } - if (timeout > 0) { - displayDataAvailable.wait(timeout); - } - data = displayData.get(); - } - - return data; - } - } - - private void setDisplayPower(boolean on) { - // Change the power of the main display when mirroring a virtual display - int targetDisplayId = displayId != Device.DISPLAY_ID_NONE ? displayId : 0; - boolean setDisplayPowerOk = Device.setDisplayPower(targetDisplayId, on); - if (setDisplayPowerOk) { - // Do not keep display power off for virtual displays: MOD+p must wake up the physical device - keepDisplayPowerOff = displayId != Device.DISPLAY_ID_NONE && !on; - Ln.i("Device display turned " + (on ? "on" : "off")); - if (cleanUp != null) { - boolean mustRestoreOnExit = !on; - cleanUp.setRestoreDisplayPower(mustRestoreOnExit); - } - } - } - - private void resetVideo() { - if (surfaceCapture != null) { - Ln.i("Video capture reset"); - surfaceCapture.requestInvalidate(); - } - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessage.java b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessage.java deleted file mode 100644 index 079a7a04..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessage.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.genymobile.scrcpy.control; - -public final class DeviceMessage { - - public static final int TYPE_CLIPBOARD = 0; - public static final int TYPE_ACK_CLIPBOARD = 1; - public static final int TYPE_UHID_OUTPUT = 2; - - private int type; - private String text; - private long sequence; - private int id; - private byte[] data; - - private DeviceMessage() { - } - - 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 static DeviceMessage createUhidOutput(int id, byte[] data) { - DeviceMessage event = new DeviceMessage(); - event.type = TYPE_UHID_OUTPUT; - event.id = id; - event.data = data; - return event; - } - - public int getType() { - return type; - } - - public String getText() { - return text; - } - - public long getSequence() { - return sequence; - } - - public int getId() { - return id; - } - - public byte[] getData() { - return data; - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageSender.java b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageSender.java deleted file mode 100644 index dc5e6be0..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageSender.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.genymobile.scrcpy.control; - -import com.genymobile.scrcpy.util.Ln; - -import java.io.IOException; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.BlockingQueue; - -public final class DeviceMessageSender { - - private final ControlChannel controlChannel; - - private Thread thread; - private final BlockingQueue queue = new ArrayBlockingQueue<>(16); - - public DeviceMessageSender(ControlChannel controlChannel) { - this.controlChannel = controlChannel; - } - - public void send(DeviceMessage msg) { - if (!queue.offer(msg)) { - Ln.w("Device message dropped: " + msg.getType()); - } - } - - private void loop() throws IOException, InterruptedException { - while (!Thread.currentThread().isInterrupted()) { - DeviceMessage msg = queue.take(); - controlChannel.send(msg); - } - } - - public void start() { - thread = new Thread(() -> { - try { - loop(); - } catch (IOException | InterruptedException e) { - // this is expected on close - } finally { - Ln.d("Device message sender stopped"); - } - }, "control-send"); - thread.start(); - } - - public void stop() { - if (thread != null) { - thread.interrupt(); - } - } - - public void join() throws InterruptedException { - if (thread != null) { - thread.join(); - } - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageWriter.java b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageWriter.java deleted file mode 100644 index a18a2e5d..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageWriter.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.genymobile.scrcpy.control; - -import com.genymobile.scrcpy.util.StringUtils; - -import java.io.BufferedOutputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.io.OutputStream; -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 DataOutputStream dos; - - public DeviceMessageWriter(OutputStream rawOutputStream) { - dos = new DataOutputStream(new BufferedOutputStream(rawOutputStream)); - } - - public void write(DeviceMessage msg) throws IOException { - int type = msg.getType(); - dos.writeByte(type); - switch (type) { - case DeviceMessage.TYPE_CLIPBOARD: - String text = msg.getText(); - byte[] raw = text.getBytes(StandardCharsets.UTF_8); - int len = StringUtils.getUtf8TruncationIndex(raw, CLIPBOARD_TEXT_MAX_LENGTH); - dos.writeInt(len); - dos.write(raw, 0, len); - break; - case DeviceMessage.TYPE_ACK_CLIPBOARD: - dos.writeLong(msg.getSequence()); - break; - case DeviceMessage.TYPE_UHID_OUTPUT: - dos.writeShort(msg.getId()); - byte[] data = msg.getData(); - dos.writeShort(data.length); - dos.write(data); - break; - default: - throw new ControlProtocolException("Unknown event type: " + type); - } - dos.flush(); - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Pointer.java b/server/src/main/java/com/genymobile/scrcpy/control/Pointer.java deleted file mode 100644 index 02e33e10..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/control/Pointer.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.genymobile.scrcpy.control; - -import com.genymobile.scrcpy.device.Point; - -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; - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/control/PointersState.java b/server/src/main/java/com/genymobile/scrcpy/control/PointersState.java deleted file mode 100644 index a12da71d..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/control/PointersState.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.genymobile.scrcpy.control; - -import com.genymobile.scrcpy.device.Point; - -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); - } - } - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java b/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java deleted file mode 100644 index 60109b51..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.genymobile.scrcpy.control; - -import com.genymobile.scrcpy.device.Point; -import com.genymobile.scrcpy.device.Position; -import com.genymobile.scrcpy.device.Size; -import com.genymobile.scrcpy.util.AffineMatrix; - -public final class PositionMapper { - - private final Size videoSize; - private final AffineMatrix videoToDeviceMatrix; - - public PositionMapper(Size videoSize, AffineMatrix videoToDeviceMatrix) { - this.videoSize = videoSize; - this.videoToDeviceMatrix = videoToDeviceMatrix; - } - - public static PositionMapper create(Size videoSize, AffineMatrix filterTransform, Size targetSize) { - boolean convertToPixels = !videoSize.equals(targetSize) || filterTransform != null; - AffineMatrix transform = filterTransform; - if (convertToPixels) { - AffineMatrix inputTransform = AffineMatrix.ndcFromPixels(videoSize); - AffineMatrix outputTransform = AffineMatrix.ndcToPixels(targetSize); - transform = outputTransform.multiply(transform).multiply(inputTransform); - } - - return new PositionMapper(videoSize, transform); - } - - public Size getVideoSize() { - return videoSize; - } - - public Point map(Position position) { - Size clientVideoSize = position.getScreenSize(); - if (!videoSize.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; - } - - Point point = position.getPoint(); - if (videoToDeviceMatrix != null) { - point = videoToDeviceMatrix.apply(point); - } - return point; - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java deleted file mode 100644 index 20532c0b..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java +++ /dev/null @@ -1,287 +0,0 @@ -package com.genymobile.scrcpy.control; - -import com.genymobile.scrcpy.AndroidVersions; -import com.genymobile.scrcpy.util.Ln; -import com.genymobile.scrcpy.util.StringUtils; -import com.genymobile.scrcpy.wrappers.ServiceManager; - -import android.os.Build; -import android.os.HandlerThread; -import android.os.MessageQueue; -import android.system.ErrnoException; -import android.system.Os; -import android.system.OsConstants; -import android.util.ArrayMap; - -import java.io.FileDescriptor; -import java.io.IOException; -import java.io.InterruptedIOException; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.nio.charset.StandardCharsets; - -public final class UhidManager { - - // Linux: include/uapi/linux/uhid.h - private static final int UHID_OUTPUT = 6; - private static final int UHID_CREATE2 = 11; - private static final int UHID_INPUT2 = 12; - - // Linux: include/uapi/linux/input.h - private static final short BUS_VIRTUAL = 0x06; - - private static final int SIZE_OF_UHID_EVENT = 4380; // sizeof(struct uhid_event) - - // Must be unique across the system - private static final String INPUT_PORT = "scrcpy:" + Os.getpid(); - - private final String displayUniqueId; - - private final ArrayMap fds = new ArrayMap<>(); - private final ByteBuffer buffer = ByteBuffer.allocate(SIZE_OF_UHID_EVENT).order(ByteOrder.nativeOrder()); - - private final DeviceMessageSender sender; - private final MessageQueue queue; - - public UhidManager(DeviceMessageSender sender, String displayUniqueId) { - this.sender = sender; - this.displayUniqueId = displayUniqueId; - if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0) { - HandlerThread thread = new HandlerThread("UHidManager"); - thread.start(); - queue = thread.getLooper().getQueue(); - } else { - queue = null; - } - } - - public void open(int id, int vendorId, int productId, String name, byte[] reportDesc) throws IOException { - try { - FileDescriptor fd = Os.open("/dev/uhid", OsConstants.O_RDWR, 0); - try { - // First UHID device added - boolean firstDevice = fds.isEmpty(); - - FileDescriptor old = fds.put(id, fd); - if (old != null) { - Ln.w("Duplicate UHID id: " + id); - close(old); - } - - String phys = mustUseInputPort() ? INPUT_PORT : null; - byte[] req = buildUhidCreate2Req(vendorId, productId, name, reportDesc, phys); - Os.write(fd, req, 0, req.length); - - if (firstDevice) { - addUniqueIdAssociation(); - } - registerUhidListener(id, fd); - } catch (Exception e) { - close(fd); - throw e; - } - } catch (ErrnoException e) { - throw new IOException(e); - } - } - - private void registerUhidListener(int id, FileDescriptor fd) { - if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0) { - queue.addOnFileDescriptorEventListener(fd, MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT, (fd2, events) -> { - try { - buffer.clear(); - int r = Os.read(fd2, buffer); - buffer.flip(); - if (r > 0) { - int type = buffer.getInt(); - if (type == UHID_OUTPUT) { - byte[] data = extractHidOutputData(buffer); - if (data != null) { - DeviceMessage msg = DeviceMessage.createUhidOutput(id, data); - sender.send(msg); - } - } - } - } catch (ErrnoException | InterruptedIOException e) { - Ln.e("Failed to read UHID output", e); - return 0; - } - return events; - }); - } - } - - private void unregisterUhidListener(FileDescriptor fd) { - if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0) { - queue.removeOnFileDescriptorEventListener(fd); - } - } - - private static byte[] extractHidOutputData(ByteBuffer buffer) { - /* - * #define UHID_DATA_MAX 4096 - * struct uhid_event { - * uint32_t type; - * union { - * // ... - * struct uhid_output_req { - * __u8 data[UHID_DATA_MAX]; - * __u16 size; - * __u8 rtype; - * }; - * }; - * } __attribute__((__packed__)); - */ - - if (buffer.remaining() < 4099) { - Ln.w("Incomplete HID output"); - return null; - } - int size = buffer.getShort(buffer.position() + 4096) & 0xFFFF; - if (size > 4096) { - Ln.w("Incorrect HID output size: " + size); - return null; - } - byte[] data = new byte[size]; - buffer.get(data); - return data; - } - - public void writeInput(int id, byte[] data) throws IOException { - FileDescriptor fd = fds.get(id); - if (fd == null) { - Ln.w("Unknown UHID id: " + id); - return; - } - - try { - byte[] req = buildUhidInput2Req(data); - Os.write(fd, req, 0, req.length); - } catch (ErrnoException e) { - throw new IOException(e); - } - } - - private static byte[] buildUhidCreate2Req(int vendorId, int productId, String name, byte[] reportDesc, String phys) { - /* - * struct uhid_event { - * uint32_t type; - * union { - * // ... - * struct uhid_create2_req { - * uint8_t name[128]; - * uint8_t phys[64]; - * uint8_t uniq[64]; - * uint16_t rd_size; - * uint16_t bus; - * uint32_t vendor; - * uint32_t product; - * uint32_t version; - * uint32_t country; - * uint8_t rd_data[HID_MAX_DESCRIPTOR_SIZE]; - * }; - * }; - * } __attribute__((__packed__)); - */ - - ByteBuffer buf = ByteBuffer.allocate(280 + reportDesc.length).order(ByteOrder.nativeOrder()); - buf.putInt(UHID_CREATE2); - - String actualName = name.isEmpty() ? "scrcpy" : name; - byte[] nameBytes = actualName.getBytes(StandardCharsets.UTF_8); - int nameLen = StringUtils.getUtf8TruncationIndex(nameBytes, 127); - assert nameLen <= 127; - buf.put(nameBytes, 0, nameLen); - - if (phys != null) { - buf.position(4 + 128); - byte[] physBytes = phys.getBytes(StandardCharsets.US_ASCII); - assert physBytes.length <= 63; - buf.put(physBytes); - } - - buf.position(4 + 256); - buf.putShort((short) reportDesc.length); - buf.putShort(BUS_VIRTUAL); - buf.putInt(vendorId); - buf.putInt(productId); - buf.putInt(0); // version - buf.putInt(0); // country; - buf.put(reportDesc); - return buf.array(); - } - - private static byte[] buildUhidInput2Req(byte[] data) { - /* - * struct uhid_event { - * uint32_t type; - * union { - * // ... - * struct uhid_input2_req { - * uint16_t size; - * uint8_t data[UHID_DATA_MAX]; - * }; - * }; - * } __attribute__((__packed__)); - */ - - ByteBuffer buf = ByteBuffer.allocate(6 + data.length).order(ByteOrder.nativeOrder()); - buf.putInt(UHID_INPUT2); - buf.putShort((short) data.length); - buf.put(data); - return buf.array(); - } - - public void close(int id) { - // Linux: Documentation/hid/uhid.rst - // If you close() the fd, the device is automatically unregistered and destroyed internally. - FileDescriptor fd = fds.remove(id); - if (fd != null) { - unregisterUhidListener(fd); - close(fd); - - if (fds.isEmpty()) { - // Last UHID device removed - removeUniqueIdAssociation(); - } - } else { - Ln.w("Closing unknown UHID device: " + id); - } - } - - public void closeAll() { - if (fds.isEmpty()) { - return; - } - - for (FileDescriptor fd : fds.values()) { - close(fd); - } - - removeUniqueIdAssociation(); - } - - private static void close(FileDescriptor fd) { - try { - Os.close(fd); - } catch (ErrnoException e) { - Ln.e("Failed to close uhid: " + e.getMessage()); - } - } - - private boolean mustUseInputPort() { - return Build.VERSION.SDK_INT >= AndroidVersions.API_35_ANDROID_15 && displayUniqueId != null; - } - - private void addUniqueIdAssociation() { - if (mustUseInputPort()) { - ServiceManager.getInputManager().addUniqueIdAssociationByPort(INPUT_PORT, displayUniqueId); - } - } - - private void removeUniqueIdAssociation() { - if (mustUseInputPort()) { - ServiceManager.getInputManager().removeUniqueIdAssociationByPort(INPUT_PORT); - } - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/device/ConfigurationException.java b/server/src/main/java/com/genymobile/scrcpy/device/ConfigurationException.java deleted file mode 100644 index 17729342..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/device/ConfigurationException.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.genymobile.scrcpy.device; - -public class ConfigurationException extends Exception { - public ConfigurationException(String message) { - super(message); - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/device/DesktopConnection.java b/server/src/main/java/com/genymobile/scrcpy/device/DesktopConnection.java deleted file mode 100644 index db75aec6..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/device/DesktopConnection.java +++ /dev/null @@ -1,178 +0,0 @@ -package com.genymobile.scrcpy.device; - -import com.genymobile.scrcpy.control.ControlChannel; -import com.genymobile.scrcpy.util.IO; -import com.genymobile.scrcpy.util.StringUtils; - -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.nio.charset.StandardCharsets; - -public final class DesktopConnection implements Closeable { - - private static final int DEVICE_NAME_FIELD_LENGTH = 64; - - private static final String SOCKET_NAME_PREFIX = "scrcpy"; - - private final LocalSocket videoSocket; - private final FileDescriptor videoFd; - - private final LocalSocket audioSocket; - private final FileDescriptor audioFd; - - private final LocalSocket controlSocket; - private final ControlChannel controlChannel; - - private DesktopConnection(LocalSocket videoSocket, LocalSocket audioSocket, LocalSocket controlSocket) throws IOException { - this.videoSocket = videoSocket; - this.audioSocket = audioSocket; - this.controlSocket = controlSocket; - - videoFd = videoSocket != null ? videoSocket.getFileDescriptor() : null; - audioFd = audioSocket != null ? audioSocket.getFileDescriptor() : null; - controlChannel = controlSocket != null ? new ControlChannel(controlSocket) : null; - } - - private static LocalSocket connect(String abstractName) throws IOException { - LocalSocket localSocket = new LocalSocket(); - localSocket.connect(new LocalSocketAddress(abstractName)); - return localSocket; - } - - private static String getSocketName(int scid) { - if (scid == -1) { - // If no SCID is set, use "scrcpy" to simplify using scrcpy-server alone - return SOCKET_NAME_PREFIX; - } - - return SOCKET_NAME_PREFIX + String.format("_%08x", scid); - } - - public static DesktopConnection open(int scid, boolean tunnelForward, boolean video, boolean audio, boolean control, boolean sendDummyByte) - throws IOException { - String socketName = getSocketName(scid); - - LocalSocket videoSocket = null; - LocalSocket audioSocket = null; - LocalSocket controlSocket = null; - try { - if (tunnelForward) { - try (LocalServerSocket localServerSocket = new LocalServerSocket(socketName)) { - if (video) { - videoSocket = localServerSocket.accept(); - if (sendDummyByte) { - // send one byte so the client may read() to detect a connection error - videoSocket.getOutputStream().write(0); - sendDummyByte = false; - } - } - if (audio) { - audioSocket = localServerSocket.accept(); - if (sendDummyByte) { - // send one byte so the client may read() to detect a connection error - audioSocket.getOutputStream().write(0); - sendDummyByte = false; - } - } - if (control) { - controlSocket = localServerSocket.accept(); - if (sendDummyByte) { - // send one byte so the client may read() to detect a connection error - controlSocket.getOutputStream().write(0); - sendDummyByte = false; - } - } - } - } else { - if (video) { - videoSocket = connect(socketName); - } - if (audio) { - audioSocket = connect(socketName); - } - if (control) { - controlSocket = connect(socketName); - } - } - } catch (IOException | RuntimeException e) { - if (videoSocket != null) { - videoSocket.close(); - } - if (audioSocket != null) { - audioSocket.close(); - } - if (controlSocket != null) { - controlSocket.close(); - } - throw e; - } - - return new DesktopConnection(videoSocket, audioSocket, controlSocket); - } - - private LocalSocket getFirstSocket() { - if (videoSocket != null) { - return videoSocket; - } - if (audioSocket != null) { - return audioSocket; - } - return controlSocket; - } - - public void shutdown() throws IOException { - if (videoSocket != null) { - videoSocket.shutdownInput(); - videoSocket.shutdownOutput(); - } - if (audioSocket != null) { - audioSocket.shutdownInput(); - audioSocket.shutdownOutput(); - } - if (controlSocket != null) { - controlSocket.shutdownInput(); - controlSocket.shutdownOutput(); - } - } - - public void close() throws IOException { - if (videoSocket != null) { - videoSocket.close(); - } - if (audioSocket != null) { - audioSocket.close(); - } - if (controlSocket != null) { - controlSocket.close(); - } - } - - public void sendDeviceMeta(String deviceName) throws IOException { - byte[] buffer = new byte[DEVICE_NAME_FIELD_LENGTH]; - - byte[] deviceNameBytes = deviceName.getBytes(StandardCharsets.UTF_8); - int len = StringUtils.getUtf8TruncationIndex(deviceNameBytes, DEVICE_NAME_FIELD_LENGTH - 1); - System.arraycopy(deviceNameBytes, 0, buffer, 0, len); - // byte[] are always 0-initialized in java, no need to set '\0' explicitly - - FileDescriptor fd = getFirstSocket().getFileDescriptor(); - IO.writeFully(fd, buffer, 0, buffer.length); - } - - public FileDescriptor getVideoFd() { - return videoFd; - } - - public FileDescriptor getAudioFd() { - return audioFd; - } - - public ControlChannel getControlChannel() { - return controlChannel; - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java deleted file mode 100644 index 3553dc27..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ /dev/null @@ -1,315 +0,0 @@ -package com.genymobile.scrcpy.device; - -import com.genymobile.scrcpy.AndroidVersions; -import com.genymobile.scrcpy.FakeContext; -import com.genymobile.scrcpy.util.Ln; -import com.genymobile.scrcpy.wrappers.ActivityManager; -import com.genymobile.scrcpy.wrappers.ClipboardManager; -import com.genymobile.scrcpy.wrappers.DisplayControl; -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.annotation.SuppressLint; -import android.content.Intent; -import android.app.ActivityOptions; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.os.Build; -import android.os.Bundle; -import android.os.IBinder; -import android.os.SystemClock; -import android.view.InputDevice; -import android.view.InputEvent; -import android.view.KeyCharacterMap; -import android.view.KeyEvent; - -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; - -public final class Device { - - public static final int DISPLAY_ID_NONE = -1; - - 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; - - // The new display power method introduced in Android 15 does not work as expected: - // - private static final boolean USE_ANDROID_15_DISPLAY_POWER = false; - - private Device() { - // not instantiable - } - - public static String getDeviceName() { - return Build.MODEL; - } - - public static boolean supportsInputEvents(int displayId) { - // main display or any display on Android >= 10 - return displayId == 0 || Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10; - } - - 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 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 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 static boolean isScreenOn(int displayId) { - assert displayId != DISPLAY_ID_NONE; - return ServiceManager.getPowerManager().isScreenOn(displayId); - } - - 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 static 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; - } - - return clipboardManager.setText(text); - } - - public static boolean setDisplayPower(int displayId, boolean on) { - assert displayId != Device.DISPLAY_ID_NONE; - - if (USE_ANDROID_15_DISPLAY_POWER && Build.VERSION.SDK_INT >= AndroidVersions.API_35_ANDROID_15) { - return ServiceManager.getDisplayManager().requestDisplayPower(displayId, on); - } - - boolean applyToMultiPhysicalDisplays = Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10; - - if (applyToMultiPhysicalDisplays - && Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14 - && Build.BRAND.equalsIgnoreCase("honor") - && SurfaceControl.hasGetBuildInDisplayMethod()) { - // Workaround for Honor devices with Android 14: - // - - // - - applyToMultiPhysicalDisplays = false; - } - - int mode = on ? POWER_MODE_NORMAL : POWER_MODE_OFF; - if (applyToMultiPhysicalDisplays) { - // On Android 14, these internal methods have been moved to DisplayControl - boolean useDisplayControl = - Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14 && !SurfaceControl.hasGetPhysicalDisplayIdsMethod(); - - // Change the power mode for all physical displays - long[] physicalDisplayIds = useDisplayControl ? DisplayControl.getPhysicalDisplayIds() : SurfaceControl.getPhysicalDisplayIds(); - if (physicalDisplayIds == null) { - Ln.e("Could not get physical display ids"); - return false; - } - - boolean allOk = true; - for (long physicalDisplayId : physicalDisplayIds) { - IBinder binder = useDisplayControl ? DisplayControl.getPhysicalDisplayToken( - physicalDisplayId) : SurfaceControl.getPhysicalDisplayToken(physicalDisplayId); - allOk &= SurfaceControl.setDisplayPowerMode(binder, mode); - } - return allOk; - } - - // Older Android versions, only 1 display - IBinder d = SurfaceControl.getBuiltInDisplay(); - if (d == null) { - Ln.e("Could not get built-in display"); - return false; - } - return SurfaceControl.setDisplayPowerMode(d, mode); - } - - public static boolean powerOffScreen(int displayId) { - assert displayId != DISPLAY_ID_NONE; - - if (!isScreenOn(displayId)) { - 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(int displayId) { - assert displayId != DISPLAY_ID_NONE; - - WindowManager wm = ServiceManager.getWindowManager(); - - boolean accelerometerRotation = !wm.isRotationFrozen(displayId); - - int currentRotation = getCurrentRotation(displayId); - int newRotation = (currentRotation & 1) ^ 1; // 0->1, 1->0, 2->1, 3->0 - String newRotationString = newRotation == 0 ? "portrait" : "landscape"; - - Ln.i("Device rotation requested: " + newRotationString); - wm.freezeRotation(displayId, newRotation); - - // restore auto-rotate if necessary - if (accelerometerRotation) { - wm.thawRotation(displayId); - } - } - - private static int getCurrentRotation(int displayId) { - assert displayId != DISPLAY_ID_NONE; - - if (displayId == 0) { - return ServiceManager.getWindowManager().getRotation(); - } - - DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId); - return displayInfo.getRotation(); - } - - public static List listApps() { - List apps = new ArrayList<>(); - PackageManager pm = FakeContext.get().getPackageManager(); - for (ApplicationInfo appInfo : getLaunchableApps(pm)) { - apps.add(toApp(pm, appInfo)); - } - - return apps; - } - - @SuppressLint("QueryPermissionsNeeded") - private static List getLaunchableApps(PackageManager pm) { - List result = new ArrayList<>(); - for (ApplicationInfo appInfo : pm.getInstalledApplications(PackageManager.GET_META_DATA)) { - if (appInfo.enabled && getLaunchIntent(pm, appInfo.packageName) != null) { - result.add(appInfo); - } - } - - return result; - } - - public static Intent getLaunchIntent(PackageManager pm, String packageName) { - Intent launchIntent = pm.getLaunchIntentForPackage(packageName); - if (launchIntent != null) { - return launchIntent; - } - - return pm.getLeanbackLaunchIntentForPackage(packageName); - } - - private static DeviceApp toApp(PackageManager pm, ApplicationInfo appInfo) { - String name = pm.getApplicationLabel(appInfo).toString(); - boolean system = (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; - return new DeviceApp(appInfo.packageName, name, system); - } - - @SuppressLint("QueryPermissionsNeeded") - public static DeviceApp findByPackageName(String packageName) { - PackageManager pm = FakeContext.get().getPackageManager(); - // No need to filter by "launchable" apps, an error will be reported on start if the app is not launchable - for (ApplicationInfo appInfo : pm.getInstalledApplications(PackageManager.GET_META_DATA)) { - if (packageName.equals(appInfo.packageName)) { - return toApp(pm, appInfo); - } - } - - return null; - } - - @SuppressLint("QueryPermissionsNeeded") - public static List findByName(String searchName) { - List result = new ArrayList<>(); - searchName = searchName.toLowerCase(Locale.getDefault()); - - PackageManager pm = FakeContext.get().getPackageManager(); - for (ApplicationInfo appInfo : getLaunchableApps(pm)) { - String name = pm.getApplicationLabel(appInfo).toString(); - if (name.toLowerCase(Locale.getDefault()).startsWith(searchName)) { - boolean system = (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; - result.add(new DeviceApp(appInfo.packageName, name, system)); - } - } - - return result; - } - - public static void startApp(String packageName, int displayId, boolean forceStop) { - PackageManager pm = FakeContext.get().getPackageManager(); - - Intent launchIntent = getLaunchIntent(pm, packageName); - if (launchIntent == null) { - Ln.w("Cannot create launch intent for app " + packageName); - return; - } - - launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - - Bundle options = null; - if (Build.VERSION.SDK_INT >= AndroidVersions.API_26_ANDROID_8_0) { - ActivityOptions launchOptions = ActivityOptions.makeBasic(); - launchOptions.setLaunchDisplayId(displayId); - options = launchOptions.toBundle(); - } - - ActivityManager am = ServiceManager.getActivityManager(); - if (forceStop) { - am.forceStopPackage(packageName); - } - am.startActivity(launchIntent, options); - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/device/DeviceApp.java b/server/src/main/java/com/genymobile/scrcpy/device/DeviceApp.java deleted file mode 100644 index ed292efa..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/device/DeviceApp.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.genymobile.scrcpy.device; - -public final class DeviceApp { - - private final String packageName; - private final String name; - private final boolean system; - - public DeviceApp(String packageName, String name, boolean system) { - this.packageName = packageName; - this.name = name; - this.system = system; - } - - public String getPackageName() { - return packageName; - } - - public String getName() { - return name; - } - - public boolean isSystem() { - return system; - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java b/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java deleted file mode 100644 index 8d26b7ce..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.genymobile.scrcpy.device; - -public final class DisplayInfo { - private final int displayId; - private final Size size; - private final int rotation; - private final int layerStack; - private final int flags; - private final int dpi; - private final String uniqueId; - - public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 0x00000001; - - public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags, int dpi, String uniqueId) { - this.displayId = displayId; - this.size = size; - this.rotation = rotation; - this.layerStack = layerStack; - this.flags = flags; - this.dpi = dpi; - this.uniqueId = uniqueId; - } - - 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; - } - - public int getDpi() { - return dpi; - } - - public String getUniqueId() { - return uniqueId; - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java b/server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java deleted file mode 100644 index 3aa2996a..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.genymobile.scrcpy.device; - -public final class NewDisplay { - private Size size; - private int dpi; - - public NewDisplay() { - // Auto size and dpi - } - - public NewDisplay(Size size, int dpi) { - this.size = size; - this.dpi = dpi; - } - - public Size getSize() { - return size; - } - - public int getDpi() { - return dpi; - } - - public boolean hasExplicitSize() { - return size != null; - } - - public boolean hasExplicitDpi() { - return dpi != 0; - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Orientation.java b/server/src/main/java/com/genymobile/scrcpy/device/Orientation.java deleted file mode 100644 index 81168aae..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/device/Orientation.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.genymobile.scrcpy.device; - -public enum Orientation { - - // @formatter:off - Orient0("0"), - Orient90("90"), - Orient180("180"), - Orient270("270"), - Flip0("flip0"), - Flip90("flip90"), - Flip180("flip180"), - Flip270("flip270"); - - public enum Lock { - Unlocked, LockedInitial, LockedValue, - } - - private final String name; - - Orientation(String name) { - this.name = name; - } - - public static Orientation getByName(String name) { - for (Orientation orientation : values()) { - if (orientation.name.equals(name)) { - return orientation; - } - } - - throw new IllegalArgumentException("Unknown orientation: " + name); - } - - public static Orientation fromRotation(int ccwRotation) { - assert ccwRotation >= 0 && ccwRotation < 4; - // Display rotation is expressed counter-clockwise, orientation is expressed clockwise - int cwRotation = (4 - ccwRotation) % 4; - return values()[cwRotation]; - } - - public boolean isFlipped() { - return (ordinal() & 4) != 0; - } - - public int getRotation() { - return ordinal() & 3; - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Position.java b/server/src/main/java/com/genymobile/scrcpy/device/Position.java deleted file mode 100644 index 7ce4e256..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/device/Position.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.genymobile.scrcpy.device; - -import java.util.Objects; - -public class Position { - private final Point point; - private final 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 + '}'; - } - -} diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Size.java b/server/src/main/java/com/genymobile/scrcpy/device/Size.java deleted file mode 100644 index b448273d..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/device/Size.java +++ /dev/null @@ -1,112 +0,0 @@ -package com.genymobile.scrcpy.device; - -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 int getMax() { - return Math.max(width, height); - } - - public Size rotate() { - return new Size(height, width); - } - - public Size limit(int maxSize) { - assert maxSize >= 0 : "Max size may not be negative"; - assert maxSize % 8 == 0 : "Max size must be a multiple of 8"; - - if (maxSize == 0) { - // No limit - return this; - } - - boolean portrait = height > width; - int major = portrait ? height : width; - if (major <= maxSize) { - return this; - } - - int minor = portrait ? width : height; - - int newMajor = maxSize; - int newMinor = maxSize * minor / major; - - int w = portrait ? newMinor : newMajor; - int h = portrait ? newMajor : newMinor; - return new Size(w, h); - } - - /** - * Round both dimensions of this size to be a multiple of 8 (as required by many encoders). - * - * @return The current size rounded. - */ - public Size round8() { - if (isMultipleOf8()) { - // Already a multiple of 8 - return this; - } - - boolean portrait = height > width; - int major = portrait ? height : width; - int minor = portrait ? width : height; - - major &= ~7; // round down to not exceed the initial size - minor = (minor + 4) & ~7; // round to the nearest to minimize aspect ratio distortion - if (minor > major) { - minor = major; - } - - int w = portrait ? minor : major; - int h = portrait ? major : minor; - return new Size(w, h); - } - - public boolean isMultipleOf8() { - return (width & 7) == 0 && (height & 7) == 0; - } - - 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 width + "x" + height; - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Streamer.java b/server/src/main/java/com/genymobile/scrcpy/device/Streamer.java deleted file mode 100644 index f54d0567..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/device/Streamer.java +++ /dev/null @@ -1,190 +0,0 @@ -package com.genymobile.scrcpy.device; - -import com.genymobile.scrcpy.audio.AudioCodec; -import com.genymobile.scrcpy.util.Codec; -import com.genymobile.scrcpy.util.IO; - -import android.media.MediaCodec; - -import java.io.FileDescriptor; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.util.Arrays; - -public final class Streamer { - - private static final long PACKET_FLAG_CONFIG = 1L << 63; - private static final long PACKET_FLAG_KEY_FRAME = 1L << 62; - - private final FileDescriptor fd; - private final Codec codec; - private final boolean sendCodecMeta; - private final boolean sendFrameMeta; - - private final ByteBuffer headerBuffer = ByteBuffer.allocate(12); - - public Streamer(FileDescriptor fd, Codec codec, boolean sendCodecMeta, boolean sendFrameMeta) { - this.fd = fd; - this.codec = codec; - this.sendCodecMeta = sendCodecMeta; - this.sendFrameMeta = sendFrameMeta; - } - - public Codec getCodec() { - return codec; - } - - public void writeAudioHeader() throws IOException { - if (sendCodecMeta) { - ByteBuffer buffer = ByteBuffer.allocate(4); - buffer.putInt(codec.getId()); - buffer.flip(); - IO.writeFully(fd, buffer); - } - } - - public void writeVideoHeader(Size videoSize) throws IOException { - if (sendCodecMeta) { - ByteBuffer buffer = ByteBuffer.allocate(12); - buffer.putInt(codec.getId()); - buffer.putInt(videoSize.getWidth()); - buffer.putInt(videoSize.getHeight()); - buffer.flip(); - IO.writeFully(fd, buffer); - } - } - - public void writeDisableStream(boolean error) throws IOException { - // Writing a specific code as codec-id means that the device disables the stream - // code 0: it explicitly disables the stream (because it could not capture audio), scrcpy should continue mirroring video only - // code 1: a configuration error occurred, scrcpy must be stopped - byte[] code = new byte[4]; - if (error) { - code[3] = 1; - } - IO.writeFully(fd, code, 0, code.length); - } - - public void writePacket(ByteBuffer buffer, long pts, boolean config, boolean keyFrame) throws IOException { - if (config) { - if (codec == AudioCodec.OPUS) { - fixOpusConfigPacket(buffer); - } else if (codec == AudioCodec.FLAC) { - fixFlacConfigPacket(buffer); - } - } - - if (sendFrameMeta) { - writeFrameMeta(fd, buffer.remaining(), pts, config, keyFrame); - } - - IO.writeFully(fd, buffer); - } - - public void writePacket(ByteBuffer codecBuffer, MediaCodec.BufferInfo bufferInfo) throws IOException { - long pts = bufferInfo.presentationTimeUs; - boolean config = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0; - boolean keyFrame = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0; - writePacket(codecBuffer, pts, config, keyFrame); - } - - private void writeFrameMeta(FileDescriptor fd, int packetSize, long pts, boolean config, boolean keyFrame) throws IOException { - headerBuffer.clear(); - - long ptsAndFlags; - if (config) { - ptsAndFlags = PACKET_FLAG_CONFIG; // non-media data packet - } else { - ptsAndFlags = pts; - if (keyFrame) { - ptsAndFlags |= PACKET_FLAG_KEY_FRAME; - } - } - - headerBuffer.putLong(ptsAndFlags); - headerBuffer.putInt(packetSize); - headerBuffer.flip(); - IO.writeFully(fd, headerBuffer); - } - - private static void fixOpusConfigPacket(ByteBuffer buffer) throws IOException { - // Here is an example of the config packet received for an OPUS stream: - // - // 00000000 41 4f 50 55 53 48 44 52 13 00 00 00 00 00 00 00 |AOPUSHDR........| - // -------------- BELOW IS THE PART WE MUST PUT AS EXTRADATA ------------------- - // 00000010 4f 70 75 73 48 65 61 64 01 01 38 01 80 bb 00 00 |OpusHead..8.....| - // 00000020 00 00 00 |... | - // ------------------------------------------------------------------------------ - // 00000020 41 4f 50 55 53 44 4c 59 08 00 00 00 00 | AOPUSDLY.....| - // 00000030 00 00 00 a0 2e 63 00 00 00 00 00 41 4f 50 55 53 |.....c.....AOPUS| - // 00000040 50 52 4c 08 00 00 00 00 00 00 00 00 b4 c4 04 00 |PRL.............| - // 00000050 00 00 00 |...| - // - // Each "section" is prefixed by a 64-bit ID and a 64-bit length. - // - // - - if (buffer.remaining() < 16) { - throw new IOException("Not enough data in OPUS config packet"); - } - - final byte[] opusHeaderId = {'A', 'O', 'P', 'U', 'S', 'H', 'D', 'R'}; - byte[] idBuffer = new byte[8]; - buffer.get(idBuffer); - if (!Arrays.equals(idBuffer, opusHeaderId)) { - throw new IOException("OPUS header not found"); - } - - // The size is in native byte-order - long sizeLong = buffer.getLong(); - if (sizeLong < 0 || sizeLong >= 0x7FFFFFFF) { - throw new IOException("Invalid block size in OPUS header: " + sizeLong); - } - - int size = (int) sizeLong; - if (buffer.remaining() < size) { - throw new IOException("Not enough data in OPUS header (invalid size: " + size + ")"); - } - - // Set the buffer to point to the OPUS header slice - buffer.limit(buffer.position() + size); - } - - private static void fixFlacConfigPacket(ByteBuffer buffer) throws IOException { - // 00000000 66 4c 61 43 00 00 00 22 |fLaC..." | - // -------------- BELOW IS THE PART WE MUST PUT AS EXTRADATA ------------------- - // 00000000 10 00 10 00 00 00 00 00 | ........| - // 00000010 00 00 0b b8 02 f0 00 00 00 00 00 00 00 00 00 00 |................| - // 00000020 00 00 00 00 00 00 00 00 00 00 |.......... | - // ------------------------------------------------------------------------------ - // 00000020 84 00 00 28 20 00 | ...( .| - // 00000030 00 00 72 65 66 65 72 65 6e 63 65 20 6c 69 62 46 |..reference libF| - // 00000040 4c 41 43 20 31 2e 33 2e 32 20 32 30 32 32 31 30 |LAC 1.3.2 202210| - // 00000050 32 32 00 00 00 00 |22....| - // - // - - if (buffer.remaining() < 8) { - throw new IOException("Not enough data in FLAC config packet"); - } - - final byte[] flacHeaderId = {'f', 'L', 'a', 'C'}; - byte[] idBuffer = new byte[4]; - buffer.get(idBuffer); - if (!Arrays.equals(idBuffer, flacHeaderId)) { - throw new IOException("FLAC header not found"); - } - - // The size is in big-endian - buffer.order(ByteOrder.BIG_ENDIAN); - - int size = buffer.getInt(); - if (buffer.remaining() < size) { - throw new IOException("Not enough data in FLAC header (invalid size: " + size + ")"); - } - - // Set the buffer to point to the FLAC header slice - buffer.limit(buffer.position() + size); - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/opengl/AffineOpenGLFilter.java b/server/src/main/java/com/genymobile/scrcpy/opengl/AffineOpenGLFilter.java deleted file mode 100644 index 7608a574..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/opengl/AffineOpenGLFilter.java +++ /dev/null @@ -1,135 +0,0 @@ -package com.genymobile.scrcpy.opengl; - -import com.genymobile.scrcpy.util.AffineMatrix; - -import android.opengl.GLES11Ext; -import android.opengl.GLES20; - -import java.nio.FloatBuffer; - -public class AffineOpenGLFilter implements OpenGLFilter { - - private int program; - private FloatBuffer vertexBuffer; - private FloatBuffer texCoordsBuffer; - private final float[] userMatrix; - - private int vertexPosLoc; - private int texCoordsInLoc; - - private int texLoc; - private int texMatrixLoc; - private int userMatrixLoc; - - public AffineOpenGLFilter(AffineMatrix transform) { - userMatrix = transform.to4x4(); - } - - @Override - public void init() throws OpenGLException { - // @formatter:off - String vertexShaderCode = "#version 100\n" - + "attribute vec4 vertex_pos;\n" - + "attribute vec4 tex_coords_in;\n" - + "varying vec2 tex_coords;\n" - + "uniform mat4 tex_matrix;\n" - + "uniform mat4 user_matrix;\n" - + "void main() {\n" - + " gl_Position = vertex_pos;\n" - + " tex_coords = (tex_matrix * user_matrix * tex_coords_in).xy;\n" - + "}"; - - // @formatter:off - String fragmentShaderCode = "#version 100\n" - + "#extension GL_OES_EGL_image_external : require\n" - + "precision highp float;\n" - + "uniform samplerExternalOES tex;\n" - + "varying vec2 tex_coords;\n" - + "void main() {\n" - + " if (tex_coords.x >= 0.0 && tex_coords.x <= 1.0\n" - + " && tex_coords.y >= 0.0 && tex_coords.y <= 1.0) {\n" - + " gl_FragColor = texture2D(tex, tex_coords);\n" - + " } else {\n" - + " gl_FragColor = vec4(0.0);\n" - + " }\n" - + "}"; - - program = GLUtils.createProgram(vertexShaderCode, fragmentShaderCode); - if (program == 0) { - throw new OpenGLException("Cannot create OpenGL program"); - } - - float[] vertices = { - -1, -1, // Bottom-left - 1, -1, // Bottom-right - -1, 1, // Top-left - 1, 1, // Top-right - }; - - float[] texCoords = { - 0, 0, // Bottom-left - 1, 0, // Bottom-right - 0, 1, // Top-left - 1, 1, // Top-right - }; - - // OpenGL will fill the 3rd and 4th coordinates of the vec4 automatically with 0.0 and 1.0 respectively - vertexBuffer = GLUtils.createFloatBuffer(vertices); - texCoordsBuffer = GLUtils.createFloatBuffer(texCoords); - - vertexPosLoc = GLES20.glGetAttribLocation(program, "vertex_pos"); - assert vertexPosLoc != -1; - - texCoordsInLoc = GLES20.glGetAttribLocation(program, "tex_coords_in"); - assert texCoordsInLoc != -1; - - texLoc = GLES20.glGetUniformLocation(program, "tex"); - assert texLoc != -1; - - texMatrixLoc = GLES20.glGetUniformLocation(program, "tex_matrix"); - assert texMatrixLoc != -1; - - userMatrixLoc = GLES20.glGetUniformLocation(program, "user_matrix"); - assert userMatrixLoc != -1; - } - - @Override - public void draw(int textureId, float[] texMatrix) { - GLES20.glUseProgram(program); - GLUtils.checkGlError(); - - GLES20.glEnableVertexAttribArray(vertexPosLoc); - GLUtils.checkGlError(); - GLES20.glEnableVertexAttribArray(texCoordsInLoc); - GLUtils.checkGlError(); - - GLES20.glVertexAttribPointer(vertexPosLoc, 2, GLES20.GL_FLOAT, false, 0, vertexBuffer); - GLUtils.checkGlError(); - GLES20.glVertexAttribPointer(texCoordsInLoc, 2, GLES20.GL_FLOAT, false, 0, texCoordsBuffer); - GLUtils.checkGlError(); - - GLES20.glActiveTexture(GLES20.GL_TEXTURE0); - GLUtils.checkGlError(); - GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId); - GLUtils.checkGlError(); - GLES20.glUniform1i(texLoc, 0); - GLUtils.checkGlError(); - - GLES20.glUniformMatrix4fv(texMatrixLoc, 1, false, texMatrix, 0); - GLUtils.checkGlError(); - - GLES20.glUniformMatrix4fv(userMatrixLoc, 1, false, userMatrix, 0); - GLUtils.checkGlError(); - - GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); - GLUtils.checkGlError(); - GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); - GLUtils.checkGlError(); - } - - @Override - public void release() { - GLES20.glDeleteProgram(program); - GLUtils.checkGlError(); - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/opengl/GLUtils.java b/server/src/main/java/com/genymobile/scrcpy/opengl/GLUtils.java deleted file mode 100644 index 72a3f400..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/opengl/GLUtils.java +++ /dev/null @@ -1,124 +0,0 @@ -package com.genymobile.scrcpy.opengl; - -import com.genymobile.scrcpy.BuildConfig; -import com.genymobile.scrcpy.util.Ln; - -import android.opengl.GLES20; -import android.opengl.GLU; - -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.nio.FloatBuffer; - -public final class GLUtils { - - private static final boolean DEBUG = BuildConfig.DEBUG; - - private GLUtils() { - // not instantiable - } - - public static int createProgram(String vertexSource, String fragmentSource) { - int vertexShader = createShader(GLES20.GL_VERTEX_SHADER, vertexSource); - if (vertexShader == 0) { - return 0; - } - - int fragmentShader = createShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource); - if (fragmentShader == 0) { - GLES20.glDeleteShader(vertexShader); - return 0; - } - - int program = GLES20.glCreateProgram(); - if (program == 0) { - GLES20.glDeleteShader(fragmentShader); - GLES20.glDeleteShader(vertexShader); - return 0; - } - - GLES20.glAttachShader(program, vertexShader); - checkGlError(); - GLES20.glAttachShader(program, fragmentShader); - checkGlError(); - GLES20.glLinkProgram(program); - checkGlError(); - - int[] linkStatus = new int[1]; - GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0); - if (linkStatus[0] == 0) { - Ln.e("Could not link program: " + GLES20.glGetProgramInfoLog(program)); - GLES20.glDeleteProgram(program); - GLES20.glDeleteShader(fragmentShader); - GLES20.glDeleteShader(vertexShader); - return 0; - } - - return program; - } - - public static int createShader(int type, String source) { - int shader = GLES20.glCreateShader(type); - if (shader == 0) { - Ln.e(getGlErrorMessage("Could not create shader")); - return 0; - } - - GLES20.glShaderSource(shader, source); - GLES20.glCompileShader(shader); - - int[] compileStatus = new int[1]; - GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compileStatus, 0); - if (compileStatus[0] == 0) { - Ln.e("Could not compile " + getShaderTypeString(type) + ": " + GLES20.glGetShaderInfoLog(shader)); - GLES20.glDeleteShader(shader); - return 0; - } - - return shader; - } - - private static String getShaderTypeString(int type) { - switch (type) { - case GLES20.GL_VERTEX_SHADER: - return "vertex shader"; - case GLES20.GL_FRAGMENT_SHADER: - return "fragment shader"; - default: - return "shader"; - } - } - - /** - * Throws a runtime exception if {@link GLES20#glGetError()} returns an error (useful for debugging). - */ - public static void checkGlError() { - if (DEBUG) { - int error = GLES20.glGetError(); - if (error != GLES20.GL_NO_ERROR) { - throw new RuntimeException(toErrorString(error)); - } - } - } - - public static String getGlErrorMessage(String userError) { - int glError = GLES20.glGetError(); - if (glError == GLES20.GL_NO_ERROR) { - return userError; - } - - return userError + " (" + toErrorString(glError) + ")"; - } - - private static String toErrorString(int glError) { - String errorString = GLU.gluErrorString(glError); - return "glError 0x" + Integer.toHexString(glError) + " " + errorString; - } - - public static FloatBuffer createFloatBuffer(float[] values) { - FloatBuffer fb = ByteBuffer.allocateDirect(values.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); - fb.put(values); - fb.position(0); - return fb; - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLException.java b/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLException.java deleted file mode 100644 index cbc9539b..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLException.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.genymobile.scrcpy.opengl; - -import java.io.IOException; - -public class OpenGLException extends IOException { - public OpenGLException(String message) { - super(message); - } - - public OpenGLException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLFilter.java b/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLFilter.java deleted file mode 100644 index 6f27777e..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLFilter.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.genymobile.scrcpy.opengl; - -public interface OpenGLFilter { - - /** - * Initialize the OpenGL filter (typically compile the shaders and create the program). - * - * @throws OpenGLException if an initialization error occurs - */ - void init() throws OpenGLException; - - /** - * Render a frame (call for each frame). - */ - void draw(int textureId, float[] texMatrix); - - /** - * Release resources. - */ - void release(); -} diff --git a/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLRunner.java b/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLRunner.java deleted file mode 100644 index 86bd1859..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLRunner.java +++ /dev/null @@ -1,258 +0,0 @@ -package com.genymobile.scrcpy.opengl; - -import com.genymobile.scrcpy.device.Size; - -import android.graphics.SurfaceTexture; -import android.opengl.EGL14; -import android.opengl.EGLConfig; -import android.opengl.EGLContext; -import android.opengl.EGLDisplay; -import android.opengl.EGLExt; -import android.opengl.EGLSurface; -import android.opengl.GLES11Ext; -import android.opengl.GLES20; -import android.os.Handler; -import android.os.HandlerThread; -import android.view.Surface; - -import java.util.concurrent.Semaphore; - -public final class OpenGLRunner { - - private static HandlerThread handlerThread; - private static Handler handler; - private static boolean quit; - - private EGLDisplay eglDisplay; - private EGLContext eglContext; - private EGLSurface eglSurface; - - private final OpenGLFilter filter; - private final float[] overrideTransformMatrix; - - private SurfaceTexture surfaceTexture; - private Surface inputSurface; - private int textureId; - - private boolean stopped; - - public OpenGLRunner(OpenGLFilter filter, float[] overrideTransformMatrix) { - this.filter = filter; - this.overrideTransformMatrix = overrideTransformMatrix; - } - - public OpenGLRunner(OpenGLFilter filter) { - this(filter, null); - } - - public static synchronized void initOnce() { - if (handlerThread == null) { - if (quit) { - throw new IllegalStateException("Could not init OpenGLRunner after it is quit"); - } - handlerThread = new HandlerThread("OpenGLRunner"); - handlerThread.start(); - handler = new Handler(handlerThread.getLooper()); - } - } - - public static void quit() { - HandlerThread thread; - synchronized (OpenGLRunner.class) { - thread = handlerThread; - quit = true; - } - if (thread != null) { - thread.quitSafely(); - } - } - - public static void join() throws InterruptedException { - HandlerThread thread; - synchronized (OpenGLRunner.class) { - thread = handlerThread; - } - if (thread != null) { - thread.join(); - } - } - - public Surface start(Size inputSize, Size outputSize, Surface outputSurface) throws OpenGLException { - initOnce(); - - // Simulate CompletableFuture, but working for all Android versions - final Semaphore sem = new Semaphore(0); - Throwable[] throwableRef = new Throwable[1]; - - // The whole OpenGL execution must be performed on a Handler, so that SurfaceTexture.setOnFrameAvailableListener() works correctly. - // See - handler.post(() -> { - try { - run(inputSize, outputSize, outputSurface); - } catch (Throwable throwable) { - throwableRef[0] = throwable; - } finally { - sem.release(); - } - }); - - try { - sem.acquire(); - } catch (InterruptedException e) { - // Behave as if this method call was synchronous - Thread.currentThread().interrupt(); - } - - Throwable throwable = throwableRef[0]; - if (throwable != null) { - if (throwable instanceof OpenGLException) { - throw (OpenGLException) throwable; - } - throw new OpenGLException("Asynchronous OpenGL runner init failed", throwable); - } - - // Synchronization is ok: inputSurface is written before sem.release() and read after sem.acquire() - return inputSurface; - } - - private void run(Size inputSize, Size outputSize, Surface outputSurface) throws OpenGLException { - eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); - if (eglDisplay == EGL14.EGL_NO_DISPLAY) { - throw new OpenGLException("Unable to get EGL14 display"); - } - - int[] version = new int[2]; - if (!EGL14.eglInitialize(eglDisplay, version, 0, version, 1)) { - throw new OpenGLException("Unable to initialize EGL14"); - } - - // @formatter:off - int[] attribList = { - EGL14.EGL_RED_SIZE, 8, - EGL14.EGL_GREEN_SIZE, 8, - EGL14.EGL_BLUE_SIZE, 8, - EGL14.EGL_ALPHA_SIZE, 8, - EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, - EGL14.EGL_NONE - }; - - EGLConfig[] configs = new EGLConfig[1]; - int[] numConfigs = new int[1]; - EGL14.eglChooseConfig(eglDisplay, attribList, 0, configs, 0, configs.length, numConfigs, 0); - if (numConfigs[0] <= 0) { - EGL14.eglTerminate(eglDisplay); - throw new OpenGLException("Unable to find ES2 EGL config"); - } - EGLConfig eglConfig = configs[0]; - - // @formatter:off - int[] contextAttribList = { - EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, - EGL14.EGL_NONE - }; - eglContext = EGL14.eglCreateContext(eglDisplay, eglConfig, EGL14.EGL_NO_CONTEXT, contextAttribList, 0); - if (eglContext == null) { - EGL14.eglTerminate(eglDisplay); - throw new OpenGLException("Failed to create EGL context"); - } - - int[] surfaceAttribList = { - EGL14.EGL_NONE - }; - eglSurface = EGL14.eglCreateWindowSurface(eglDisplay, eglConfig, outputSurface, surfaceAttribList, 0); - if (eglSurface == null) { - EGL14.eglDestroyContext(eglDisplay, eglContext); - EGL14.eglTerminate(eglDisplay); - throw new OpenGLException("Failed to create EGL window surface"); - } - - if (!EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)) { - EGL14.eglDestroySurface(eglDisplay, eglSurface); - EGL14.eglDestroyContext(eglDisplay, eglContext); - EGL14.eglTerminate(eglDisplay); - throw new OpenGLException("Failed to make EGL context current"); - } - - int[] textures = new int[1]; - GLES20.glGenTextures(1, textures, 0); - GLUtils.checkGlError(); - textureId = textures[0]; - - GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); - GLUtils.checkGlError(); - GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); - GLUtils.checkGlError(); - GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); - GLUtils.checkGlError(); - GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); - GLUtils.checkGlError(); - - surfaceTexture = new SurfaceTexture(textureId); - surfaceTexture.setDefaultBufferSize(inputSize.getWidth(), inputSize.getHeight()); - inputSurface = new Surface(surfaceTexture); - - filter.init(); - - surfaceTexture.setOnFrameAvailableListener(surfaceTexture -> { - if (stopped) { - // Make sure to never render after resources have been released - return; - } - - render(outputSize); - }, handler); - } - - private void render(Size outputSize) { - GLES20.glViewport(0, 0, outputSize.getWidth(), outputSize.getHeight()); - GLUtils.checkGlError(); - - surfaceTexture.updateTexImage(); - - float[] matrix; - if (overrideTransformMatrix != null) { - matrix = overrideTransformMatrix; - } else { - matrix = new float[16]; - surfaceTexture.getTransformMatrix(matrix); - } - - filter.draw(textureId, matrix); - - EGLExt.eglPresentationTimeANDROID(eglDisplay, eglSurface, surfaceTexture.getTimestamp()); - EGL14.eglSwapBuffers(eglDisplay, eglSurface); - } - - public void stopAndRelease() { - final Semaphore sem = new Semaphore(0); - - handler.post(() -> { - stopped = true; - surfaceTexture.setOnFrameAvailableListener(null, handler); - - filter.release(); - - int[] textures = {textureId}; - GLES20.glDeleteTextures(1, textures, 0); - GLUtils.checkGlError(); - - EGL14.eglDestroySurface(eglDisplay, eglSurface); - EGL14.eglDestroyContext(eglDisplay, eglContext); - EGL14.eglTerminate(eglDisplay); - eglDisplay = EGL14.EGL_NO_DISPLAY; - eglContext = EGL14.EGL_NO_CONTEXT; - eglSurface = EGL14.EGL_NO_SURFACE; - surfaceTexture.release(); - inputSurface.release(); - - sem.release(); - }); - - try { - sem.acquire(); - } catch (InterruptedException e) { - // Behave as if this method call was synchronous - Thread.currentThread().interrupt(); - } - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/util/AffineMatrix.java b/server/src/main/java/com/genymobile/scrcpy/util/AffineMatrix.java deleted file mode 100644 index 0db74af6..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/util/AffineMatrix.java +++ /dev/null @@ -1,368 +0,0 @@ -package com.genymobile.scrcpy.util; - -import com.genymobile.scrcpy.device.Point; -import com.genymobile.scrcpy.device.Size; - -/** - * Represents a 2D affine transform (a 3x3 matrix): - * - *

- *     / a c e \
- *     | b d f |
- *     \ 0 0 1 /
- * 
- *

- * Or, a 4x4 matrix if we add a z axis: - * - *

- *     / a c 0 e \
- *     | b d 0 f |
- *     | 0 0 1 0 |
- *     \ 0 0 0 1 /
- * 
- */ -public class AffineMatrix { - - private final double a, b, c, d, e, f; - - /** - * The identity matrix. - */ - public static final AffineMatrix IDENTITY = new AffineMatrix(1, 0, 0, 1, 0, 0); - - /** - * Create a new matrix: - * - *
-     *     / a c e \
-     *     | b d f |
-     *     \ 0 0 1 /
-     * 
- */ - public AffineMatrix(double a, double b, double c, double d, double e, double f) { - this.a = a; - this.b = b; - this.c = c; - this.d = d; - this.e = e; - this.f = f; - } - - @Override - public String toString() { - return "[" + a + ", " + c + ", " + e + "; " + b + ", " + d + ", " + f + "]"; - } - - /** - * Return a matrix which converts from Normalized Device Coordinates to pixels. - * - * @param size the target size - * @return the transform matrix - */ - public static AffineMatrix ndcFromPixels(Size size) { - double w = size.getWidth(); - double h = size.getHeight(); - return new AffineMatrix(1 / w, 0, 0, -1 / h, 0, 1); - } - - /** - * Return a matrix which converts from pixels to Normalized Device Coordinates. - * - * @param size the source size - * @return the transform matrix - */ - public static AffineMatrix ndcToPixels(Size size) { - double w = size.getWidth(); - double h = size.getHeight(); - return new AffineMatrix(w, 0, 0, -h, 0, h); - } - - /** - * Apply the transform to a point ({@code this} should be a matrix converted to pixels coordinates via {@link #ndcToPixels(Size)}). - * - * @param point the source point - * @return the converted point - */ - public Point apply(Point point) { - int x = point.getX(); - int y = point.getY(); - int xx = (int) (a * x + c * y + e); - int yy = (int) (b * x + d * y + f); - return new Point(xx, yy); - } - - /** - * Compute this * rhs. - * - * @param rhs the matrix to multiply - * @return the product - */ - public AffineMatrix multiply(AffineMatrix rhs) { - if (rhs == null) { - // For convenience - return this; - } - - double aa = this.a * rhs.a + this.c * rhs.b; - double bb = this.b * rhs.a + this.d * rhs.b; - double cc = this.a * rhs.c + this.c * rhs.d; - double dd = this.b * rhs.c + this.d * rhs.d; - double ee = this.a * rhs.e + this.c * rhs.f + this.e; - double ff = this.b * rhs.e + this.d * rhs.f + this.f; - return new AffineMatrix(aa, bb, cc, dd, ee, ff); - } - - /** - * Multiply all matrices from left to right, ignoring any {@code null} matrix (for convenience). - * - * @param matrices the matrices - * @return the product - */ - public static AffineMatrix multiplyAll(AffineMatrix... matrices) { - AffineMatrix result = null; - for (AffineMatrix matrix : matrices) { - if (result == null) { - result = matrix; - } else { - result = result.multiply(matrix); - } - } - return result; - } - - /** - * Invert the matrix. - * - * @return the inverse matrix (or {@code null} if not invertible). - */ - public AffineMatrix invert() { - // The 3x3 matrix M can be decomposed into M = M1 * M2: - // M1 M2 - // / 1 0 e \ / a c 0 \ - // | 0 1 f | * | b d 0 | - // \ 0 0 1 / \ 0 0 1 / - // - // The inverse of an invertible 2x2 matrix is given by this formula: - // - // / A B \⁻¹ 1 / D -B \ - // \ C D / = ----- \ -C A / - // AD-BC - // - // Let B=c and C=b (to apply the general formula with the same letters). - // - // M⁻¹ = (M1 * M2)⁻¹ = M2⁻¹ * M1⁻¹ - // - // M2⁻¹ M1⁻¹ - // /----------------\ - // 1 / d -B 0 \ / 1 0 -e \ - // = ----- | -C a 0 | * | 0 1 -f | - // ad-BC \ 0 0 1 / \ 0 0 1 / - // - // With the original letters: - // - // 1 / d -c 0 \ / 1 0 -e \ - // M⁻¹ = ----- | -b a 0 | * | 0 1 -f | - // ad-cb \ 0 0 1 / \ 0 0 1 / - // - // 1 / d -c cf-de \ - // = ----- | -b a be-af | - // ad-cb \ 0 0 1 / - - double det = a * d - c * b; - if (det == 0) { - // Not invertible - return null; - } - - double aa = d / det; - double bb = -b / det; - double cc = -c / det; - double dd = a / det; - double ee = (c * f - d * e) / det; - double ff = (b * e - a * f) / det; - - return new AffineMatrix(aa, bb, cc, dd, ee, ff); - } - - /** - * Return this transform applied from the center (0.5, 0.5). - * - * @return the resulting matrix - */ - public AffineMatrix fromCenter() { - return translate(0.5, 0.5).multiply(this).multiply(translate(-0.5, -0.5)); - } - - /** - * Return this transform with the specified aspect ratio. - * - * @param ar the aspect ratio - * @return the resulting matrix - */ - public AffineMatrix withAspectRatio(double ar) { - return scale(1 / ar, 1).multiply(this).multiply(scale(ar, 1)); - } - - /** - * Return this transform with the specified aspect ratio. - * - * @param size the size describing the aspect ratio - * @return the transform - */ - public AffineMatrix withAspectRatio(Size size) { - double ar = (double) size.getWidth() / size.getHeight(); - return withAspectRatio(ar); - } - - /** - * Return a translation matrix. - * - * @param x the horizontal translation - * @param y the vertical translation - * @return the matrix - */ - public static AffineMatrix translate(double x, double y) { - return new AffineMatrix(1, 0, 0, 1, x, y); - } - - /** - * Return a scaling matrix. - * - * @param x the horizontal scaling - * @param y the vertical scaling - * @return the matrix - */ - public static AffineMatrix scale(double x, double y) { - return new AffineMatrix(x, 0, 0, y, 0, 0); - } - - /** - * Return a scaling matrix. - * - * @param from the source size - * @param to the destination size - * @return the matrix - */ - public static AffineMatrix scale(Size from, Size to) { - double scaleX = (double) to.getWidth() / from.getWidth(); - double scaleY = (double) to.getHeight() / from.getHeight(); - return scale(scaleX, scaleY); - } - - /** - * Return a matrix applying a "reframing" (cropping a rectangle). - *

- * (x, y) is the bottom-left corner, (w, h) is the size of the rectangle. - * - * @param x horizontal coordinate (increasing to the right) - * @param y vertical coordinate (increasing upwards) - * @param w width - * @param h height - * @return the matrix - */ - public static AffineMatrix reframe(double x, double y, double w, double h) { - if (w == 0 || h == 0) { - throw new IllegalArgumentException("Cannot reframe to an empty area: " + w + "x" + h); - } - return scale(1 / w, 1 / h).multiply(translate(-x, -y)); - } - - /** - * Return an orthogonal rotation matrix. - * - * @param ccwRotation the counter-clockwise rotation - * @return the matrix - */ - public static AffineMatrix rotateOrtho(int ccwRotation) { - switch (ccwRotation) { - case 0: - return IDENTITY; - case 1: - // 90° counter-clockwise - return new AffineMatrix(0, 1, -1, 0, 1, 0); - case 2: - // 180° - return new AffineMatrix(-1, 0, 0, -1, 1, 1); - case 3: - // 90° clockwise - return new AffineMatrix(0, -1, 1, 0, 0, 1); - default: - throw new IllegalArgumentException("Invalid rotation: " + ccwRotation); - } - } - - /** - * Return an horizontal flip matrix. - * - * @return the matrix - */ - public static AffineMatrix hflip() { - return new AffineMatrix(-1, 0, 0, 1, 1, 0); - } - - /** - * Return a vertical flip matrix. - * - * @return the matrix - */ - public static AffineMatrix vflip() { - return new AffineMatrix(1, 0, 0, -1, 0, 1); - } - - /** - * Return a rotation matrix. - * - * @param ccwDegrees the angle, in degrees (counter-clockwise) - * @return the matrix - */ - public static AffineMatrix rotate(double ccwDegrees) { - double radians = Math.toRadians(ccwDegrees); - double cos = Math.cos(radians); - double sin = Math.sin(radians); - return new AffineMatrix(cos, sin, -sin, cos, 0, 0); - } - - /** - * Export this affine transform to a 4x4 column-major order matrix. - * - * @param matrix output 4x4 matrix - */ - public void to4x4(float[] matrix) { - // matrix is a 4x4 matrix in column-major order - - // Column 0 - matrix[0] = (float) a; - matrix[1] = (float) b; - matrix[2] = 0; - matrix[3] = 0; - - // Column 1 - matrix[4] = (float) c; - matrix[5] = (float) d; - matrix[6] = 0; - matrix[7] = 0; - - // Column 2 - matrix[8] = 0; - matrix[9] = 0; - matrix[10] = 1; - matrix[11] = 0; - - // Column 3 - matrix[12] = (float) e; - matrix[13] = (float) f; - matrix[14] = 0; - matrix[15] = 1; - } - - /** - * Export this affine transform to a 4x4 column-major order matrix. - * - * @return 4x4 matrix - */ - public float[] to4x4() { - float[] matrix = new float[16]; - to4x4(matrix); - return matrix; - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/util/Binary.java b/server/src/main/java/com/genymobile/scrcpy/util/Binary.java deleted file mode 100644 index f46ba695..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/util/Binary.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.genymobile.scrcpy.util; - -public final class Binary { - private Binary() { - // not instantiable - } - - public static int toUnsigned(short value) { - return value & 0xffff; - } - - public static int toUnsigned(byte value) { - return value & 0xff; - } - - /** - * Convert unsigned 16-bit fixed-point to a float between 0 and 1 - * - * @param value encoded value - * @return Float value between 0 and 1 - */ - public static float u16FixedPointToFloat(short value) { - int unsignedShort = Binary.toUnsigned(value); - // 0x1p16f is 2^16 as float - return unsignedShort == 0xffff ? 1f : (unsignedShort / 0x1p16f); - } - - /** - * Convert signed 16-bit fixed-point to a float between -1 and 1 - * - * @param value encoded value - * @return Float value between -1 and 1 - */ - public static float i16FixedPointToFloat(short value) { - // 0x1p15f is 2^15 as float - return value == 0x7fff ? 1f : (value / 0x1p15f); - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/util/Codec.java b/server/src/main/java/com/genymobile/scrcpy/util/Codec.java deleted file mode 100644 index b350409b..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/util/Codec.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.genymobile.scrcpy.util; - -import android.media.MediaCodec; - -public interface Codec { - - enum Type { - VIDEO, - AUDIO, - } - - Type getType(); - - int getId(); - - String getName(); - - String getMimeType(); - - static String getMimeType(MediaCodec codec) { - String[] types = codec.getCodecInfo().getSupportedTypes(); - return types.length > 0 ? types[0] : null; - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/util/CodecOption.java b/server/src/main/java/com/genymobile/scrcpy/util/CodecOption.java deleted file mode 100644 index bed2be9a..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/util/CodecOption.java +++ /dev/null @@ -1,112 +0,0 @@ -package com.genymobile.scrcpy.util; - -import java.util.ArrayList; -import java.util.List; - -public class CodecOption { - private final String key; - private final 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); - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/util/CodecUtils.java b/server/src/main/java/com/genymobile/scrcpy/util/CodecUtils.java deleted file mode 100644 index 3a01256a..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/util/CodecUtils.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.genymobile.scrcpy.util; - -import android.media.MediaCodecInfo; -import android.media.MediaCodecList; -import android.media.MediaFormat; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -public final class CodecUtils { - - private CodecUtils() { - // not instantiable - } - - public static void setCodecOption(MediaFormat format, String key, Object value) { - if (value instanceof Integer) { - format.setInteger(key, (Integer) value); - } else if (value instanceof Long) { - format.setLong(key, (Long) value); - } else if (value instanceof Float) { - format.setFloat(key, (Float) value); - } else if (value instanceof String) { - format.setString(key, (String) value); - } - } - - public static MediaCodecInfo[] getEncoders(MediaCodecList codecs, String mimeType) { - List result = new ArrayList<>(); - for (MediaCodecInfo codecInfo : codecs.getCodecInfos()) { - if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(mimeType)) { - result.add(codecInfo); - } - } - return result.toArray(new MediaCodecInfo[result.size()]); - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/util/Command.java b/server/src/main/java/com/genymobile/scrcpy/util/Command.java deleted file mode 100644 index b26158e6..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/util/Command.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.genymobile.scrcpy.util; - -import java.io.IOException; -import java.util.Arrays; -import java.util.Scanner; - -public final class Command { - private Command() { - // not instantiable - } - - public static void exec(String... cmd) throws IOException, InterruptedException { - Process process = Runtime.getRuntime().exec(cmd); - int exitCode = process.waitFor(); - if (exitCode != 0) { - throw new IOException("Command " + Arrays.toString(cmd) + " returned with value " + exitCode); - } - } - - public static String execReadLine(String... cmd) throws IOException, InterruptedException { - String result = null; - Process process = Runtime.getRuntime().exec(cmd); - Scanner scanner = new Scanner(process.getInputStream()); - if (scanner.hasNextLine()) { - result = scanner.nextLine(); - } - int exitCode = process.waitFor(); - if (exitCode != 0) { - throw new IOException("Command " + Arrays.toString(cmd) + " returned with value " + exitCode); - } - return result; - } - - public static String execReadOutput(String... cmd) throws IOException, InterruptedException { - Process process = Runtime.getRuntime().exec(cmd); - String output = IO.toString(process.getInputStream()); - int exitCode = process.waitFor(); - if (exitCode != 0) { - throw new IOException("Command " + Arrays.toString(cmd) + " returned with value " + exitCode); - } - return output; - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/util/HandlerExecutor.java b/server/src/main/java/com/genymobile/scrcpy/util/HandlerExecutor.java deleted file mode 100644 index 03309989..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/util/HandlerExecutor.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.genymobile.scrcpy.util; - -import android.os.Handler; - -import java.util.concurrent.Executor; -import java.util.concurrent.RejectedExecutionException; - -// Inspired from hidden android.os.HandlerExecutor - -public class HandlerExecutor implements Executor { - private final Handler handler; - - public HandlerExecutor(Handler handler) { - this.handler = handler; - } - - @Override - public void execute(Runnable command) { - if (!handler.post(command)) { - throw new RejectedExecutionException(handler + " is shutting down"); - } - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/util/IO.java b/server/src/main/java/com/genymobile/scrcpy/util/IO.java deleted file mode 100644 index 16ddaedd..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/util/IO.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.genymobile.scrcpy.util; - -import com.genymobile.scrcpy.AndroidVersions; -import com.genymobile.scrcpy.BuildConfig; - -import android.os.Build; -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 - } - - private static int write(FileDescriptor fd, ByteBuffer from) throws IOException { - while (true) { - try { - return Os.write(fd, from); - } catch (ErrnoException e) { - if (e.errno != OsConstants.EINTR) { - throw new IOException(e); - } - } - } - } - - public static void writeFully(FileDescriptor fd, ByteBuffer from) throws IOException { - if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0) { - while (from.hasRemaining()) { - write(fd, from); - } - } else { - // ByteBuffer position is not updated as expected by Os.write() on old Android versions, so - // handle the position and the remaining bytes manually. - // See . - int position = from.position(); - int remaining = from.remaining(); - while (remaining > 0) { - int w = 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; - position += w; - from.position(position); - } - } - } - - public static void writeFully(FileDescriptor fd, byte[] buffer, int offset, int len) throws IOException { - writeFully(fd, ByteBuffer.wrap(buffer, offset, len)); - } - - public static String toString(InputStream inputStream) { - StringBuilder builder = new StringBuilder(); - Scanner scanner = new Scanner(inputStream); - while (scanner.hasNextLine()) { - builder.append(scanner.nextLine()).append('\n'); - } - return builder.toString(); - } - - public static boolean isBrokenPipe(IOException e) { - Throwable cause = e.getCause(); - return cause instanceof ErrnoException && ((ErrnoException) cause).errno == OsConstants.EPIPE; - } - - public static boolean isBrokenPipe(Exception e) { - return e instanceof IOException && isBrokenPipe((IOException) e); - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/util/Ln.java b/server/src/main/java/com/genymobile/scrcpy/util/Ln.java deleted file mode 100644 index c0700125..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/util/Ln.java +++ /dev/null @@ -1,118 +0,0 @@ -package com.genymobile.scrcpy.util; - -import android.util.Log; - -import java.io.FileDescriptor; -import java.io.FileOutputStream; -import java.io.OutputStream; -import java.io.PrintStream; - -/** - * Log both to Android logger (so that logs are visible in "adb logcat") and standard output/error (so that they are visible in the terminal - * directly). - */ -public final class Ln { - - private static final String TAG = "scrcpy"; - private static final String PREFIX = "[server] "; - - private static final PrintStream CONSOLE_OUT = new PrintStream(new FileOutputStream(FileDescriptor.out)); - private static final PrintStream CONSOLE_ERR = new PrintStream(new FileOutputStream(FileDescriptor.err)); - - public enum Level { - VERBOSE, DEBUG, INFO, WARN, ERROR - } - - private static Level threshold = Level.INFO; - - private Ln() { - // not instantiable - } - - public static void disableSystemStreams() { - PrintStream nullStream = new PrintStream(new NullOutputStream()); - System.setOut(nullStream); - System.setErr(nullStream); - } - - /** - * 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); - CONSOLE_OUT.print(PREFIX + "VERBOSE: " + message + '\n'); - } - } - - public static void d(String message) { - if (isEnabled(Level.DEBUG)) { - Log.d(TAG, message); - CONSOLE_OUT.print(PREFIX + "DEBUG: " + message + '\n'); - } - } - - public static void i(String message) { - if (isEnabled(Level.INFO)) { - Log.i(TAG, message); - CONSOLE_OUT.print(PREFIX + "INFO: " + message + '\n'); - } - } - - public static void w(String message, Throwable throwable) { - if (isEnabled(Level.WARN)) { - Log.w(TAG, message, throwable); - CONSOLE_ERR.print(PREFIX + "WARN: " + message + '\n'); - if (throwable != null) { - throwable.printStackTrace(CONSOLE_ERR); - } - } - } - - public static void w(String message) { - w(message, null); - } - - public static void e(String message, Throwable throwable) { - if (isEnabled(Level.ERROR)) { - Log.e(TAG, message, throwable); - CONSOLE_ERR.print(PREFIX + "ERROR: " + message + '\n'); - if (throwable != null) { - throwable.printStackTrace(CONSOLE_ERR); - } - } - } - - public static void e(String message) { - e(message, null); - } - - static class NullOutputStream extends OutputStream { - @Override - public void write(byte[] b) { - // ignore - } - - @Override - public void write(byte[] b, int off, int len) { - // ignore - } - - @Override - public void write(int b) { - // ignore - } - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java deleted file mode 100644 index 4f8927ec..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java +++ /dev/null @@ -1,268 +0,0 @@ -package com.genymobile.scrcpy.util; - -import com.genymobile.scrcpy.AndroidVersions; -import com.genymobile.scrcpy.audio.AudioCodec; -import com.genymobile.scrcpy.device.Device; -import com.genymobile.scrcpy.device.DeviceApp; -import com.genymobile.scrcpy.device.DisplayInfo; -import com.genymobile.scrcpy.device.Size; -import com.genymobile.scrcpy.video.VideoCodec; -import com.genymobile.scrcpy.wrappers.DisplayManager; -import com.genymobile.scrcpy.wrappers.ServiceManager; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.graphics.Rect; -import android.hardware.camera2.CameraAccessException; -import android.hardware.camera2.CameraCharacteristics; -import android.hardware.camera2.CameraManager; -import android.hardware.camera2.params.StreamConfigurationMap; -import android.media.MediaCodec; -import android.media.MediaCodecInfo; -import android.media.MediaCodecList; -import android.os.Build; -import android.util.Range; - -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.SortedSet; -import java.util.TreeSet; - -public final class LogUtils { - - private LogUtils() { - // not instantiable - } - - private static String buildEncoderListMessage(String type, Codec[] codecs) { - StringBuilder builder = new StringBuilder("List of ").append(type).append(" encoders:"); - MediaCodecList codecList = new MediaCodecList(MediaCodecList.REGULAR_CODECS); - for (Codec codec : codecs) { - MediaCodecInfo[] encoders = CodecUtils.getEncoders(codecList, codec.getMimeType()); - for (MediaCodecInfo info : encoders) { - int lineStart = builder.length(); - builder.append("\n --").append(type).append("-codec=").append(codec.getName()); - builder.append(" --").append(type).append("-encoder=").append(info.getName()); - if (Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10) { - int lineLength = builder.length() - lineStart; - final int column = 70; - if (lineLength < column) { - int padding = column - lineLength; - builder.append(String.format("%" + padding + "s", " ")); - } - builder.append(" (").append(getHwCodecType(info)).append(')'); - if (info.isVendor()) { - builder.append(" [vendor]"); - } - if (info.isAlias()) { - builder.append(" (alias for ").append(info.getCanonicalName()).append(')'); - } - } - - } - } - - return builder.toString(); - } - - public static String buildVideoEncoderListMessage() { - return buildEncoderListMessage("video", VideoCodec.values()); - } - - public static String buildAudioEncoderListMessage() { - return buildEncoderListMessage("audio", AudioCodec.values()); - } - - @TargetApi(AndroidVersions.API_29_ANDROID_10) - private static String getHwCodecType(MediaCodecInfo info) { - if (info.isSoftwareOnly()) { - return "sw"; - } - if (info.isHardwareAccelerated()) { - return "hw"; - } - return "hybrid"; - } - - public static String buildDisplayListMessage() { - StringBuilder builder = new StringBuilder("List of displays:"); - DisplayManager displayManager = ServiceManager.getDisplayManager(); - int[] displayIds = displayManager.getDisplayIds(); - if (displayIds == null || displayIds.length == 0) { - builder.append("\n (none)"); - } else { - for (int id : displayIds) { - builder.append("\n --display-id=").append(id).append(" ("); - DisplayInfo displayInfo = displayManager.getDisplayInfo(id); - if (displayInfo != null) { - Size size = displayInfo.getSize(); - builder.append(size.getWidth()).append("x").append(size.getHeight()); - } else { - builder.append("size unknown"); - } - builder.append(")"); - } - } - return builder.toString(); - } - - private static String getCameraFacingName(int facing) { - switch (facing) { - case CameraCharacteristics.LENS_FACING_FRONT: - return "front"; - case CameraCharacteristics.LENS_FACING_BACK: - return "back"; - case CameraCharacteristics.LENS_FACING_EXTERNAL: - return "external"; - default: - return "unknown"; - } - } - - private static boolean isCameraBackwardCompatible(CameraCharacteristics characteristics) { - int[] capabilities = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES); - if (capabilities == null) { - return false; - } - - for (int capability : capabilities) { - if (capability == CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE) { - return true; - } - } - - return false; - } - - public static String buildCameraListMessage(boolean includeSizes) { - StringBuilder builder = new StringBuilder("List of cameras:"); - CameraManager cameraManager = ServiceManager.getCameraManager(); - try { - String[] cameraIds = cameraManager.getCameraIdList(); - if (cameraIds.length == 0) { - builder.append("\n (none)"); - } else { - for (String id : cameraIds) { - CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id); - - if (!isCameraBackwardCompatible(characteristics)) { - // Ignore depth cameras as suggested by official documentation - // - continue; - } - - builder.append("\n --camera-id=").append(id); - - int facing = characteristics.get(CameraCharacteristics.LENS_FACING); - builder.append(" (").append(getCameraFacingName(facing)).append(", "); - - Rect activeSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); - builder.append(activeSize.width()).append("x").append(activeSize.height()); - - try { - // Capture frame rates for low-FPS mode are the same for every resolution - Range[] lowFpsRanges = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); - if (lowFpsRanges != null) { - SortedSet uniqueLowFps = getUniqueSet(lowFpsRanges); - builder.append(", fps=").append(uniqueLowFps); - } - } catch (Exception e) { - // Some devices may provide invalid ranges, causing an IllegalArgumentException "lower must be less than or equal to upper" - Ln.w("Could not get available frame rates for camera " + id, e); - } - - builder.append(')'); - - if (includeSizes) { - StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); - - android.util.Size[] sizes = configs.getOutputSizes(MediaCodec.class); - if (sizes == null || sizes.length == 0) { - builder.append("\n (none)"); - } else { - for (android.util.Size size : sizes) { - builder.append("\n - ").append(size.getWidth()).append('x').append(size.getHeight()); - } - } - - android.util.Size[] highSpeedSizes = configs.getHighSpeedVideoSizes(); - if (highSpeedSizes != null && highSpeedSizes.length > 0) { - builder.append("\n High speed capture (--camera-high-speed):"); - for (android.util.Size size : highSpeedSizes) { - Range[] highFpsRanges = configs.getHighSpeedVideoFpsRanges(); - SortedSet uniqueHighFps = getUniqueSet(highFpsRanges); - builder.append("\n - ").append(size.getWidth()).append("x").append(size.getHeight()); - builder.append(" (fps=").append(uniqueHighFps).append(')'); - } - } - } - } - } - } catch (CameraAccessException e) { - builder.append("\n (access denied)"); - } - return builder.toString(); - } - - private static SortedSet getUniqueSet(Range[] ranges) { - SortedSet set = new TreeSet<>(); - for (Range range : ranges) { - set.add(range.getUpper()); - } - return set; - } - - - public static String buildAppListMessage() { - List apps = Device.listApps(); - return buildAppListMessage("List of apps:", apps); - } - - @SuppressLint("QueryPermissionsNeeded") - public static String buildAppListMessage(String title, List apps) { - StringBuilder builder = new StringBuilder(title); - - // Sort by: - // 1. system flag (system apps are before non-system apps) - // 2. name - // 3. package name - // Comparator.comparing() was introduced in API 24, so it cannot be used here to simplify the code - Collections.sort(apps, (thisApp, otherApp) -> { - // System apps first - int cmp = -Boolean.compare(thisApp.isSystem(), otherApp.isSystem()); - if (cmp != 0) { - return cmp; - } - - cmp = Objects.compare(thisApp.getName(), otherApp.getName(), String::compareTo); - if (cmp != 0) { - return cmp; - } - - return Objects.compare(thisApp.getPackageName(), otherApp.getPackageName(), String::compareTo); - }); - - final int column = 30; - for (DeviceApp app : apps) { - String name = app.getName(); - int padding = column - name.length(); - builder.append("\n "); - if (app.isSystem()) { - builder.append("* "); - } else { - builder.append("- "); - - } - builder.append(name); - if (padding > 0) { - builder.append(String.format("%" + padding + "s", " ")); - } else { - builder.append("\n ").append(String.format("%" + column + "s", " ")); - } - builder.append(" ").append(app.getPackageName()); - } - - return builder.toString(); - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/util/Settings.java b/server/src/main/java/com/genymobile/scrcpy/util/Settings.java deleted file mode 100644 index e6465525..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/util/Settings.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.genymobile.scrcpy.util; - -import com.genymobile.scrcpy.AndroidVersions; -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 <= AndroidVersions.API_30_ANDROID_11) { - // 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 <= AndroidVersions.API_30_ANDROID_11) { - // 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 <= AndroidVersions.API_30_ANDROID_11) { - // on Android >= 12, it always fails: - try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { - String oldValue = provider.getValue(table, key); - if (!value.equals(oldValue)) { - provider.putValue(table, key, value); - } - return oldValue; - } catch (SettingsException e) { - Ln.w("Could not get and put settings value via ContentProvider, fallback to settings process", e); - } - } - - String oldValue = getValue(table, key); - if (!value.equals(oldValue)) { - putValue(table, key, value); - } - return oldValue; - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/util/SettingsException.java b/server/src/main/java/com/genymobile/scrcpy/util/SettingsException.java deleted file mode 100644 index 87fa3884..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/util/SettingsException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.genymobile.scrcpy.util; - -public class SettingsException extends Exception { - private static String createMessage(String method, String table, String key, String value) { - return "Could not access settings: " + method + " " + table + " " + key + (value != null ? " " + value : ""); - } - - public SettingsException(String method, String table, String key, String value, Throwable cause) { - super(createMessage(method, table, key, value), cause); - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/video/CameraAspectRatio.java b/server/src/main/java/com/genymobile/scrcpy/video/CameraAspectRatio.java deleted file mode 100644 index bf1cba5d..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/video/CameraAspectRatio.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.genymobile.scrcpy.video; - -public final class CameraAspectRatio { - private static final float SENSOR = -1; - - private float ar; - - private CameraAspectRatio(float ar) { - this.ar = ar; - } - - public static CameraAspectRatio fromFloat(float ar) { - if (ar < 0) { - throw new IllegalArgumentException("Invalid aspect ratio: " + ar); - } - return new CameraAspectRatio(ar); - } - - public static CameraAspectRatio fromFraction(int w, int h) { - if (w <= 0 || h <= 0) { - throw new IllegalArgumentException("Invalid aspect ratio: " + w + ":" + h); - } - return new CameraAspectRatio((float) w / h); - } - - public static CameraAspectRatio sensorAspectRatio() { - return new CameraAspectRatio(SENSOR); - } - - public boolean isSensor() { - return ar == SENSOR; - } - - public float getAspectRatio() { - return ar; - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java deleted file mode 100644 index 0e147cb7..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java +++ /dev/null @@ -1,426 +0,0 @@ -package com.genymobile.scrcpy.video; - -import com.genymobile.scrcpy.AndroidVersions; -import com.genymobile.scrcpy.Options; -import com.genymobile.scrcpy.device.ConfigurationException; -import com.genymobile.scrcpy.device.Orientation; -import com.genymobile.scrcpy.device.Size; -import com.genymobile.scrcpy.opengl.AffineOpenGLFilter; -import com.genymobile.scrcpy.opengl.OpenGLFilter; -import com.genymobile.scrcpy.opengl.OpenGLRunner; -import com.genymobile.scrcpy.util.AffineMatrix; -import com.genymobile.scrcpy.util.HandlerExecutor; -import com.genymobile.scrcpy.util.Ln; -import com.genymobile.scrcpy.util.LogUtils; -import com.genymobile.scrcpy.wrappers.ServiceManager; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.graphics.Rect; -import android.hardware.camera2.CameraAccessException; -import android.hardware.camera2.CameraCaptureSession; -import android.hardware.camera2.CameraCharacteristics; -import android.hardware.camera2.CameraConstrainedHighSpeedCaptureSession; -import android.hardware.camera2.CameraDevice; -import android.hardware.camera2.CameraManager; -import android.hardware.camera2.CaptureFailure; -import android.hardware.camera2.CaptureRequest; -import android.hardware.camera2.params.OutputConfiguration; -import android.hardware.camera2.params.SessionConfiguration; -import android.hardware.camera2.params.StreamConfigurationMap; -import android.media.MediaCodec; -import android.os.Handler; -import android.os.HandlerThread; -import android.util.Range; -import android.view.Surface; - -import java.io.IOException; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executor; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.stream.Stream; - -public class CameraCapture extends SurfaceCapture { - - public static final float[] VFLIP_MATRIX = { - 1, 0, 0, 0, // column 1 - 0, -1, 0, 0, // column 2 - 0, 0, 1, 0, // column 3 - 0, 1, 0, 1, // column 4 - }; - - private final String explicitCameraId; - private final CameraFacing cameraFacing; - private final Size explicitSize; - private int maxSize; - private final CameraAspectRatio aspectRatio; - private final int fps; - private final boolean highSpeed; - private final Rect crop; - private final Orientation captureOrientation; - private final float angle; - - private String cameraId; - private Size captureSize; - private Size videoSize; // after OpenGL transforms - - private AffineMatrix transform; - private OpenGLRunner glRunner; - - private HandlerThread cameraThread; - private Handler cameraHandler; - private CameraDevice cameraDevice; - private Executor cameraExecutor; - - private final AtomicBoolean disconnected = new AtomicBoolean(); - - public CameraCapture(Options options) { - this.explicitCameraId = options.getCameraId(); - this.cameraFacing = options.getCameraFacing(); - this.explicitSize = options.getCameraSize(); - this.maxSize = options.getMaxSize(); - this.aspectRatio = options.getCameraAspectRatio(); - this.fps = options.getCameraFps(); - this.highSpeed = options.getCameraHighSpeed(); - this.crop = options.getCrop(); - this.captureOrientation = options.getCaptureOrientation(); - assert captureOrientation != null; - this.angle = options.getAngle(); - } - - @Override - protected void init() throws ConfigurationException, IOException { - cameraThread = new HandlerThread("camera"); - cameraThread.start(); - cameraHandler = new Handler(cameraThread.getLooper()); - cameraExecutor = new HandlerExecutor(cameraHandler); - - try { - cameraId = selectCamera(explicitCameraId, cameraFacing); - if (cameraId == null) { - throw new ConfigurationException("No matching camera found"); - } - - Ln.i("Using camera '" + cameraId + "'"); - cameraDevice = openCamera(cameraId); - } catch (CameraAccessException | InterruptedException e) { - throw new IOException(e); - } - } - - @Override - public void prepare() throws IOException { - try { - captureSize = selectSize(cameraId, explicitSize, maxSize, aspectRatio, highSpeed); - if (captureSize == null) { - throw new IOException("Could not select camera size"); - } - } catch (CameraAccessException e) { - throw new IOException(e); - } - - VideoFilter filter = new VideoFilter(captureSize); - - if (crop != null) { - filter.addCrop(crop, false); - } - - if (captureOrientation != Orientation.Orient0) { - filter.addOrientation(captureOrientation); - } - - filter.addAngle(angle); - - transform = filter.getInverseTransform(); - videoSize = filter.getOutputSize().limit(maxSize).round8(); - } - - private static String selectCamera(String explicitCameraId, CameraFacing cameraFacing) throws CameraAccessException, ConfigurationException { - CameraManager cameraManager = ServiceManager.getCameraManager(); - - String[] cameraIds = cameraManager.getCameraIdList(); - if (explicitCameraId != null) { - if (!Arrays.asList(cameraIds).contains(explicitCameraId)) { - Ln.e("Camera with id " + explicitCameraId + " not found\n" + LogUtils.buildCameraListMessage(false)); - throw new ConfigurationException("Camera id not found"); - } - return explicitCameraId; - } - - if (cameraFacing == null) { - // Use the first one - return cameraIds.length > 0 ? cameraIds[0] : null; - } - - for (String cameraId : cameraIds) { - CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId); - - int facing = characteristics.get(CameraCharacteristics.LENS_FACING); - if (cameraFacing.value() == facing) { - return cameraId; - } - } - - // Not found - return null; - } - - @TargetApi(AndroidVersions.API_24_ANDROID_7_0) - private static Size selectSize(String cameraId, Size explicitSize, int maxSize, CameraAspectRatio aspectRatio, boolean highSpeed) - throws CameraAccessException { - if (explicitSize != null) { - return explicitSize; - } - - CameraManager cameraManager = ServiceManager.getCameraManager(); - CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId); - - StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); - android.util.Size[] sizes = highSpeed ? configs.getHighSpeedVideoSizes() : configs.getOutputSizes(MediaCodec.class); - if (sizes == null) { - return null; - } - - Stream stream = Arrays.stream(sizes); - if (maxSize > 0) { - stream = stream.filter(it -> it.getWidth() <= maxSize && it.getHeight() <= maxSize); - } - - Float targetAspectRatio = resolveAspectRatio(aspectRatio, characteristics); - if (targetAspectRatio != null) { - stream = stream.filter(it -> { - float ar = ((float) it.getWidth() / it.getHeight()); - float arRatio = ar / targetAspectRatio; - // Accept if the aspect ratio is the target aspect ratio + or - 10% - return arRatio >= 0.9f && arRatio <= 1.1f; - }); - } - - Optional selected = stream.max((s1, s2) -> { - // Greater width is better - int cmp = Integer.compare(s1.getWidth(), s2.getWidth()); - if (cmp != 0) { - return cmp; - } - - if (targetAspectRatio != null) { - // Closer to the target aspect ratio is better - float ar1 = ((float) s1.getWidth() / s1.getHeight()); - float arRatio1 = ar1 / targetAspectRatio; - float distance1 = Math.abs(1 - arRatio1); - - float ar2 = ((float) s2.getWidth() / s2.getHeight()); - float arRatio2 = ar2 / targetAspectRatio; - float distance2 = Math.abs(1 - arRatio2); - - // Reverse the order because lower distance is better - cmp = Float.compare(distance2, distance1); - if (cmp != 0) { - return cmp; - } - } - - // Greater height is better - return Integer.compare(s1.getHeight(), s2.getHeight()); - }); - - if (selected.isPresent()) { - android.util.Size size = selected.get(); - return new Size(size.getWidth(), size.getHeight()); - } - - // Not found - return null; - } - - private static Float resolveAspectRatio(CameraAspectRatio ratio, CameraCharacteristics characteristics) { - if (ratio == null) { - return null; - } - - if (ratio.isSensor()) { - Rect activeSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); - return (float) activeSize.width() / activeSize.height(); - } - - return ratio.getAspectRatio(); - } - - @Override - public void start(Surface surface) throws IOException { - if (transform != null) { - assert glRunner == null; - OpenGLFilter glFilter = new AffineOpenGLFilter(transform); - // The transform matrix returned by SurfaceTexture is incorrect for camera capture (it often contains an additional unexpected 90° - // rotation). Use a vertical flip transform matrix instead. - glRunner = new OpenGLRunner(glFilter, VFLIP_MATRIX); - surface = glRunner.start(captureSize, videoSize, surface); - } - - try { - CameraCaptureSession session = createCaptureSession(cameraDevice, surface); - CaptureRequest request = createCaptureRequest(surface); - setRepeatingRequest(session, request); - } catch (CameraAccessException | InterruptedException e) { - stop(); - throw new IOException(e); - } - } - - @Override - public void stop() { - if (glRunner != null) { - glRunner.stopAndRelease(); - glRunner = null; - } - } - - @Override - public void release() { - if (cameraDevice != null) { - cameraDevice.close(); - } - if (cameraThread != null) { - cameraThread.quitSafely(); - } - } - - @Override - public Size getSize() { - return videoSize; - } - - @Override - public boolean setMaxSize(int maxSize) { - if (explicitSize != null) { - return false; - } - - this.maxSize = maxSize; - return true; - } - - @SuppressLint("MissingPermission") - @TargetApi(AndroidVersions.API_31_ANDROID_12) - private CameraDevice openCamera(String id) throws CameraAccessException, InterruptedException { - CompletableFuture future = new CompletableFuture<>(); - ServiceManager.getCameraManager().openCamera(id, new CameraDevice.StateCallback() { - @Override - public void onOpened(CameraDevice camera) { - Ln.d("Camera opened successfully"); - future.complete(camera); - } - - @Override - public void onDisconnected(CameraDevice camera) { - Ln.w("Camera disconnected"); - disconnected.set(true); - invalidate(); - } - - @Override - public void onError(CameraDevice camera, int error) { - int cameraAccessExceptionErrorCode; - switch (error) { - case CameraDevice.StateCallback.ERROR_CAMERA_IN_USE: - cameraAccessExceptionErrorCode = CameraAccessException.CAMERA_IN_USE; - break; - case CameraDevice.StateCallback.ERROR_MAX_CAMERAS_IN_USE: - cameraAccessExceptionErrorCode = CameraAccessException.MAX_CAMERAS_IN_USE; - break; - case CameraDevice.StateCallback.ERROR_CAMERA_DISABLED: - cameraAccessExceptionErrorCode = CameraAccessException.CAMERA_DISABLED; - break; - case CameraDevice.StateCallback.ERROR_CAMERA_DEVICE: - case CameraDevice.StateCallback.ERROR_CAMERA_SERVICE: - default: - cameraAccessExceptionErrorCode = CameraAccessException.CAMERA_ERROR; - break; - } - future.completeExceptionally(new CameraAccessException(cameraAccessExceptionErrorCode)); - } - }, cameraHandler); - - try { - return future.get(); - } catch (ExecutionException e) { - throw (CameraAccessException) e.getCause(); - } - } - - @TargetApi(AndroidVersions.API_31_ANDROID_12) - private CameraCaptureSession createCaptureSession(CameraDevice camera, Surface surface) throws CameraAccessException, InterruptedException { - CompletableFuture future = new CompletableFuture<>(); - OutputConfiguration outputConfig = new OutputConfiguration(surface); - List outputs = Arrays.asList(outputConfig); - - int sessionType = highSpeed ? SessionConfiguration.SESSION_HIGH_SPEED : SessionConfiguration.SESSION_REGULAR; - SessionConfiguration sessionConfig = new SessionConfiguration(sessionType, outputs, cameraExecutor, new CameraCaptureSession.StateCallback() { - @Override - public void onConfigured(CameraCaptureSession session) { - future.complete(session); - } - - @Override - public void onConfigureFailed(CameraCaptureSession session) { - future.completeExceptionally(new CameraAccessException(CameraAccessException.CAMERA_ERROR)); - } - }); - - camera.createCaptureSession(sessionConfig); - - try { - return future.get(); - } catch (ExecutionException e) { - throw (CameraAccessException) e.getCause(); - } - } - - private CaptureRequest createCaptureRequest(Surface surface) throws CameraAccessException { - CaptureRequest.Builder requestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD); - requestBuilder.addTarget(surface); - - if (fps > 0) { - requestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, new Range<>(fps, fps)); - } - - return requestBuilder.build(); - } - - @TargetApi(AndroidVersions.API_31_ANDROID_12) - private void setRepeatingRequest(CameraCaptureSession session, CaptureRequest request) throws CameraAccessException, InterruptedException { - CameraCaptureSession.CaptureCallback callback = new CameraCaptureSession.CaptureCallback() { - @Override - public void onCaptureStarted(CameraCaptureSession session, CaptureRequest request, long timestamp, long frameNumber) { - // Called for each frame captured, do nothing - } - - @Override - public void onCaptureFailed(CameraCaptureSession session, CaptureRequest request, CaptureFailure failure) { - Ln.w("Camera capture failed: frame " + failure.getFrameNumber()); - } - }; - - if (highSpeed) { - CameraConstrainedHighSpeedCaptureSession highSpeedSession = (CameraConstrainedHighSpeedCaptureSession) session; - List requests = highSpeedSession.createHighSpeedRequestList(request); - highSpeedSession.setRepeatingBurst(requests, callback, cameraHandler); - } else { - session.setRepeatingRequest(request, callback, cameraHandler); - } - } - - @Override - public boolean isClosed() { - return disconnected.get(); - } - - @Override - public void requestInvalidate() { - // do nothing (the user could not request a reset anyway for now, since there is no controller for camera mirroring) - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/video/CameraFacing.java b/server/src/main/java/com/genymobile/scrcpy/video/CameraFacing.java deleted file mode 100644 index f818e665..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/video/CameraFacing.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.genymobile.scrcpy.video; - -import android.annotation.SuppressLint; -import android.hardware.camera2.CameraCharacteristics; - -public enum CameraFacing { - FRONT("front", CameraCharacteristics.LENS_FACING_FRONT), - BACK("back", CameraCharacteristics.LENS_FACING_BACK), - @SuppressLint("InlinedApi") // introduced in API 23 - EXTERNAL("external", CameraCharacteristics.LENS_FACING_EXTERNAL); - - private final String name; - private final int value; - - CameraFacing(String name, int value) { - this.name = name; - this.value = value; - } - - int value() { - return value; - } - - public static CameraFacing findByName(String name) { - for (CameraFacing facing : CameraFacing.values()) { - if (name.equals(facing.name)) { - return facing; - } - } - - return null; - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/video/CaptureReset.java b/server/src/main/java/com/genymobile/scrcpy/video/CaptureReset.java deleted file mode 100644 index 79d32d7c..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/video/CaptureReset.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.genymobile.scrcpy.video; - -import android.media.MediaCodec; - -import java.util.concurrent.atomic.AtomicBoolean; - -public class CaptureReset implements SurfaceCapture.CaptureListener { - - private final AtomicBoolean reset = new AtomicBoolean(); - - // Current instance of MediaCodec to "interrupt" on reset - private MediaCodec runningMediaCodec; - - public boolean consumeReset() { - return reset.getAndSet(false); - } - - public synchronized void reset() { - reset.set(true); - if (runningMediaCodec != null) { - try { - runningMediaCodec.signalEndOfInputStream(); - } catch (IllegalStateException e) { - // ignore - } - } - } - - public synchronized void setRunningMediaCodec(MediaCodec runningMediaCodec) { - this.runningMediaCodec = runningMediaCodec; - } - - @Override - public void onInvalidated() { - reset(); - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/video/DisplaySizeMonitor.java b/server/src/main/java/com/genymobile/scrcpy/video/DisplaySizeMonitor.java deleted file mode 100644 index 3d7cccfe..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/video/DisplaySizeMonitor.java +++ /dev/null @@ -1,141 +0,0 @@ -package com.genymobile.scrcpy.video; - -import com.genymobile.scrcpy.AndroidVersions; -import com.genymobile.scrcpy.device.Device; -import com.genymobile.scrcpy.device.DisplayInfo; -import com.genymobile.scrcpy.device.Size; -import com.genymobile.scrcpy.util.Ln; -import com.genymobile.scrcpy.wrappers.DisplayManager; -import com.genymobile.scrcpy.wrappers.DisplayWindowListener; -import com.genymobile.scrcpy.wrappers.ServiceManager; - -import android.content.res.Configuration; -import android.os.Build; -import android.os.Handler; -import android.os.HandlerThread; -import android.view.IDisplayWindowListener; - -public class DisplaySizeMonitor { - - public interface Listener { - void onDisplaySizeChanged(); - } - - // On Android 14, DisplayListener may be broken (it never sends events). This is fixed in recent Android 14 upgrades, but we can't really - // detect it directly, so register a DisplayWindowListener (introduced in Android 11) to listen to configuration changes instead. - // It has been broken again after an Android 15 upgrade: - // So use the default method only before Android 14. - private static final boolean USE_DEFAULT_METHOD = Build.VERSION.SDK_INT < AndroidVersions.API_34_ANDROID_14; - - private DisplayManager.DisplayListenerHandle displayListenerHandle; - private HandlerThread handlerThread; - - private IDisplayWindowListener displayWindowListener; - - private int displayId = Device.DISPLAY_ID_NONE; - - private Size sessionDisplaySize; - - private Listener listener; - - public void start(int displayId, Listener listener) { - // Once started, the listener and the displayId must never change - assert listener != null; - this.listener = listener; - - assert this.displayId == Device.DISPLAY_ID_NONE; - this.displayId = displayId; - - if (USE_DEFAULT_METHOD) { - handlerThread = new HandlerThread("DisplayListener"); - handlerThread.start(); - Handler handler = new Handler(handlerThread.getLooper()); - displayListenerHandle = ServiceManager.getDisplayManager().registerDisplayListener(eventDisplayId -> { - if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Ln.v("DisplaySizeMonitor: onDisplayChanged(" + eventDisplayId + ")"); - } - - if (eventDisplayId == displayId) { - checkDisplaySizeChanged(); - } - }, handler); - } else { - displayWindowListener = new DisplayWindowListener() { - @Override - public void onDisplayConfigurationChanged(int eventDisplayId, Configuration newConfig) { - if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Ln.v("DisplaySizeMonitor: onDisplayConfigurationChanged(" + eventDisplayId + ")"); - } - - if (eventDisplayId == displayId) { - checkDisplaySizeChanged(); - } - } - }; - ServiceManager.getWindowManager().registerDisplayWindowListener(displayWindowListener); - } - } - - /** - * Stop and release the monitor. - *

- * It must not be used anymore. - * It is ok to call this method even if {@link #start(int, Listener)} was not called. - */ - public void stopAndRelease() { - if (USE_DEFAULT_METHOD) { - // displayListenerHandle may be null if registration failed - if (displayListenerHandle != null) { - ServiceManager.getDisplayManager().unregisterDisplayListener(displayListenerHandle); - displayListenerHandle = null; - } - - if (handlerThread != null) { - handlerThread.quitSafely(); - } - } else if (displayWindowListener != null) { - ServiceManager.getWindowManager().unregisterDisplayWindowListener(displayWindowListener); - } - } - - private synchronized Size getSessionDisplaySize() { - return sessionDisplaySize; - } - - public synchronized void setSessionDisplaySize(Size sessionDisplaySize) { - this.sessionDisplaySize = sessionDisplaySize; - } - - private void checkDisplaySizeChanged() { - DisplayInfo di = ServiceManager.getDisplayManager().getDisplayInfo(displayId); - if (di == null) { - Ln.w("DisplayInfo for " + displayId + " cannot be retrieved"); - // We can't compare with the current size, so reset unconditionally - if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Ln.v("DisplaySizeMonitor: requestReset(): " + getSessionDisplaySize() + " -> (unknown)"); - } - setSessionDisplaySize(null); - listener.onDisplaySizeChanged(); - } else { - Size size = di.getSize(); - - // The field is hidden on purpose, to read it with synchronization - @SuppressWarnings("checkstyle:HiddenField") - Size sessionDisplaySize = getSessionDisplaySize(); // synchronized - - // .equals() also works if sessionDisplaySize == null - if (!size.equals(sessionDisplaySize)) { - // Reset only if the size is different - if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Ln.v("DisplaySizeMonitor: requestReset(): " + sessionDisplaySize + " -> " + size); - } - // Set the new size immediately, so that a future onDisplayChanged() event called before the asynchronous prepare() - // considers that the current size is the requested size (to avoid a duplicate requestReset()) - setSessionDisplaySize(size); - listener.onDisplaySizeChanged(); - } else if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Ln.v("DisplaySizeMonitor: Size not changed (" + size + "): do not requestReset()"); - } - } - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java deleted file mode 100644 index 792b3a8a..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ /dev/null @@ -1,267 +0,0 @@ -package com.genymobile.scrcpy.video; - -import com.genymobile.scrcpy.AndroidVersions; -import com.genymobile.scrcpy.Options; -import com.genymobile.scrcpy.control.PositionMapper; -import com.genymobile.scrcpy.device.DisplayInfo; -import com.genymobile.scrcpy.device.NewDisplay; -import com.genymobile.scrcpy.device.Orientation; -import com.genymobile.scrcpy.device.Size; -import com.genymobile.scrcpy.opengl.AffineOpenGLFilter; -import com.genymobile.scrcpy.opengl.OpenGLFilter; -import com.genymobile.scrcpy.opengl.OpenGLRunner; -import com.genymobile.scrcpy.util.AffineMatrix; -import com.genymobile.scrcpy.util.Ln; -import com.genymobile.scrcpy.wrappers.ServiceManager; - -import android.graphics.Rect; -import android.hardware.display.VirtualDisplay; -import android.os.Build; -import android.view.Surface; - -import java.io.IOException; - -public class NewDisplayCapture extends SurfaceCapture { - - // Internal fields copied from android.hardware.display.DisplayManager - private static final int VIRTUAL_DISPLAY_FLAG_PUBLIC = android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC; - private static final int VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY = android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY; - private static final int VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH = 1 << 6; - private static final int VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT = 1 << 7; - private static final int VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL = 1 << 8; - private static final int VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS = 1 << 9; - private static final int VIRTUAL_DISPLAY_FLAG_TRUSTED = 1 << 10; - private static final int VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP = 1 << 11; - private static final int VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED = 1 << 12; - private static final int VIRTUAL_DISPLAY_FLAG_TOUCH_FEEDBACK_DISABLED = 1 << 13; - private static final int VIRTUAL_DISPLAY_FLAG_OWN_FOCUS = 1 << 14; - private static final int VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP = 1 << 15; - - private final VirtualDisplayListener vdListener; - private final NewDisplay newDisplay; - - private final DisplaySizeMonitor displaySizeMonitor = new DisplaySizeMonitor(); - - private AffineMatrix displayTransform; - private AffineMatrix eventTransform; - private OpenGLRunner glRunner; - - private Size mainDisplaySize; - private int mainDisplayDpi; - private int maxSize; - private int displayImePolicy; - private final Rect crop; - private final boolean captureOrientationLocked; - private final Orientation captureOrientation; - private final float angle; - private final boolean vdDestroyContent; - private final boolean vdSystemDecorations; - - private VirtualDisplay virtualDisplay; - private Size videoSize; - private Size displaySize; // the logical size of the display (including rotation) - private Size physicalSize; // the physical size of the display (without rotation) - - private int dpi; - - public NewDisplayCapture(VirtualDisplayListener vdListener, Options options) { - this.vdListener = vdListener; - this.newDisplay = options.getNewDisplay(); - assert newDisplay != null; - this.maxSize = options.getMaxSize(); - this.displayImePolicy = options.getDisplayImePolicy(); - this.crop = options.getCrop(); - assert options.getCaptureOrientationLock() != null; - this.captureOrientationLocked = options.getCaptureOrientationLock() != Orientation.Lock.Unlocked; - this.captureOrientation = options.getCaptureOrientation(); - assert captureOrientation != null; - this.angle = options.getAngle(); - this.vdDestroyContent = options.getVDDestroyContent(); - this.vdSystemDecorations = options.getVDSystemDecorations(); - } - - @Override - protected void init() { - displaySize = newDisplay.getSize(); - dpi = newDisplay.getDpi(); - if (displaySize == null || dpi == 0) { - DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(0); - if (displayInfo != null) { - mainDisplaySize = displayInfo.getSize(); - if ((displayInfo.getRotation() % 2) != 0) { - mainDisplaySize = mainDisplaySize.rotate(); // Use the natural device orientation (at rotation 0), not the current one - } - mainDisplayDpi = displayInfo.getDpi(); - } else { - Ln.w("Main display not found, fallback to 1920x1080 240dpi"); - mainDisplaySize = new Size(1920, 1080); - mainDisplayDpi = 240; - } - } - } - - @Override - public void prepare() { - int displayRotation; - if (virtualDisplay == null) { - if (!newDisplay.hasExplicitSize()) { - displaySize = mainDisplaySize; - } - if (!newDisplay.hasExplicitDpi()) { - dpi = scaleDpi(mainDisplaySize, mainDisplayDpi, displaySize); - } - - videoSize = displaySize; - displayRotation = 0; - // Set the current display size to avoid an unnecessary call to invalidate() - displaySizeMonitor.setSessionDisplaySize(displaySize); - } else { - DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(virtualDisplay.getDisplay().getDisplayId()); - displaySize = displayInfo.getSize(); - dpi = displayInfo.getDpi(); - displayRotation = displayInfo.getRotation(); - } - - VideoFilter filter = new VideoFilter(displaySize); - - if (crop != null) { - boolean transposed = (displayRotation % 2) != 0; - filter.addCrop(crop, transposed); - } - - filter.addOrientation(displayRotation, captureOrientationLocked, captureOrientation); - filter.addAngle(angle); - - Size filteredSize = filter.getOutputSize(); - if (!filteredSize.isMultipleOf8() || (maxSize != 0 && filteredSize.getMax() > maxSize)) { - if (maxSize != 0) { - filteredSize = filteredSize.limit(maxSize); - } - filteredSize = filteredSize.round8(); - filter.addResize(filteredSize); - } - - eventTransform = filter.getInverseTransform(); - - // DisplayInfo gives the oriented size (so videoSize includes the display rotation) - videoSize = filter.getOutputSize(); - - // But the virtual display video always remains in the origin orientation (the video itself is not rotated, so it must rotated manually). - // This additional display rotation must not be included in the input events transform (the expected coordinates are already in the - // physical display size) - if ((displayRotation % 2) == 0) { - physicalSize = displaySize; - } else { - physicalSize = displaySize.rotate(); - } - VideoFilter displayFilter = new VideoFilter(physicalSize); - displayFilter.addRotation(displayRotation); - AffineMatrix displayRotationMatrix = displayFilter.getInverseTransform(); - - // Take care of multiplication order: - // displayTransform = (FILTER_MATRIX * DISPLAY_FILTER_MATRIX)⁻¹ - // = DISPLAY_FILTER_MATRIX⁻¹ * FILTER_MATRIX⁻¹ - // = displayRotationMatrix * eventTransform - displayTransform = AffineMatrix.multiplyAll(displayRotationMatrix, eventTransform); - } - - public void startNew(Surface surface) { - int virtualDisplayId; - try { - int flags = VIRTUAL_DISPLAY_FLAG_PUBLIC - | VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY - | VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH - | VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT; - if (vdDestroyContent) { - flags |= VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL; - } - if (vdSystemDecorations) { - flags |= VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS; - } - if (Build.VERSION.SDK_INT >= AndroidVersions.API_33_ANDROID_13) { - flags |= VIRTUAL_DISPLAY_FLAG_TRUSTED - | VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP - | VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED - | VIRTUAL_DISPLAY_FLAG_TOUCH_FEEDBACK_DISABLED; - if (Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14) { - flags |= VIRTUAL_DISPLAY_FLAG_OWN_FOCUS - | VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP; - } - } - virtualDisplay = ServiceManager.getDisplayManager() - .createNewVirtualDisplay("scrcpy", displaySize.getWidth(), displaySize.getHeight(), dpi, surface, flags); - virtualDisplayId = virtualDisplay.getDisplay().getDisplayId(); - Ln.i("New display: " + displaySize.getWidth() + "x" + displaySize.getHeight() + "/" + dpi + " (id=" + virtualDisplayId + ")"); - - if (displayImePolicy != -1) { - ServiceManager.getWindowManager().setDisplayImePolicy(virtualDisplayId, displayImePolicy); - } - - displaySizeMonitor.start(virtualDisplayId, this::invalidate); - } catch (Exception e) { - Ln.e("Could not create display", e); - throw new AssertionError("Could not create display"); - } - } - - @Override - public void start(Surface surface) throws IOException { - if (displayTransform != null) { - assert glRunner == null; - OpenGLFilter glFilter = new AffineOpenGLFilter(displayTransform); - glRunner = new OpenGLRunner(glFilter); - surface = glRunner.start(physicalSize, videoSize, surface); - } - - if (virtualDisplay == null) { - startNew(surface); - } else { - virtualDisplay.setSurface(surface); - } - - if (vdListener != null) { - PositionMapper positionMapper = PositionMapper.create(videoSize, eventTransform, displaySize); - vdListener.onNewVirtualDisplay(virtualDisplay.getDisplay().getDisplayId(), positionMapper); - } - } - - @Override - public void stop() { - if (glRunner != null) { - glRunner.stopAndRelease(); - glRunner = null; - } - } - - @Override - public void release() { - displaySizeMonitor.stopAndRelease(); - - if (virtualDisplay != null) { - virtualDisplay.release(); - virtualDisplay = null; - } - } - - @Override - public synchronized Size getSize() { - return videoSize; - } - - @Override - public synchronized boolean setMaxSize(int newMaxSize) { - maxSize = newMaxSize; - return true; - } - - private static int scaleDpi(Size initialSize, int initialDpi, Size size) { - int den = initialSize.getMax(); - int num = size.getMax(); - return initialDpi * num / den; - } - - @Override - public void requestInvalidate() { - invalidate(); - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java deleted file mode 100644 index 5f4e1803..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ /dev/null @@ -1,219 +0,0 @@ -package com.genymobile.scrcpy.video; - -import com.genymobile.scrcpy.AndroidVersions; -import com.genymobile.scrcpy.Options; -import com.genymobile.scrcpy.control.PositionMapper; -import com.genymobile.scrcpy.device.ConfigurationException; -import com.genymobile.scrcpy.device.Device; -import com.genymobile.scrcpy.device.DisplayInfo; -import com.genymobile.scrcpy.device.Orientation; -import com.genymobile.scrcpy.device.Size; -import com.genymobile.scrcpy.opengl.AffineOpenGLFilter; -import com.genymobile.scrcpy.opengl.OpenGLFilter; -import com.genymobile.scrcpy.opengl.OpenGLRunner; -import com.genymobile.scrcpy.util.AffineMatrix; -import com.genymobile.scrcpy.util.Ln; -import com.genymobile.scrcpy.util.LogUtils; -import com.genymobile.scrcpy.wrappers.ServiceManager; -import com.genymobile.scrcpy.wrappers.SurfaceControl; - -import android.graphics.Rect; -import android.hardware.display.VirtualDisplay; -import android.os.Build; -import android.os.IBinder; -import android.view.Surface; - -import java.io.IOException; - -public class ScreenCapture extends SurfaceCapture { - - private final VirtualDisplayListener vdListener; - private final int displayId; - private int maxSize; - private final Rect crop; - private Orientation.Lock captureOrientationLock; - private Orientation captureOrientation; - private final float angle; - - private DisplayInfo displayInfo; - private Size videoSize; - - private final DisplaySizeMonitor displaySizeMonitor = new DisplaySizeMonitor(); - - private IBinder display; - private VirtualDisplay virtualDisplay; - - private AffineMatrix transform; - private OpenGLRunner glRunner; - - public ScreenCapture(VirtualDisplayListener vdListener, Options options) { - this.vdListener = vdListener; - this.displayId = options.getDisplayId(); - assert displayId != Device.DISPLAY_ID_NONE; - this.maxSize = options.getMaxSize(); - this.crop = options.getCrop(); - this.captureOrientationLock = options.getCaptureOrientationLock(); - this.captureOrientation = options.getCaptureOrientation(); - assert captureOrientationLock != null; - assert captureOrientation != null; - this.angle = options.getAngle(); - } - - @Override - public void init() { - displaySizeMonitor.start(displayId, this::invalidate); - } - - @Override - public void prepare() throws ConfigurationException { - displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId); - if (displayInfo == null) { - Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage()); - throw new ConfigurationException("Unknown display id: " + displayId); - } - - if ((displayInfo.getFlags() & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) { - Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted"); - } - - Size displaySize = displayInfo.getSize(); - displaySizeMonitor.setSessionDisplaySize(displaySize); - - if (captureOrientationLock == Orientation.Lock.LockedInitial) { - // The user requested to lock the video orientation to the current orientation - captureOrientationLock = Orientation.Lock.LockedValue; - captureOrientation = Orientation.fromRotation(displayInfo.getRotation()); - } - - VideoFilter filter = new VideoFilter(displaySize); - - if (crop != null) { - boolean transposed = (displayInfo.getRotation() % 2) != 0; - filter.addCrop(crop, transposed); - } - - boolean locked = captureOrientationLock != Orientation.Lock.Unlocked; - filter.addOrientation(displayInfo.getRotation(), locked, captureOrientation); - filter.addAngle(angle); - - transform = filter.getInverseTransform(); - videoSize = filter.getOutputSize().limit(maxSize).round8(); - } - - @Override - public void start(Surface surface) throws IOException { - if (display != null) { - SurfaceControl.destroyDisplay(display); - display = null; - } - if (virtualDisplay != null) { - virtualDisplay.release(); - virtualDisplay = null; - } - - Size inputSize; - if (transform != null) { - // If there is a filter, it must receive the full display content - inputSize = displayInfo.getSize(); - assert glRunner == null; - OpenGLFilter glFilter = new AffineOpenGLFilter(transform); - glRunner = new OpenGLRunner(glFilter); - surface = glRunner.start(inputSize, videoSize, surface); - } else { - // If there is no filter, the display must be rendered at target video size directly - inputSize = videoSize; - } - - try { - virtualDisplay = ServiceManager.getDisplayManager() - .createVirtualDisplay("scrcpy", inputSize.getWidth(), inputSize.getHeight(), displayId, surface); - Ln.d("Display: using DisplayManager API"); - } catch (Exception displayManagerException) { - try { - display = createDisplay(); - - Size deviceSize = displayInfo.getSize(); - int layerStack = displayInfo.getLayerStack(); - setDisplaySurface(display, surface, deviceSize.toRect(), inputSize.toRect(), layerStack); - Ln.d("Display: using SurfaceControl API"); - } catch (Exception surfaceControlException) { - Ln.e("Could not create display using DisplayManager", displayManagerException); - Ln.e("Could not create display using SurfaceControl", surfaceControlException); - throw new AssertionError("Could not create display"); - } - } - - if (vdListener != null) { - int virtualDisplayId; - PositionMapper positionMapper; - if (virtualDisplay == null || displayId == 0) { - // Surface control or main display: send all events to the original display, relative to the device size - Size deviceSize = displayInfo.getSize(); - positionMapper = PositionMapper.create(videoSize, transform, deviceSize); - virtualDisplayId = displayId; - } else { - // The positions are relative to the virtual display, not the original display (so use inputSize, not deviceSize!) - positionMapper = PositionMapper.create(videoSize, transform, inputSize); - virtualDisplayId = virtualDisplay.getDisplay().getDisplayId(); - } - vdListener.onNewVirtualDisplay(virtualDisplayId, positionMapper); - } - } - - @Override - public void stop() { - if (glRunner != null) { - glRunner.stopAndRelease(); - glRunner = null; - } - } - - @Override - public void release() { - displaySizeMonitor.stopAndRelease(); - - if (display != null) { - SurfaceControl.destroyDisplay(display); - display = null; - } - if (virtualDisplay != null) { - virtualDisplay.release(); - virtualDisplay = null; - } - } - - @Override - public Size getSize() { - return videoSize; - } - - @Override - public boolean setMaxSize(int newMaxSize) { - maxSize = newMaxSize; - return true; - } - - private static IBinder createDisplay() throws Exception { - // Since Android 12 (preview), secure displays could not be created with shell permissions anymore. - // On Android 12 preview, SDK_INT is still R (not S), but CODENAME is "S". - boolean secure = Build.VERSION.SDK_INT < AndroidVersions.API_30_ANDROID_11 || (Build.VERSION.SDK_INT == AndroidVersions.API_30_ANDROID_11 - && !"S".equals(Build.VERSION.CODENAME)); - return SurfaceControl.createDisplay("scrcpy", secure); - } - - private static void setDisplaySurface(IBinder display, Surface surface, Rect deviceRect, Rect displayRect, int layerStack) { - SurfaceControl.openTransaction(); - try { - SurfaceControl.setDisplaySurface(display, surface); - SurfaceControl.setDisplayProjection(display, 0, deviceRect, displayRect); - SurfaceControl.setDisplayLayerStack(display, layerStack); - } finally { - SurfaceControl.closeTransaction(); - } - } - - @Override - public void requestInvalidate() { - invalidate(); - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java deleted file mode 100644 index 39d3bdb8..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.genymobile.scrcpy.video; - -import com.genymobile.scrcpy.device.ConfigurationException; -import com.genymobile.scrcpy.device.Size; - -import android.view.Surface; - -import java.io.IOException; - -/** - * A video source which can be rendered on a Surface for encoding. - */ -public abstract class SurfaceCapture { - - public interface CaptureListener { - void onInvalidated(); - } - - private CaptureListener listener; - - /** - * Notify the listener that the capture has been invalidated (for example, because its size changed). - */ - protected void invalidate() { - listener.onInvalidated(); - } - - /** - * Called once before the first capture starts. - */ - public final void init(CaptureListener listener) throws ConfigurationException, IOException { - this.listener = listener; - init(); - } - - /** - * Called once before the first capture starts. - */ - protected abstract void init() throws ConfigurationException, IOException; - - /** - * Called after the last capture ends (if and only if {@link #init()} has been called). - */ - public abstract void release(); - - /** - * Called once before each capture starts, before {@link #getSize()}. - */ - public void prepare() throws ConfigurationException, IOException { - // empty by default - } - - /** - * Start the capture to the target surface. - * - * @param surface the surface which will be encoded - */ - public abstract void start(Surface surface) throws IOException; - - /** - * Stop the capture. - */ - public void stop() { - // Do nothing by default - } - - /** - * Return the video size - * - * @return the video size - */ - public abstract Size getSize(); - - /** - * Set the maximum capture size (set by the encoder if it does not support the current size). - * - * @param maxSize Maximum size - */ - public abstract boolean setMaxSize(int maxSize); - - /** - * Indicate if the capture has been closed internally. - * - * @return {@code true} is the capture is closed, {@code false} otherwise. - */ - public boolean isClosed() { - return false; - } - - /** - * Manually request to invalidate (typically a user request). - *

- * The capture implementation is free to ignore the request and do nothing. - */ - public abstract void requestInvalidate(); -} diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java deleted file mode 100644 index 236a5f48..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java +++ /dev/null @@ -1,326 +0,0 @@ -package com.genymobile.scrcpy.video; - -import com.genymobile.scrcpy.AndroidVersions; -import com.genymobile.scrcpy.AsyncProcessor; -import com.genymobile.scrcpy.Options; -import com.genymobile.scrcpy.device.ConfigurationException; -import com.genymobile.scrcpy.device.Size; -import com.genymobile.scrcpy.device.Streamer; -import com.genymobile.scrcpy.util.Codec; -import com.genymobile.scrcpy.util.CodecOption; -import com.genymobile.scrcpy.util.CodecUtils; -import com.genymobile.scrcpy.util.IO; -import com.genymobile.scrcpy.util.Ln; -import com.genymobile.scrcpy.util.LogUtils; - -import android.media.MediaCodec; -import android.media.MediaCodecInfo; -import android.media.MediaFormat; -import android.os.Build; -import android.os.Looper; -import android.os.SystemClock; -import android.view.Surface; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; - -public class SurfaceEncoder implements AsyncProcessor { - - private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds - private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms - private static final String KEY_MAX_FPS_TO_ENCODER = "max-fps-to-encoder"; - - // Keep the values in descending order - private static final int[] MAX_SIZE_FALLBACK = {2560, 1920, 1600, 1280, 1024, 800}; - private static final int MAX_CONSECUTIVE_ERRORS = 3; - - private final SurfaceCapture capture; - private final Streamer streamer; - private final String encoderName; - private final List codecOptions; - private final int videoBitRate; - private final float maxFps; - private final boolean downsizeOnError; - - private boolean firstFrameSent; - private int consecutiveErrors; - - private Thread thread; - private final AtomicBoolean stopped = new AtomicBoolean(); - - private final CaptureReset reset = new CaptureReset(); - - public SurfaceEncoder(SurfaceCapture capture, Streamer streamer, Options options) { - this.capture = capture; - this.streamer = streamer; - this.videoBitRate = options.getVideoBitRate(); - this.maxFps = options.getMaxFps(); - this.codecOptions = options.getVideoCodecOptions(); - this.encoderName = options.getVideoEncoder(); - this.downsizeOnError = options.getDownsizeOnError(); - } - - private void streamCapture() throws IOException, ConfigurationException { - Codec codec = streamer.getCodec(); - MediaCodec mediaCodec = createMediaCodec(codec, encoderName); - MediaFormat format = createFormat(codec.getMimeType(), videoBitRate, maxFps, codecOptions); - - capture.init(reset); - - try { - boolean alive; - boolean headerWritten = false; - - do { - reset.consumeReset(); // If a capture reset was requested, it is implicitly fulfilled - capture.prepare(); - Size size = capture.getSize(); - if (!headerWritten) { - streamer.writeVideoHeader(size); - headerWritten = true; - } - - format.setInteger(MediaFormat.KEY_WIDTH, size.getWidth()); - format.setInteger(MediaFormat.KEY_HEIGHT, size.getHeight()); - - Surface surface = null; - boolean mediaCodecStarted = false; - boolean captureStarted = false; - try { - mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); - surface = mediaCodec.createInputSurface(); - - capture.start(surface); - captureStarted = true; - - mediaCodec.start(); - mediaCodecStarted = true; - - // Set the MediaCodec instance to "interrupt" (by signaling an EOS) on reset - reset.setRunningMediaCodec(mediaCodec); - - if (stopped.get()) { - alive = false; - } else { - boolean resetRequested = reset.consumeReset(); - if (!resetRequested) { - // If a reset is requested during encode(), it will interrupt the encoding by an EOS - encode(mediaCodec, streamer); - } - // The capture might have been closed internally (for example if the camera is disconnected) - alive = !stopped.get() && !capture.isClosed(); - } - } catch (IllegalStateException | IllegalArgumentException | IOException e) { - if (IO.isBrokenPipe(e)) { - // Do not retry on broken pipe, which is expected on close because the socket is closed by the client - throw e; - } - Ln.e("Capture/encoding error: " + e.getClass().getName() + ": " + e.getMessage()); - if (!prepareRetry(size)) { - throw e; - } - alive = true; - } finally { - reset.setRunningMediaCodec(null); - if (captureStarted) { - capture.stop(); - } - if (mediaCodecStarted) { - try { - mediaCodec.stop(); - } catch (IllegalStateException e) { - // ignore (just in case) - } - } - mediaCodec.reset(); - if (surface != null) { - surface.release(); - } - } - } while (alive); - } finally { - mediaCodec.release(); - capture.release(); - } - } - - private boolean prepareRetry(Size currentSize) { - if (firstFrameSent) { - ++consecutiveErrors; - if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) { - // Definitively fail - return false; - } - - // Wait a bit to increase the probability that retrying will fix the problem - SystemClock.sleep(50); - return true; - } - - if (!downsizeOnError) { - // Must fail immediately - return false; - } - - // Downsizing on error is only enabled if an encoding failure occurs before the first frame (downsizing later could be surprising) - - int newMaxSize = chooseMaxSizeFallback(currentSize); - if (newMaxSize == 0) { - // Must definitively fail - return false; - } - - boolean accepted = capture.setMaxSize(newMaxSize); - if (!accepted) { - return false; - } - - // Retry with a smaller size - Ln.i("Retrying with -m" + newMaxSize + "..."); - return true; - } - - private static int chooseMaxSizeFallback(Size failedSize) { - int currentMaxSize = Math.max(failedSize.getWidth(), failedSize.getHeight()); - for (int value : MAX_SIZE_FALLBACK) { - if (value < currentMaxSize) { - // We found a smaller value to reduce the video size - return value; - } - } - // No fallback, fail definitively - return 0; - } - - private void encode(MediaCodec codec, Streamer streamer) throws IOException { - MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); - - boolean eos; - do { - int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1); - try { - eos = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0; - // On EOS, there might be data or not, depending on bufferInfo.size - if (outputBufferId >= 0 && bufferInfo.size > 0) { - ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId); - - boolean isConfig = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0; - if (!isConfig) { - // If this is not a config packet, then it contains a frame - firstFrameSent = true; - consecutiveErrors = 0; - } - - streamer.writePacket(codecBuffer, bufferInfo); - } - } finally { - if (outputBufferId >= 0) { - codec.releaseOutputBuffer(outputBufferId, false); - } - } - } while (!eos); - } - - private static MediaCodec createMediaCodec(Codec codec, String encoderName) throws IOException, ConfigurationException { - if (encoderName != null) { - Ln.d("Creating encoder by name: '" + encoderName + "'"); - try { - MediaCodec mediaCodec = MediaCodec.createByCodecName(encoderName); - String mimeType = Codec.getMimeType(mediaCodec); - if (!codec.getMimeType().equals(mimeType)) { - Ln.e("Video encoder type for \"" + encoderName + "\" (" + mimeType + ") does not match codec type (" + codec.getMimeType() + ")"); - throw new ConfigurationException("Incorrect encoder type: " + encoderName); - } - return mediaCodec; - } catch (IllegalArgumentException e) { - Ln.e("Video encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildVideoEncoderListMessage()); - throw new ConfigurationException("Unknown encoder: " + encoderName); - } catch (IOException e) { - Ln.e("Could not create video encoder '" + encoderName + "' for " + codec.getName() + "\n" + LogUtils.buildVideoEncoderListMessage()); - throw e; - } - } - - try { - MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType()); - Ln.d("Using video encoder: '" + mediaCodec.getName() + "'"); - return mediaCodec; - } catch (IOException | IllegalArgumentException e) { - Ln.e("Could not create default video encoder for " + codec.getName() + "\n" + LogUtils.buildVideoEncoderListMessage()); - throw e; - } - } - - private static MediaFormat createFormat(String videoMimeType, int bitRate, float maxFps, List codecOptions) { - MediaFormat format = new MediaFormat(); - format.setString(MediaFormat.KEY_MIME, videoMimeType); - format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); - // must be present to configure the encoder, but does not impact the actual frame rate, which is variable - format.setInteger(MediaFormat.KEY_FRAME_RATE, 60); - format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); - if (Build.VERSION.SDK_INT >= AndroidVersions.API_24_ANDROID_7_0) { - format.setInteger(MediaFormat.KEY_COLOR_RANGE, MediaFormat.COLOR_RANGE_LIMITED); - } - format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, DEFAULT_I_FRAME_INTERVAL); - // display the very first frame, and recover from bad quality when no new frames - format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, REPEAT_FRAME_DELAY_US); // µs - if (maxFps > 0) { - // The key existed privately before Android 10: - // - // - format.setFloat(KEY_MAX_FPS_TO_ENCODER, maxFps); - } - - if (codecOptions != null) { - for (CodecOption option : codecOptions) { - String key = option.getKey(); - Object value = option.getValue(); - CodecUtils.setCodecOption(format, key, value); - Ln.d("Video codec option set: " + key + " (" + value.getClass().getSimpleName() + ") = " + value); - } - } - - return format; - } - - @Override - public void start(TerminationListener listener) { - thread = new Thread(() -> { - // Some devices (Meizu) deadlock if the video encoding thread has no Looper - // - Looper.prepare(); - - try { - streamCapture(); - } catch (ConfigurationException e) { - // Do not print stack trace, a user-friendly error-message has already been logged - } catch (IOException e) { - // Broken pipe is expected on close, because the socket is closed by the client - if (!IO.isBrokenPipe(e)) { - Ln.e("Video encoding error", e); - } - } finally { - Ln.d("Screen streaming stopped"); - listener.onTerminated(true); - } - }, "video"); - thread.start(); - } - - @Override - public void stop() { - if (thread != null) { - stopped.set(true); - reset.reset(); - } - } - - @Override - public void join() throws InterruptedException { - if (thread != null) { - thread.join(); - } - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/video/VideoCodec.java b/server/src/main/java/com/genymobile/scrcpy/video/VideoCodec.java deleted file mode 100644 index 5d528da1..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/video/VideoCodec.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.genymobile.scrcpy.video; - -import com.genymobile.scrcpy.util.Codec; - -import android.annotation.SuppressLint; -import android.media.MediaFormat; - -public enum VideoCodec implements Codec { - H264(0x68_32_36_34, "h264", MediaFormat.MIMETYPE_VIDEO_AVC), - H265(0x68_32_36_35, "h265", MediaFormat.MIMETYPE_VIDEO_HEVC), - @SuppressLint("InlinedApi") // introduced in API 29 - AV1(0x00_61_76_31, "av1", MediaFormat.MIMETYPE_VIDEO_AV1); - - private final int id; // 4-byte ASCII representation of the name - private final String name; - private final String mimeType; - - VideoCodec(int id, String name, String mimeType) { - this.id = id; - this.name = name; - this.mimeType = mimeType; - } - - @Override - public Type getType() { - return Type.VIDEO; - } - - @Override - public int getId() { - return id; - } - - @Override - public String getName() { - return name; - } - - @Override - public String getMimeType() { - return mimeType; - } - - public static VideoCodec findByName(String name) { - for (VideoCodec codec : values()) { - if (codec.name.equals(name)) { - return codec; - } - } - return null; - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java b/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java deleted file mode 100644 index a27915ee..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java +++ /dev/null @@ -1,119 +0,0 @@ -package com.genymobile.scrcpy.video; - -import com.genymobile.scrcpy.device.Orientation; -import com.genymobile.scrcpy.device.Size; -import com.genymobile.scrcpy.util.AffineMatrix; - -import android.graphics.Rect; - -public class VideoFilter { - - private Size size; - private AffineMatrix transform; - - public VideoFilter(Size inputSize) { - this.size = inputSize; - } - - public Size getOutputSize() { - return size; - } - - public AffineMatrix getTransform() { - return transform; - } - - /** - * Return the inverse transform. - *

- * The direct affine transform describes how the input image is transformed. - *

- * It is often useful to retrieve the inverse transform instead: - *

    - *
  • The OpenGL filter expects the matrix to transform the image coordinates, which is the inverse transform;
  • - *
  • The click positions must be transformed back to the device positions, using the inverse transform too.
  • - *
- * - * @return the inverse transform - */ - public AffineMatrix getInverseTransform() { - if (transform == null) { - return null; - } - return transform.invert(); - } - - private static Rect transposeRect(Rect rect) { - return new Rect(rect.top, rect.left, rect.bottom, rect.right); - } - - public void addCrop(Rect crop, boolean transposed) { - if (transposed) { - crop = transposeRect(crop); - } - - double inputWidth = size.getWidth(); - double inputHeight = size.getHeight(); - - if (crop.left < 0 || crop.top < 0 || crop.right > inputWidth || crop.bottom > inputHeight) { - throw new IllegalArgumentException("Crop " + crop + " exceeds the input area (" + size + ")"); - } - - double x = crop.left / inputWidth; - double y = 1 - (crop.bottom / inputHeight); // OpenGL origin is bottom-left - double w = crop.width() / inputWidth; - double h = crop.height() / inputHeight; - - transform = AffineMatrix.reframe(x, y, w, h).multiply(transform); - size = new Size(crop.width(), crop.height()); - } - - public void addRotation(int ccwRotation) { - if (ccwRotation == 0) { - return; - } - - transform = AffineMatrix.rotateOrtho(ccwRotation).multiply(transform); - if (ccwRotation % 2 != 0) { - size = size.rotate(); - } - } - - public void addOrientation(Orientation captureOrientation) { - if (captureOrientation.isFlipped()) { - transform = AffineMatrix.hflip().multiply(transform); - } - int ccwRotation = (4 - captureOrientation.getRotation()) % 4; - addRotation(ccwRotation); - } - - public void addOrientation(int displayRotation, boolean locked, Orientation captureOrientation) { - if (locked) { - // flip/rotate the current display from the natural device orientation (i.e. where display rotation is 0) - int reverseDisplayRotation = (4 - displayRotation) % 4; - addRotation(reverseDisplayRotation); - } - addOrientation(captureOrientation); - } - - public void addAngle(double cwAngle) { - if (cwAngle == 0) { - return; - } - double ccwAngle = -cwAngle; - transform = AffineMatrix.rotate(ccwAngle).withAspectRatio(size).fromCenter().multiply(transform); - } - - public void addResize(Size targetSize) { - if (size.equals(targetSize)) { - return; - } - - if (transform == null) { - // The requested scaling is performed by the viewport (by changing the output size), but the OpenGL filter must still run, even if - // resizing is not performed by the shader. So transform MUST NOT be null. - transform = AffineMatrix.IDENTITY; - } - size = targetSize; - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/video/VideoSource.java b/server/src/main/java/com/genymobile/scrcpy/video/VideoSource.java deleted file mode 100644 index 53b54a52..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/video/VideoSource.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.genymobile.scrcpy.video; - -public enum VideoSource { - DISPLAY("display"), - CAMERA("camera"); - - private final String name; - - VideoSource(String name) { - this.name = name; - } - - public static VideoSource findByName(String name) { - for (VideoSource videoSource : VideoSource.values()) { - if (name.equals(videoSource.name)) { - return videoSource; - } - } - - return null; - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/video/VirtualDisplayListener.java b/server/src/main/java/com/genymobile/scrcpy/video/VirtualDisplayListener.java deleted file mode 100644 index c079265e..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/video/VirtualDisplayListener.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.genymobile.scrcpy.video; - -import com.genymobile.scrcpy.control.PositionMapper; - -public interface VirtualDisplayListener { - void onNewVirtualDisplay(int displayId, PositionMapper positionMapper); -} diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java deleted file mode 100644 index 255483c6..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java +++ /dev/null @@ -1,165 +0,0 @@ -package com.genymobile.scrcpy.wrappers; - -import com.genymobile.scrcpy.AndroidVersions; -import com.genymobile.scrcpy.FakeContext; -import com.genymobile.scrcpy.util.Ln; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.content.IContentProvider; -import android.content.Intent; -import android.os.Binder; -import android.os.Bundle; -import android.os.IBinder; -import android.os.IInterface; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; - -@SuppressLint("PrivateApi,DiscouragedPrivateApi") -public final class ActivityManager { - - private final IInterface manager; - private Method getContentProviderExternalMethod; - private boolean getContentProviderExternalMethodNewVersion = true; - private Method removeContentProviderExternalMethod; - private Method startActivityAsUserMethod; - private Method forceStopPackageMethod; - - static ActivityManager create() { - try { - // On old Android versions, the ActivityManager is not exposed via AIDL, - // so use ActivityManagerNative.getDefault() - Class cls = Class.forName("android.app.ActivityManagerNative"); - Method getDefaultMethod = cls.getDeclaredMethod("getDefault"); - IInterface am = (IInterface) getDefaultMethod.invoke(null); - return new ActivityManager(am); - } catch (ReflectiveOperationException e) { - throw new AssertionError(e); - } - } - - private ActivityManager(IInterface manager) { - this.manager = manager; - } - - 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; - } - - @TargetApi(AndroidVersions.API_29_ANDROID_10) - public IContentProvider getContentProviderExternal(String name, IBinder token) { - try { - Method method = getGetContentProviderExternalMethod(); - Object[] args; - if (getContentProviderExternalMethodNewVersion) { - // new version - args = new Object[]{name, FakeContext.ROOT_UID, token, null}; - } else { - // old version - args = new Object[]{name, FakeContext.ROOT_UID, 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); - return (IContentProvider) providerField.get(providerHolder); - } catch (ReflectiveOperationException 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 (ReflectiveOperationException e) { - Ln.e("Could not invoke method", e); - } - } - - public ContentProvider createSettingsProvider() { - IBinder token = new Binder(); - IContentProvider provider = getContentProviderExternal("settings", token); - if (provider == null) { - return null; - } - return new ContentProvider(this, provider, "settings", token); - } - - private Method getStartActivityAsUserMethod() throws NoSuchMethodException, ClassNotFoundException { - if (startActivityAsUserMethod == null) { - Class iApplicationThreadClass = Class.forName("android.app.IApplicationThread"); - Class profilerInfo = Class.forName("android.app.ProfilerInfo"); - startActivityAsUserMethod = manager.getClass() - .getMethod("startActivityAsUser", iApplicationThreadClass, String.class, Intent.class, String.class, IBinder.class, String.class, - int.class, int.class, profilerInfo, Bundle.class, int.class); - } - return startActivityAsUserMethod; - } - - public int startActivity(Intent intent) { - return startActivity(intent, null); - } - - @SuppressWarnings("ConstantConditions") - public int startActivity(Intent intent, Bundle options) { - try { - Method method = getStartActivityAsUserMethod(); - return (int) method.invoke( - /* this */ manager, - /* caller */ null, - /* callingPackage */ FakeContext.PACKAGE_NAME, - /* intent */ intent, - /* resolvedType */ null, - /* resultTo */ null, - /* resultWho */ null, - /* requestCode */ 0, - /* startFlags */ 0, - /* profilerInfo */ null, - /* bOptions */ options, - /* userId */ /* UserHandle.USER_CURRENT */ -2); - } catch (Throwable e) { - Ln.e("Could not invoke method", e); - return 0; - } - } - - private Method getForceStopPackageMethod() throws NoSuchMethodException { - if (forceStopPackageMethod == null) { - forceStopPackageMethod = manager.getClass().getMethod("forceStopPackage", String.class, int.class); - } - return forceStopPackageMethod; - } - - public void forceStopPackage(String packageName) { - try { - Method method = getForceStopPackageMethod(); - method.invoke(manager, packageName, /* userId */ /* UserHandle.USER_CURRENT */ -2); - } catch (Throwable e) { - Ln.e("Could not invoke method", e); - } - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java index 54936122..a058a8bb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -1,43 +1,44 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.FakeContext; - import android.content.ClipData; -import android.content.Context; +import android.os.IInterface; -public final class ClipboardManager { - private final android.content.ClipboardManager manager; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; - static ClipboardManager create() { - android.content.ClipboardManager manager = (android.content.ClipboardManager) FakeContext.get().getSystemService(Context.CLIPBOARD_SERVICE); - if (manager == null) { - // Some devices have no clipboard manager - // - // - return null; - } - return new ClipboardManager(manager); - } +public class ClipboardManager { + private final IInterface manager; + private final Method getPrimaryClipMethod; + private final Method setPrimaryClipMethod; - private ClipboardManager(android.content.ClipboardManager manager) { + public ClipboardManager(IInterface manager) { this.manager = manager; + try { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class); + setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class); + } catch (NoSuchMethodException e) { + throw new AssertionError(e); + } } public CharSequence getText() { - ClipData clipData = manager.getPrimaryClip(); - if (clipData == null || clipData.getItemCount() == 0) { - return null; + try { + ClipData clipData = (ClipData) getPrimaryClipMethod.invoke(manager, "com.android.shell"); + if (clipData == null || clipData.getItemCount() == 0) { + return null; + } + return clipData.getItemAt(0).getText(); + } catch (InvocationTargetException | IllegalAccessException e) { + throw new AssertionError(e); } - return clipData.getItemAt(0).getText(); } - public boolean setText(CharSequence text) { + public void setText(CharSequence text) { ClipData clipData = ClipData.newPlainText(null, text); - manager.setPrimaryClip(clipData); - return true; - } - - public void addPrimaryClipChangedListener(android.content.ClipboardManager.OnPrimaryClipChangedListener listener) { - manager.addPrimaryClipChangedListener(listener); + try { + setPrimaryClipMethod.invoke(manager, clipData, "com.android.shell"); + } catch (InvocationTargetException | IllegalAccessException e) { + throw new AssertionError(e); + } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java deleted file mode 100644 index f625b398..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java +++ /dev/null @@ -1,162 +0,0 @@ -package com.genymobile.scrcpy.wrappers; - -import com.genymobile.scrcpy.AndroidVersions; -import com.genymobile.scrcpy.FakeContext; -import com.genymobile.scrcpy.util.Ln; -import com.genymobile.scrcpy.util.SettingsException; - -import android.annotation.SuppressLint; -import android.content.AttributionSource; -import android.os.Build; -import android.os.Bundle; -import android.os.IBinder; - -import java.io.Closeable; -import java.lang.reflect.Method; - -public final 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; - - 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) { - if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) { - callMethod = provider.getClass().getMethod("call", AttributionSource.class, String.class, String.class, String.class, Bundle.class); - callMethodVersion = 0; - } else { - // 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; - } - - private Bundle call(String callMethod, String arg, Bundle extras) throws ReflectiveOperationException { - try { - Method method = getCallMethod(); - Object[] args; - - if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12 && callMethodVersion == 0) { - args = new Object[]{FakeContext.get().getAttributionSource(), "settings", callMethod, arg, extras}; - } else { - switch (callMethodVersion) { - case 1: - args = new Object[]{FakeContext.PACKAGE_NAME, null, "settings", callMethod, arg, extras}; - break; - case 2: - args = new Object[]{FakeContext.PACKAGE_NAME, "settings", callMethod, arg, extras}; - break; - default: - args = new Object[]{FakeContext.PACKAGE_NAME, callMethod, arg, extras}; - break; - } - } - return (Bundle) method.invoke(provider, args); - } catch (ReflectiveOperationException 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, FakeContext.ROOT_UID); - try { - Bundle bundle = call(method, key, arg); - if (bundle == null) { - return null; - } - return bundle.getString("value"); - } catch (Exception e) { - throw new SettingsException(table, "get", key, null, e); - } - - } - - 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, FakeContext.ROOT_UID); - arg.putString(NAME_VALUE_TABLE_VALUE, value); - try { - call(method, key, arg); - } catch (Exception e) { - throw new SettingsException(table, "put", key, value, e); - } - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java deleted file mode 100644 index 88ca3d3d..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.genymobile.scrcpy.wrappers; - -import com.genymobile.scrcpy.AndroidVersions; -import com.genymobile.scrcpy.util.Ln; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.os.IBinder; -import android.system.Os; - -import java.lang.reflect.Method; - -@SuppressLint({"PrivateApi", "SoonBlockedPrivateApi", "BlockedPrivateApi"}) -@TargetApi(AndroidVersions.API_34_ANDROID_14) -public final class DisplayControl { - - private static final Class CLASS; - - static { - Class displayControlClass = null; - try { - Class classLoaderFactoryClass = Class.forName("com.android.internal.os.ClassLoaderFactory"); - Method createClassLoaderMethod = classLoaderFactoryClass.getDeclaredMethod("createClassLoader", String.class, String.class, String.class, - ClassLoader.class, int.class, boolean.class, String.class); - - String systemServerClasspath = Os.getenv("SYSTEMSERVERCLASSPATH"); - ClassLoader classLoader = (ClassLoader) createClassLoaderMethod.invoke(null, systemServerClasspath, null, null, - ClassLoader.getSystemClassLoader(), 0, true, null); - - displayControlClass = classLoader.loadClass("com.android.server.display.DisplayControl"); - - Method loadMethod = Runtime.class.getDeclaredMethod("loadLibrary0", Class.class, String.class); - loadMethod.setAccessible(true); - loadMethod.invoke(Runtime.getRuntime(), displayControlClass, "android_servers"); - } catch (Throwable e) { - Ln.e("Could not initialize DisplayControl", e); - // Do not throw an exception here, the methods will fail when they are called - } - CLASS = displayControlClass; - } - - private static Method getPhysicalDisplayTokenMethod; - private static Method getPhysicalDisplayIdsMethod; - - private DisplayControl() { - // only static methods - } - - private static Method getGetPhysicalDisplayTokenMethod() throws NoSuchMethodException { - if (getPhysicalDisplayTokenMethod == null) { - getPhysicalDisplayTokenMethod = CLASS.getMethod("getPhysicalDisplayToken", long.class); - } - return getPhysicalDisplayTokenMethod; - } - - public static IBinder getPhysicalDisplayToken(long physicalDisplayId) { - try { - Method method = getGetPhysicalDisplayTokenMethod(); - return (IBinder) method.invoke(null, physicalDisplayId); - } catch (ReflectiveOperationException e) { - Ln.e("Could not invoke method", e); - return null; - } - } - - private static Method getGetPhysicalDisplayIdsMethod() throws NoSuchMethodException { - if (getPhysicalDisplayIdsMethod == null) { - getPhysicalDisplayIdsMethod = CLASS.getMethod("getPhysicalDisplayIds"); - } - return getPhysicalDisplayIdsMethod; - } - - public static long[] getPhysicalDisplayIds() { - try { - Method method = getGetPhysicalDisplayIdsMethod(); - return (long[]) method.invoke(null); - } catch (ReflectiveOperationException e) { - Ln.e("Could not invoke method", e); - return null; - } - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java index a12470a4..568afacd 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -1,244 +1,28 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.AndroidVersions; -import com.genymobile.scrcpy.FakeContext; -import com.genymobile.scrcpy.device.DisplayInfo; -import com.genymobile.scrcpy.device.Size; -import com.genymobile.scrcpy.util.Command; -import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.DisplayInfo; +import com.genymobile.scrcpy.Size; -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.content.Context; -import android.hardware.display.VirtualDisplay; -import android.os.Handler; -import android.view.Display; -import android.view.Surface; +import android.os.IInterface; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -@SuppressLint("PrivateApi,DiscouragedPrivateApi") public final class DisplayManager { + private final IInterface manager; - // android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_CHANGED - public static final long EVENT_FLAG_DISPLAY_CHANGED = 1L << 2; - - public interface DisplayListener { - /** - * Called whenever the properties of a logical {@link android.view.Display}, - * such as size and density, have changed. - * - * @param displayId The id of the logical display that changed. - */ - void onDisplayChanged(int displayId); - } - - public static final class DisplayListenerHandle { - private final Object displayListenerProxy; - private DisplayListenerHandle(Object displayListenerProxy) { - this.displayListenerProxy = displayListenerProxy; - } - } - - private final Object manager; // instance of hidden class android.hardware.display.DisplayManagerGlobal - private Method getDisplayInfoMethod; - private Method createVirtualDisplayMethod; - private Method requestDisplayPowerMethod; - - static DisplayManager create() { - try { - Class clazz = Class.forName("android.hardware.display.DisplayManagerGlobal"); - Method getInstanceMethod = clazz.getDeclaredMethod("getInstance"); - Object dmg = getInstanceMethod.invoke(null); - return new DisplayManager(dmg); - } catch (ReflectiveOperationException e) { - throw new AssertionError(e); - } - } - - private DisplayManager(Object manager) { + public DisplayManager(IInterface 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]+).*?, density ([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 density = Integer.parseInt(m.group(5)); - int layerStack = Integer.parseInt(m.group(6)); - - return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, density, null); - } - - private static DisplayInfo getDisplayInfoFromDumpsysDisplay(int displayId) { + public DisplayInfo getDisplayInfo() { 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) { - if (text == null) { - return 0; - } - - int flags = 0; - Pattern regex = Pattern.compile("FLAG_[A-Z_]+"); - Matcher m = regex.matcher(text); - while (m.find()) { - String flagString = m.group(); - try { - Field filed = Display.class.getDeclaredField(flagString); - flags |= filed.getInt(null); - } catch (ReflectiveOperationException e) { - // Silently ignore, some flags reported by "dumpsys display" are @TestApi - } - } - return flags; - } - - // getDisplayInfo() may be used from both the Controller thread and the video (main) thread - private synchronized Method getGetDisplayInfoMethod() throws NoSuchMethodException { - if (getDisplayInfoMethod == null) { - getDisplayInfoMethod = manager.getClass().getMethod("getDisplayInfo", int.class); - } - return getDisplayInfoMethod; - } - - public DisplayInfo getDisplayInfo(int displayId) { - try { - Method method = getGetDisplayInfoMethod(); - Object displayInfo = method.invoke(manager, displayId); - if (displayInfo == null) { - // fallback when displayInfo is null - return getDisplayInfoFromDumpsysDisplay(displayId); - } + Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, 0); 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); - int dpi = cls.getDeclaredField("logicalDensityDpi").getInt(displayInfo); - String uniqueId = (String) cls.getDeclaredField("uniqueId").get(displayInfo); - return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, dpi, uniqueId); - } catch (ReflectiveOperationException e) { - throw new AssertionError(e); - } - } - - public int[] getDisplayIds() { - try { - return (int[]) manager.getClass().getMethod("getDisplayIds").invoke(manager); - } catch (ReflectiveOperationException e) { - throw new AssertionError(e); - } - } - - private Method getCreateVirtualDisplayMethod() throws NoSuchMethodException { - if (createVirtualDisplayMethod == null) { - createVirtualDisplayMethod = android.hardware.display.DisplayManager.class - .getMethod("createVirtualDisplay", String.class, int.class, int.class, int.class, Surface.class); - } - return createVirtualDisplayMethod; - } - - public VirtualDisplay createVirtualDisplay(String name, int width, int height, int displayIdToMirror, Surface surface) throws Exception { - Method method = getCreateVirtualDisplayMethod(); - return (VirtualDisplay) method.invoke(null, name, width, height, displayIdToMirror, surface); - } - - public VirtualDisplay createNewVirtualDisplay(String name, int width, int height, int dpi, Surface surface, int flags) throws Exception { - Constructor ctor = android.hardware.display.DisplayManager.class.getDeclaredConstructor( - Context.class); - ctor.setAccessible(true); - android.hardware.display.DisplayManager dm = ctor.newInstance(FakeContext.get()); - return dm.createVirtualDisplay(name, width, height, dpi, surface, flags); - } - - private Method getRequestDisplayPowerMethod() throws NoSuchMethodException { - if (requestDisplayPowerMethod == null) { - requestDisplayPowerMethod = manager.getClass().getMethod("requestDisplayPower", int.class, boolean.class); - } - return requestDisplayPowerMethod; - } - - @TargetApi(AndroidVersions.API_35_ANDROID_15) - public boolean requestDisplayPower(int displayId, boolean on) { - try { - Method method = getRequestDisplayPowerMethod(); - return (boolean) method.invoke(manager, displayId, on); - } catch (ReflectiveOperationException e) { - Ln.e("Could not invoke method", e); - return false; - } - } - - public DisplayListenerHandle registerDisplayListener(DisplayListener listener, Handler handler) { - try { - Class displayListenerClass = Class.forName("android.hardware.display.DisplayManager$DisplayListener"); - Object displayListenerProxy = Proxy.newProxyInstance( - ClassLoader.getSystemClassLoader(), - new Class[] {displayListenerClass}, - (proxy, method, args) -> { - if ("onDisplayChanged".equals(method.getName())) { - listener.onDisplayChanged((int) args[0]); - } - if ("toString".equals(method.getName())) { - return "DisplayListener"; - } - return null; - }); - try { - manager.getClass() - .getMethod("registerDisplayListener", displayListenerClass, Handler.class, long.class, String.class) - .invoke(manager, displayListenerProxy, handler, EVENT_FLAG_DISPLAY_CHANGED, FakeContext.PACKAGE_NAME); - } catch (NoSuchMethodException e) { - try { - manager.getClass() - .getMethod("registerDisplayListener", displayListenerClass, Handler.class, long.class) - .invoke(manager, displayListenerProxy, handler, EVENT_FLAG_DISPLAY_CHANGED); - } catch (NoSuchMethodException e2) { - manager.getClass() - .getMethod("registerDisplayListener", displayListenerClass, Handler.class) - .invoke(manager, displayListenerProxy, handler); - } - } - - return new DisplayListenerHandle(displayListenerProxy); + return new DisplayInfo(new Size(width, height), rotation); } catch (Exception e) { - // Rotation and screen size won't be updated, not a fatal error - Ln.e("Could not register display listener", e); - } - - return null; - } - - public void unregisterDisplayListener(DisplayListenerHandle listener) { - try { - Class displayListenerClass = Class.forName("android.hardware.display.DisplayManager$DisplayListener"); - manager.getClass().getMethod("unregisterDisplayListener", displayListenerClass).invoke(manager, listener.displayListenerProxy); - } catch (Exception e) { - Ln.e("Could not unregister display listener", e); + throw new AssertionError(e); } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayWindowListener.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayWindowListener.java deleted file mode 100644 index f2ecb158..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayWindowListener.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.genymobile.scrcpy.wrappers; - -import android.content.res.Configuration; -import android.graphics.Rect; -import android.view.IDisplayWindowListener; - -import java.util.List; - -public class DisplayWindowListener extends IDisplayWindowListener.Stub { - @Override - public void onDisplayAdded(int displayId) { - // empty default implementation - } - - @Override - public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { - // empty default implementation - } - - @Override - public void onDisplayRemoved(int displayId) { - // empty default implementation - } - - @Override - public void onFixedRotationStarted(int displayId, int newRotation) { - // empty default implementation - } - - @Override - public void onFixedRotationFinished(int displayId) { - // empty default implementation - } - - @Override - public void onKeepClearAreasChanged(int displayId, List restricted, List unrestricted) { - // empty default implementation - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java index f55648d5..1fc78c27 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java @@ -1,146 +1,34 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.AndroidVersions; -import com.genymobile.scrcpy.FakeContext; -import com.genymobile.scrcpy.util.Ln; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; +import android.os.IInterface; import android.view.InputEvent; -import android.view.MotionEvent; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -@SuppressLint("PrivateApi,DiscouragedPrivateApi") public final class InputManager { public static final int INJECT_INPUT_EVENT_MODE_ASYNC = 0; public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT = 1; public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2; - private final android.hardware.input.InputManager manager; - private long lastPermissionLogDate; + private final IInterface manager; + private final Method injectInputEventMethod; - private static Method injectInputEventMethod; - private static Method setDisplayIdMethod; - private static Method setActionButtonMethod; - private static Method addUniqueIdAssociationByPortMethod; - private static Method removeUniqueIdAssociationByPortMethod; - - static InputManager create() { - android.hardware.input.InputManager manager = (android.hardware.input.InputManager) FakeContext.get() - .getSystemService(FakeContext.INPUT_SERVICE); - return new InputManager(manager); - } - - private InputManager(android.hardware.input.InputManager manager) { + public InputManager(IInterface manager) { this.manager = manager; - } - - private static Method getInjectInputEventMethod() throws NoSuchMethodException { - if (injectInputEventMethod == null) { - injectInputEventMethod = android.hardware.input.InputManager.class.getMethod("injectInputEvent", InputEvent.class, int.class); + try { + injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class); + } catch (NoSuchMethodException e) { + throw new AssertionError(e); } - return injectInputEventMethod; } public boolean injectInputEvent(InputEvent inputEvent, int mode) { try { - Method method = getInjectInputEventMethod(); - return (boolean) method.invoke(manager, inputEvent, mode); - } catch (ReflectiveOperationException e) { - if (e instanceof InvocationTargetException) { - Throwable cause = e.getCause(); - if (cause instanceof SecurityException) { - String message = e.getCause().getMessage(); - if (message != null && message.contains("INJECT_EVENTS permission")) { - // Do not flood the console, limit to one permission error log every 3 seconds - long now = System.currentTimeMillis(); - if (lastPermissionLogDate <= now - 3000) { - Ln.e(message); - Ln.e("Make sure you have enabled \"USB debugging (Security Settings)\" and then rebooted your device."); - lastPermissionLogDate = now; - } - // Do not print the stack trace - return false; - } - } - } - 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 (ReflectiveOperationException e) { - Ln.e("Cannot associate a display id to the input event", e); - return false; - } - } - - private static Method getSetActionButtonMethod() throws NoSuchMethodException { - if (setActionButtonMethod == null) { - setActionButtonMethod = MotionEvent.class.getMethod("setActionButton", int.class); - } - return setActionButtonMethod; - } - - public static boolean setActionButton(MotionEvent motionEvent, int actionButton) { - try { - Method method = getSetActionButtonMethod(); - method.invoke(motionEvent, actionButton); - return true; - } catch (ReflectiveOperationException e) { - Ln.e("Cannot set action button on MotionEvent", e); - return false; - } - } - - private static Method getAddUniqueIdAssociationByPortMethod() throws NoSuchMethodException { - if (addUniqueIdAssociationByPortMethod == null) { - addUniqueIdAssociationByPortMethod = android.hardware.input.InputManager.class.getMethod( - "addUniqueIdAssociationByPort", String.class, String.class); - } - return addUniqueIdAssociationByPortMethod; - } - - @TargetApi(AndroidVersions.API_35_ANDROID_15) - public void addUniqueIdAssociationByPort(String inputPort, String uniqueId) { - try { - Method method = getAddUniqueIdAssociationByPortMethod(); - method.invoke(manager, inputPort, uniqueId); - } catch (ReflectiveOperationException e) { - Ln.e("Cannot add unique id association by port", e); - } - } - - private static Method getRemoveUniqueIdAssociationByPortMethod() throws NoSuchMethodException { - if (removeUniqueIdAssociationByPortMethod == null) { - removeUniqueIdAssociationByPortMethod = android.hardware.input.InputManager.class.getMethod( - "removeUniqueIdAssociationByPort", String.class); - } - return removeUniqueIdAssociationByPortMethod; - } - - @TargetApi(AndroidVersions.API_35_ANDROID_15) - public void removeUniqueIdAssociationByPort(String inputPort) { - try { - Method method = getRemoveUniqueIdAssociationByPortMethod(); - method.invoke(manager, inputPort); - } catch (ReflectiveOperationException e) { - Ln.e("Cannot remove unique id association by port", e); + return (Boolean) injectInputEventMethod.invoke(manager, inputEvent, mode); + } catch (InvocationTargetException | IllegalAccessException e) { + throw new AssertionError(e); } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java index b5fefdd8..a730d1b1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java @@ -1,48 +1,32 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.AndroidVersions; -import com.genymobile.scrcpy.util.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; + private final Method isScreenOnMethod; - static PowerManager create() { - IInterface manager = ServiceManager.getService("power", "android.os.IPowerManager"); - return new PowerManager(manager); - } - - private PowerManager(IInterface manager) { + public PowerManager(IInterface manager) { this.manager = manager; - } - - private Method getIsScreenOnMethod() throws NoSuchMethodException { - if (isScreenOnMethod == null) { - if (Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14) { - isScreenOnMethod = manager.getClass().getMethod("isDisplayInteractive", int.class); - } else { - isScreenOnMethod = manager.getClass().getMethod("isInteractive"); - } - } - return isScreenOnMethod; - } - - public boolean isScreenOn(int displayId) { - try { - Method method = getIsScreenOnMethod(); - if (Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14) { - return (boolean) method.invoke(manager, displayId); - } - return (boolean) method.invoke(manager); - } catch (ReflectiveOperationException e) { - Ln.e("Could not invoke method", e); - return false; + @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); + } catch (NoSuchMethodException e) { + throw new AssertionError(e); + } + } + + public boolean isScreenOn() { + try { + return (Boolean) isScreenOnMethod.invoke(manager); + } catch (InvocationTargetException | IllegalAccessException e) { + throw new AssertionError(e); } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java index b1123b55..0b625c92 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java @@ -1,45 +1,33 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.FakeContext; - import android.annotation.SuppressLint; -import android.content.Context; -import android.hardware.camera2.CameraManager; import android.os.IBinder; import android.os.IInterface; -import java.lang.reflect.Constructor; import java.lang.reflect.Method; -@SuppressLint("PrivateApi,DiscouragedPrivateApi") +@SuppressLint("PrivateApi") public final class ServiceManager { + private final Method getServiceMethod; - private static final Method GET_SERVICE_METHOD; + private WindowManager windowManager; + private DisplayManager displayManager; + private InputManager inputManager; + private PowerManager powerManager; + private StatusBarManager statusBarManager; + private ClipboardManager clipboardManager; - static { + public ServiceManager() { try { - GET_SERVICE_METHOD = Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String.class); + getServiceMethod = 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 static CameraManager cameraManager; - - private ServiceManager() { - /* not instantiable */ - } - - static IInterface getService(String service, String type) { + private IInterface getService(String service, String type) { try { - IBinder binder = (IBinder) GET_SERVICE_METHOD.invoke(null, service); + IBinder binder = (IBinder) getServiceMethod.invoke(null, service); Method asInterfaceMethod = Class.forName(type + "$Stub").getMethod("asInterface", IBinder.class); return (IInterface) asInterfaceMethod.invoke(null, binder); } catch (Exception e) { @@ -47,66 +35,45 @@ public final class ServiceManager { } } - public static WindowManager getWindowManager() { + public WindowManager getWindowManager() { if (windowManager == null) { - windowManager = WindowManager.create(); + windowManager = new WindowManager(getService("window", "android.view.IWindowManager")); } return windowManager; } - // The DisplayManager may be used from both the Controller thread and the video (main) thread - public static synchronized DisplayManager getDisplayManager() { + public DisplayManager getDisplayManager() { if (displayManager == null) { - displayManager = DisplayManager.create(); + displayManager = new DisplayManager(getService("display", "android.hardware.display.IDisplayManager")); } return displayManager; } - public static InputManager getInputManager() { + public InputManager getInputManager() { if (inputManager == null) { - inputManager = InputManager.create(); + inputManager = new InputManager(getService("input", "android.hardware.input.IInputManager")); } return inputManager; } - public static PowerManager getPowerManager() { + public PowerManager getPowerManager() { if (powerManager == null) { - powerManager = PowerManager.create(); + powerManager = new PowerManager(getService("power", "android.os.IPowerManager")); } return powerManager; } - public static StatusBarManager getStatusBarManager() { + public StatusBarManager getStatusBarManager() { if (statusBarManager == null) { - statusBarManager = StatusBarManager.create(); + statusBarManager = new StatusBarManager(getService("statusbar", "com.android.internal.statusbar.IStatusBarService")); } return statusBarManager; } - public static ClipboardManager getClipboardManager() { + public ClipboardManager getClipboardManager() { if (clipboardManager == null) { - // May be null, some devices have no clipboard manager - clipboardManager = ClipboardManager.create(); + clipboardManager = new ClipboardManager(getService("clipboard", "android.content.IClipboard")); } return clipboardManager; } - - public static ActivityManager getActivityManager() { - if (activityManager == null) { - activityManager = ActivityManager.create(); - } - return activityManager; - } - - public static CameraManager getCameraManager() { - if (cameraManager == null) { - try { - Constructor ctor = CameraManager.class.getDeclaredConstructor(Context.class); - cameraManager = ctor.newInstance(FakeContext.get()); - } catch (Exception e) { - throw new AssertionError(e); - } - } - return cameraManager; - } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java index ca80dde2..74003b64 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java @@ -1,97 +1,51 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.Ln; import android.os.IInterface; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -public final class StatusBarManager { +public class StatusBarManager { private final IInterface manager; private Method expandNotificationsPanelMethod; - private boolean expandNotificationPanelMethodCustomVersion; - private Method expandSettingsPanelMethod; - private boolean expandSettingsPanelMethodNewVersion = true; private Method collapsePanelsMethod; - static StatusBarManager create() { - IInterface manager = ServiceManager.getService("statusbar", "com.android.internal.statusbar.IStatusBarService"); - return new StatusBarManager(manager); - } - - private StatusBarManager(IInterface manager) { + public StatusBarManager(IInterface manager) { this.manager = manager; } - private Method getExpandNotificationsPanelMethod() throws NoSuchMethodException { + public void expandNotificationsPanel() { 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; + Ln.e("ServiceBarManager.expandNotificationsPanel() is not available on this device"); + return; } } - 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 (ReflectiveOperationException 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 (ReflectiveOperationException e) { - Ln.e("Could not invoke method", e); + expandNotificationsPanelMethod.invoke(manager); + } catch (InvocationTargetException | IllegalAccessException e) { + Ln.e("Cannot invoke ServiceBarManager.expandNotificationsPanel()", e); } } public void collapsePanels() { + if (collapsePanelsMethod == null) { + try { + collapsePanelsMethod = manager.getClass().getMethod("collapsePanels"); + } catch (NoSuchMethodException e) { + Ln.e("ServiceBarManager.collapsePanels() is not available on this device"); + return; + } + } try { - Method method = getCollapsePanelsMethod(); - method.invoke(manager); - } catch (ReflectiveOperationException e) { - Ln.e("Could not invoke method", e); + collapsePanelsMethod.invoke(manager); + } catch (InvocationTargetException | IllegalAccessException e) { + Ln.e("Cannot invoke ServiceBarManager.collapsePanels()", e); } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java index 3bae4a37..bed21b3c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -1,16 +1,10 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.AndroidVersions; -import com.genymobile.scrcpy.util.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.Method; - @SuppressLint("PrivateApi") public final class SurfaceControl { @@ -28,11 +22,6 @@ public final class SurfaceControl { } } - private static Method getBuiltInDisplayMethod; - private static Method setDisplayPowerModeMethod; - private static Method getPhysicalDisplayTokenMethod; - private static Method getPhysicalDisplayIdsMethod; - private SurfaceControl() { // only static methods } @@ -78,106 +67,27 @@ public final class SurfaceControl { } } - public static IBinder createDisplay(String name, boolean secure) throws Exception { - return (IBinder) CLASS.getMethod("createDisplay", String.class, boolean.class).invoke(null, name, secure); - } - - private static Method getGetBuiltInDisplayMethod() throws NoSuchMethodException { - if (getBuiltInDisplayMethod == null) { - // the method signature has changed in Android 10 - // - if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { - getBuiltInDisplayMethod = CLASS.getMethod("getBuiltInDisplay", int.class); - } else { - getBuiltInDisplayMethod = CLASS.getMethod("getInternalDisplayToken"); - } - } - return getBuiltInDisplayMethod; - } - - public static boolean hasGetBuildInDisplayMethod() { + public static IBinder createDisplay(String name, boolean secure) { try { - getGetBuiltInDisplayMethod(); - return true; - } catch (NoSuchMethodException e) { - return false; + return (IBinder) CLASS.getMethod("createDisplay", String.class, boolean.class).invoke(null, name, secure); + } catch (Exception e) { + throw new AssertionError(e); } } - public static IBinder getBuiltInDisplay() { + public static IBinder getBuiltInDisplay(int builtInDisplayId) { try { - Method method = getGetBuiltInDisplayMethod(); - if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { - // call getBuiltInDisplay(0) - return (IBinder) method.invoke(null, 0); - } - - // call getInternalDisplayToken() - return (IBinder) method.invoke(null); - } catch (ReflectiveOperationException e) { - Ln.e("Could not invoke method", e); - return null; + return (IBinder) CLASS.getMethod("getBuiltInDisplay", int.class).invoke(null, builtInDisplayId); + } catch (Exception e) { + throw new AssertionError(e); } } - private static Method getGetPhysicalDisplayTokenMethod() throws NoSuchMethodException { - if (getPhysicalDisplayTokenMethod == null) { - getPhysicalDisplayTokenMethod = CLASS.getMethod("getPhysicalDisplayToken", long.class); - } - return getPhysicalDisplayTokenMethod; - } - - public static IBinder getPhysicalDisplayToken(long physicalDisplayId) { + public static void setDisplayPowerMode(IBinder displayToken, int mode) { try { - Method method = getGetPhysicalDisplayTokenMethod(); - return (IBinder) method.invoke(null, physicalDisplayId); - } catch (ReflectiveOperationException e) { - Ln.e("Could not invoke method", e); - return null; - } - } - - private static Method getGetPhysicalDisplayIdsMethod() throws NoSuchMethodException { - if (getPhysicalDisplayIdsMethod == null) { - getPhysicalDisplayIdsMethod = CLASS.getMethod("getPhysicalDisplayIds"); - } - return getPhysicalDisplayIdsMethod; - } - - public static boolean hasGetPhysicalDisplayIdsMethod() { - try { - getGetPhysicalDisplayIdsMethod(); - return true; - } catch (NoSuchMethodException e) { - return false; - } - } - - public static long[] getPhysicalDisplayIds() { - try { - Method method = getGetPhysicalDisplayIdsMethod(); - return (long[]) method.invoke(null); - } catch (ReflectiveOperationException e) { - Ln.e("Could not invoke method", e); - return null; - } - } - - 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 (ReflectiveOperationException e) { - Ln.e("Could not invoke method", e); - return false; + CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class).invoke(null, displayToken, mode); + } catch (Exception e) { + throw new AssertionError(e); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java index 7ba5cc06..56330f9d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -1,267 +1,42 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.AndroidVersions; -import com.genymobile.scrcpy.util.Ln; - -import android.annotation.TargetApi; -import android.os.Build; import android.os.IInterface; -import android.view.IDisplayWindowListener; - -import java.lang.reflect.Method; +import android.view.IRotationWatcher; public final class WindowManager { - - @SuppressWarnings("checkstyle:LineLength") - // - public static final int DISPLAY_IME_POLICY_LOCAL = 0; - public static final int DISPLAY_IME_POLICY_FALLBACK_DISPLAY = 1; - public static final int DISPLAY_IME_POLICY_HIDE = 2; - private final IInterface manager; - private Method getRotationMethod; - private Method freezeDisplayRotationMethod; - private int freezeDisplayRotationMethodVersion; - - private Method isDisplayRotationFrozenMethod; - private int isDisplayRotationFrozenMethodVersion; - - private Method thawDisplayRotationMethod; - private int thawDisplayRotationMethodVersion; - - private Method getDisplayImePolicyMethod; - private Method setDisplayImePolicyMethod; - - static WindowManager create() { - IInterface manager = ServiceManager.getService("window", "android.view.IWindowManager"); - return new WindowManager(manager); - } - - private WindowManager(IInterface manager) { + 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 getFreezeDisplayRotationMethod() throws NoSuchMethodException { - if (freezeDisplayRotationMethod == null) { - try { - // Android 15 preview and 14 QPR3 Beta added a String caller parameter for debugging: - // - freezeDisplayRotationMethod = manager.getClass().getMethod("freezeDisplayRotation", int.class, int.class, String.class); - freezeDisplayRotationMethodVersion = 0; - } catch (NoSuchMethodException e) { - try { - // New method added by this commit: - // - freezeDisplayRotationMethod = manager.getClass().getMethod("freezeDisplayRotation", int.class, int.class); - freezeDisplayRotationMethodVersion = 1; - } catch (NoSuchMethodException e1) { - freezeDisplayRotationMethod = manager.getClass().getMethod("freezeRotation", int.class); - freezeDisplayRotationMethodVersion = 2; - } - } - } - return freezeDisplayRotationMethod; - } - - private Method getIsDisplayRotationFrozenMethod() throws NoSuchMethodException { - if (isDisplayRotationFrozenMethod == null) { - try { - // New method added by this commit: - // - isDisplayRotationFrozenMethod = manager.getClass().getMethod("isDisplayRotationFrozen", int.class); - isDisplayRotationFrozenMethodVersion = 0; - } catch (NoSuchMethodException e) { - isDisplayRotationFrozenMethod = manager.getClass().getMethod("isRotationFrozen"); - isDisplayRotationFrozenMethodVersion = 1; - } - } - return isDisplayRotationFrozenMethod; - } - - private Method getThawDisplayRotationMethod() throws NoSuchMethodException { - if (thawDisplayRotationMethod == null) { - try { - // Android 15 preview and 14 QPR3 Beta added a String caller parameter for debugging: - // - thawDisplayRotationMethod = manager.getClass().getMethod("thawDisplayRotation", int.class, String.class); - thawDisplayRotationMethodVersion = 0; - } catch (NoSuchMethodException e) { - try { - // New method added by this commit: - // - thawDisplayRotationMethod = manager.getClass().getMethod("thawDisplayRotation", int.class); - thawDisplayRotationMethodVersion = 1; - } catch (NoSuchMethodException e1) { - thawDisplayRotationMethod = manager.getClass().getMethod("thawRotation"); - thawDisplayRotationMethodVersion = 2; - } - } - } - return thawDisplayRotationMethod; - } - public int getRotation() { try { - Method method = getGetRotationMethod(); - return (int) method.invoke(manager); - } catch (ReflectiveOperationException e) { - Ln.e("Could not invoke method", e); - return 0; - } - } - - public void freezeRotation(int displayId, int rotation) { - try { - Method method = getFreezeDisplayRotationMethod(); - switch (freezeDisplayRotationMethodVersion) { - case 0: - method.invoke(manager, displayId, rotation, "scrcpy#freezeRotation"); - break; - case 1: - method.invoke(manager, displayId, rotation); - break; - default: - if (displayId != 0) { - Ln.e("Secondary display rotation not supported on this device"); - return; - } - method.invoke(manager, rotation); - break; + Class cls = manager.getClass(); + try { + return (Integer) manager.getClass().getMethod("getRotation").invoke(manager); + } catch (NoSuchMethodException e) { + // method changed since this commit: + // https://android.googlesource.com/platform/frameworks/base/+/8ee7285128c3843401d4c4d0412cd66e86ba49e3%5E%21/#F2 + return (Integer) cls.getMethod("getDefaultDisplayRotation").invoke(manager); } - } catch (ReflectiveOperationException e) { - Ln.e("Could not invoke method", e); - } - } - - public boolean isRotationFrozen(int displayId) { - try { - Method method = getIsDisplayRotationFrozenMethod(); - switch (isDisplayRotationFrozenMethodVersion) { - case 0: - return (boolean) method.invoke(manager, displayId); - default: - if (displayId != 0) { - Ln.e("Secondary display rotation not supported on this device"); - return false; - } - return (boolean) method.invoke(manager); - } - } catch (ReflectiveOperationException e) { - Ln.e("Could not invoke method", e); - return false; - } - } - - public void thawRotation(int displayId) { - try { - Method method = getThawDisplayRotationMethod(); - switch (thawDisplayRotationMethodVersion) { - case 0: - method.invoke(manager, displayId, "scrcpy#thawRotation"); - break; - case 1: - method.invoke(manager, displayId); - break; - default: - if (displayId != 0) { - Ln.e("Secondary display rotation not supported on this device"); - return; - } - method.invoke(manager); - break; - } - } catch (ReflectiveOperationException e) { - Ln.e("Could not invoke method", e); - } - } - - @TargetApi(AndroidVersions.API_30_ANDROID_11) - public int[] registerDisplayWindowListener(IDisplayWindowListener listener) { - try { - return (int[]) manager.getClass().getMethod("registerDisplayWindowListener", IDisplayWindowListener.class).invoke(manager, listener); } catch (Exception e) { - Ln.e("Could not register display window listener", e); + throw new AssertionError(e); } - return null; } - @TargetApi(AndroidVersions.API_30_ANDROID_11) - public void unregisterDisplayWindowListener(IDisplayWindowListener listener) { + public void registerRotationWatcher(IRotationWatcher rotationWatcher) { try { - manager.getClass().getMethod("unregisterDisplayWindowListener", IDisplayWindowListener.class).invoke(manager, listener); + Class cls = manager.getClass(); + try { + cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher); + } catch (NoSuchMethodException e) { + // display parameter added since this commit: + // https://android.googlesource.com/platform/frameworks/base/+/35fa3c26adcb5f6577849fd0df5228b1f67cf2c6%5E%21/#F1 + cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, 0); + } } catch (Exception e) { - Ln.e("Could not unregister display window listener", e); - } - } - - @TargetApi(AndroidVersions.API_29_ANDROID_10) - private Method getGetDisplayImePolicyMethod() throws NoSuchMethodException { - if (getDisplayImePolicyMethod == null) { - if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) { - getDisplayImePolicyMethod = manager.getClass().getMethod("getDisplayImePolicy", int.class); - } else { - getDisplayImePolicyMethod = manager.getClass().getMethod("shouldShowIme", int.class); - } - } - return getDisplayImePolicyMethod; - } - - @TargetApi(AndroidVersions.API_29_ANDROID_10) - public int getDisplayImePolicy(int displayId) { - try { - Method method = getGetDisplayImePolicyMethod(); - if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) { - return (int) method.invoke(manager, displayId); - } - boolean shouldShowIme = (boolean) method.invoke(manager, displayId); - return shouldShowIme ? DISPLAY_IME_POLICY_LOCAL : DISPLAY_IME_POLICY_FALLBACK_DISPLAY; - } catch (ReflectiveOperationException e) { - Ln.e("Could not invoke method", e); - return -1; - } - } - - @TargetApi(AndroidVersions.API_29_ANDROID_10) - private Method getSetDisplayImePolicyMethod() throws NoSuchMethodException { - if (setDisplayImePolicyMethod == null) { - if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) { - setDisplayImePolicyMethod = manager.getClass().getMethod("setDisplayImePolicy", int.class, int.class); - } else { - setDisplayImePolicyMethod = manager.getClass().getMethod("setShouldShowIme", int.class, boolean.class); - } - } - return setDisplayImePolicyMethod; - } - - @TargetApi(AndroidVersions.API_29_ANDROID_10) - public void setDisplayImePolicy(int displayId, int displayImePolicy) { - try { - Method method = getSetDisplayImePolicyMethod(); - if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) { - method.invoke(manager, displayId, displayImePolicy); - } else if (displayImePolicy != DISPLAY_IME_POLICY_HIDE) { - method.invoke(manager, displayId, displayImePolicy == DISPLAY_IME_POLICY_LOCAL); - } else { - Ln.w("DISPLAY_IME_POLICY_HIDE is not supported before Android 12"); - } - } catch (ReflectiveOperationException e) { - Ln.e("Could not invoke method", e); + throw new AssertionError(e); } } } diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java new file mode 100644 index 00000000..df1db1a6 --- /dev/null +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -0,0 +1,304 @@ +package 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(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(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.writeShort(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.TEXT_MAX_LENGTH]; + Arrays.fill(text, (byte) 'a'); + dos.writeShort(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 testParseMouseEvent() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); + dos.writeByte(MotionEvent.ACTION_DOWN); + dos.writeInt(MotionEvent.BUTTON_PRIMARY); + 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(MotionEvent.ACTION_DOWN, event.getAction()); + Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode()); + Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); + } + + @Test + @SuppressWarnings("checkstyle:MagicNumber") + public void testParseScrollEvent() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_INJECT_SCROLL_EVENT); + dos.writeInt(260); + dos.writeInt(1026); + dos.writeShort(1080); + dos.writeShort(1920); + dos.writeInt(1); + dos.writeInt(-1); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_INJECT_SCROLL_EVENT, event.getType()); + Assert.assertEquals(260, event.getPosition().getPoint().getX()); + Assert.assertEquals(1026, event.getPosition().getPoint().getY()); + Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth()); + Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight()); + Assert.assertEquals(1, event.getHScroll()); + Assert.assertEquals(-1, event.getVScroll()); + } + + @Test + public void testParseBackOrScreenOnEvent() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_BACK_OR_SCREEN_ON); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_BACK_OR_SCREEN_ON, event.getType()); + } + + @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 testParseCollapseNotificationPanelEvent() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL, 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); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_GET_CLIPBOARD, event.getType()); + } + + @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); + byte[] text = "testé".getBytes(StandardCharsets.UTF_8); + dos.writeShort(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("testé", event.getText()); + } + + @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(); + + 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 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(KeyEvent.META_CTRL_ON); + + dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); + dos.writeByte(MotionEvent.ACTION_DOWN); + dos.writeInt(MotionEvent.BUTTON_PRIMARY); + 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(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(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(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(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(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(KeyEvent.META_CTRL_ON, event.getMetaState()); + } +} diff --git a/server/src/test/java/com/genymobile/scrcpy/util/StringUtilsTest.java b/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java similarity index 93% rename from server/src/test/java/com/genymobile/scrcpy/util/StringUtilsTest.java rename to server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java index c72b112a..7d89ee64 100644 --- a/server/src/test/java/com/genymobile/scrcpy/util/StringUtilsTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy.util; +package com.genymobile.scrcpy; import org.junit.Assert; import org.junit.Test; @@ -8,6 +8,7 @@ import java.nio.charset.StandardCharsets; public class StringUtilsTest { @Test + @SuppressWarnings("checkstyle:MagicNumber") public void testUtf8Truncate() { String s = "aÉbÔc"; byte[] utf8 = s.getBytes(StandardCharsets.UTF_8); diff --git a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java deleted file mode 100644 index 0cc0a6b5..00000000 --- a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java +++ /dev/null @@ -1,496 +0,0 @@ -package com.genymobile.scrcpy.control; - -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.EOFException; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; - -public class ControlMessageReaderTest { - - @Test - public void testParseKeycodeEvent() throws IOException { - 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(); - - ByteArrayInputStream bis = new ByteArrayInputStream(packet); - ControlMessageReader reader = new ControlMessageReader(bis); - - ControlMessage event = reader.read(); - 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()); - - Assert.assertEquals(-1, bis.read()); // EOS - } - - @Test - public void testParseTextEvent() throws IOException { - 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(); - - ByteArrayInputStream bis = new ByteArrayInputStream(packet); - ControlMessageReader reader = new ControlMessageReader(bis); - - ControlMessage event = reader.read(); - Assert.assertEquals(ControlMessage.TYPE_INJECT_TEXT, event.getType()); - Assert.assertEquals("testé", event.getText()); - - Assert.assertEquals(-1, bis.read()); // EOS - } - - @Test - public void testParseLongTextEvent() throws IOException { - 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(); - - ByteArrayInputStream bis = new ByteArrayInputStream(packet); - ControlMessageReader reader = new ControlMessageReader(bis); - - ControlMessage event = reader.read(); - Assert.assertEquals(ControlMessage.TYPE_INJECT_TEXT, event.getType()); - Assert.assertEquals(new String(text, StandardCharsets.US_ASCII), event.getText()); - - Assert.assertEquals(-1, bis.read()); // EOS - } - - @Test - public void testParseTouchEvent() throws IOException { - 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); // action button - dos.writeInt(MotionEvent.BUTTON_PRIMARY); // buttons - - byte[] packet = bos.toByteArray(); - - ByteArrayInputStream bis = new ByteArrayInputStream(packet); - ControlMessageReader reader = new ControlMessageReader(bis); - - ControlMessage event = reader.read(); - 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.getActionButton()); - Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getButtons()); - - Assert.assertEquals(-1, bis.read()); // EOS - } - - @Test - public void testParseScrollEvent() throws IOException { - 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); // -16.0f encoded as i16 (the range is [-16, 16]) - dos.writeInt(1); - byte[] packet = bos.toByteArray(); - - ByteArrayInputStream bis = new ByteArrayInputStream(packet); - ControlMessageReader reader = new ControlMessageReader(bis); - - ControlMessage event = reader.read(); - 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(-16f, event.getVScroll(), 0f); - Assert.assertEquals(1, event.getButtons()); - - Assert.assertEquals(-1, bis.read()); // EOS - } - - @Test - public void testParseBackOrScreenOnEvent() throws IOException { - 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(); - - ByteArrayInputStream bis = new ByteArrayInputStream(packet); - ControlMessageReader reader = new ControlMessageReader(bis); - - ControlMessage event = reader.read(); - Assert.assertEquals(ControlMessage.TYPE_BACK_OR_SCREEN_ON, event.getType()); - Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); - - Assert.assertEquals(-1, bis.read()); // EOS - } - - @Test - public void testParseExpandNotificationPanelEvent() throws IOException { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL); - byte[] packet = bos.toByteArray(); - - ByteArrayInputStream bis = new ByteArrayInputStream(packet); - ControlMessageReader reader = new ControlMessageReader(bis); - - ControlMessage event = reader.read(); - Assert.assertEquals(ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL, event.getType()); - - Assert.assertEquals(-1, bis.read()); // EOS - } - - @Test - public void testParseExpandSettingsPanelEvent() throws IOException { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(ControlMessage.TYPE_EXPAND_SETTINGS_PANEL); - byte[] packet = bos.toByteArray(); - - ByteArrayInputStream bis = new ByteArrayInputStream(packet); - ControlMessageReader reader = new ControlMessageReader(bis); - - ControlMessage event = reader.read(); - Assert.assertEquals(ControlMessage.TYPE_EXPAND_SETTINGS_PANEL, event.getType()); - - Assert.assertEquals(-1, bis.read()); // EOS - } - - @Test - public void testParseCollapsePanelsEvent() throws IOException { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(ControlMessage.TYPE_COLLAPSE_PANELS); - byte[] packet = bos.toByteArray(); - - ByteArrayInputStream bis = new ByteArrayInputStream(packet); - ControlMessageReader reader = new ControlMessageReader(bis); - - ControlMessage event = reader.read(); - Assert.assertEquals(ControlMessage.TYPE_COLLAPSE_PANELS, event.getType()); - - Assert.assertEquals(-1, bis.read()); // EOS - } - - @Test - public void testParseGetClipboardEvent() throws IOException { - 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(); - - ByteArrayInputStream bis = new ByteArrayInputStream(packet); - ControlMessageReader reader = new ControlMessageReader(bis); - - ControlMessage event = reader.read(); - Assert.assertEquals(ControlMessage.TYPE_GET_CLIPBOARD, event.getType()); - Assert.assertEquals(ControlMessage.COPY_KEY_COPY, event.getCopyKey()); - - Assert.assertEquals(-1, bis.read()); // EOS - } - - @Test - public void testParseSetClipboardEvent() throws IOException { - 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(); - - ByteArrayInputStream bis = new ByteArrayInputStream(packet); - ControlMessageReader reader = new ControlMessageReader(bis); - - ControlMessage event = reader.read(); - Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType()); - Assert.assertEquals(0x0102030405060708L, event.getSequence()); - Assert.assertEquals("testé", event.getText()); - Assert.assertTrue(event.getPaste()); - - Assert.assertEquals(-1, bis.read()); // EOS - } - - @Test - public void testParseBigSetClipboardEvent() throws IOException { - 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(); - - ByteArrayInputStream bis = new ByteArrayInputStream(packet); - ControlMessageReader reader = new ControlMessageReader(bis); - - ControlMessage event = reader.read(); - Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType()); - Assert.assertEquals(0x0807060504030201L, event.getSequence()); - Assert.assertEquals(text, event.getText()); - Assert.assertTrue(event.getPaste()); - - Assert.assertEquals(-1, bis.read()); // EOS - } - - @Test - public void testParseSetDisplayPower() throws IOException { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(ControlMessage.TYPE_SET_DISPLAY_POWER); - dos.writeBoolean(true); - byte[] packet = bos.toByteArray(); - - ByteArrayInputStream bis = new ByteArrayInputStream(packet); - ControlMessageReader reader = new ControlMessageReader(bis); - - ControlMessage event = reader.read(); - Assert.assertEquals(ControlMessage.TYPE_SET_DISPLAY_POWER, event.getType()); - Assert.assertTrue(event.getOn()); - - Assert.assertEquals(-1, bis.read()); // EOS - } - - @Test - public void testParseRotateDevice() throws IOException { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(ControlMessage.TYPE_ROTATE_DEVICE); - byte[] packet = bos.toByteArray(); - - ByteArrayInputStream bis = new ByteArrayInputStream(packet); - ControlMessageReader reader = new ControlMessageReader(bis); - - ControlMessage event = reader.read(); - Assert.assertEquals(ControlMessage.TYPE_ROTATE_DEVICE, event.getType()); - - Assert.assertEquals(-1, bis.read()); // EOS - } - - @Test - public void testParseUhidCreate() throws IOException { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(ControlMessage.TYPE_UHID_CREATE); - dos.writeShort(42); // id - dos.writeShort(0x1234); // vendorId - dos.writeShort(0x5678); // productId - dos.writeByte(3); // name size - dos.write("ABC".getBytes(StandardCharsets.US_ASCII)); - byte[] data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; - dos.writeShort(data.length); // report desc size - dos.write(data); - byte[] packet = bos.toByteArray(); - - ByteArrayInputStream bis = new ByteArrayInputStream(packet); - ControlMessageReader reader = new ControlMessageReader(bis); - - ControlMessage event = reader.read(); - Assert.assertEquals(ControlMessage.TYPE_UHID_CREATE, event.getType()); - Assert.assertEquals(42, event.getId()); - Assert.assertEquals(0x1234, event.getVendorId()); - Assert.assertEquals(0x5678, event.getProductId()); - Assert.assertEquals("ABC", event.getText()); - Assert.assertArrayEquals(data, event.getData()); - - Assert.assertEquals(-1, bis.read()); // EOS - } - - @Test - public void testParseUhidInput() throws IOException { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(ControlMessage.TYPE_UHID_INPUT); - dos.writeShort(42); // id - byte[] data = {1, 2, 3, 4, 5}; - dos.writeShort(data.length); // size - dos.write(data); - byte[] packet = bos.toByteArray(); - - ByteArrayInputStream bis = new ByteArrayInputStream(packet); - ControlMessageReader reader = new ControlMessageReader(bis); - - ControlMessage event = reader.read(); - Assert.assertEquals(ControlMessage.TYPE_UHID_INPUT, event.getType()); - Assert.assertEquals(42, event.getId()); - Assert.assertArrayEquals(data, event.getData()); - - Assert.assertEquals(-1, bis.read()); // EOS - } - - @Test - public void testParseUhidDestroy() throws IOException { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(ControlMessage.TYPE_UHID_DESTROY); - dos.writeShort(42); // id - byte[] packet = bos.toByteArray(); - - ByteArrayInputStream bis = new ByteArrayInputStream(packet); - ControlMessageReader reader = new ControlMessageReader(bis); - - ControlMessage event = reader.read(); - Assert.assertEquals(ControlMessage.TYPE_UHID_DESTROY, event.getType()); - Assert.assertEquals(42, event.getId()); - - Assert.assertEquals(-1, bis.read()); // EOS - } - - @Test - public void testParseOpenHardKeyboardSettings() throws IOException { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS); - byte[] packet = bos.toByteArray(); - - ByteArrayInputStream bis = new ByteArrayInputStream(packet); - ControlMessageReader reader = new ControlMessageReader(bis); - - ControlMessage event = reader.read(); - Assert.assertEquals(ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS, event.getType()); - - Assert.assertEquals(-1, bis.read()); // EOS - } - - @Test - public void testParseStartApp() throws IOException { - byte[] name = "firefox".getBytes(StandardCharsets.UTF_8); - - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(ControlMessage.TYPE_START_APP); - dos.writeByte(name.length); - dos.write(name); - byte[] packet = bos.toByteArray(); - - ByteArrayInputStream bis = new ByteArrayInputStream(packet); - ControlMessageReader reader = new ControlMessageReader(bis); - - ControlMessage event = reader.read(); - Assert.assertEquals(ControlMessage.TYPE_START_APP, event.getType()); - Assert.assertEquals("firefox", event.getText()); - - Assert.assertEquals(-1, bis.read()); // EOS - } - - @Test - public void testMultiEvents() throws IOException { - 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(); - - ByteArrayInputStream bis = new ByteArrayInputStream(packet); - ControlMessageReader reader = new ControlMessageReader(bis); - - ControlMessage event = reader.read(); - 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.read(); - 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()); - - Assert.assertEquals(-1, bis.read()); // EOS - } - - @Test - public void testPartialEvents() throws IOException { - 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(); - ByteArrayInputStream bis = new ByteArrayInputStream(packet); - ControlMessageReader reader = new ControlMessageReader(bis); - - ControlMessage event = reader.read(); - 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()); - - try { - event = reader.read(); - Assert.fail("Reader did not reach EOF"); - } catch (EOFException e) { - // expected - } - } -} diff --git a/server/src/test/java/com/genymobile/scrcpy/control/DeviceMessageWriterTest.java b/server/src/test/java/com/genymobile/scrcpy/control/DeviceMessageWriterTest.java deleted file mode 100644 index 4e4717fd..00000000 --- a/server/src/test/java/com/genymobile/scrcpy/control/DeviceMessageWriterTest.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.genymobile.scrcpy.control; - -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 { - 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(); - - bos = new ByteArrayOutputStream(); - DeviceMessageWriter writer = new DeviceMessageWriter(bos); - - DeviceMessage msg = DeviceMessage.createClipboard(text); - writer.write(msg); - - byte[] actual = bos.toByteArray(); - - Assert.assertArrayEquals(expected, actual); - } - - @Test - public void testSerializeAckSetClipboard() throws IOException { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(DeviceMessage.TYPE_ACK_CLIPBOARD); - dos.writeLong(0x0102030405060708L); - byte[] expected = bos.toByteArray(); - - bos = new ByteArrayOutputStream(); - DeviceMessageWriter writer = new DeviceMessageWriter(bos); - - DeviceMessage msg = DeviceMessage.createAckClipboard(0x0102030405060708L); - writer.write(msg); - - byte[] actual = bos.toByteArray(); - - Assert.assertArrayEquals(expected, actual); - } - - @Test - public void testSerializeUhidOutput() throws IOException { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(DeviceMessage.TYPE_UHID_OUTPUT); - dos.writeShort(42); // id - byte[] data = {1, 2, 3, 4, 5}; - dos.writeShort(data.length); - dos.write(data); - byte[] expected = bos.toByteArray(); - - bos = new ByteArrayOutputStream(); - DeviceMessageWriter writer = new DeviceMessageWriter(bos); - - DeviceMessage msg = DeviceMessage.createUhidOutput(42, data); - writer.write(msg); - - byte[] actual = bos.toByteArray(); - - Assert.assertArrayEquals(expected, actual); - } -} diff --git a/server/src/test/java/com/genymobile/scrcpy/util/BinaryTest.java b/server/src/test/java/com/genymobile/scrcpy/util/BinaryTest.java deleted file mode 100644 index 7ee95ac5..00000000 --- a/server/src/test/java/com/genymobile/scrcpy/util/BinaryTest.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.genymobile.scrcpy.util; - -import org.junit.Assert; -import org.junit.Test; - -public class BinaryTest { - - @Test - public void testU16FixedPointToFloat() { - final float delta = 0.0f; // on these values, there MUST be no rounding error - Assert.assertEquals(0.0f, Binary.u16FixedPointToFloat((short) 0), delta); - Assert.assertEquals(0.03125f, Binary.u16FixedPointToFloat((short) 0x800), delta); - Assert.assertEquals(0.0625f, Binary.u16FixedPointToFloat((short) 0x1000), delta); - Assert.assertEquals(0.125f, Binary.u16FixedPointToFloat((short) 0x2000), delta); - Assert.assertEquals(0.25f, Binary.u16FixedPointToFloat((short) 0x4000), delta); - Assert.assertEquals(0.5f, Binary.u16FixedPointToFloat((short) 0x8000), delta); - Assert.assertEquals(0.75f, Binary.u16FixedPointToFloat((short) 0xc000), delta); - Assert.assertEquals(1.0f, Binary.u16FixedPointToFloat((short) 0xffff), delta); - } - - @Test - public void testI16FixedPointToFloat() { - final float delta = 0.0f; // on these values, there MUST be no rounding error - - Assert.assertEquals(0.0f, Binary.i16FixedPointToFloat((short) 0), delta); - Assert.assertEquals(0.03125f, Binary.i16FixedPointToFloat((short) 0x400), delta); - Assert.assertEquals(0.0625f, Binary.i16FixedPointToFloat((short) 0x800), delta); - Assert.assertEquals(0.125f, Binary.i16FixedPointToFloat((short) 0x1000), delta); - Assert.assertEquals(0.25f, Binary.i16FixedPointToFloat((short) 0x2000), delta); - Assert.assertEquals(0.5f, Binary.i16FixedPointToFloat((short) 0x4000), delta); - Assert.assertEquals(0.75f, Binary.i16FixedPointToFloat((short) 0x6000), delta); - Assert.assertEquals(1.0f, Binary.i16FixedPointToFloat((short) 0x7fff), delta); - - Assert.assertEquals(-0.03125f, Binary.i16FixedPointToFloat((short) -0x400), delta); - Assert.assertEquals(-0.0625f, Binary.i16FixedPointToFloat((short) -0x800), delta); - Assert.assertEquals(-0.125f, Binary.i16FixedPointToFloat((short) -0x1000), delta); - Assert.assertEquals(-0.25f, Binary.i16FixedPointToFloat((short) -0x2000), delta); - Assert.assertEquals(-0.5f, Binary.i16FixedPointToFloat((short) -0x4000), delta); - Assert.assertEquals(-0.75f, Binary.i16FixedPointToFloat((short) -0x6000), delta); - Assert.assertEquals(-1.0f, Binary.i16FixedPointToFloat((short) -0x8000), delta); - } -} diff --git a/server/src/test/java/com/genymobile/scrcpy/util/CodecOptionsTest.java b/server/src/test/java/com/genymobile/scrcpy/util/CodecOptionsTest.java deleted file mode 100644 index ffd8e32e..00000000 --- a/server/src/test/java/com/genymobile/scrcpy/util/CodecOptionsTest.java +++ /dev/null @@ -1,114 +0,0 @@ -package com.genymobile.scrcpy.util; - -import org.junit.Assert; -import org.junit.Test; - -import java.util.List; - -public class CodecOptionsTest { - - @Test - public void testIntegerImplicit() { - List codecOptions = CodecOption.parse("some_key=5"); - - Assert.assertEquals(1, codecOptions.size()); - - CodecOption option = codecOptions.get(0); - Assert.assertEquals("some_key", option.getKey()); - Assert.assertEquals(5, option.getValue()); - } - - @Test - public void testInteger() { - List codecOptions = CodecOption.parse("some_key:int=5"); - - Assert.assertEquals(1, codecOptions.size()); - - CodecOption option = codecOptions.get(0); - Assert.assertEquals("some_key", option.getKey()); - Assert.assertTrue(option.getValue() instanceof Integer); - Assert.assertEquals(5, option.getValue()); - } - - @Test - public void testLong() { - List codecOptions = CodecOption.parse("some_key:long=5"); - - Assert.assertEquals(1, codecOptions.size()); - - CodecOption option = codecOptions.get(0); - Assert.assertEquals("some_key", option.getKey()); - Assert.assertTrue(option.getValue() instanceof Long); - Assert.assertEquals(5L, option.getValue()); - } - - @Test - public void testFloat() { - List codecOptions = CodecOption.parse("some_key:float=4.5"); - - Assert.assertEquals(1, codecOptions.size()); - - CodecOption option = codecOptions.get(0); - Assert.assertEquals("some_key", option.getKey()); - Assert.assertTrue(option.getValue() instanceof Float); - Assert.assertEquals(4.5f, option.getValue()); - } - - @Test - public void testString() { - List codecOptions = CodecOption.parse("some_key:string=some_value"); - - Assert.assertEquals(1, codecOptions.size()); - - CodecOption option = codecOptions.get(0); - Assert.assertEquals("some_key", option.getKey()); - Assert.assertTrue(option.getValue() instanceof String); - Assert.assertEquals("some_value", option.getValue()); - } - - @Test - public void testStringEscaped() { - List codecOptions = CodecOption.parse("some_key:string=warning\\,this_is_not=a_new_key"); - - Assert.assertEquals(1, codecOptions.size()); - - CodecOption option = codecOptions.get(0); - Assert.assertEquals("some_key", option.getKey()); - Assert.assertTrue(option.getValue() instanceof String); - Assert.assertEquals("warning,this_is_not=a_new_key", option.getValue()); - } - - @Test - public void testList() { - List codecOptions = CodecOption.parse("a=1,b:int=2,c:long=3,d:float=4.5,e:string=a\\,b=c"); - - Assert.assertEquals(5, codecOptions.size()); - - CodecOption option; - - option = codecOptions.get(0); - Assert.assertEquals("a", option.getKey()); - Assert.assertTrue(option.getValue() instanceof Integer); - Assert.assertEquals(1, option.getValue()); - - option = codecOptions.get(1); - Assert.assertEquals("b", option.getKey()); - Assert.assertTrue(option.getValue() instanceof Integer); - Assert.assertEquals(2, option.getValue()); - - option = codecOptions.get(2); - Assert.assertEquals("c", option.getKey()); - Assert.assertTrue(option.getValue() instanceof Long); - Assert.assertEquals(3L, option.getValue()); - - option = codecOptions.get(3); - Assert.assertEquals("d", option.getKey()); - Assert.assertTrue(option.getValue() instanceof Float); - Assert.assertEquals(4.5f, option.getValue()); - - option = codecOptions.get(4); - Assert.assertEquals("e", option.getKey()); - Assert.assertTrue(option.getValue() instanceof String); - Assert.assertEquals("a,b=c", option.getValue()); - } -} diff --git a/server/src/test/java/com/genymobile/scrcpy/util/CommandParserTest.java b/server/src/test/java/com/genymobile/scrcpy/util/CommandParserTest.java deleted file mode 100644 index 7e1d55b5..00000000 --- a/server/src/test/java/com/genymobile/scrcpy/util/CommandParserTest.java +++ /dev/null @@ -1,243 +0,0 @@ -package com.genymobile.scrcpy.util; - -import com.genymobile.scrcpy.device.DisplayInfo; -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()); - } -}