pax_global_header00006660000000000000000000000064150570274140014517gustar00rootroot0000000000000052 comment=f663bbec129b10287377580306f06bf8c50ffb6a Genymobile-scrcpy-facefde/000077500000000000000000000000001505702741400160315ustar00rootroot00000000000000Genymobile-scrcpy-facefde/.github/000077500000000000000000000000001505702741400173715ustar00rootroot00000000000000Genymobile-scrcpy-facefde/.github/FUNDING.yml000066400000000000000000000001051505702741400212020ustar00rootroot00000000000000github: [rom1v] liberapay: rom1v custom: ["https://paypal.me/rom2v"] Genymobile-scrcpy-facefde/.github/ISSUE_TEMPLATE/000077500000000000000000000000001505702741400215545ustar00rootroot00000000000000Genymobile-scrcpy-facefde/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000016671505702741400242600ustar00rootroot00000000000000--- 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. Genymobile-scrcpy-facefde/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000014051505702741400253010ustar00rootroot00000000000000--- 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. Genymobile-scrcpy-facefde/.github/ISSUE_TEMPLATE/question.md000066400000000000000000000001361505702741400237450ustar00rootroot00000000000000--- name: Question about: Ask a question about scrcpy title: '' labels: '' assignees: '' --- Genymobile-scrcpy-facefde/.github/workflows/000077500000000000000000000000001505702741400214265ustar00rootroot00000000000000Genymobile-scrcpy-facefde/.github/workflows/release.yml000066400000000000000000000340021505702741400235700ustar00rootroot00000000000000name: 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 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 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 Genymobile-scrcpy-facefde/.gitignore000066400000000000000000000001421505702741400200160ustar00rootroot00000000000000build/ /dist/ /build-*/ /build_*/ /release-*/ .idea/ .gradle/ /x/ local.properties /scrcpy-server Genymobile-scrcpy-facefde/FAQ.md000066400000000000000000000145001505702741400167620ustar00rootroot00000000000000# Frequently Asked Questions [Read in another language](#translations) Here are the common reported problems and their status. If you encounter any error, the first step is to upgrade to the latest version. ## `adb` and USB issues `scrcpy` execute `adb` commands to initialize the connection with the device. If `adb` fails, then scrcpy will not work. This is typically not a bug in _scrcpy_, but a problem in your environment. ### `adb` not found You need `adb` accessible from your `PATH`. On Windows, the current directory is in your `PATH`, and `adb.exe` is included in the release, so it should work out-of-the-box. ### Device not detected > ERROR: Could not find any ADB device Check that you correctly enabled [adb debugging][enable-adb]. Your device must be detected by `adb`: ``` adb devices ``` If your device is not detected, you may need some [drivers] (on Windows). There is a separate [USB driver for Google devices][google-usb-driver]. [enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling [drivers]: https://developer.android.com/studio/run/oem-usb.html [google-usb-driver]: https://developer.android.com/studio/run/win-usb ### Device unauthorized > ERROR: Device is unauthorized: > ERROR: --> (usb) 0123456789abcdef unauthorized > ERROR: A popup should open on the device to request authorization. When connecting, a popup should open on the device. You must authorize USB debugging. If it does not open, check [stackoverflow][device-unauthorized]. [device-unauthorized]: https://stackoverflow.com/questions/23081263/adb-android-device-unauthorized ### Several devices connected If several devices are connected, you will encounter this error: > ERROR: Multiple (2) ADB devices: > ERROR: --> (usb) 0123456789abcdef device Nexus_5 > ERROR: --> (tcpip) 192.168.1.5:5555 device GM1913 > ERROR: Select a device via -s (--serial), -d (--select-usb) or -e (--select-tcpip) In that case, you can either provide the identifier of the device you want to mirror: ```bash scrcpy -s 0123456789abcdef ``` Or request the single USB (or TCP/IP) device: ```bash scrcpy -d # USB device scrcpy -e # TCP/IP device ``` Note that if your device is connected over TCP/IP, you might get this message: > adb: error: more than one device/emulator > ERROR: "adb reverse" returned with value 1 > WARN: 'adb reverse' failed, fallback to 'adb forward' This is expected (due to a bug on old Android versions, see [#5]), but in that case, scrcpy fallbacks to a different method, which should work. [#5]: https://github.com/Genymobile/scrcpy/issues/5 ### Conflicts between adb versions > adb server version (41) doesn't match this client (39); killing... This error occurs when you use several `adb` versions simultaneously. You must find the program using a different `adb` version, and use the same `adb` version everywhere. You could overwrite the `adb` binary in the other program, or ask _scrcpy_ to use a specific `adb` binary, by setting the `ADB` environment variable: ```bash # in bash export ADB=/path/to/your/adb scrcpy ``` ```cmd :: in cmd set ADB=C:\path\to\your\adb.exe scrcpy ``` ```powershell # in PowerShell $env:ADB = 'C:\path\to\your\adb.exe' scrcpy ``` ### Device disconnected If _scrcpy_ stops itself with the warning "Device disconnected", then the `adb` connection has been closed. Try with another USB cable or plug it into another USB port. See [#281] and [#283]. [#281]: https://github.com/Genymobile/scrcpy/issues/281 [#283]: https://github.com/Genymobile/scrcpy/issues/283 ## 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 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 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]. To avoid the problem, [change the keyboard mode to simulate a physical keyboard][hid]. [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 ```bash export SDL_VIDEODRIVER=wayland scrcpy ``` On some distributions (at least Fedora), the package `libdecor` must be installed manually. See issues [#2554] and [#2559]. [#2554]: https://github.com/Genymobile/scrcpy/issues/2554 [#2559]: https://github.com/Genymobile/scrcpy/issues/2559 ### KWin compositor crashes On Plasma Desktop, compositor is disabled while _scrcpy_ is running. As a workaround, [disable "Block compositing"][kwin]. [kwin]: https://github.com/Genymobile/scrcpy/issues/114#issuecomment-378778613 ## Crashes ### Exception 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. Genymobile-scrcpy-facefde/LICENSE000066400000000000000000000261731505702741400170470ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright (C) 2018 Genymobile Copyright (C) 2018-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. 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. Genymobile-scrcpy-facefde/README.md000066400000000000000000000164601505702741400173170ustar00rootroot00000000000000**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 (v3.3.2) 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_. ![screenshot](assets/screenshot-debian-600.jpg) It focuses on: - **lightness**: native, displays only the device screen - **performance**: 30~120fps, depending on the device - **quality**: 1920×1080 or above - **low latency**: [35~70ms][lowlatency] - **low startup time**: ~1 second to display the first image - **non-intrusiveness**: nothing is left installed on the Android device - **user benefits**: no account, no ads, no internet required - **freedom**: free and open source software [lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 Its features include: - [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… [hid-keyboard]: doc/keyboard.md#physical-keyboard-simulation [hid-mouse]: doc/mouse.md#physical-mouse-simulation ## 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. [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) ## Must-know tips - [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) ## Usage examples There are a lot of options, [documented](#user-documentation) in separate pages. Here are just some common examples. - Capture the screen in H.265 (better quality), limit the size to 1920, limit the frame rate to 60fps, disable audio, and control the device by simulating a physical keyboard: ```bash 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 ``` - Start VLC in a new virtual display (separate from the device display): ```bash scrcpy --new-display=1920x1080 --start-app=org.videolan.vlc ``` - 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) ## Resources - [FAQ](FAQ.md) - [Translations][wiki] (not necessarily up to date) - [Build instructions](doc/build.md) - [Developers](doc/develop.md) [wiki]: https://github.com/Genymobile/scrcpy/wiki ## Articles - [Introducing scrcpy][article-intro] - [Scrcpy now works wirelessly][article-tcpip] - [Scrcpy 2.0, with audio][article-scrcpy2] [article-intro]: https://blog.rom1v.com/2018/03/introducing-scrcpy/ [article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/ [article-scrcpy2]: https://blog.rom1v.com/2023/03/scrcpy-2-0-with-audio/ ## Contact 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) ## Donate I'm [@rom1v](https://github.com/rom1v), the author and maintainer of _scrcpy_. If you appreciate this application, you can [support my open source work][donate]: - [GitHub Sponsors](https://github.com/sponsors/rom1v) - [Liberapay](https://liberapay.com/rom1v/) - [PayPal](https://paypal.me/rom2v) [donate]: https://blog.rom1v.com/about/#support-my-open-source-work ## License 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. 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. Genymobile-scrcpy-facefde/app/000077500000000000000000000000001505702741400166115ustar00rootroot00000000000000Genymobile-scrcpy-facefde/app/data/000077500000000000000000000000001505702741400175225ustar00rootroot00000000000000Genymobile-scrcpy-facefde/app/data/bash-completion/000077500000000000000000000000001505702741400226065ustar00rootroot00000000000000Genymobile-scrcpy-facefde/app/data/bash-completion/scrcpy000066400000000000000000000143521505702741400240410ustar00rootroot00000000000000_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 Genymobile-scrcpy-facefde/app/data/icon.ico000066400000000000000000000212101505702741400211420ustar00rootroot00000000000000 r"PNG  IHDR\rf"9IDATxy$W̺+{{43@h4: b#Wf6`F8XvZbX6k de,#9@ `f$k4wGG^Tw'B1Wٕ|!B!B!B!B!B!B!B!B!B!B!B!B!Bi&PxgB@]72.*PL=L_}ɗ?!8@?,!/ .ٷ~TVc'=mj}0B3~I~wV}O}"IfO';4~~ l~q'V9x7 >p$GXU .,3 g{ ?) 2 : *1DA80}wϊѳop\BN~BBl|a@SPmVϒ(Ug5ͪE+)\ L.I x'$9ha1"T]G2_jc@&8kT4 M~Mi`5[}n8K B"],[`oI(U7ORuEh@.-="xU%j-Ì q+)K@7 BLd9eIP^Be9eSB#Pax!F@Qax!F@Qax!F@Qax!F@Qax!F@Qax!F@Qax!F@QaG]K_D> 9vH k CUt&sȧUowg{#w9n`|#\Fr*ow1I5.o .'r8} .3kw1-"dU^t4/7_.2y[7V07-s8BStl׶0񡫞^ ob۰Kuq7CXÿu*[:U캭E'wD붞u+-7w]t˸2D}~pi(`t_ih;$;lh߿tn a1(*88$&"|!BooG.1։XChF)=(dv\_38euƵc LZ |Sq=Y~= :BjEcw[˯$gZ iwvseJ +zoR#c̿Hd$},g"ze'qPq@1"y&WǴD `t+4oF.Tגo+u&bR̕AM&TV4Hdzcص%VP]10ʜ2 -c?i6  8~vq/`|( iT~> p 2 iCv$r;E8VP?{H巒N·KA!ןu _N_i@Qߎ\95sK|DA"_jTΡtCiPy }b:mo)גo)]10Œ>%\ w7TQ@( 3`=}ު!u-aĚÐeBA UV(U* LTɩi"۷2*MUHd *zDvًמ{זhE]kk~ hz(I!5Gr"4/QH YV@?s.H֖Qm;2pG;PecZ]5a,O',/w C[_=ze4uɈo$vM2Rf M,/Gk6 7_2c􂎱?1,:m #bb` /%o\n}s=A#DADn8t&0zrIA 73P>F)=K h9Wx& y*@nBP/?{֔EشC U~CC9xs7v",`,ο9\'RE(P1*V\ި/TD:!߽w­@϶&ʪODnoFS[tWzP 4C?59RXh aLo, @d DHL LXa\Ėز3?1oOWa}0/\,>\FI=q{܄)( Dۊm*g7T"Vk!H*i~ 14hl\>33Ƒ^TͤV_Xիlq-D">칳G@]gonGwocqZ Y^D(MXa{y4n@ X?_CP!~?>/o[ [0y1Ko=HRj<@'T1-n_oAז&\9U}s_ [oDsh@( m36k?&9CA fRd!Y@b9 hCKoL]GŪ8ʳ𸧻[QDBϡ^,-6^L_LV z|+\+zy^;l*d"FBY Bke{[13[/_ũ! {+N;2 =>³:|FmV`BK!:u }sw~ӈ4tů۰1sn\\E^S14̇FmBckb;'֑ >"KGmWr$YwNV]AHnF$zo:p7=M7T$Rzy;1- -Bf#bz>${z@_]+W\_tǯyg".8N#Rםp"2YݪUj U&1g1 2t&^9p@bտKyQ&1%ӎSq-UkkeY3(銁 (9K8LcTKT<\L-3g569ƞo\*{;YVI- oѯ&k!"#ԮIjdHU(Vx%HO*,[!k(_) $5nV0A!. S8؋0TDަAb  ejZN蘤VpGRs:1( V,@at}LrT\duLb輯 +{v+l?W^T8v݃.J͡ǀKԆ=Cp\Rd>4I2n! '9jwj ؆=;iA @z|KŪz`AQ|..oh5й[6G WSal6(7t4mqe':{;e C`ųg@n7/C{4n:,pQxd#>7/[TS.3ϭOjJH>7l3uk" A`cD{\#B`iN(Ϳ8>λ{Ծ8M \SB@[r{z7UgTthks\Rg,EB+bmoAVU hC{[K, ϳ:lݷ C&0 $1-f!*FDAyc$'`n4,3gн$yQ8L,v*\5-chbόQĒ+1B}8ي=h AY\RgY<.]ݾÃh#UOEɝı9ӖUdTݎ3s>ow:I{jSMe__i\**Ű? pO#ikieRIB;ǀM۞uL@OĖ[5JYɥr&wi͟5ԫF9gmoqU-50p H3L"`᱐҆yN5_3mOܳ+@S7rÍtOS߈M(ɍodI1Vx"ش<^\fTxam=muΞ5u{xEq!_F(Zh82|aVn7+L^ZvٿnX::X9^9m0ߕ aΗ͹8:q*ǨzbϽkʶ4C>Y7zoa(y8@8>V/.7);?2҉8Os/Bv*Kj{1HhPzTc%&_1Μж?4^= s7 8>swOoh;Ϟ-ocweكc{`G{w yC|~?/ Ǿ*hΩ⿜yiAo.F1x𷧞[6AZ:{_8P L`>/p9Cc@g/.2k5 B4`N_=Ќ6,$cy3QW#ȁ{''1UXۣ= xWƜR* X:73Ѭ{G :KS]K4#csH aҾCcoߟ{ bNѶeE<{8~x답& cppxim| ؇=`+' 0K1P>x{scΜͱ54yCD91쇞[GP  " Aōo r% ,Ceh0(HU0 cO>䊏AH9 b B<*5sYrNYY{,b8cv- |>?i [:, cBɓGخ%;IEh6{NmKjSSSS~`!FFFԔ%z  _xJQQ !W,/, :3GBP1FswNa~:dw9*dtbSFF g.֭[?ZGy_ر  RCax ;}~]k0PjHWC]\xW `e!rb߾};cE5{n^݅U8|{rspΟ$aAL Q(VR b#Gh(V4vn(}B %>,rοnw958B( -vXf(~B y-rk1Z" y tAY?cw!jpIvTt:СCbON~/e9e U5ιkW_}{.Ǐ?hw!2{0<16 @l `7 ]QУPa@9hwd2.G6=jw8v.DZc~nwF^zht!Փfߺ뮻TUub'?0 ӗ^&0ÇBUU_iDѣ6m:.fF_o|oe|/Wlٲ5](b|>?+'?].;y +a;sTWWw#cowI8J2͛o/r Gi).Ϣ([x).(Y\.rwgffV^W (YV p R bwAHUL`QePȭ8苾6qq Rk}NF:M9RC_@ nw!B!B!B!B!B!B!B!B!B!B!W;=IENDB`Genymobile-scrcpy-facefde/app/data/icon.png000066400000000000000000000146021505702741400211630ustar00rootroot00000000000000PNG  IHDR\rfIIDATx te3c" B""JyB=d_vpaqd (B ;FpIw^:U]9s<Iw׷98@AAAAAAAAAAAAAAAAAAAAAAAAAru[pWŮ*ZkkkO}b_vp)&z8aw'.!K <,d]Sgw ,~r2R"!X6,?:aU >1"VP3γ1q6=ܗ&ǀy.ݹxkkkkk F=< ڡhgL}R{GS /3%ND,z\=D  Y7$z*Z 5$5S8G~r_" ZR|i\S1>E)@'4kJ5ftpMA@BM\: )j                          A``c:E'@rYB=Sc<$\# z<^ΈYչ4a_[F$ДTz]kwqG䅵5j4D:F{+)m?Wa/ewdCZoSkF-Ryn-3gqYLwǴ)mw0do>\ |(0h(mtWҌV+vS~&CLH/Q1&tKb1`gv[hrȋgPpyˊ4(\bتCSDZm,m!̋fɪxɮBσHAhuC+%\.ѴS-Z(K|$kqoʮ0ahi}-?<}' x+LGK/`ۤdq̸U~v~"s߉9]7o'U%?&역T4ЖH/f `A0h o7:wF 9C7*~>hoٗ 4z[S0`Z6^i>ɸP`vZ&Ecޫxm`[ܛ0%-a dze7,4`䂤F~LOKv`nw,k47c~M 4`̮0{664Zé=^hD8`S8  `?br~.,1W)^soߡ-?k7~ܼA_ ~K{$Zی*-Mm\tXls0<}wF1 ]<;nt}s.E%W/RKn'ғU(nğr\lt!R^Uf<|i&9̃Fl]"ۤ_;MJRCƿ P jS@归}~pRpdR J_$ɒdÏR]y&o\HׄT/gy*}a(=~ @mI7,9B{00o'AFt4 P]?3(&}I$Z|6_mPx\6 'GxX}/9;0` wqR Eo[q?hՃMwYl:G` t( К1ҫ?n%'CʬJS?>mmPUmZx< vg90m3Z0hYg8'<{xG ]j7),O]y6зGvgbΉ11rifι7@ktbЗ~uOʏ{$ߋ]ca7!Bį`ߕLs xT 5 fd@de&*OM=OXE.<_70oMTJgX- fx"qHmTۚgy¯a<8F{F|'6o z=Dއc<{!8 @ 8rv66Bncn.Ɩ7a0{xit' bӿ- W1A `ܻG?ͮ,Iw-(Cn_Ǥ5>~I _ `{bOcV7b*` 1{ٯ9N  ?N!i'n_7Vf>6omC h>;pҡD- ]\di;I ħʼn>[KjX !ئBrs[+s>S&Qd6)~6]%Nm|0 <$u8 <~0A<L* R LJ Cb~'IoH  "\$P$^`\N (8\0UH6DeSXl@P 7$P<@e @^BEru J_q5 ()թ3 `VxK+ ( \Fd@/90 4gd2ꑰ uti:@s.Pp$P4 j> ?$ }Ēc @ׁ#WP=>8vJX@ x@ Abp( i0:P߼ ؁V]_GRJHO~b*,,@8v,d4d>;98l$ *ƾzt !S(l~~!áf%_C\_PKO3( m"בzPSc7QῑP $yf8^m*2%$C! Bsџx?bDTP %ҟv9 \A!hP0(Q4; X и7޽{Fh\ xH$&& \'A B[۷D6mܐ__W9 w4aW^F]|P?Kw 8p)[ݓ_ 2JMM ڳgk(/]X6qMXg?g0!ݻwý2fdd1ua=&2RftR]_,~jN9mސ$^pT.vb AAAAAAAAAAAAAAAAAAAAA "eoIENDB`Genymobile-scrcpy-facefde/app/data/icon.svg000066400000000000000000000110241505702741400211710ustar00rootroot00000000000000 Genymobile-scrcpy-facefde/app/data/open_a_terminal_here.bat000066400000000000000000000000051505702741400243440ustar00rootroot00000000000000@cmd Genymobile-scrcpy-facefde/app/data/scrcpy-console.bat000066400000000000000000000000631505702741400231540ustar00rootroot00000000000000@echo off scrcpy.exe --pause-on-exit=if-error %* Genymobile-scrcpy-facefde/app/data/scrcpy-console.desktop000066400000000000000000000007321505702741400240620ustar00rootroot00000000000000[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 Genymobile-scrcpy-facefde/app/data/scrcpy-noconsole.vbs000066400000000000000000000003241505702741400235350ustar00rootroot00000000000000strCommand = "cmd /c scrcpy.exe" For Each Arg In WScript.Arguments strCommand = strCommand & " """ & replace(Arg, """", """""""""") & """" Next CreateObject("Wscript.Shell").Run strCommand, 0, false Genymobile-scrcpy-facefde/app/data/scrcpy.desktop000066400000000000000000000006661505702741400224300ustar00rootroot00000000000000[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 Genymobile-scrcpy-facefde/app/data/zsh-completion/000077500000000000000000000000001505702741400224755ustar00rootroot00000000000000Genymobile-scrcpy-facefde/app/data/zsh-completion/_scrcpy000066400000000000000000000172261505702741400240720ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/deps/000077500000000000000000000000001505702741400175445ustar00rootroot00000000000000Genymobile-scrcpy-facefde/app/deps/.gitignore000066400000000000000000000000061505702741400215300ustar00rootroot00000000000000/work Genymobile-scrcpy-facefde/app/deps/README000066400000000000000000000017041505702741400204260ustar00rootroot00000000000000This 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/ Genymobile-scrcpy-facefde/app/deps/adb_linux.sh000077500000000000000000000013561505702741400220550ustar00rootroot00000000000000#!/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/" Genymobile-scrcpy-facefde/app/deps/adb_macos.sh000077500000000000000000000013601505702741400220130ustar00rootroot00000000000000#!/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=d3e9fa1df3345cf728586908426615a60863d2632f73f1ce14f0f1349ef000fd 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/" Genymobile-scrcpy-facefde/app/deps/adb_windows.sh000077500000000000000000000015211505702741400224020ustar00rootroot00000000000000#!/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=12c2841f354e92a0eb2fd7bf6f0f9bf8538abce7bd6b060ac8349d6f6a61107c 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/" Genymobile-scrcpy-facefde/app/deps/common000066400000000000000000000034121505702741400207570ustar00rootroot00000000000000#!/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" } Genymobile-scrcpy-facefde/app/deps/dav1d.sh000077500000000000000000000030631505702741400211040ustar00rootroot00000000000000#!/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 Genymobile-scrcpy-facefde/app/deps/ffmpeg.sh000077500000000000000000000065171505702741400213600ustar00rootroot00000000000000#!/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 Genymobile-scrcpy-facefde/app/deps/libusb.sh000077500000000000000000000024541505702741400213700ustar00rootroot00000000000000#!/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 Genymobile-scrcpy-facefde/app/deps/sdl.sh000077500000000000000000000030741505702741400206710ustar00rootroot00000000000000#!/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/$DIRNAME' 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 Genymobile-scrcpy-facefde/app/meson.build000066400000000000000000000173051505702741400207610ustar00rootroot00000000000000src = [ '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/control_msg.c', 'src/controller.c', 'src/decoder.c', 'src/delay_buffer.c', 'src/demuxer.c', 'src/device_msg.c', 'src/display.c', 'src/events.c', 'src/icon.c', 'src/file_pusher.c', 'src/fps_counter.c', 'src/frame_buffer.c', 'src/input_manager.c', 'src/keyboard_sdk.c', 'src/mouse_capture.c', 'src/mouse_sdk.c', 'src/opengl.c', 'src/options.c', 'src/packet_merger.c', 'src/receiver.c', 'src/recorder.c', 'src/scrcpy.c', 'src/screen.c', 'src/server.c', 'src/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', ] conf = configuration_data() conf.set('_POSIX_C_SOURCE', '200809L') conf.set('_XOPEN_SOURCE', '700') conf.set('_GNU_SOURCE', true) if host_machine.system() == 'windows' windows = import('windows') src += [ 'src/sys/win/file.c', 'src/sys/win/process.c', windows.compile_resources('scrcpy-windows.rc'), ] conf.set('_WIN32_WINNT', '0x0600') conf.set('WINVER', '0x0600') else src += [ 'src/sys/unix/file.c', 'src/sys/unix/process.c', ] if host_machine.system() == 'darwin' conf.set('_DARWIN_C_SOURCE', true) endif endif v4l2_support = get_option('v4l2') and host_machine.system() == 'linux' if v4l2_support src += [ 'src/v4l2_sink.c' ] endif usb_support = get_option('usb') if usb_support src += [ 'src/usb/aoa_hid.c', 'src/usb/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', ] 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') dependencies += cc.find_library('ws2_32') endif check_functions = [ 'strdup', 'asprintf', 'vasprintf', 'nrand48', 'jrand48', 'reallocarray', ] foreach f : check_functions if cc.has_function(f) define = 'HAVE_' + f.underscorify().to_upper() conf.set(define, true) endif endforeach conf.set('HAVE_SOCK_CLOEXEC', host_machine.system() != 'windows' and cc.has_header_symbol('sys/socket.h', 'SOCK_CLOEXEC')) # the version, updated on release conf.set_quoted('SCRCPY_VERSION', meson.project_version()) # the prefix used during configuration (meson --prefix=PREFIX) conf.set_quoted('PREFIX', get_option('prefix')) # build a "portable" version (with scrcpy-server accessible from the same # directory as the executable) conf.set('PORTABLE', get_option('portable')) # the default client TCP port range for the "adb reverse" tunnel # overridden by option --port conf.set('DEFAULT_LOCAL_PORT_RANGE_FIRST', '27183') conf.set('DEFAULT_LOCAL_PORT_RANGE_LAST', '27199') # run a server debugger and wait for a client to be attached conf.set('SERVER_DEBUGGER', get_option('server_debugger')) # enable V4L2 support (linux only) conf.set('HAVE_V4L2', v4l2_support) # enable HID over AOA support (linux only) conf.set('HAVE_USB', usb_support) configure_file(configuration: conf, output: 'config.h') src_dir = include_directories('src') executable('scrcpy', src, dependencies: dependencies, include_directories: src_dir, install: true, c_args: []) # datadir = get_option('datadir') # by default 'share' install_man('scrcpy.1') install_data('data/icon.png', rename: 'scrcpy.png', install_dir: 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 ### 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', ]], ] 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 Genymobile-scrcpy-facefde/app/scrcpy-windows.manifest000066400000000000000000000007571505702741400233450ustar00rootroot00000000000000 true PerMonitorV2 Genymobile-scrcpy-facefde/app/scrcpy-windows.rc000066400000000000000000000010431505702741400221300ustar00rootroot00000000000000#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.2" END END BLOCK "VarFileInfo" BEGIN VALUE "Translation", 0x409, 1252 END END Genymobile-scrcpy-facefde/app/scrcpy.1000066400000000000000000000555401505702741400202070ustar00rootroot00000000000000.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 Genymobile-scrcpy-facefde/app/src/000077500000000000000000000000001505702741400174005ustar00rootroot00000000000000Genymobile-scrcpy-facefde/app/src/adb/000077500000000000000000000000001505702741400201265ustar00rootroot00000000000000Genymobile-scrcpy-facefde/app/src/adb/adb.c000066400000000000000000000561511505702741400210300ustar00rootroot00000000000000#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 _WIN32 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 _WIN32 // 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 _WIN32 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 _WIN32 // 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 _WIN32 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; } Genymobile-scrcpy-facefde/app/src/adb/adb.h000066400000000000000000000060731505702741400210330ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/adb/adb_device.c000066400000000000000000000020341505702741400223360ustar00rootroot00000000000000#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; } Genymobile-scrcpy-facefde/app/src/adb/adb_device.h000066400000000000000000000017211505702741400223450ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/adb/adb_parser.c000066400000000000000000000137071505702741400224040ustar00rootroot00000000000000#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; } Genymobile-scrcpy-facefde/app/src/adb/adb_parser.h000066400000000000000000000012001505702741400223720ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/adb/adb_tunnel.c000066400000000000000000000132241505702741400224070ustar00rootroot00000000000000#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; } Genymobile-scrcpy-facefde/app/src/adb/adb_tunnel.h000066400000000000000000000022351505702741400224140ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/android/000077500000000000000000000000001505702741400210205ustar00rootroot00000000000000Genymobile-scrcpy-facefde/app/src/android/input.h000066400000000000000000000765711505702741400223500ustar00rootroot00000000000000// copied from // blob 08299899b6305a0fe74d7d2b8471b7cd0af49dc7 // (and modified) /* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #ifndef _ANDROID_INPUT_H #define _ANDROID_INPUT_H /** * Meta key / modifier state. */ enum android_metastate { /** No meta keys are pressed. */ AMETA_NONE = 0, /** This mask is used to check whether one of the ALT meta keys is pressed. */ AMETA_ALT_ON = 0x02, /** This mask is used to check whether the left ALT meta key is pressed. */ AMETA_ALT_LEFT_ON = 0x10, /** This mask is used to check whether the right ALT meta key is pressed. */ AMETA_ALT_RIGHT_ON = 0x20, /** This mask is used to check whether one of the SHIFT meta keys is pressed. */ AMETA_SHIFT_ON = 0x01, /** This mask is used to check whether the left SHIFT meta key is pressed. */ AMETA_SHIFT_LEFT_ON = 0x40, /** This mask is used to check whether the right SHIFT meta key is pressed. */ AMETA_SHIFT_RIGHT_ON = 0x80, /** This mask is used to check whether the SYM meta key is pressed. */ AMETA_SYM_ON = 0x04, /** This mask is used to check whether the FUNCTION meta key is pressed. */ AMETA_FUNCTION_ON = 0x08, /** This mask is used to check whether one of the CTRL meta keys is pressed. */ AMETA_CTRL_ON = 0x1000, /** This mask is used to check whether the left CTRL meta key is pressed. */ AMETA_CTRL_LEFT_ON = 0x2000, /** This mask is used to check whether the right CTRL meta key is pressed. */ AMETA_CTRL_RIGHT_ON = 0x4000, /** This mask is used to check whether one of the META meta keys is pressed. */ AMETA_META_ON = 0x10000, /** This mask is used to check whether the left META meta key is pressed. */ AMETA_META_LEFT_ON = 0x20000, /** This mask is used to check whether the right META meta key is pressed. */ AMETA_META_RIGHT_ON = 0x40000, /** This mask is used to check whether the CAPS LOCK meta key is on. */ AMETA_CAPS_LOCK_ON = 0x100000, /** This mask is used to check whether the NUM LOCK meta key is on. */ AMETA_NUM_LOCK_ON = 0x200000, /** This mask is used to check whether the SCROLL LOCK meta key is on. */ AMETA_SCROLL_LOCK_ON = 0x400000, }; /** * Input event types. */ enum android_input_event_type { /** Indicates that the input event is a key event. */ AINPUT_EVENT_TYPE_KEY = 1, /** Indicates that the input event is a motion event. */ AINPUT_EVENT_TYPE_MOTION = 2 }; /** * Key event actions. */ enum android_keyevent_action { /** The key has been pressed down. */ AKEY_EVENT_ACTION_DOWN = 0, /** The key has been released. */ AKEY_EVENT_ACTION_UP = 1, /** * Multiple duplicate key events have occurred in a row, or a * complex string is being delivered. The repeat_count property * of the key event contains the number of times the given key * code should be executed. */ AKEY_EVENT_ACTION_MULTIPLE = 2 }; /** * Key event flags. */ enum android_keyevent_flags { /** This mask is set if the device woke because of this key event. */ AKEY_EVENT_FLAG_WOKE_HERE = 0x1, /** This mask is set if the key event was generated by a software keyboard. */ AKEY_EVENT_FLAG_SOFT_KEYBOARD = 0x2, /** This mask is set if we don't want the key event to cause us to leave touch mode. */ AKEY_EVENT_FLAG_KEEP_TOUCH_MODE = 0x4, /** * This mask is set if an event was known to come from a trusted * part of the system. That is, the event is known to come from * the user, and could not have been spoofed by a third party * component. */ AKEY_EVENT_FLAG_FROM_SYSTEM = 0x8, /** * This mask is used for compatibility, to identify enter keys that are * coming from an IME whose enter key has been auto-labelled "next" or * "done". This allows TextView to dispatch these as normal enter keys * for old applications, but still do the appropriate action when * receiving them. */ AKEY_EVENT_FLAG_EDITOR_ACTION = 0x10, /** * When associated with up key events, this indicates that the key press * has been canceled. Typically this is used with virtual touch screen * keys, where the user can slide from the virtual key area on to the * display: in that case, the application will receive a canceled up * event and should not perform the action normally associated with the * key. Note that for this to work, the application can not perform an * action for a key until it receives an up or the long press timeout has * expired. */ AKEY_EVENT_FLAG_CANCELED = 0x20, /** * This key event was generated by a virtual (on-screen) hard key area. * Typically this is an area of the touchscreen, outside of the regular * display, dedicated to "hardware" buttons. */ AKEY_EVENT_FLAG_VIRTUAL_HARD_KEY = 0x40, /** * This flag is set for the first key repeat that occurs after the * long press timeout. */ AKEY_EVENT_FLAG_LONG_PRESS = 0x80, /** * Set when a key event has AKEY_EVENT_FLAG_CANCELED set because a long * press action was executed while it was down. */ AKEY_EVENT_FLAG_CANCELED_LONG_PRESS = 0x100, /** * Set for AKEY_EVENT_ACTION_UP when this event's key code is still being * tracked from its initial down. That is, somebody requested that tracking * started on the key down and a long press has not caused * the tracking to be canceled. */ AKEY_EVENT_FLAG_TRACKING = 0x200, /** * Set when a key event has been synthesized to implement default behavior * for an event that the application did not handle. * Fallback key events are generated by unhandled trackball motions * (to emulate a directional keypad) and by certain unhandled key presses * that are declared in the key map (such as special function numeric keypad * keys when numlock is off). */ AKEY_EVENT_FLAG_FALLBACK = 0x400, }; /** * Bit shift for the action bits holding the pointer index as * defined by AMOTION_EVENT_ACTION_POINTER_INDEX_MASK. */ #define AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT 8 /** Motion event actions */ enum android_motionevent_action { /** Bit mask of the parts of the action code that are the action itself. */ AMOTION_EVENT_ACTION_MASK = 0xff, /** * Bits in the action code that represent a pointer index, used with * AMOTION_EVENT_ACTION_POINTER_DOWN and AMOTION_EVENT_ACTION_POINTER_UP. Shifting * down by AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT provides the actual pointer * index where the data for the pointer going up or down can be found. */ AMOTION_EVENT_ACTION_POINTER_INDEX_MASK = 0xff00, /** A pressed gesture has started, the motion contains the initial starting location. */ AMOTION_EVENT_ACTION_DOWN = 0, /** * A pressed gesture has finished, the motion contains the final release location * as well as any intermediate points since the last down or move event. */ AMOTION_EVENT_ACTION_UP = 1, /** * A change has happened during a press gesture (between AMOTION_EVENT_ACTION_DOWN and * AMOTION_EVENT_ACTION_UP). The motion contains the most recent point, as well as * any intermediate points since the last down or move event. */ AMOTION_EVENT_ACTION_MOVE = 2, /** * The current gesture has been aborted. * You will not receive any more points in it. You should treat this as * an up event, but not perform any action that you normally would. */ AMOTION_EVENT_ACTION_CANCEL = 3, /** * A movement has happened outside of the normal bounds of the UI element. * This does not provide a full gesture, but only the initial location of the movement/touch. */ AMOTION_EVENT_ACTION_OUTSIDE = 4, /** * A non-primary pointer has gone down. * The bits in AMOTION_EVENT_ACTION_POINTER_INDEX_MASK indicate which pointer changed. */ AMOTION_EVENT_ACTION_POINTER_DOWN = 5, /** * A non-primary pointer has gone up. * The bits in AMOTION_EVENT_ACTION_POINTER_INDEX_MASK indicate which pointer changed. */ AMOTION_EVENT_ACTION_POINTER_UP = 6, /** * A change happened but the pointer is not down (unlike AMOTION_EVENT_ACTION_MOVE). * The motion contains the most recent point, as well as any intermediate points since * the last hover move event. */ AMOTION_EVENT_ACTION_HOVER_MOVE = 7, /** * The motion event contains relative vertical and/or horizontal scroll offsets. * Use getAxisValue to retrieve the information from AMOTION_EVENT_AXIS_VSCROLL * and AMOTION_EVENT_AXIS_HSCROLL. * The pointer may or may not be down when this event is dispatched. * This action is always delivered to the winder under the pointer, which * may not be the window currently touched. */ AMOTION_EVENT_ACTION_SCROLL = 8, /** The pointer is not down but has entered the boundaries of a window or view. */ AMOTION_EVENT_ACTION_HOVER_ENTER = 9, /** The pointer is not down but has exited the boundaries of a window or view. */ AMOTION_EVENT_ACTION_HOVER_EXIT = 10, /* One or more buttons have been pressed. */ AMOTION_EVENT_ACTION_BUTTON_PRESS = 11, /* One or more buttons have been released. */ AMOTION_EVENT_ACTION_BUTTON_RELEASE = 12, }; /** * Motion event flags. */ enum android_motionevent_flags { /** * This flag indicates that the window that received this motion event is partly * or wholly obscured by another visible window above it. This flag is set to true * even if the event did not directly pass through the obscured area. * A security sensitive application can check this flag to identify situations in which * a malicious application may have covered up part of its content for the purpose * of misleading the user or hijacking touches. An appropriate response might be * to drop the suspect touches or to take additional precautions to confirm the user's * actual intent. */ AMOTION_EVENT_FLAG_WINDOW_IS_OBSCURED = 0x1, }; /** * Motion event edge touch flags. */ enum android_motionevent_edge_touch_flags { /** No edges intersected. */ AMOTION_EVENT_EDGE_FLAG_NONE = 0, /** Flag indicating the motion event intersected the top edge of the screen. */ AMOTION_EVENT_EDGE_FLAG_TOP = 0x01, /** Flag indicating the motion event intersected the bottom edge of the screen. */ AMOTION_EVENT_EDGE_FLAG_BOTTOM = 0x02, /** Flag indicating the motion event intersected the left edge of the screen. */ AMOTION_EVENT_EDGE_FLAG_LEFT = 0x04, /** Flag indicating the motion event intersected the right edge of the screen. */ AMOTION_EVENT_EDGE_FLAG_RIGHT = 0x08 }; /** * Constants that identify each individual axis of a motion event. * @anchor AMOTION_EVENT_AXIS */ enum android_motionevent_axis { /** * Axis constant: X axis of a motion event. * * - For a touch screen, reports the absolute X screen position of the center of * the touch contact area. The units are display pixels. * - For a touch pad, reports the absolute X surface position of the center of the touch * contact area. The units are device-dependent. * - For a mouse, reports the absolute X screen position of the mouse pointer. * The units are display pixels. * - For a trackball, reports the relative horizontal displacement of the trackball. * The value is normalized to a range from -1.0 (left) to 1.0 (right). * - For a joystick, reports the absolute X position of the joystick. * The value is normalized to a range from -1.0 (left) to 1.0 (right). */ AMOTION_EVENT_AXIS_X = 0, /** * Axis constant: Y axis of a motion event. * * - For a touch screen, reports the absolute Y screen position of the center of * the touch contact area. The units are display pixels. * - For a touch pad, reports the absolute Y surface position of the center of the touch * contact area. The units are device-dependent. * - For a mouse, reports the absolute Y screen position of the mouse pointer. * The units are display pixels. * - For a trackball, reports the relative vertical displacement of the trackball. * The value is normalized to a range from -1.0 (up) to 1.0 (down). * - For a joystick, reports the absolute Y position of the joystick. * The value is normalized to a range from -1.0 (up or far) to 1.0 (down or near). */ AMOTION_EVENT_AXIS_Y = 1, /** * Axis constant: Pressure axis of a motion event. * * - For a touch screen or touch pad, reports the approximate pressure applied to the surface * by a finger or other tool. The value is normalized to a range from * 0 (no pressure at all) to 1 (normal pressure), although values higher than 1 * may be generated depending on the calibration of the input device. * - For a trackball, the value is set to 1 if the trackball button is pressed * or 0 otherwise. * - For a mouse, the value is set to 1 if the primary mouse button is pressed * or 0 otherwise. */ AMOTION_EVENT_AXIS_PRESSURE = 2, /** * Axis constant: Size axis of a motion event. * * - For a touch screen or touch pad, reports the approximate size of the contact area in * relation to the maximum detectable size for the device. The value is normalized * to a range from 0 (smallest detectable size) to 1 (largest detectable size), * although it is not a linear scale. This value is of limited use. * To obtain calibrated size information, see * {@link AMOTION_EVENT_AXIS_TOUCH_MAJOR} or {@link AMOTION_EVENT_AXIS_TOOL_MAJOR}. */ AMOTION_EVENT_AXIS_SIZE = 3, /** * Axis constant: TouchMajor axis of a motion event. * * - For a touch screen, reports the length of the major axis of an ellipse that * represents the touch area at the point of contact. * The units are display pixels. * - For a touch pad, reports the length of the major axis of an ellipse that * represents the touch area at the point of contact. * The units are device-dependent. */ AMOTION_EVENT_AXIS_TOUCH_MAJOR = 4, /** * Axis constant: TouchMinor axis of a motion event. * * - For a touch screen, reports the length of the minor axis of an ellipse that * represents the touch area at the point of contact. * The units are display pixels. * - For a touch pad, reports the length of the minor axis of an ellipse that * represents the touch area at the point of contact. * The units are device-dependent. * * When the touch is circular, the major and minor axis lengths will be equal to one another. */ AMOTION_EVENT_AXIS_TOUCH_MINOR = 5, /** * Axis constant: ToolMajor axis of a motion event. * * - For a touch screen, reports the length of the major axis of an ellipse that * represents the size of the approaching finger or tool used to make contact. * - For a touch pad, reports the length of the major axis of an ellipse that * represents the size of the approaching finger or tool used to make contact. * The units are device-dependent. * * When the touch is circular, the major and minor axis lengths will be equal to one another. * * The tool size may be larger than the touch size since the tool may not be fully * in contact with the touch sensor. */ AMOTION_EVENT_AXIS_TOOL_MAJOR = 6, /** * Axis constant: ToolMinor axis of a motion event. * * - For a touch screen, reports the length of the minor axis of an ellipse that * represents the size of the approaching finger or tool used to make contact. * - For a touch pad, reports the length of the minor axis of an ellipse that * represents the size of the approaching finger or tool used to make contact. * The units are device-dependent. * * When the touch is circular, the major and minor axis lengths will be equal to one another. * * The tool size may be larger than the touch size since the tool may not be fully * in contact with the touch sensor. */ AMOTION_EVENT_AXIS_TOOL_MINOR = 7, /** * Axis constant: Orientation axis of a motion event. * * - For a touch screen or touch pad, reports the orientation of the finger * or tool in radians relative to the vertical plane of the device. * An angle of 0 radians indicates that the major axis of contact is oriented * upwards, is perfectly circular or is of unknown orientation. A positive angle * indicates that the major axis of contact is oriented to the right. A negative angle * indicates that the major axis of contact is oriented to the left. * The full range is from -PI/2 radians (finger pointing fully left) to PI/2 radians * (finger pointing fully right). * - For a stylus, the orientation indicates the direction in which the stylus * is pointing in relation to the vertical axis of the current orientation of the screen. * The range is from -PI radians to PI radians, where 0 is pointing up, * -PI/2 radians is pointing left, -PI or PI radians is pointing down, and PI/2 radians * is pointing right. See also {@link AMOTION_EVENT_AXIS_TILT}. */ AMOTION_EVENT_AXIS_ORIENTATION = 8, /** * Axis constant: Vertical Scroll axis of a motion event. * * - For a mouse, reports the relative movement of the vertical scroll wheel. * The value is normalized to a range from -1.0 (down) to 1.0 (up). * * This axis should be used to scroll views vertically. */ AMOTION_EVENT_AXIS_VSCROLL = 9, /** * Axis constant: Horizontal Scroll axis of a motion event. * * - For a mouse, reports the relative movement of the horizontal scroll wheel. * The value is normalized to a range from -1.0 (left) to 1.0 (right). * * This axis should be used to scroll views horizontally. */ AMOTION_EVENT_AXIS_HSCROLL = 10, /** * Axis constant: Z axis of a motion event. * * - For a joystick, reports the absolute Z position of the joystick. * The value is normalized to a range from -1.0 (high) to 1.0 (low). * On game pads with two analog joysticks, this axis is often reinterpreted * to report the absolute X position of the second joystick instead. */ AMOTION_EVENT_AXIS_Z = 11, /** * Axis constant: X Rotation axis of a motion event. * * - For a joystick, reports the absolute rotation angle about the X axis. * The value is normalized to a range from -1.0 (counter-clockwise) to 1.0 (clockwise). */ AMOTION_EVENT_AXIS_RX = 12, /** * Axis constant: Y Rotation axis of a motion event. * * - For a joystick, reports the absolute rotation angle about the Y axis. * The value is normalized to a range from -1.0 (counter-clockwise) to 1.0 (clockwise). */ AMOTION_EVENT_AXIS_RY = 13, /** * Axis constant: Z Rotation axis of a motion event. * * - For a joystick, reports the absolute rotation angle about the Z axis. * The value is normalized to a range from -1.0 (counter-clockwise) to 1.0 (clockwise). * On game pads with two analog joysticks, this axis is often reinterpreted * to report the absolute Y position of the second joystick instead. */ AMOTION_EVENT_AXIS_RZ = 14, /** * Axis constant: Hat X axis of a motion event. * * - For a joystick, reports the absolute X position of the directional hat control. * The value is normalized to a range from -1.0 (left) to 1.0 (right). */ AMOTION_EVENT_AXIS_HAT_X = 15, /** * Axis constant: Hat Y axis of a motion event. * * - For a joystick, reports the absolute Y position of the directional hat control. * The value is normalized to a range from -1.0 (up) to 1.0 (down). */ AMOTION_EVENT_AXIS_HAT_Y = 16, /** * Axis constant: Left Trigger axis of a motion event. * * - For a joystick, reports the absolute position of the left trigger control. * The value is normalized to a range from 0.0 (released) to 1.0 (fully pressed). */ AMOTION_EVENT_AXIS_LTRIGGER = 17, /** * Axis constant: Right Trigger axis of a motion event. * * - For a joystick, reports the absolute position of the right trigger control. * The value is normalized to a range from 0.0 (released) to 1.0 (fully pressed). */ AMOTION_EVENT_AXIS_RTRIGGER = 18, /** * Axis constant: Throttle axis of a motion event. * * - For a joystick, reports the absolute position of the throttle control. * The value is normalized to a range from 0.0 (fully open) to 1.0 (fully closed). */ AMOTION_EVENT_AXIS_THROTTLE = 19, /** * Axis constant: Rudder axis of a motion event. * * - For a joystick, reports the absolute position of the rudder control. * The value is normalized to a range from -1.0 (turn left) to 1.0 (turn right). */ AMOTION_EVENT_AXIS_RUDDER = 20, /** * Axis constant: Wheel axis of a motion event. * * - For a joystick, reports the absolute position of the steering wheel control. * The value is normalized to a range from -1.0 (turn left) to 1.0 (turn right). */ AMOTION_EVENT_AXIS_WHEEL = 21, /** * Axis constant: Gas axis of a motion event. * * - For a joystick, reports the absolute position of the gas (accelerator) control. * The value is normalized to a range from 0.0 (no acceleration) * to 1.0 (maximum acceleration). */ AMOTION_EVENT_AXIS_GAS = 22, /** * Axis constant: Brake axis of a motion event. * * - For a joystick, reports the absolute position of the brake control. * The value is normalized to a range from 0.0 (no braking) to 1.0 (maximum braking). */ AMOTION_EVENT_AXIS_BRAKE = 23, /** * Axis constant: Distance axis of a motion event. * * - For a stylus, reports the distance of the stylus from the screen. * A value of 0.0 indicates direct contact and larger values indicate increasing * distance from the surface. */ AMOTION_EVENT_AXIS_DISTANCE = 24, /** * Axis constant: Tilt axis of a motion event. * * - For a stylus, reports the tilt angle of the stylus in radians where * 0 radians indicates that the stylus is being held perpendicular to the * surface, and PI/2 radians indicates that the stylus is being held flat * against the surface. */ AMOTION_EVENT_AXIS_TILT = 25, /** * Axis constant: Generic scroll axis of a motion event. * * - This is used for scroll axis motion events that can't be classified as strictly * vertical or horizontal. The movement of a rotating scroller is an example of this. */ AMOTION_EVENT_AXIS_SCROLL = 26, /** * Axis constant: The movement of x position of a motion event. * * - For a mouse, reports a difference of x position between the previous position. * This is useful when pointer is captured, in that case the mouse pointer doesn't * change the location but this axis reports the difference which allows the app * to see how the mouse is moved. */ AMOTION_EVENT_AXIS_RELATIVE_X = 27, /** * Axis constant: The movement of y position of a motion event. * * Same as {@link RELATIVE_X}, but for y position. */ AMOTION_EVENT_AXIS_RELATIVE_Y = 28, /** * Axis constant: Generic 1 axis of a motion event. * The interpretation of a generic axis is device-specific. */ AMOTION_EVENT_AXIS_GENERIC_1 = 32, /** * Axis constant: Generic 2 axis of a motion event. * The interpretation of a generic axis is device-specific. */ AMOTION_EVENT_AXIS_GENERIC_2 = 33, /** * Axis constant: Generic 3 axis of a motion event. * The interpretation of a generic axis is device-specific. */ AMOTION_EVENT_AXIS_GENERIC_3 = 34, /** * Axis constant: Generic 4 axis of a motion event. * The interpretation of a generic axis is device-specific. */ AMOTION_EVENT_AXIS_GENERIC_4 = 35, /** * Axis constant: Generic 5 axis of a motion event. * The interpretation of a generic axis is device-specific. */ AMOTION_EVENT_AXIS_GENERIC_5 = 36, /** * Axis constant: Generic 6 axis of a motion event. * The interpretation of a generic axis is device-specific. */ AMOTION_EVENT_AXIS_GENERIC_6 = 37, /** * Axis constant: Generic 7 axis of a motion event. * The interpretation of a generic axis is device-specific. */ AMOTION_EVENT_AXIS_GENERIC_7 = 38, /** * Axis constant: Generic 8 axis of a motion event. * The interpretation of a generic axis is device-specific. */ AMOTION_EVENT_AXIS_GENERIC_8 = 39, /** * Axis constant: Generic 9 axis of a motion event. * The interpretation of a generic axis is device-specific. */ AMOTION_EVENT_AXIS_GENERIC_9 = 40, /** * Axis constant: Generic 10 axis of a motion event. * The interpretation of a generic axis is device-specific. */ AMOTION_EVENT_AXIS_GENERIC_10 = 41, /** * Axis constant: Generic 11 axis of a motion event. * The interpretation of a generic axis is device-specific. */ AMOTION_EVENT_AXIS_GENERIC_11 = 42, /** * Axis constant: Generic 12 axis of a motion event. * The interpretation of a generic axis is device-specific. */ AMOTION_EVENT_AXIS_GENERIC_12 = 43, /** * Axis constant: Generic 13 axis of a motion event. * The interpretation of a generic axis is device-specific. */ AMOTION_EVENT_AXIS_GENERIC_13 = 44, /** * Axis constant: Generic 14 axis of a motion event. * The interpretation of a generic axis is device-specific. */ AMOTION_EVENT_AXIS_GENERIC_14 = 45, /** * Axis constant: Generic 15 axis of a motion event. * The interpretation of a generic axis is device-specific. */ AMOTION_EVENT_AXIS_GENERIC_15 = 46, /** * Axis constant: Generic 16 axis of a motion event. * The interpretation of a generic axis is device-specific. */ AMOTION_EVENT_AXIS_GENERIC_16 = 47, // NOTE: If you add a new axis here you must also add it to several other files. // Refer to frameworks/base/core/java/android/view/MotionEvent.java for the full list. }; /** * Constants that identify buttons that are associated with motion events. * Refer to the documentation on the MotionEvent class for descriptions of each button. */ enum android_motionevent_buttons { /** primary */ AMOTION_EVENT_BUTTON_PRIMARY = 1 << 0, /** secondary */ AMOTION_EVENT_BUTTON_SECONDARY = 1 << 1, /** tertiary */ AMOTION_EVENT_BUTTON_TERTIARY = 1 << 2, /** back */ AMOTION_EVENT_BUTTON_BACK = 1 << 3, /** forward */ AMOTION_EVENT_BUTTON_FORWARD = 1 << 4, AMOTION_EVENT_BUTTON_STYLUS_PRIMARY = 1 << 5, AMOTION_EVENT_BUTTON_STYLUS_SECONDARY = 1 << 6, }; /** * Constants that identify tool types. * Refer to the documentation on the MotionEvent class for descriptions of each tool type. */ enum android_motionevent_tool_type { /** unknown */ AMOTION_EVENT_TOOL_TYPE_UNKNOWN = 0, /** finger */ AMOTION_EVENT_TOOL_TYPE_FINGER = 1, /** stylus */ AMOTION_EVENT_TOOL_TYPE_STYLUS = 2, /** mouse */ AMOTION_EVENT_TOOL_TYPE_MOUSE = 3, /** eraser */ AMOTION_EVENT_TOOL_TYPE_ERASER = 4, }; /** * Input source masks. * * Refer to the documentation on android.view.InputDevice for more details about input sources * and their correct interpretation. */ enum android_input_source_class { /** mask */ AINPUT_SOURCE_CLASS_MASK = 0x000000ff, /** none */ AINPUT_SOURCE_CLASS_NONE = 0x00000000, /** button */ AINPUT_SOURCE_CLASS_BUTTON = 0x00000001, /** pointer */ AINPUT_SOURCE_CLASS_POINTER = 0x00000002, /** navigation */ AINPUT_SOURCE_CLASS_NAVIGATION = 0x00000004, /** position */ AINPUT_SOURCE_CLASS_POSITION = 0x00000008, /** joystick */ AINPUT_SOURCE_CLASS_JOYSTICK = 0x00000010, }; /** * Input sources. */ enum android_input_source { /** unknown */ AINPUT_SOURCE_UNKNOWN = 0x00000000, /** keyboard */ AINPUT_SOURCE_KEYBOARD = 0x00000100 | AINPUT_SOURCE_CLASS_BUTTON, /** dpad */ AINPUT_SOURCE_DPAD = 0x00000200 | AINPUT_SOURCE_CLASS_BUTTON, /** gamepad */ AINPUT_SOURCE_GAMEPAD = 0x00000400 | AINPUT_SOURCE_CLASS_BUTTON, /** touchscreen */ AINPUT_SOURCE_TOUCHSCREEN = 0x00001000 | AINPUT_SOURCE_CLASS_POINTER, /** mouse */ AINPUT_SOURCE_MOUSE = 0x00002000 | AINPUT_SOURCE_CLASS_POINTER, /** stylus */ AINPUT_SOURCE_STYLUS = 0x00004000 | AINPUT_SOURCE_CLASS_POINTER, /** bluetooth stylus */ AINPUT_SOURCE_BLUETOOTH_STYLUS = 0x00008000 | AINPUT_SOURCE_STYLUS, /** trackball */ AINPUT_SOURCE_TRACKBALL = 0x00010000 | AINPUT_SOURCE_CLASS_NAVIGATION, /** mouse relative */ AINPUT_SOURCE_MOUSE_RELATIVE = 0x00020000 | AINPUT_SOURCE_CLASS_NAVIGATION, /** touchpad */ AINPUT_SOURCE_TOUCHPAD = 0x00100000 | AINPUT_SOURCE_CLASS_POSITION, /** navigation */ AINPUT_SOURCE_TOUCH_NAVIGATION = 0x00200000 | AINPUT_SOURCE_CLASS_NONE, /** joystick */ AINPUT_SOURCE_JOYSTICK = 0x01000000 | AINPUT_SOURCE_CLASS_JOYSTICK, /** rotary encoder */ AINPUT_SOURCE_ROTARY_ENCODER = 0x00400000 | AINPUT_SOURCE_CLASS_NONE, }; /** * Keyboard types. * * Refer to the documentation on android.view.InputDevice for more details. */ enum android_keyboard_type { /** none */ AINPUT_KEYBOARD_TYPE_NONE = 0, /** non alphabetic */ AINPUT_KEYBOARD_TYPE_NON_ALPHABETIC = 1, /** alphabetic */ AINPUT_KEYBOARD_TYPE_ALPHABETIC = 2, }; /** * Constants used to retrieve information about the range of motion for a particular * coordinate of a motion event. * * Refer to the documentation on android.view.InputDevice for more details about input sources * and their correct interpretation. * * @deprecated These constants are deprecated. Use {@link AMOTION_EVENT_AXIS AMOTION_EVENT_AXIS_*} constants instead. */ enum android_motion_range { /** x */ AINPUT_MOTION_RANGE_X = AMOTION_EVENT_AXIS_X, /** y */ AINPUT_MOTION_RANGE_Y = AMOTION_EVENT_AXIS_Y, /** pressure */ AINPUT_MOTION_RANGE_PRESSURE = AMOTION_EVENT_AXIS_PRESSURE, /** size */ AINPUT_MOTION_RANGE_SIZE = AMOTION_EVENT_AXIS_SIZE, /** touch major */ AINPUT_MOTION_RANGE_TOUCH_MAJOR = AMOTION_EVENT_AXIS_TOUCH_MAJOR, /** touch minor */ AINPUT_MOTION_RANGE_TOUCH_MINOR = AMOTION_EVENT_AXIS_TOUCH_MINOR, /** tool major */ AINPUT_MOTION_RANGE_TOOL_MAJOR = AMOTION_EVENT_AXIS_TOOL_MAJOR, /** tool minor */ AINPUT_MOTION_RANGE_TOOL_MINOR = AMOTION_EVENT_AXIS_TOOL_MINOR, /** orientation */ AINPUT_MOTION_RANGE_ORIENTATION = AMOTION_EVENT_AXIS_ORIENTATION, }; #endif // _ANDROID_INPUT_H Genymobile-scrcpy-facefde/app/src/android/keycodes.h000066400000000000000000000672611505702741400230130ustar00rootroot00000000000000// copied from // blob 2164d6163e1646c22825e364cad4f3c47638effd // (and modified) /* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #ifndef _ANDROID_KEYCODES_H #define _ANDROID_KEYCODES_H /** * Key codes. */ enum android_keycode { /** Unknown key code. */ AKEYCODE_UNKNOWN = 0, /** Soft Left key. * Usually situated below the display on phones and used as a multi-function * feature key for selecting a software defined function shown on the bottom left * of the display. */ AKEYCODE_SOFT_LEFT = 1, /** Soft Right key. * Usually situated below the display on phones and used as a multi-function * feature key for selecting a software defined function shown on the bottom right * of the display. */ AKEYCODE_SOFT_RIGHT = 2, /** Home key. * This key is handled by the framework and is never delivered to applications. */ AKEYCODE_HOME = 3, /** Back key. */ AKEYCODE_BACK = 4, /** Call key. */ AKEYCODE_CALL = 5, /** End Call key. */ AKEYCODE_ENDCALL = 6, /** '0' key. */ AKEYCODE_0 = 7, /** '1' key. */ AKEYCODE_1 = 8, /** '2' key. */ AKEYCODE_2 = 9, /** '3' key. */ AKEYCODE_3 = 10, /** '4' key. */ AKEYCODE_4 = 11, /** '5' key. */ AKEYCODE_5 = 12, /** '6' key. */ AKEYCODE_6 = 13, /** '7' key. */ AKEYCODE_7 = 14, /** '8' key. */ AKEYCODE_8 = 15, /** '9' key. */ AKEYCODE_9 = 16, /** '*' key. */ AKEYCODE_STAR = 17, /** '#' key. */ AKEYCODE_POUND = 18, /** Directional Pad Up key. * May also be synthesized from trackball motions. */ AKEYCODE_DPAD_UP = 19, /** Directional Pad Down key. * May also be synthesized from trackball motions. */ AKEYCODE_DPAD_DOWN = 20, /** Directional Pad Left key. * May also be synthesized from trackball motions. */ AKEYCODE_DPAD_LEFT = 21, /** Directional Pad Right key. * May also be synthesized from trackball motions. */ AKEYCODE_DPAD_RIGHT = 22, /** Directional Pad Center key. * May also be synthesized from trackball motions. */ AKEYCODE_DPAD_CENTER = 23, /** Volume Up key. * Adjusts the speaker volume up. */ AKEYCODE_VOLUME_UP = 24, /** Volume Down key. * Adjusts the speaker volume down. */ AKEYCODE_VOLUME_DOWN = 25, /** Power key. */ AKEYCODE_POWER = 26, /** Camera key. * Used to launch a camera application or take pictures. */ AKEYCODE_CAMERA = 27, /** Clear key. */ AKEYCODE_CLEAR = 28, /** 'A' key. */ AKEYCODE_A = 29, /** 'B' key. */ AKEYCODE_B = 30, /** 'C' key. */ AKEYCODE_C = 31, /** 'D' key. */ AKEYCODE_D = 32, /** 'E' key. */ AKEYCODE_E = 33, /** 'F' key. */ AKEYCODE_F = 34, /** 'G' key. */ AKEYCODE_G = 35, /** 'H' key. */ AKEYCODE_H = 36, /** 'I' key. */ AKEYCODE_I = 37, /** 'J' key. */ AKEYCODE_J = 38, /** 'K' key. */ AKEYCODE_K = 39, /** 'L' key. */ AKEYCODE_L = 40, /** 'M' key. */ AKEYCODE_M = 41, /** 'N' key. */ AKEYCODE_N = 42, /** 'O' key. */ AKEYCODE_O = 43, /** 'P' key. */ AKEYCODE_P = 44, /** 'Q' key. */ AKEYCODE_Q = 45, /** 'R' key. */ AKEYCODE_R = 46, /** 'S' key. */ AKEYCODE_S = 47, /** 'T' key. */ AKEYCODE_T = 48, /** 'U' key. */ AKEYCODE_U = 49, /** 'V' key. */ AKEYCODE_V = 50, /** 'W' key. */ AKEYCODE_W = 51, /** 'X' key. */ AKEYCODE_X = 52, /** 'Y' key. */ AKEYCODE_Y = 53, /** 'Z' key. */ AKEYCODE_Z = 54, /** ',' key. */ AKEYCODE_COMMA = 55, /** '.' key. */ AKEYCODE_PERIOD = 56, /** Left Alt modifier key. */ AKEYCODE_ALT_LEFT = 57, /** Right Alt modifier key. */ AKEYCODE_ALT_RIGHT = 58, /** Left Shift modifier key. */ AKEYCODE_SHIFT_LEFT = 59, /** Right Shift modifier key. */ AKEYCODE_SHIFT_RIGHT = 60, /** Tab key. */ AKEYCODE_TAB = 61, /** Space key. */ AKEYCODE_SPACE = 62, /** Symbol modifier key. * Used to enter alternate symbols. */ AKEYCODE_SYM = 63, /** Explorer special function key. * Used to launch a browser application. */ AKEYCODE_EXPLORER = 64, /** Envelope special function key. * Used to launch a mail application. */ AKEYCODE_ENVELOPE = 65, /** Enter key. */ AKEYCODE_ENTER = 66, /** Backspace key. * Deletes characters before the insertion point, unlike {@link AKEYCODE_FORWARD_DEL}. */ AKEYCODE_DEL = 67, /** '`' (backtick) key. */ AKEYCODE_GRAVE = 68, /** '-'. */ AKEYCODE_MINUS = 69, /** '=' key. */ AKEYCODE_EQUALS = 70, /** '[' key. */ AKEYCODE_LEFT_BRACKET = 71, /** ']' key. */ AKEYCODE_RIGHT_BRACKET = 72, /** '\' key. */ AKEYCODE_BACKSLASH = 73, /** ';' key. */ AKEYCODE_SEMICOLON = 74, /** ''' (apostrophe) key. */ AKEYCODE_APOSTROPHE = 75, /** '/' key. */ AKEYCODE_SLASH = 76, /** '@' key. */ AKEYCODE_AT = 77, /** Number modifier key. * Used to enter numeric symbols. * This key is not {@link AKEYCODE_NUM_LOCK}; it is more like {@link AKEYCODE_ALT_LEFT}. */ AKEYCODE_NUM = 78, /** Headset Hook key. * Used to hang up calls and stop media. */ AKEYCODE_HEADSETHOOK = 79, /** Camera Focus key. * Used to focus the camera. */ AKEYCODE_FOCUS = 80, /** '+' key. */ AKEYCODE_PLUS = 81, /** Menu key. */ AKEYCODE_MENU = 82, /** Notification key. */ AKEYCODE_NOTIFICATION = 83, /** Search key. */ AKEYCODE_SEARCH = 84, /** Play/Pause media key. */ AKEYCODE_MEDIA_PLAY_PAUSE= 85, /** Stop media key. */ AKEYCODE_MEDIA_STOP = 86, /** Play Next media key. */ AKEYCODE_MEDIA_NEXT = 87, /** Play Previous media key. */ AKEYCODE_MEDIA_PREVIOUS = 88, /** Rewind media key. */ AKEYCODE_MEDIA_REWIND = 89, /** Fast Forward media key. */ AKEYCODE_MEDIA_FAST_FORWARD = 90, /** Mute key. * Mutes the microphone, unlike {@link AKEYCODE_VOLUME_MUTE}. */ AKEYCODE_MUTE = 91, /** Page Up key. */ AKEYCODE_PAGE_UP = 92, /** Page Down key. */ AKEYCODE_PAGE_DOWN = 93, /** Picture Symbols modifier key. * Used to switch symbol sets (Emoji, Kao-moji). */ AKEYCODE_PICTSYMBOLS = 94, /** Switch Charset modifier key. * Used to switch character sets (Kanji, Katakana). */ AKEYCODE_SWITCH_CHARSET = 95, /** A Button key. * On a game controller, the A button should be either the button labeled A * or the first button on the bottom row of controller buttons. */ AKEYCODE_BUTTON_A = 96, /** B Button key. * On a game controller, the B button should be either the button labeled B * or the second button on the bottom row of controller buttons. */ AKEYCODE_BUTTON_B = 97, /** C Button key. * On a game controller, the C button should be either the button labeled C * or the third button on the bottom row of controller buttons. */ AKEYCODE_BUTTON_C = 98, /** X Button key. * On a game controller, the X button should be either the button labeled X * or the first button on the upper row of controller buttons. */ AKEYCODE_BUTTON_X = 99, /** Y Button key. * On a game controller, the Y button should be either the button labeled Y * or the second button on the upper row of controller buttons. */ AKEYCODE_BUTTON_Y = 100, /** Z Button key. * On a game controller, the Z button should be either the button labeled Z * or the third button on the upper row of controller buttons. */ AKEYCODE_BUTTON_Z = 101, /** L1 Button key. * On a game controller, the L1 button should be either the button labeled L1 (or L) * or the top left trigger button. */ AKEYCODE_BUTTON_L1 = 102, /** R1 Button key. * On a game controller, the R1 button should be either the button labeled R1 (or R) * or the top right trigger button. */ AKEYCODE_BUTTON_R1 = 103, /** L2 Button key. * On a game controller, the L2 button should be either the button labeled L2 * or the bottom left trigger button. */ AKEYCODE_BUTTON_L2 = 104, /** R2 Button key. * On a game controller, the R2 button should be either the button labeled R2 * or the bottom right trigger button. */ AKEYCODE_BUTTON_R2 = 105, /** Left Thumb Button key. * On a game controller, the left thumb button indicates that the left (or only) * joystick is pressed. */ AKEYCODE_BUTTON_THUMBL = 106, /** Right Thumb Button key. * On a game controller, the right thumb button indicates that the right * joystick is pressed. */ AKEYCODE_BUTTON_THUMBR = 107, /** Start Button key. * On a game controller, the button labeled Start. */ AKEYCODE_BUTTON_START = 108, /** Select Button key. * On a game controller, the button labeled Select. */ AKEYCODE_BUTTON_SELECT = 109, /** Mode Button key. * On a game controller, the button labeled Mode. */ AKEYCODE_BUTTON_MODE = 110, /** Escape key. */ AKEYCODE_ESCAPE = 111, /** Forward Delete key. * Deletes characters ahead of the insertion point, unlike {@link AKEYCODE_DEL}. */ AKEYCODE_FORWARD_DEL = 112, /** Left Control modifier key. */ AKEYCODE_CTRL_LEFT = 113, /** Right Control modifier key. */ AKEYCODE_CTRL_RIGHT = 114, /** Caps Lock key. */ AKEYCODE_CAPS_LOCK = 115, /** Scroll Lock key. */ AKEYCODE_SCROLL_LOCK = 116, /** Left Meta modifier key. */ AKEYCODE_META_LEFT = 117, /** Right Meta modifier key. */ AKEYCODE_META_RIGHT = 118, /** Function modifier key. */ AKEYCODE_FUNCTION = 119, /** System Request / Print Screen key. */ AKEYCODE_SYSRQ = 120, /** Break / Pause key. */ AKEYCODE_BREAK = 121, /** Home Movement key. * Used for scrolling or moving the cursor around to the start of a line * or to the top of a list. */ AKEYCODE_MOVE_HOME = 122, /** End Movement key. * Used for scrolling or moving the cursor around to the end of a line * or to the bottom of a list. */ AKEYCODE_MOVE_END = 123, /** Insert key. * Toggles insert / overwrite edit mode. */ AKEYCODE_INSERT = 124, /** Forward key. * Navigates forward in the history stack. Complement of {@link AKEYCODE_BACK}. */ AKEYCODE_FORWARD = 125, /** Play media key. */ AKEYCODE_MEDIA_PLAY = 126, /** Pause media key. */ AKEYCODE_MEDIA_PAUSE = 127, /** Close media key. * May be used to close a CD tray, for example. */ AKEYCODE_MEDIA_CLOSE = 128, /** Eject media key. * May be used to eject a CD tray, for example. */ AKEYCODE_MEDIA_EJECT = 129, /** Record media key. */ AKEYCODE_MEDIA_RECORD = 130, /** F1 key. */ AKEYCODE_F1 = 131, /** F2 key. */ AKEYCODE_F2 = 132, /** F3 key. */ AKEYCODE_F3 = 133, /** F4 key. */ AKEYCODE_F4 = 134, /** F5 key. */ AKEYCODE_F5 = 135, /** F6 key. */ AKEYCODE_F6 = 136, /** F7 key. */ AKEYCODE_F7 = 137, /** F8 key. */ AKEYCODE_F8 = 138, /** F9 key. */ AKEYCODE_F9 = 139, /** F10 key. */ AKEYCODE_F10 = 140, /** F11 key. */ AKEYCODE_F11 = 141, /** F12 key. */ AKEYCODE_F12 = 142, /** Num Lock key. * This is the Num Lock key; it is different from {@link AKEYCODE_NUM}. * This key alters the behavior of other keys on the numeric keypad. */ AKEYCODE_NUM_LOCK = 143, /** Numeric keypad '0' key. */ AKEYCODE_NUMPAD_0 = 144, /** Numeric keypad '1' key. */ AKEYCODE_NUMPAD_1 = 145, /** Numeric keypad '2' key. */ AKEYCODE_NUMPAD_2 = 146, /** Numeric keypad '3' key. */ AKEYCODE_NUMPAD_3 = 147, /** Numeric keypad '4' key. */ AKEYCODE_NUMPAD_4 = 148, /** Numeric keypad '5' key. */ AKEYCODE_NUMPAD_5 = 149, /** Numeric keypad '6' key. */ AKEYCODE_NUMPAD_6 = 150, /** Numeric keypad '7' key. */ AKEYCODE_NUMPAD_7 = 151, /** Numeric keypad '8' key. */ AKEYCODE_NUMPAD_8 = 152, /** Numeric keypad '9' key. */ AKEYCODE_NUMPAD_9 = 153, /** Numeric keypad '/' key (for division). */ AKEYCODE_NUMPAD_DIVIDE = 154, /** Numeric keypad '*' key (for multiplication). */ AKEYCODE_NUMPAD_MULTIPLY = 155, /** Numeric keypad '-' key (for subtraction). */ AKEYCODE_NUMPAD_SUBTRACT = 156, /** Numeric keypad '+' key (for addition). */ AKEYCODE_NUMPAD_ADD = 157, /** Numeric keypad '.' key (for decimals or digit grouping). */ AKEYCODE_NUMPAD_DOT = 158, /** Numeric keypad ',' key (for decimals or digit grouping). */ AKEYCODE_NUMPAD_COMMA = 159, /** Numeric keypad Enter key. */ AKEYCODE_NUMPAD_ENTER = 160, /** Numeric keypad '=' key. */ AKEYCODE_NUMPAD_EQUALS = 161, /** Numeric keypad '(' key. */ AKEYCODE_NUMPAD_LEFT_PAREN = 162, /** Numeric keypad ')' key. */ AKEYCODE_NUMPAD_RIGHT_PAREN = 163, /** Volume Mute key. * Mutes the speaker, unlike {@link AKEYCODE_MUTE}. * This key should normally be implemented as a toggle such that the first press * mutes the speaker and the second press restores the original volume. */ AKEYCODE_VOLUME_MUTE = 164, /** Info key. * Common on TV remotes to show additional information related to what is * currently being viewed. */ AKEYCODE_INFO = 165, /** Channel up key. * On TV remotes, increments the television channel. */ AKEYCODE_CHANNEL_UP = 166, /** Channel down key. * On TV remotes, decrements the television channel. */ AKEYCODE_CHANNEL_DOWN = 167, /** Zoom in key. */ AKEYCODE_ZOOM_IN = 168, /** Zoom out key. */ AKEYCODE_ZOOM_OUT = 169, /** TV key. * On TV remotes, switches to viewing live TV. */ AKEYCODE_TV = 170, /** Window key. * On TV remotes, toggles picture-in-picture mode or other windowing functions. */ AKEYCODE_WINDOW = 171, /** Guide key. * On TV remotes, shows a programming guide. */ AKEYCODE_GUIDE = 172, /** DVR key. * On some TV remotes, switches to a DVR mode for recorded shows. */ AKEYCODE_DVR = 173, /** Bookmark key. * On some TV remotes, bookmarks content or web pages. */ AKEYCODE_BOOKMARK = 174, /** Toggle captions key. * Switches the mode for closed-captioning text, for example during television shows. */ AKEYCODE_CAPTIONS = 175, /** Settings key. * Starts the system settings activity. */ AKEYCODE_SETTINGS = 176, /** TV power key. * On TV remotes, toggles the power on a television screen. */ AKEYCODE_TV_POWER = 177, /** TV input key. * On TV remotes, switches the input on a television screen. */ AKEYCODE_TV_INPUT = 178, /** Set-top-box power key. * On TV remotes, toggles the power on an external Set-top-box. */ AKEYCODE_STB_POWER = 179, /** Set-top-box input key. * On TV remotes, switches the input mode on an external Set-top-box. */ AKEYCODE_STB_INPUT = 180, /** A/V Receiver power key. * On TV remotes, toggles the power on an external A/V Receiver. */ AKEYCODE_AVR_POWER = 181, /** A/V Receiver input key. * On TV remotes, switches the input mode on an external A/V Receiver. */ AKEYCODE_AVR_INPUT = 182, /** Red "programmable" key. * On TV remotes, acts as a contextual/programmable key. */ AKEYCODE_PROG_RED = 183, /** Green "programmable" key. * On TV remotes, actsas a contextual/programmable key. */ AKEYCODE_PROG_GREEN = 184, /** Yellow "programmable" key. * On TV remotes, acts as a contextual/programmable key. */ AKEYCODE_PROG_YELLOW = 185, /** Blue "programmable" key. * On TV remotes, acts as a contextual/programmable key. */ AKEYCODE_PROG_BLUE = 186, /** App switch key. * Should bring up the application switcher dialog. */ AKEYCODE_APP_SWITCH = 187, /** Generic Game Pad Button #1.*/ AKEYCODE_BUTTON_1 = 188, /** Generic Game Pad Button #2.*/ AKEYCODE_BUTTON_2 = 189, /** Generic Game Pad Button #3.*/ AKEYCODE_BUTTON_3 = 190, /** Generic Game Pad Button #4.*/ AKEYCODE_BUTTON_4 = 191, /** Generic Game Pad Button #5.*/ AKEYCODE_BUTTON_5 = 192, /** Generic Game Pad Button #6.*/ AKEYCODE_BUTTON_6 = 193, /** Generic Game Pad Button #7.*/ AKEYCODE_BUTTON_7 = 194, /** Generic Game Pad Button #8.*/ AKEYCODE_BUTTON_8 = 195, /** Generic Game Pad Button #9.*/ AKEYCODE_BUTTON_9 = 196, /** Generic Game Pad Button #10.*/ AKEYCODE_BUTTON_10 = 197, /** Generic Game Pad Button #11.*/ AKEYCODE_BUTTON_11 = 198, /** Generic Game Pad Button #12.*/ AKEYCODE_BUTTON_12 = 199, /** Generic Game Pad Button #13.*/ AKEYCODE_BUTTON_13 = 200, /** Generic Game Pad Button #14.*/ AKEYCODE_BUTTON_14 = 201, /** Generic Game Pad Button #15.*/ AKEYCODE_BUTTON_15 = 202, /** Generic Game Pad Button #16.*/ AKEYCODE_BUTTON_16 = 203, /** Language Switch key. * Toggles the current input language such as switching between English and Japanese on * a QWERTY keyboard. On some devices, the same function may be performed by * pressing Shift+Spacebar. */ AKEYCODE_LANGUAGE_SWITCH = 204, /** Manner Mode key. * Toggles silent or vibrate mode on and off to make the device behave more politely * in certain settings such as on a crowded train. On some devices, the key may only * operate when long-pressed. */ AKEYCODE_MANNER_MODE = 205, /** 3D Mode key. * Toggles the display between 2D and 3D mode. */ AKEYCODE_3D_MODE = 206, /** Contacts special function key. * Used to launch an address book application. */ AKEYCODE_CONTACTS = 207, /** Calendar special function key. * Used to launch a calendar application. */ AKEYCODE_CALENDAR = 208, /** Music special function key. * Used to launch a music player application. */ AKEYCODE_MUSIC = 209, /** Calculator special function key. * Used to launch a calculator application. */ AKEYCODE_CALCULATOR = 210, /** Japanese full-width / half-width key. */ AKEYCODE_ZENKAKU_HANKAKU = 211, /** Japanese alphanumeric key. */ AKEYCODE_EISU = 212, /** Japanese non-conversion key. */ AKEYCODE_MUHENKAN = 213, /** Japanese conversion key. */ AKEYCODE_HENKAN = 214, /** Japanese katakana / hiragana key. */ AKEYCODE_KATAKANA_HIRAGANA = 215, /** Japanese Yen key. */ AKEYCODE_YEN = 216, /** Japanese Ro key. */ AKEYCODE_RO = 217, /** Japanese kana key. */ AKEYCODE_KANA = 218, /** Assist key. * Launches the global assist activity. Not delivered to applications. */ AKEYCODE_ASSIST = 219, /** Brightness Down key. * Adjusts the screen brightness down. */ AKEYCODE_BRIGHTNESS_DOWN = 220, /** Brightness Up key. * Adjusts the screen brightness up. */ AKEYCODE_BRIGHTNESS_UP = 221, /** Audio Track key. * Switches the audio tracks. */ AKEYCODE_MEDIA_AUDIO_TRACK = 222, /** Sleep key. * Puts the device to sleep. Behaves somewhat like {@link AKEYCODE_POWER} but it * has no effect if the device is already asleep. */ AKEYCODE_SLEEP = 223, /** Wakeup key. * Wakes up the device. Behaves somewhat like {@link AKEYCODE_POWER} but it * has no effect if the device is already awake. */ AKEYCODE_WAKEUP = 224, /** Pairing key. * Initiates peripheral pairing mode. Useful for pairing remote control * devices or game controllers, especially if no other input mode is * available. */ AKEYCODE_PAIRING = 225, /** Media Top Menu key. * Goes to the top of media menu. */ AKEYCODE_MEDIA_TOP_MENU = 226, /** '11' key. */ AKEYCODE_11 = 227, /** '12' key. */ AKEYCODE_12 = 228, /** Last Channel key. * Goes to the last viewed channel. */ AKEYCODE_LAST_CHANNEL = 229, /** TV data service key. * Displays data services like weather, sports. */ AKEYCODE_TV_DATA_SERVICE = 230, /** Voice Assist key. * Launches the global voice assist activity. Not delivered to applications. */ AKEYCODE_VOICE_ASSIST = 231, /** Radio key. * Toggles TV service / Radio service. */ AKEYCODE_TV_RADIO_SERVICE = 232, /** Teletext key. * Displays Teletext service. */ AKEYCODE_TV_TELETEXT = 233, /** Number entry key. * Initiates to enter multi-digit channel nubmber when each digit key is assigned * for selecting separate channel. Corresponds to Number Entry Mode (0x1D) of CEC * User Control Code. */ AKEYCODE_TV_NUMBER_ENTRY = 234, /** Analog Terrestrial key. * Switches to analog terrestrial broadcast service. */ AKEYCODE_TV_TERRESTRIAL_ANALOG = 235, /** Digital Terrestrial key. * Switches to digital terrestrial broadcast service. */ AKEYCODE_TV_TERRESTRIAL_DIGITAL = 236, /** Satellite key. * Switches to digital satellite broadcast service. */ AKEYCODE_TV_SATELLITE = 237, /** BS key. * Switches to BS digital satellite broadcasting service available in Japan. */ AKEYCODE_TV_SATELLITE_BS = 238, /** CS key. * Switches to CS digital satellite broadcasting service available in Japan. */ AKEYCODE_TV_SATELLITE_CS = 239, /** BS/CS key. * Toggles between BS and CS digital satellite services. */ AKEYCODE_TV_SATELLITE_SERVICE = 240, /** Toggle Network key. * Toggles selecting broadcast services. */ AKEYCODE_TV_NETWORK = 241, /** Antenna/Cable key. * Toggles broadcast input source between antenna and cable. */ AKEYCODE_TV_ANTENNA_CABLE = 242, /** HDMI #1 key. * Switches to HDMI input #1. */ AKEYCODE_TV_INPUT_HDMI_1 = 243, /** HDMI #2 key. * Switches to HDMI input #2. */ AKEYCODE_TV_INPUT_HDMI_2 = 244, /** HDMI #3 key. * Switches to HDMI input #3. */ AKEYCODE_TV_INPUT_HDMI_3 = 245, /** HDMI #4 key. * Switches to HDMI input #4. */ AKEYCODE_TV_INPUT_HDMI_4 = 246, /** Composite #1 key. * Switches to composite video input #1. */ AKEYCODE_TV_INPUT_COMPOSITE_1 = 247, /** Composite #2 key. * Switches to composite video input #2. */ AKEYCODE_TV_INPUT_COMPOSITE_2 = 248, /** Component #1 key. * Switches to component video input #1. */ AKEYCODE_TV_INPUT_COMPONENT_1 = 249, /** Component #2 key. * Switches to component video input #2. */ AKEYCODE_TV_INPUT_COMPONENT_2 = 250, /** VGA #1 key. * Switches to VGA (analog RGB) input #1. */ AKEYCODE_TV_INPUT_VGA_1 = 251, /** Audio description key. * Toggles audio description off / on. */ AKEYCODE_TV_AUDIO_DESCRIPTION = 252, /** Audio description mixing volume up key. * Louden audio description volume as compared with normal audio volume. */ AKEYCODE_TV_AUDIO_DESCRIPTION_MIX_UP = 253, /** Audio description mixing volume down key. * Lessen audio description volume as compared with normal audio volume. */ AKEYCODE_TV_AUDIO_DESCRIPTION_MIX_DOWN = 254, /** Zoom mode key. * Changes Zoom mode (Normal, Full, Zoom, Wide-zoom, etc.) */ AKEYCODE_TV_ZOOM_MODE = 255, /** Contents menu key. * Goes to the title list. Corresponds to Contents Menu (0x0B) of CEC User Control * Code */ AKEYCODE_TV_CONTENTS_MENU = 256, /** Media context menu key. * Goes to the context menu of media contents. Corresponds to Media Context-sensitive * Menu (0x11) of CEC User Control Code. */ AKEYCODE_TV_MEDIA_CONTEXT_MENU = 257, /** Timer programming key. * Goes to the timer recording menu. Corresponds to Timer Programming (0x54) of * CEC User Control Code. */ AKEYCODE_TV_TIMER_PROGRAMMING = 258, /** Help key. */ AKEYCODE_HELP = 259, AKEYCODE_NAVIGATE_PREVIOUS = 260, AKEYCODE_NAVIGATE_NEXT = 261, AKEYCODE_NAVIGATE_IN = 262, AKEYCODE_NAVIGATE_OUT = 263, /** Primary stem key for Wear * Main power/reset button on watch. */ AKEYCODE_STEM_PRIMARY = 264, /** Generic stem key 1 for Wear */ AKEYCODE_STEM_1 = 265, /** Generic stem key 2 for Wear */ AKEYCODE_STEM_2 = 266, /** Generic stem key 3 for Wear */ AKEYCODE_STEM_3 = 267, /** Directional Pad Up-Left */ AKEYCODE_DPAD_UP_LEFT = 268, /** Directional Pad Down-Left */ AKEYCODE_DPAD_DOWN_LEFT = 269, /** Directional Pad Up-Right */ AKEYCODE_DPAD_UP_RIGHT = 270, /** Directional Pad Down-Right */ AKEYCODE_DPAD_DOWN_RIGHT = 271, /** Skip forward media key */ AKEYCODE_MEDIA_SKIP_FORWARD = 272, /** Skip backward media key */ AKEYCODE_MEDIA_SKIP_BACKWARD = 273, /** Step forward media key. * Steps media forward one from at a time. */ AKEYCODE_MEDIA_STEP_FORWARD = 274, /** Step backward media key. * Steps media backward one from at a time. */ AKEYCODE_MEDIA_STEP_BACKWARD = 275, /** Put device to sleep unless a wakelock is held. */ AKEYCODE_SOFT_SLEEP = 276, /** Cut key. */ AKEYCODE_CUT = 277, /** Copy key. */ AKEYCODE_COPY = 278, /** Paste key. */ AKEYCODE_PASTE = 279, /** fingerprint navigation key, up. */ AKEYCODE_SYSTEM_NAVIGATION_UP = 280, /** fingerprint navigation key, down. */ AKEYCODE_SYSTEM_NAVIGATION_DOWN = 281, /** fingerprint navigation key, left. */ AKEYCODE_SYSTEM_NAVIGATION_LEFT = 282, /** fingerprint navigation key, right. */ AKEYCODE_SYSTEM_NAVIGATION_RIGHT = 283, /** all apps */ AKEYCODE_ALL_APPS = 284 }; #endif // _ANDROID_KEYCODES_H Genymobile-scrcpy-facefde/app/src/audio_player.c000066400000000000000000000072451505702741400222310ustar00rootroot00000000000000#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; } Genymobile-scrcpy-facefde/app/src/audio_player.h000066400000000000000000000015501505702741400222270ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/audio_regulator.c000066400000000000000000000416221505702741400227360ustar00rootroot00000000000000#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); } Genymobile-scrcpy-facefde/app/src/audio_regulator.h000066400000000000000000000044111505702741400227360ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/cli.c000066400000000000000000003226401505702741400203220ustar00rootroot00000000000000#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; } Genymobile-scrcpy-facefde/app/src/cli.h000066400000000000000000000011021505702741400203120ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/clock.c000066400000000000000000000016241505702741400206420ustar00rootroot00000000000000#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; } Genymobile-scrcpy-facefde/app/src/clock.h000066400000000000000000000020631505702741400206450ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/common.h000066400000000000000000000005601505702741400210420ustar00rootroot00000000000000#ifndef SC_COMMON_H #define SC_COMMON_H #include "config.h" #include "compat.h" #define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0])) #define MIN(X,Y) ((X) < (Y) ? (X) : (Y)) #define MAX(X,Y) ((X) > (Y) ? (X) : (Y)) #define CLAMP(V,X,Y) MIN( MAX((V),(X)), (Y) ) #define container_of(ptr, type, member) \ ((type *) (((char *) (ptr)) - offsetof(type, member))) #endif Genymobile-scrcpy-facefde/app/src/compat.c000066400000000000000000000043231505702741400210310ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/compat.h000066400000000000000000000062601505702741400210400ustar00rootroot00000000000000#ifndef SC_COMPAT_H #define SC_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" #endif // In ffmpeg/doc/APIchanges: // 2018-02-06 - 0694d87024 - lavf 58.9.100 - avformat.h // Deprecate use of av_register_input_format(), av_register_output_format(), // av_register_all(), av_iformat_next(), av_oformat_next(). // Add av_demuxer_iterate(), and av_muxer_iterate(). #if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(58, 9, 100) # define SCRCPY_LAVF_HAS_NEW_MUXER_ITERATOR_API #else # define SCRCPY_LAVF_REQUIRES_REGISTER_ALL #endif // 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 #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 #endif #if SDL_VERSION_ATLEAST(2, 0, 8) // # 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 Genymobile-scrcpy-facefde/app/src/control_msg.c000066400000000000000000000327541505702741400221050ustar00rootroot00000000000000#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; } } 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 truncated string, and return the size 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; } size_t sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { buf[0] = msg->type; switch (msg->type) { case SC_CONTROL_MSG_TYPE_INJECT_KEYCODE: buf[1] = msg->inject_keycode.action; sc_write32be(&buf[2], msg->inject_keycode.keycode); sc_write32be(&buf[6], msg->inject_keycode.repeat); sc_write32be(&buf[10], msg->inject_keycode.metastate); return 14; case SC_CONTROL_MSG_TYPE_INJECT_TEXT: { size_t len = write_string(&buf[1], msg->inject_text.text, SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); 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: 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); 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); 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: // no additional data return 1; default: LOGW("Unknown message type: %u", (unsigned) msg->type); return 0; } } void sc_control_msg_log(const struct sc_control_msg *msg) { #define LOG_CMSG(fmt, ...) LOGV("input: " fmt, ## __VA_ARGS__) switch (msg->type) { case SC_CONTROL_MSG_TYPE_INJECT_KEYCODE: LOG_CMSG("key %-4s code=%d repeat=%" PRIu32 " meta=%06lx", KEYEVENT_ACTION_LABEL(msg->inject_keycode.action), (int) msg->inject_keycode.keycode, msg->inject_keycode.repeat, (long) msg->inject_keycode.metastate); break; case SC_CONTROL_MSG_TYPE_INJECT_TEXT: LOG_CMSG("text \"%s\"", msg->inject_text.text); break; case SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT: { int action = msg->inject_touch_event.action & AMOTION_EVENT_ACTION_MASK; uint64_t id = msg->inject_touch_event.pointer_id; const char *pointer_name = get_well_known_pointer_id_name(id); if (pointer_name) { // string pointer id LOG_CMSG("touch [id=%s] %-4s position=%" PRIi32 ",%" PRIi32 " pressure=%f 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); break; default: // do nothing break; } } Genymobile-scrcpy-facefde/app/src/control_msg.h000066400000000000000000000076061505702741400221100ustar00rootroot00000000000000#ifndef SC_CONTROLMSG_H #define SC_CONTROLMSG_H #include "common.h" #include #include #include #include "android/input.h" #include "android/keycodes.h" #include "coords.h" #include "hid/hid_event.h" #define SC_CONTROL_MSG_MAX_SIZE (1 << 18) // 256k #define SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH 300 // type: 1 byte; sequence: 8 bytes; paste flag: 1 byte; length: 4 bytes #define SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH (SC_CONTROL_MSG_MAX_SIZE - 14) #define 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 sc_copy_key { SC_COPY_KEY_NONE, SC_COPY_KEY_COPY, SC_COPY_KEY_CUT, }; struct sc_control_msg { enum sc_control_msg_type type; union { struct { enum android_keyevent_action action; enum android_keycode keycode; uint32_t repeat; enum android_metastate metastate; } inject_keycode; struct { char *text; // owned, to be freed by free() } inject_text; struct { enum android_motionevent_action action; enum android_motionevent_buttons action_button; enum android_motionevent_buttons buttons; uint64_t pointer_id; struct sc_position position; float pressure; } inject_touch_event; struct { struct sc_position position; float hscroll; float vscroll; enum android_motionevent_buttons buttons; } inject_scroll_event; struct { enum android_keyevent_action action; // action for the BACK key // screen may only be turned on on ACTION_DOWN } back_or_screen_on; struct { enum sc_copy_key copy_key; } get_clipboard; struct { uint64_t sequence; char *text; // owned, to be freed by free() bool paste; } set_clipboard; struct { 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; }; }; // buf size must be at least CONTROL_MSG_MAX_SIZE // return the number of bytes written size_t sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *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); #endif Genymobile-scrcpy-facefde/app/src/controller.c000066400000000000000000000140571505702741400217360ustar00rootroot00000000000000#include "controller.h" #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); } 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); // 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) { 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); 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); 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; } 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); } sc_vecdeque_destroy(&controller->queue); sc_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); } 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; } 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); 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; } static int run_controller(void *data) { struct sc_controller *controller = data; bool error = false; for (;;) { sc_mutex_lock(&controller->mutex); while (!controller->stopped && sc_vecdeque_is_empty(&controller->queue)) { sc_cond_wait(&controller->msg_cond, &controller->mutex); } if (controller->stopped) { // stop immediately, do not process further msgs sc_mutex_unlock(&controller->mutex); LOGD("Controller stopped"); break; } 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); if (!ok) { if (eos) { LOGD("Controller stopped (socket closed)"); } // else error already logged error = !eos; break; } } controller->cbs->on_ended(controller, error, controller->cbs_userdata); return 0; } bool sc_controller_start(struct sc_controller *controller) { LOGD("Starting controller thread"); bool ok = sc_thread_create(&controller->thread, run_controller, "scrcpy-ctl", controller); if (!ok) { LOGE("Could not start controller thread"); return false; } if (!sc_receiver_start(&controller->receiver)) { sc_controller_stop(controller); sc_thread_join(&controller->thread, NULL); return false; } return true; } void sc_controller_stop(struct sc_controller *controller) { sc_mutex_lock(&controller->mutex); controller->stopped = true; sc_cond_signal(&controller->msg_cond); sc_mutex_unlock(&controller->mutex); } void sc_controller_join(struct sc_controller *controller) { sc_thread_join(&controller->thread, NULL); sc_receiver_join(&controller->receiver); } Genymobile-scrcpy-facefde/app/src/controller.h000066400000000000000000000026741505702741400217450ustar00rootroot00000000000000#ifndef SC_CONTROLLER_H #define SC_CONTROLLER_H #include "common.h" #include #include "control_msg.h" #include "receiver.h" #include "util/acksync.h" #include "util/net.h" #include "util/thread.h" #include "util/vecdeque.h" struct sc_control_msg_queue SC_VECDEQUE(struct sc_control_msg); struct sc_controller { sc_socket control_socket; sc_thread thread; sc_mutex mutex; sc_cond msg_cond; bool stopped; struct sc_control_msg_queue queue; struct 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); }; bool sc_controller_init(struct sc_controller *controller, sc_socket control_socket, const struct sc_controller_callbacks *cbs, void *cbs_userdata); 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); bool sc_controller_start(struct sc_controller *controller); void sc_controller_stop(struct sc_controller *controller); void sc_controller_join(struct sc_controller *controller); bool sc_controller_push_msg(struct sc_controller *controller, const struct sc_control_msg *msg); #endif Genymobile-scrcpy-facefde/app/src/coords.h000066400000000000000000000006621505702741400210460ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/decoder.c000066400000000000000000000054121505702741400211530ustar00rootroot00000000000000#include "decoder.h" #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; } 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; } 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); } 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; } Genymobile-scrcpy-facefde/app/src/decoder.h000066400000000000000000000010761505702741400211620ustar00rootroot00000000000000#ifndef SC_DECODER_H #define SC_DECODER_H #include "common.h" #include #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; }; // The name must be statically allocated (e.g. a string literal) void sc_decoder_init(struct sc_decoder *decoder, const char *name); #endif Genymobile-scrcpy-facefde/app/src/delay_buffer.c000066400000000000000000000136251505702741400222020ustar00rootroot00000000000000#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; } Genymobile-scrcpy-facefde/app/src/delay_buffer.h000066400000000000000000000026171505702741400222060ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/demuxer.c000066400000000000000000000221301505702741400212130ustar00rootroot00000000000000#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); } Genymobile-scrcpy-facefde/app/src/demuxer.h000066400000000000000000000020321505702741400212170ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/device_msg.c000066400000000000000000000050411505702741400216510ustar00rootroot00000000000000#include "device_msg.h" #include #include #include #include "util/binary.h" #include "util/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 } 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 } size_t clipboard_len = sc_read32be(&buf[1]); if (clipboard_len > len - 5) { return 0; // no complete message } char *text = malloc(clipboard_len + 1); if (!text) { LOG_OOM(); return -1; } if (clipboard_len) { memcpy(text, &buf[5], clipboard_len); } text[clipboard_len] = '\0'; msg->clipboard.text = text; return 5 + clipboard_len; } case DEVICE_MSG_TYPE_ACK_CLIPBOARD: { if (len < 9) { return 0; // no complete message } uint64_t sequence = sc_read64be(&buf[1]); msg->ack_clipboard.sequence = sequence; return 9; } case DEVICE_MSG_TYPE_UHID_OUTPUT: { if (len < 5) { // at least id + size return 0; // not available } uint16_t id = sc_read16be(&buf[1]); size_t size = sc_read16be(&buf[3]); if (size < len - 5) { return 0; // not available } uint8_t *data = malloc(size); if (!data) { LOG_OOM(); return -1; } if (size) { memcpy(data, &buf[5], size); } msg->uhid_output.id = id; msg->uhid_output.size = size; msg->uhid_output.data = data; return 5 + size; } default: LOGW("Unknown device message type: %d", (int) msg->type); return -1; // error, we cannot recover } } 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; } } Genymobile-scrcpy-facefde/app/src/device_msg.h000066400000000000000000000020451505702741400216570ustar00rootroot00000000000000#ifndef SC_DEVICEMSG_H #define SC_DEVICEMSG_H #include "common.h" #include #include #include #define DEVICE_MSG_MAX_SIZE (1 << 18) // 256k // type: 1 byte; length: 4 bytes #define DEVICE_MSG_TEXT_MAX_LENGTH (DEVICE_MSG_MAX_SIZE - 5) enum sc_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; union { struct { char *text; // owned, to be freed by free() } clipboard; struct { uint64_t sequence; } ack_clipboard; struct { uint16_t id; uint16_t size; uint8_t *data; // owned, to be freed by free() } uhid_output; }; }; // return the number of bytes consumed (0 for no msg available, -1 on error) ssize_t sc_device_msg_deserialize(const uint8_t *buf, size_t len, struct sc_device_msg *msg); void sc_device_msg_destroy(struct sc_device_msg *msg); #endif Genymobile-scrcpy-facefde/app/src/display.c000066400000000000000000000245761505702741400212270ustar00rootroot00000000000000#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; } Genymobile-scrcpy-facefde/app/src/display.h000066400000000000000000000025111505702741400212150ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/events.c000066400000000000000000000030521505702741400210500ustar00rootroot00000000000000#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); } Genymobile-scrcpy-facefde/app/src/events.h000066400000000000000000000014641505702741400210620ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/file_pusher.c000066400000000000000000000112471505702741400220560ustar00rootroot00000000000000#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); } } Genymobile-scrcpy-facefde/app/src/file_pusher.h000066400000000000000000000023101505702741400220520ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/fps_counter.c000066400000000000000000000116541505702741400221020ustar00rootroot00000000000000#include "fps_counter.h" #include #include #include "util/log.h" #define SC_FPS_COUNTER_INTERVAL SC_TICK_FROM_SEC(1) bool sc_fps_counter_init(struct sc_fps_counter *counter) { bool ok = sc_mutex_init(&counter->mutex); if (!ok) { return false; } ok = sc_cond_init(&counter->state_cond); if (!ok) { sc_mutex_destroy(&counter->mutex); return false; } counter->thread_started = false; atomic_init(&counter->started, 0); // no need to initialize the other fields, they are unused until started return true; } void sc_fps_counter_destroy(struct sc_fps_counter *counter) { sc_cond_destroy(&counter->state_cond); sc_mutex_destroy(&counter->mutex); } static inline bool is_started(struct sc_fps_counter *counter) { return atomic_load_explicit(&counter->started, memory_order_acquire); } static inline void set_started(struct sc_fps_counter *counter, bool started) { atomic_store_explicit(&counter->started, started, memory_order_release); } // must be called with mutex locked static void display_fps(struct sc_fps_counter *counter) { unsigned rendered_per_second = counter->nr_rendered * SC_TICK_FREQ / SC_FPS_COUNTER_INTERVAL; if (counter->nr_skipped) { LOGI("%u fps (+%u frames skipped)", rendered_per_second, counter->nr_skipped); } else { LOGI("%u fps", rendered_per_second); } } // must be called with mutex locked static void check_interval_expired(struct sc_fps_counter *counter, sc_tick now) { if (now < counter->next_timestamp) { return; } display_fps(counter); counter->nr_rendered = 0; counter->nr_skipped = 0; // add a multiple of the interval uint32_t elapsed_slices = (now - counter->next_timestamp) / SC_FPS_COUNTER_INTERVAL + 1; counter->next_timestamp += SC_FPS_COUNTER_INTERVAL * elapsed_slices; } static int run_fps_counter(void *data) { struct sc_fps_counter *counter = data; sc_mutex_lock(&counter->mutex); while (!counter->interrupted) { while (!counter->interrupted && !is_started(counter)) { sc_cond_wait(&counter->state_cond, &counter->mutex); } while (!counter->interrupted && is_started(counter)) { sc_tick now = sc_tick_now(); check_interval_expired(counter, now); // ignore the reason (timeout or signaled), we just loop anyway sc_cond_timedwait(&counter->state_cond, &counter->mutex, counter->next_timestamp); } } sc_mutex_unlock(&counter->mutex); return 0; } bool sc_fps_counter_start(struct sc_fps_counter *counter) { sc_mutex_lock(&counter->mutex); counter->interrupted = false; counter->next_timestamp = sc_tick_now() + SC_FPS_COUNTER_INTERVAL; counter->nr_rendered = 0; counter->nr_skipped = 0; sc_mutex_unlock(&counter->mutex); set_started(counter, true); sc_cond_signal(&counter->state_cond); // counter->thread_started and counter->thread are always accessed from the // same thread, no need to lock if (!counter->thread_started) { bool ok = sc_thread_create(&counter->thread, run_fps_counter, "scrcpy-fps", counter); if (!ok) { LOGE("Could not start FPS counter thread"); return false; } counter->thread_started = true; } LOGI("FPS counter started"); return true; } void sc_fps_counter_stop(struct sc_fps_counter *counter) { set_started(counter, false); sc_cond_signal(&counter->state_cond); LOGI("FPS counter stopped"); } bool sc_fps_counter_is_started(struct sc_fps_counter *counter) { return is_started(counter); } void sc_fps_counter_interrupt(struct sc_fps_counter *counter) { if (!counter->thread_started) { return; } sc_mutex_lock(&counter->mutex); counter->interrupted = true; sc_mutex_unlock(&counter->mutex); // wake up blocking wait sc_cond_signal(&counter->state_cond); } void sc_fps_counter_join(struct sc_fps_counter *counter) { if (counter->thread_started) { // interrupted must be set by the thread calling join(), so no need to // lock for the assertion assert(counter->interrupted); sc_thread_join(&counter->thread, NULL); } } void sc_fps_counter_add_rendered_frame(struct sc_fps_counter *counter) { if (!is_started(counter)) { return; } sc_mutex_lock(&counter->mutex); sc_tick now = sc_tick_now(); check_interval_expired(counter, now); ++counter->nr_rendered; sc_mutex_unlock(&counter->mutex); } void sc_fps_counter_add_skipped_frame(struct sc_fps_counter *counter) { if (!is_started(counter)) { return; } sc_mutex_lock(&counter->mutex); sc_tick now = sc_tick_now(); check_interval_expired(counter, now); ++counter->nr_skipped; sc_mutex_unlock(&counter->mutex); } Genymobile-scrcpy-facefde/app/src/fps_counter.h000066400000000000000000000023661505702741400221070ustar00rootroot00000000000000#ifndef SC_FPSCOUNTER_H #define SC_FPSCOUNTER_H #include "common.h" #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; // atomic so that we can check without locking the mutex // if the FPS counter is disabled, we don't want to lock unnecessarily atomic_bool started; // the following fields are protected by the mutex bool interrupted; unsigned nr_rendered; unsigned nr_skipped; sc_tick next_timestamp; }; bool sc_fps_counter_init(struct sc_fps_counter *counter); void sc_fps_counter_destroy(struct sc_fps_counter *counter); bool sc_fps_counter_start(struct sc_fps_counter *counter); void sc_fps_counter_stop(struct sc_fps_counter *counter); bool sc_fps_counter_is_started(struct sc_fps_counter *counter); // request to stop the thread (on quit) // must be called before sc_fps_counter_join() void sc_fps_counter_interrupt(struct sc_fps_counter *counter); void sc_fps_counter_join(struct sc_fps_counter *counter); void sc_fps_counter_add_rendered_frame(struct sc_fps_counter *counter); void sc_fps_counter_add_skipped_frame(struct sc_fps_counter *counter); #endif Genymobile-scrcpy-facefde/app/src/frame_buffer.c000066400000000000000000000042161505702741400221720ustar00rootroot00000000000000#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); } Genymobile-scrcpy-facefde/app/src/frame_buffer.h000066400000000000000000000017701505702741400222010ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/hid/000077500000000000000000000000001505702741400201445ustar00rootroot00000000000000Genymobile-scrcpy-facefde/app/src/hid/hid_event.h000066400000000000000000000006521505702741400222650ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/hid/hid_gamepad.c000066400000000000000000000340401505702741400225330ustar00rootroot00000000000000#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; } Genymobile-scrcpy-facefde/app/src/hid/hid_gamepad.h000066400000000000000000000027411505702741400225430ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/hid/hid_keyboard.c000066400000000000000000000246211505702741400227410ustar00rootroot00000000000000#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; } Genymobile-scrcpy-facefde/app/src/hid/hid_keyboard.h000066400000000000000000000033571505702741400227510ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/hid/hid_mouse.c000066400000000000000000000142771505702741400222770ustar00rootroot00000000000000#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; } Genymobile-scrcpy-facefde/app/src/hid/hid_mouse.h000066400000000000000000000013671505702741400223000ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/icon.c000066400000000000000000000174111505702741400205000ustar00rootroot00000000000000#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); } Genymobile-scrcpy-facefde/app/src/icon.h000066400000000000000000000002651505702741400205040ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/input_events.h000066400000000000000000000420541505702741400223010ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/input_manager.c000066400000000000000000001061151505702741400224010ustar00rootroot00000000000000#include "input_manager.h" #include #include #include #include #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 } 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); // send DOWN event struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_INJECT_KEYCODE; msg.inject_keycode.action = action == SC_ACTION_DOWN ? AKEY_EVENT_ACTION_DOWN : AKEY_EVENT_ACTION_UP; msg.inject_keycode.keycode = keycode; msg.inject_keycode.metastate = 0; msg.inject_keycode.repeat = 0; if (!sc_controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'inject %s'", name); } } static inline void action_home(struct sc_input_manager *im, enum sc_action action) { send_keycode(im, AKEYCODE_HOME, action, "HOME"); } static inline void action_back(struct sc_input_manager *im, enum sc_action action) { send_keycode(im, AKEYCODE_BACK, action, "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"); } static inline void action_power(struct sc_input_manager *im, enum sc_action action) { send_keycode(im, AKEYCODE_POWER, action, "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"); } static inline void action_volume_down(struct sc_input_manager *im, enum sc_action action) { send_keycode(im, AKEYCODE_VOLUME_DOWN, action, "VOLUME_DOWN"); } static inline void action_menu(struct sc_input_manager *im, enum sc_action action) { send_keycode(im, AKEYCODE_MENU, action, "MENU"); } // turn the screen on if it was off, press BACK otherwise // If the screen is off, it is turned on only on ACTION_DOWN static void press_back_or_turn_screen_on(struct sc_input_manager *im, enum sc_action action) { assert(im->controller && im->kp); struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON; msg.back_or_screen_on.action = action == SC_ACTION_DOWN ? AKEY_EVENT_ACTION_DOWN : AKEY_EVENT_ACTION_UP; if (!sc_controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'press back or turn screen on'"); } } static void expand_notification_panel(struct sc_input_manager *im) { assert(im->controller); 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'"); } } static void expand_settings_panel(struct sc_input_manager *im) { assert(im->controller); struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL; if (!sc_controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'expand settings panel'"); } } static void collapse_panels(struct sc_input_manager *im) { assert(im->controller); struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS; if (!sc_controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'collapse notification panel'"); } } static bool get_device_clipboard(struct sc_input_manager *im, enum sc_copy_key copy_key) { assert(im->controller && im->kp); struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_GET_CLIPBOARD; msg.get_clipboard.copy_key = copy_key; if (!sc_controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'get device clipboard'"); return false; } return true; } static bool set_device_clipboard(struct sc_input_manager *im, bool paste, uint64_t sequence) { assert(im->controller && im->kp); char *text = SDL_GetClipboardText(); if (!text) { LOGW("Could not get clipboard text: %s", SDL_GetError()); return 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()); return; } if (!*text) { // empty text SDL_free(text); return; } char *text_dup = strdup(text); SDL_free(text); if (!text_dup) { LOGW("Could not strdup input text"); return; } struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_INJECT_TEXT; msg.inject_text.text = text_dup; if (!sc_controller_push_msg(im->controller, &msg)) { free(text_dup); LOGW("Could not request 'paste clipboard'"); } } static void rotate_device(struct sc_input_manager *im) { assert(im->controller); struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_ROTATE_DEVICE; if (!sc_controller_push_msg(im->controller, &msg)) { LOGW("Could not request device rotation"); } } static void 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); } else { // Nothing to do return; } } 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) { 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); } } 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); } } } Genymobile-scrcpy-facefde/app/src/input_manager.h000066400000000000000000000035061505702741400224060ustar00rootroot00000000000000#ifndef SC_INPUTMANAGER_H #define SC_INPUTMANAGER_H #include "common.h" #include #include #include #include #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" 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 }; void sc_input_manager_init(struct sc_input_manager *im, const struct sc_input_manager_params *params); void sc_input_manager_handle_event(struct sc_input_manager *im, const SDL_Event *event); #endif Genymobile-scrcpy-facefde/app/src/keyboard_sdk.c000066400000000000000000000301371505702741400222110ustar00rootroot00000000000000#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; } Genymobile-scrcpy-facefde/app/src/keyboard_sdk.h000066400000000000000000000014161505702741400222140ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/main.c000066400000000000000000000063011505702741400204700ustar00rootroot00000000000000#include "common.h" #include #include #ifdef HAVE_V4L2 # include #endif #define SDL_MAIN_HANDLED // avoid link error on Linux Windows Subsystem #include #include "cli.h" #include "options.h" #include "scrcpy.h" #include "usb/scrcpy_otg.h" #include "util/log.h" #include "util/net.h" #include "util/thread.h" #include "version.h" #ifdef _WIN32 #include #include "util/str.h" #endif static int main_scrcpy(int argc, char *argv[]) { #ifdef _WIN32 // disable buffering, we want logs immediately // even line buffering (setvbuf() with mode _IOLBF) is not sufficient setbuf(stdout, NULL); setbuf(stderr, NULL); #endif printf("scrcpy " SCRCPY_VERSION " \n"); struct scrcpy_cli_args args = { .opts = scrcpy_options_default, .help = false, .version = false, .pause_on_exit = SC_PAUSE_ON_EXIT_FALSE, }; #ifndef NDEBUG args.opts.log_level = SC_LOG_LEVEL_DEBUG; #endif enum scrcpy_exit_code ret; if (!scrcpy_parse_args(&args, argc, argv)) { ret = SCRCPY_EXIT_FAILURE; goto end; } sc_set_log_level(args.opts.log_level); if (args.help) { scrcpy_print_usage(argv[0]); ret = SCRCPY_EXIT_SUCCESS; goto end; } if (args.version) { scrcpy_print_version(); ret = SCRCPY_EXIT_SUCCESS; goto end; } // 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; } sc_log_configure(); #ifdef HAVE_USB ret = args.opts.otg ? scrcpy_otg(&args.opts) : scrcpy(&args.opts); #else ret = scrcpy(&args.opts); #endif end: if (args.pause_on_exit == SC_PAUSE_ON_EXIT_TRUE || (args.pause_on_exit == SC_PAUSE_ON_EXIT_IF_ERROR && ret != SCRCPY_EXIT_SUCCESS)) { printf("Press Enter to continue...\n"); getchar(); } return ret; } int main(int argc, char *argv[]) { #ifndef _WIN32 return main_scrcpy(argc, argv); #else (void) argc; (void) argv; int wargc; wchar_t **wargv = CommandLineToArgvW(GetCommandLineW(), &wargc); if (!wargv) { LOG_OOM(); return SCRCPY_EXIT_FAILURE; } char **argv_utf8 = malloc((wargc + 1) * sizeof(*argv_utf8)); if (!argv_utf8) { LOG_OOM(); LocalFree(wargv); return SCRCPY_EXIT_FAILURE; } argv_utf8[wargc] = NULL; for (int i = 0; i < wargc; ++i) { argv_utf8[i] = sc_str_from_wchars(wargv[i]); if (!argv_utf8[i]) { LOG_OOM(); for (int j = 0; j < i; ++j) { free(argv_utf8[j]); } LocalFree(wargv); free(argv_utf8); return SCRCPY_EXIT_FAILURE; } } 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 } Genymobile-scrcpy-facefde/app/src/mouse_capture.c000066400000000000000000000077141505702741400224300ustar00rootroot00000000000000#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); } Genymobile-scrcpy-facefde/app/src/mouse_capture.h000066400000000000000000000016031505702741400224240ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/mouse_sdk.c000066400000000000000000000122511505702741400215360ustar00rootroot00000000000000#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; } Genymobile-scrcpy-facefde/app/src/mouse_sdk.h000066400000000000000000000006631505702741400215470ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/opengl.c000066400000000000000000000032041505702741400210270ustar00rootroot00000000000000#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); } Genymobile-scrcpy-facefde/app/src/opengl.h000066400000000000000000000013161505702741400210360ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/options.c000066400000000000000000000113351505702741400212420ustar00rootroot00000000000000#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; } Genymobile-scrcpy-facefde/app/src/options.h000066400000000000000000000217251505702741400212530ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/packet_merger.c000066400000000000000000000023521505702741400223560ustar00rootroot00000000000000#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; } Genymobile-scrcpy-facefde/app/src/packet_merger.h000066400000000000000000000022651505702741400223660ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/receiver.c000066400000000000000000000146451505702741400213620ustar00rootroot00000000000000#include "receiver.h" #include #include #include #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; }; 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) { 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); } 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) { 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; } break; } } static ssize_t process_msgs(struct sc_receiver *receiver, const uint8_t *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); if (r == -1) { return -1; } if (r == 0) { return head; } process_msg(receiver, &msg); // the device msg must be destroyed by process_msg() head += r; assert(head <= len); if (head == len) { return head; } } } static int run_receiver(void *data) { struct sc_receiver *receiver = data; static uint8_t buf[DEVICE_MSG_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); if (r <= 0) { LOGD("Receiver stopped"); // device disconnected: keep error=false break; } head += r; ssize_t consumed = process_msgs(receiver, buf, head); 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); } } receiver->cbs->on_ended(receiver, error, receiver->cbs_userdata); return 0; } bool sc_receiver_start(struct sc_receiver *receiver) { LOGD("Starting receiver thread"); bool ok = sc_thread_create(&receiver->thread, run_receiver, "scrcpy-receiver", receiver); if (!ok) { LOGE("Could not start receiver thread"); return false; } return true; } void sc_receiver_join(struct sc_receiver *receiver) { sc_thread_join(&receiver->thread, NULL); } Genymobile-scrcpy-facefde/app/src/receiver.h000066400000000000000000000020001505702741400213450ustar00rootroot00000000000000#ifndef SC_RECEIVER_H #define SC_RECEIVER_H #include "common.h" #include #include "uhid/uhid_output.h" #include "util/acksync.h" #include "util/net.h" #include "util/thread.h" // receive events from the device // managed by the controller struct 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); }; bool sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket, const struct sc_receiver_callbacks *cbs, void *cbs_userdata); void sc_receiver_destroy(struct sc_receiver *receiver); bool sc_receiver_start(struct sc_receiver *receiver); // no sc_receiver_stop(), it will automatically stop on control_socket shutdown void sc_receiver_join(struct sc_receiver *receiver); #endif Genymobile-scrcpy-facefde/app/src/recorder.c000066400000000000000000000605311505702741400213560ustar00rootroot00000000000000#include "recorder.h" #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) 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 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(); 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; return true; } static inline void sc_recorder_rescale_packet(AVStream *stream, AVPacket *packet) { av_packet_rescale_ts(packet, SCRCPY_TIME_BASE, stream->time_base); } static bool sc_recorder_write_stream(struct sc_recorder *recorder, struct sc_recorder_stream *st, AVPacket *packet) { AVStream *stream = recorder->ctx->streams[st->index]; sc_recorder_rescale_packet(stream, packet); if (st->last_pts != AV_NOPTS_VALUE && packet->pts <= st->last_pts) { LOGD("Fixing PTS non monotonically increasing in stream %d " "(%" PRIi64 " >= %" PRIi64 ")", st->index, st->last_pts, packet->pts); packet->pts = ++st->last_pts; packet->dts = packet->pts; } else { st->last_pts = packet->pts; } return av_interleaved_write_frame(recorder->ctx, packet) >= 0; } static inline bool sc_recorder_write_video(struct sc_recorder *recorder, AVPacket *packet) { return sc_recorder_write_stream(recorder, &recorder->video_stream, packet); } static inline bool sc_recorder_write_audio(struct sc_recorder *recorder, AVPacket *packet) { return sc_recorder_write_stream(recorder, &recorder->audio_stream, packet); } static bool sc_recorder_open_output_file(struct sc_recorder *recorder) { const char *format_name = sc_recorder_get_format_name(recorder->format); assert(format_name); const AVOutputFormat *format = find_muxer(format_name); if (!format) { LOGE("Could not find muxer"); return false; } recorder->ctx = avformat_alloc_context(); if (!recorder->ctx) { LOG_OOM(); return false; } 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); return false; } // contrary to the deprecated API (av_oformat_next()), av_muxer_iterate() // returns (on purpose) a pointer-to-const, but AVFormatContext.oformat // still expects a pointer-to-non-const (it has not be updated accordingly) // recorder->ctx->oformat = (AVOutputFormat *) format; av_dict_set(&recorder->ctx->metadata, "comment", "Recorded by scrcpy " SCRCPY_VERSION, 0); LOGI("Recording started to %s file: %s", format_name, recorder->filename); return true; } static void 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); } int ret = av_write_trailer(recorder->ctx); if (ret < 0) { LOGE("Failed to write trailer to %s", recorder->filename); error = false; } end: if (video_pkt) { av_packet_free(&video_pkt); } if (audio_pkt) { av_packet_free(&audio_pkt); } return !error; } static bool sc_recorder_record(struct sc_recorder *recorder) { bool ok = sc_recorder_open_output_file(recorder); if (!ok) { return false; } ok = sc_recorder_process_packets(recorder); sc_recorder_close_output_file(recorder); return ok; } static int run_recorder(void *data) { struct sc_recorder *recorder = data; // Recording is a background task bool ok = sc_thread_set_priority(SC_THREAD_PRIORITY_LOW); (void) ok; // We don't care if it worked bool success = sc_recorder_record(recorder); sc_mutex_lock(&recorder->mutex); // Prevent the producer to push any new packet recorder->stopped = true; // Discard pending packets sc_recorder_queue_clear(&recorder->video_queue); sc_recorder_queue_clear(&recorder->audio_queue); sc_mutex_unlock(&recorder->mutex); if (success) { const char *format_name = sc_recorder_get_format_name(recorder->format); LOGI("Recording complete to %s file: %s", format_name, recorder->filename); } else { LOGE("Recording failed to %s", recorder->filename); } LOGD("Recorder thread ended"); recorder->cbs->on_ended(recorder, success, recorder->cbs_userdata); return 0; } static bool 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; #else raw_data = av_stream_new_side_data(stream, AV_PKT_DATA_DISPLAYMATRIX, sizeof(int32_t) * 9); if (!raw_data) { LOG_OOM(); return false; } #endif int32_t *matrix = (int32_t *) raw_data; unsigned rotation = orientation; unsigned angle = rotation * 90; av_display_rotation_set(matrix, angle); return true; } static bool sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink, AVCodecContext *ctx) { struct sc_recorder *recorder = DOWNCAST_VIDEO(sink); // only written from this thread, no need to lock assert(!recorder->video_init); sc_mutex_lock(&recorder->mutex); if (recorder->stopped) { sc_mutex_unlock(&recorder->mutex); return false; } 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); return false; } LOGI("Record orientation set to %s", sc_orientation_get_name(recorder->orientation)); } 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); } Genymobile-scrcpy-facefde/app/src/recorder.h000066400000000000000000000042401505702741400213560ustar00rootroot00000000000000#ifndef SC_RECORDER_H #define SC_RECORDER_H #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; }; 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; char *filename; enum sc_record_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); }; 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); bool sc_recorder_start(struct sc_recorder *recorder); void sc_recorder_stop(struct sc_recorder *recorder); void sc_recorder_join(struct sc_recorder *recorder); void sc_recorder_destroy(struct sc_recorder *recorder); #endif Genymobile-scrcpy-facefde/app/src/scrcpy.c000066400000000000000000001007041505702741400210510ustar00rootroot00000000000000#include "scrcpy.h" #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 "controller.h" #include "decoder.h" #include "delay_buffer.h" #include "demuxer.h" #include "events.h" #include "file_pusher.h" #include "keyboard_sdk.h" #include "mouse_sdk.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 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; }; #ifdef _WIN32 static BOOL WINAPI windows_ctrl_handler(DWORD ctrl_type) { if (ctrl_type == CTRL_C_EVENT || ctrl_type == CTRL_BREAK_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"); } // 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 // Linear filtering if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1")) { LOGW("Could not enable linear filtering"); } // Handle a click to gain focus as any other click if (!SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1")) { LOGW("Could not enable mouse focus clickthrough"); } #ifdef SCRCPY_SDL_HAS_HINT_TOUCH_MOUSE_EVENTS // Disable synthetic mouse events from touch events // Touch events with id SDL_TOUCH_MOUSEID are ignored anyway, but it is // better not to generate them in the first place. if (!SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0")) { LOGW("Could not disable synthetic mouse events"); } #endif #ifdef SCRCPY_SDL_HAS_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR // Disable compositor bypassing on X11 if (!SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, "0")) { LOGW("Could not disable X11 compositor bypass"); } #endif // Do not minimize on focus loss if (!SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0")) { LOGW("Could not disable minimize on focus loss"); } if (!SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1")) { LOGW("Could not allow joystick background events"); } } static void sdl_configure(bool video_playback, bool disable_screensaver) { #ifdef _WIN32 // Clean up properly on Ctrl+C on Windows bool ok = SetConsoleCtrlHandler(windows_ctrl_handler, TRUE); if (!ok) { LOGW("Could not set Ctrl+C handler"); } #endif // _WIN32 if (!video_playback) { return; } if (disable_screensaver) { SDL_DisableScreenSaver(); } else { SDL_EnableScreenSaver(); } } 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); break; } default: if (has_screen && !sc_screen_handle_event(&s->screen, &event)) { return SCRCPY_EXIT_FAILURE; } break; } } return SCRCPY_EXIT_FAILURE; } 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) { SDL_Event event; while (SDL_WaitEvent(&event)) { switch (event.type) { case SDL_QUIT: if (connected) { *connected = false; } return true; case SC_EVENT_SERVER_CONNECTION_FAILED: return false; case SC_EVENT_SERVER_CONNECTED: if (connected) { *connected = true; } return true; default: 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; if (!success) { sc_push_event(SC_EVENT_RECORDER_ERROR); } } 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); } } 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); } } 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; } atexit(SDL_Quit); enum scrcpy_exit_code ret = SCRCPY_EXIT_FAILURE; bool server_started = false; bool file_pusher_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 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)) { goto end; } server_started = true; if (options->list) { bool ok = await_for_server(NULL); ret = ok ? SCRCPY_EXIT_SUCCESS : SCRCPY_EXIT_FAILURE; goto end; } // playback implies capture assert(!options->video_playback || options->video); assert(!options->audio_playback || options->audio); 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()); goto end; } else { LOGW("Could not initialize SDL video: %s", SDL_GetError()); } } } if (options->audio_playback) { if (SDL_Init(SDL_INIT_AUDIO)) { LOGE("Could not initialize SDL audio: %s", SDL_GetError()); goto end; } } 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)) { goto end; } 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; if (options->control) { static const struct sc_controller_callbacks controller_cbs = { .on_ended = sc_controller_on_ended, }; if (!sc_controller_init(&s->controller, s->server.control_socket, &controller_cbs, NULL)) { goto end; } controller_initialized = 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) { goto end; } ok = sc_usb_init(&s->usb); if (!ok) { LOGE("Failed to initialize USB"); sc_acksync_destroy(&s->acksync); goto end; } assert(serial); struct sc_usb_device usb_device; ok = sc_usb_select_device(&s->usb, serial, &usb_device); if (!ok) { sc_usb_destroy(&s->usb); goto end; } LOGI("USB device: %s (%04" PRIx16 ":%04" PRIx16 ") %s %s", usb_device.serial, usb_device.vid, usb_device.pid, usb_device.manufacturer, usb_device.product); ok = sc_usb_connect(&s->usb, usb_device.device, NULL, NULL); sc_usb_device_destroy(&usb_device); if (!ok) { LOGE("Failed to connect to USB device %s", serial); sc_usb_destroy(&s->usb); sc_acksync_destroy(&s->acksync); goto end; } ok = sc_aoa_init(&s->aoa, &s->usb, &s->acksync); if (!ok) { LOGE("Failed to enable HID over AOA"); sc_usb_disconnect(&s->usb); sc_usb_destroy(&s->usb); sc_acksync_destroy(&s->acksync); goto end; } 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; } 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)) { 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; } 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->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->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(); 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); } end: if (timeout_started) { sc_timeout_stop(&s->timeout); } // The demuxer is not stopped explicitly, because it will stop by itself on // end-of-stream #ifdef HAVE_USB if (aoa_hid_initialized) { if (keyboard_aoa_initialized) { sc_keyboard_aoa_destroy(&s->keyboard_aoa); } if (mouse_aoa_initialized) { sc_mouse_aoa_destroy(&s->mouse_aoa); } 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); } if (file_pusher_initialized) { sc_file_pusher_stop(&s->file_pusher); } if (recorder_initialized) { sc_recorder_stop(&s->recorder); } if (screen_initialized) { sc_screen_interrupt(&s->screen); } if (server_started) { // shutdown the sockets and kill the server sc_server_stop(&s->server); } if (timeout_started) { sc_timeout_join(&s->timeout); } if (timeout_initialized) { sc_timeout_destroy(&s->timeout); } // now that the sockets are shutdown, the demuxer and controller are // interrupted, we can join them if (video_demuxer_started) { sc_demuxer_join(&s->video_demuxer); } 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); } if (controller_initialized) { sc_controller_destroy(&s->controller); } if (recorder_started) { sc_recorder_join(&s->recorder); } if (recorder_initialized) { sc_recorder_destroy(&s->recorder); } if (file_pusher_initialized) { sc_file_pusher_join(&s->file_pusher); sc_file_pusher_destroy(&s->file_pusher); } if (server_started) { sc_server_join(&s->server); } sc_server_destroy(&s->server); return ret; } Genymobile-scrcpy-facefde/app/src/scrcpy.h000066400000000000000000000005731505702741400210610ustar00rootroot00000000000000#ifndef SCRCPY_H #define SCRCPY_H #include "common.h" #include "options.h" enum scrcpy_exit_code { // Normal program termination SCRCPY_EXIT_SUCCESS, // No connection could be established SCRCPY_EXIT_FAILURE, // Device was disconnected while running SCRCPY_EXIT_DISCONNECTED, }; enum scrcpy_exit_code scrcpy(struct scrcpy_options *options); #endif Genymobile-scrcpy-facefde/app/src/screen.c000066400000000000000000000711241505702741400210300ustar00rootroot00000000000000#include "screen.h" #include #include #include #include "events.h" #include "icon.h" #include "options.h" #include "util/log.h" #define DISPLAY_MARGINS 96 #define DOWNCAST(SINK) container_of(SINK, struct sc_screen, frame_sink) static inline struct sc_size get_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) { int width; int height; SDL_GetWindowSize(screen->window, &width, &height); struct sc_size size; size.width = width; size.height = height; return size; } static struct sc_point get_window_position(const struct sc_screen *screen) { int x; int y; SDL_GetWindowPosition(screen->window, &x, &y); struct sc_point point; point.x = x; point.y = y; return point; } // set the window size to be applied when fullscreen is disabled static void set_window_size(struct sc_screen *screen, struct sc_size new_size) { assert(!screen->fullscreen); assert(!screen->maximized); assert(!screen->minimized); SDL_SetWindowSize(screen->window, new_size.width, new_size.height); } // get the preferred display bounds (i.e. the screen bounds with some margins) static bool get_preferred_display_bounds(struct sc_size *bounds) { SDL_Rect rect; if (SDL_GetDisplayUsableBounds(0, &rect)) { LOGW("Could not get display usable bounds: %s", SDL_GetError()); return false; } bounds->width = MAX(0, rect.w - DISPLAY_MARGINS); bounds->height = MAX(0, rect.h - DISPLAY_MARGINS); return true; } static bool is_optimal_size(struct sc_size current_size, struct sc_size content_size) { // The size is optimal if we can recompute one dimension of the current // size from the other return current_size.height == current_size.width * content_size.height / content_size.width || current_size.width == current_size.height * content_size.width / content_size.height; } // return the optimal size of the window, with the following constraints: // - it attempts to keep at least one dimension of the current_size (i.e. it // crops the black borders) // - it keeps the aspect ratio // - it scales down to make it fit in the display_size static struct sc_size get_optimal_size(struct sc_size current_size, struct sc_size content_size, bool within_display_bounds) { if (content_size.width == 0 || content_size.height == 0) { // avoid division by 0 return current_size; } struct sc_size window_size; struct sc_size display_size; if (!within_display_bounds || !get_preferred_display_bounds(&display_size)) { // do not constraint the size window_size = current_size; } else { window_size.width = MIN(current_size.width, display_size.width); window_size.height = MIN(current_size.height, display_size.height); } if (is_optimal_size(window_size, content_size)) { return window_size; } bool keep_width = content_size.width * window_size.height > content_size.height * window_size.width; if (keep_width) { // remove black borders on top and bottom window_size.height = content_size.height * window_size.width / content_size.width; } else { // remove black borders on left and right (or none at all if it already // fits) window_size.width = content_size.width * window_size.height / content_size.height; } return window_size; } // initially, there is no current size, so use the frame size as current size // req_width and req_height, if not 0, are the sizes requested by the user static inline struct sc_size get_initial_optimal_size(struct sc_size content_size, uint16_t req_width, uint16_t req_height) { struct sc_size window_size; if (!req_width && !req_height) { window_size = get_optimal_size(content_size, content_size, true); } else { if (req_width) { window_size.width = req_width; } else { // compute from the requested height window_size.width = (uint32_t) req_height * content_size.width / content_size.height; } if (req_height) { window_size.height = req_height; } else { // compute from the requested width window_size.height = (uint32_t) req_width * content_size.height / content_size.width; } } return window_size; } static inline bool sc_screen_is_relative_mode(struct sc_screen *screen) { // screen->im.mp may be NULL if --no-control return screen->im.mp && screen->im.mp->relative_mode; } static void sc_screen_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; } } // 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); 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(_WIN32) # define CONTINUOUS_RESIZING_WORKAROUND #endif #ifdef CONTINUOUS_RESIZING_WORKAROUND // On Windows and MacOS, resizing blocks the event loop, so resizing events are // not triggered. As a workaround, handle them in an event handler. // // // static int event_watcher(void *data, SDL_Event *event) { struct 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; } #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); 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) { return false; } #ifndef NDEBUG screen->open = true; #endif // nothing to do, the screen is already open on the main thread return true; } static void sc_screen_frame_sink_close(struct sc_frame_sink *sink) { struct sc_screen *screen = DOWNCAST(sink); (void) screen; #ifndef NDEBUG screen->open = false; #endif // nothing to do, the screen lifecycle is not managed by the frame producer } static bool sc_screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { struct sc_screen *screen = DOWNCAST(sink); assert(screen->video); bool previous_skipped; bool ok = sc_frame_buffer_push(&screen->fb, frame, &previous_skipped); if (!ok) { return false; } 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) { return false; } } return true; } 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) { return false; } 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_window; } 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); 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 sc_screen_set_paused(struct sc_screen *screen, bool paused) { assert(screen->video); if (!paused && !screen->paused) { // nothing to do return; } 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()); return; } screen->fullscreen = !screen->fullscreen; if (!screen->fullscreen && !screen->maximized && !screen->minimized) { apply_pending_resize(screen); } LOGD("Switched to %s mode", screen->fullscreen ? "fullscreen" : "windowed"); sc_screen_render(screen, true); } void sc_screen_resize_to_fit(struct sc_screen *screen) { assert(screen->video); if (screen->fullscreen || screen->maximized || screen->minimized) { return; } struct sc_point point = get_window_position(screen); struct sc_size window_size = get_window_size(screen); struct sc_size optimal_size = get_optimal_size(window_size, screen->content_size, false); // Center the window related to the device screen assert(optimal_size.width <= window_size.width); assert(optimal_size.height <= window_size.height); uint32_t new_x = point.x + (window_size.width - optimal_size.width) / 2; uint32_t new_y = point.y + (window_size.height - optimal_size.height) / 2; SDL_SetWindowSize(screen->window, optimal_size.width, optimal_size.height); SDL_SetWindowPosition(screen->window, new_x, new_y); LOGD("Resized to optimal size: %ux%u", optimal_size.width, optimal_size.height); } void sc_screen_resize_to_pixel_perfect(struct sc_screen *screen) { assert(screen->video); if (screen->fullscreen || screen->minimized) { return; } if (screen->maximized) { SDL_RestoreWindow(screen->window); screen->maximized = false; } struct sc_size content_size = screen->content_size; SDL_SetWindowSize(screen->window, content_size.width, content_size.height); LOGD("Resized to pixel-perfect: %ux%u", content_size.width, content_size.height); } bool 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; } Genymobile-scrcpy-facefde/app/src/screen.h000066400000000000000000000111341505702741400210300ustar00rootroot00000000000000#ifndef SC_SCREEN_H #define SC_SCREEN_H #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; 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; bool has_frame; bool fullscreen; bool maximized; bool minimized; AVFrame *frame; bool paused; AVFrame *resume_frame; }; struct sc_screen_params { bool video; 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 screen, create window, renderer and texture (window is hidden) bool sc_screen_init(struct sc_screen *screen, const struct sc_screen_params *params); // request to interrupt any inner thread // must be called before screen_join() void sc_screen_interrupt(struct sc_screen *screen); // join any inner thread void sc_screen_join(struct sc_screen *screen); // destroy window, renderer and texture (if any) void sc_screen_destroy(struct sc_screen *screen); // hide the window // // It is used to hide the window immediately on closing without waiting for // screen_destroy() void sc_screen_hide_window(struct sc_screen *screen); // toggle the fullscreen mode void sc_screen_toggle_fullscreen(struct sc_screen *screen); // resize window to optimal size (remove black borders) void sc_screen_resize_to_fit(struct sc_screen *screen); // resize window to 1:1 (pixel-perfect) void sc_screen_resize_to_pixel_perfect(struct sc_screen *screen); // set the display 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); #endif Genymobile-scrcpy-facefde/app/src/server.c000066400000000000000000001100771505702741400210600ustar00rootroot00000000000000#include "server.h" #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" #define SC_SERVER_FILENAME "scrcpy-server" #define SC_SERVER_PATH_DEFAULT PREFIX "/share/scrcpy/" SC_SERVER_FILENAME #define SC_DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar" #define SC_ADB_PORT_DEFAULT 5555 #define SC_SOCKET_NAME_PREFIX "scrcpy_" static char * get_server_path(void) { char *server_path = sc_get_env("SCRCPY_SERVER_PATH"); if (server_path) { // if the envvar is set, use it LOGD("Using SCRCPY_SERVER_PATH: %s", server_path); return server_path; } #ifndef PORTABLE LOGD("Using server: " SC_SERVER_PATH_DEFAULT); server_path = strdup(SC_SERVER_PATH_DEFAULT); if (!server_path) { LOG_OOM(); return NULL; } #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); } 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; } char byte; // the connection may succeed even if the server behind the "adb tunnel" // is not listening, so read one byte to detect a working connection if (net_recv_intr(intr, socket, &byte, 1) != 1) { // the server is not listening yet behind the adb tunnel return false; } return true; } static sc_socket connect_to_server(struct sc_server *server, unsigned attempts, sc_tick delay, uint32_t host, uint16_t port) { do { LOGD("Remaining connection attempts: %u", attempts); sc_socket socket = net_socket(); if (socket != SC_SOCKET_NONE) { bool ok = connect_and_read_byte(&server->intr, socket, host, port); if (ok) { // it worked! return socket; } net_close(socket); } if (sc_intr_is_interrupted(&server->intr)) { // Stop immediately break; } if (attempts) { sc_tick deadline = sc_tick_now() + delay; bool ok = sc_server_sleep(server, deadline); if (!ok) { LOGI("Connection attempt stopped"); break; } } } while (--attempts); return SC_SOCKET_NONE; } bool sc_server_init(struct sc_server *server, const struct sc_server_params *params, const struct sc_server_callbacks *cbs, void *cbs_userdata) { // The allocated data in params (const char *) must remain valid until the // end of the program server->params = *params; 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 (!server->serial) { LOG_OOM(); return false; } return true; } char *ip_port = sc_server_switch_to_tcpip(server, serial); if (!ip_port) { return false; } server->serial = ip_port; return sc_server_connect_to_tcpip(server, ip_port, 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; } // params->tcpip_dst implies params->tcpip assert(!params->tcpip_dst || params->tcpip); // If tcpip_dst parameter is given, then it must connect to this address. // Therefore, the device is unknown, so serial is meaningless at this point. assert(!params->req_serial || !params->tcpip_dst); // A device must be selected via a serial in all cases except when --tcpip= // is called with a parameter (in that case, the device may initially not // exist, and scrcpy will execute "adb connect"). bool need_initial_serial = !params->tcpip_dst; if (need_initial_serial) { // At most one of the 3 following parameters may be set assert(!!params->req_serial + params->select_usb + params->select_tcpip <= 1); struct sc_adb_device_selector selector; if (params->req_serial) { selector.type = SC_ADB_DEVICE_SELECT_SERIAL; selector.serial = params->req_serial; } else if (params->select_usb) { selector.type = SC_ADB_DEVICE_SELECT_USB; } else if (params->select_tcpip) { selector.type = SC_ADB_DEVICE_SELECT_TCPIP; } else { // No explicit selection, check $ANDROID_SERIAL const char *env_serial = getenv("ANDROID_SERIAL"); if (env_serial) { LOGI("Using ANDROID_SERIAL: %s", env_serial); selector.type = SC_ADB_DEVICE_SELECT_SERIAL; selector.serial = env_serial; } else { selector.type = SC_ADB_DEVICE_SELECT_ALL; } } struct sc_adb_device device; ok = sc_adb_select_device(&server->intr, &selector, 0, &device); if (!ok) { goto error_connection_failed; } if (params->tcpip) { assert(!params->tcpip_dst); ok = sc_server_configure_tcpip_unknown_address(server, device.serial); sc_adb_device_destroy(&device); if (!ok) { goto error_connection_failed; } assert(server->serial); } else { // "move" the device.serial without copy server->serial = device.serial; // the serial must not be freed by the destructor device.serial = NULL; sc_adb_device_destroy(&device); } } else { // 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; } 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; } 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; } bool sc_server_start(struct sc_server *server) { bool ok = sc_thread_create(&server->thread, run_server, "scrcpy-server", server); if (!ok) { LOGE("Could not create server thread"); return false; } return true; } void sc_server_stop(struct sc_server *server) { sc_mutex_lock(&server->mutex); server->stopped = true; sc_cond_signal(&server->cond_stopped); sc_intr_interrupt(&server->intr); sc_mutex_unlock(&server->mutex); } 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(); } Genymobile-scrcpy-facefde/app/src/server.h000066400000000000000000000067541505702741400210730ustar00rootroot00000000000000#ifndef SC_SERVER_H #define SC_SERVER_H #include "common.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" #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; 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; }; struct sc_server_callbacks { /** * Called when the server failed to connect * * If it is called, then on_connected() and on_disconnected() will never be * called. */ void (*on_connection_failed)(struct sc_server *server, void *userdata); /** * Called on server connection */ void (*on_connected)(struct sc_server *server, void *userdata); /** * Called on server disconnection (after it has been connected) */ void (*on_disconnected)(struct sc_server *server, void *userdata); }; // init the server with the given params bool sc_server_init(struct sc_server *server, const struct sc_server_params *params, const struct sc_server_callbacks *cbs, void *cbs_userdata); // start the server asynchronously bool sc_server_start(struct sc_server *server); // disconnect and kill the server process void sc_server_stop(struct sc_server *server); // join the server thread void sc_server_join(struct sc_server *server); // close and release sockets void sc_server_destroy(struct sc_server *server); #endif Genymobile-scrcpy-facefde/app/src/shortcut_mod.h000066400000000000000000000034221505702741400222640ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/sys/000077500000000000000000000000001505702741400202165ustar00rootroot00000000000000Genymobile-scrcpy-facefde/app/src/sys/unix/000077500000000000000000000000001505702741400212015ustar00rootroot00000000000000Genymobile-scrcpy-facefde/app/src/sys/unix/file.c000066400000000000000000000043611505702741400222700ustar00rootroot00000000000000#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); } Genymobile-scrcpy-facefde/app/src/sys/unix/process.c000066400000000000000000000134301505702741400230240ustar00rootroot00000000000000#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"); } } Genymobile-scrcpy-facefde/app/src/sys/win/000077500000000000000000000000001505702741400210135ustar00rootroot00000000000000Genymobile-scrcpy-facefde/app/src/sys/win/file.c000066400000000000000000000014731505702741400221030ustar00rootroot00000000000000#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); } Genymobile-scrcpy-facefde/app/src/sys/win/process.c000066400000000000000000000160501505702741400226370ustar00rootroot00000000000000#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"); } } Genymobile-scrcpy-facefde/app/src/trait/000077500000000000000000000000001505702741400205235ustar00rootroot00000000000000Genymobile-scrcpy-facefde/app/src/trait/frame_sink.h000066400000000000000000000011031505702741400230050ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/trait/frame_source.c000066400000000000000000000031331505702741400233410ustar00rootroot00000000000000#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; } Genymobile-scrcpy-facefde/app/src/trait/frame_source.h000066400000000000000000000015351505702741400233520ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/trait/gamepad_processor.h000066400000000000000000000025001505702741400243660ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/trait/key_processor.h000066400000000000000000000031611505702741400235640ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/trait/mouse_processor.h000066400000000000000000000027461505702741400241340ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/trait/packet_sink.h000066400000000000000000000016771505702741400232020ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/trait/packet_source.c000066400000000000000000000036401505702741400235210ustar00rootroot00000000000000#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); } } } Genymobile-scrcpy-facefde/app/src/trait/packet_source.h000066400000000000000000000016671505702741400235350ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/uhid/000077500000000000000000000000001505702741400203315ustar00rootroot00000000000000Genymobile-scrcpy-facefde/app/src/uhid/gamepad_uhid.c000066400000000000000000000116501505702741400231070ustar00rootroot00000000000000#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; } Genymobile-scrcpy-facefde/app/src/uhid/gamepad_uhid.h000066400000000000000000000007171505702741400231160ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/uhid/keyboard_uhid.c000066400000000000000000000116111505702741400233060ustar00rootroot00000000000000#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; } Genymobile-scrcpy-facefde/app/src/uhid/keyboard_uhid.h000066400000000000000000000012011505702741400233050ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/uhid/mouse_uhid.c000066400000000000000000000063031505702741400226400ustar00rootroot00000000000000#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; } Genymobile-scrcpy-facefde/app/src/uhid/mouse_uhid.h000066400000000000000000000006001505702741400226370ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/uhid/uhid_output.c000066400000000000000000000013461505702741400230520ustar00rootroot00000000000000#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); } } Genymobile-scrcpy-facefde/app/src/uhid/uhid_output.h000066400000000000000000000011271505702741400230540ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/usb/000077500000000000000000000000001505702741400201715ustar00rootroot00000000000000Genymobile-scrcpy-facefde/app/src/usb/aoa_hid.c000066400000000000000000000356271505702741400217360ustar00rootroot00000000000000#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); } Genymobile-scrcpy-facefde/app/src/usb/aoa_hid.h000066400000000000000000000041561505702741400217340ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/usb/gamepad_aoa.c000066400000000000000000000061301505702741400225530ustar00rootroot00000000000000#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 } Genymobile-scrcpy-facefde/app/src/usb/gamepad_aoa.h000066400000000000000000000007321505702741400225620ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/usb/keyboard_aoa.c000066400000000000000000000061511505702741400227600ustar00rootroot00000000000000#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 } Genymobile-scrcpy-facefde/app/src/usb/keyboard_aoa.h000066400000000000000000000010001505702741400227510ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/usb/mouse_aoa.c000066400000000000000000000047751505702741400223220ustar00rootroot00000000000000#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 } Genymobile-scrcpy-facefde/app/src/usb/mouse_aoa.h000066400000000000000000000006321505702741400223130ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/usb/scrcpy_otg.c000066400000000000000000000150751505702741400225210ustar00rootroot00000000000000#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; } Genymobile-scrcpy-facefde/app/src/usb/scrcpy_otg.h000066400000000000000000000002641505702741400225200ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/usb/screen_otg.c000066400000000000000000000242131505702741400224670ustar00rootroot00000000000000#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; } } Genymobile-scrcpy-facefde/app/src/usb/screen_otg.h000066400000000000000000000023111505702741400224670ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/usb/usb.c000066400000000000000000000261421505702741400211330ustar00rootroot00000000000000#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); } } Genymobile-scrcpy-facefde/app/src/usb/usb.h000066400000000000000000000036011505702741400211330ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/util/000077500000000000000000000000001505702741400203555ustar00rootroot00000000000000Genymobile-scrcpy-facefde/app/src/util/acksync.c000066400000000000000000000030421505702741400221530ustar00rootroot00000000000000#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); } Genymobile-scrcpy-facefde/app/src/util/acksync.h000066400000000000000000000026561505702741400221720ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/util/audiobuf.c000066400000000000000000000107771505702741400223330ustar00rootroot00000000000000#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; } Genymobile-scrcpy-facefde/app/src/util/audiobuf.h000066400000000000000000000033211505702741400223230ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/util/average.c000066400000000000000000000007431505702741400221370ustar00rootroot00000000000000#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; } Genymobile-scrcpy-facefde/app/src/util/average.h000066400000000000000000000014421505702741400221410ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/util/binary.h000066400000000000000000000040131505702741400220100ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/util/env.c000066400000000000000000000010351505702741400213100ustar00rootroot00000000000000#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 } Genymobile-scrcpy-facefde/app/src/util/env.h000066400000000000000000000003351505702741400213170ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/util/file.c000066400000000000000000000023341505702741400214420ustar00rootroot00000000000000#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; } Genymobile-scrcpy-facefde/app/src/util/file.h000066400000000000000000000017201505702741400214450ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/util/intmap.c000066400000000000000000000005411505702741400220110ustar00rootroot00000000000000#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; } Genymobile-scrcpy-facefde/app/src/util/intmap.h000066400000000000000000000010261505702741400220150ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/util/intr.c000066400000000000000000000036341505702741400215030ustar00rootroot00000000000000#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); } Genymobile-scrcpy-facefde/app/src/util/intr.h000066400000000000000000000030731505702741400215050ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/util/log.c000066400000000000000000000106231505702741400213040ustar00rootroot00000000000000#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); } Genymobile-scrcpy-facefde/app/src/util/log.h000066400000000000000000000017661505702741400213210ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/util/memory.c000066400000000000000000000004031505702741400220260ustar00rootroot00000000000000#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); } Genymobile-scrcpy-facefde/app/src/util/memory.h000066400000000000000000000004351505702741400220400ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/util/net.c000066400000000000000000000147471505702741400213240ustar00rootroot00000000000000#include "net.h" #include #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; } Genymobile-scrcpy-facefde/app/src/util/net.h000066400000000000000000000043351505702741400213210ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/util/net_intr.c000066400000000000000000000043771505702741400223560ustar00rootroot00000000000000#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; } Genymobile-scrcpy-facefde/app/src/util/net_intr.h000066400000000000000000000016511505702741400223530ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/util/process.c000066400000000000000000000052241505702741400222020ustar00rootroot00000000000000#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); } Genymobile-scrcpy-facefde/app/src/util/process.h000066400000000000000000000103531505702741400222060ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/util/process_intr.c000066400000000000000000000014121505702741400232310ustar00rootroot00000000000000#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; } Genymobile-scrcpy-facefde/app/src/util/process_intr.h000066400000000000000000000005671505702741400232500ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/util/rand.c000066400000000000000000000012211505702741400214410ustar00rootroot00000000000000#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; } Genymobile-scrcpy-facefde/app/src/util/rand.h000066400000000000000000000004141505702741400214510ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/util/str.c000066400000000000000000000201421505702741400213300ustar00rootroot00000000000000#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; } Genymobile-scrcpy-facefde/app/src/util/str.h000066400000000000000000000073701505702741400213450ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/util/strbuf.c000066400000000000000000000034411505702741400220300ustar00rootroot00000000000000#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; } } Genymobile-scrcpy-facefde/app/src/util/strbuf.h000066400000000000000000000025161505702741400220370ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/util/term.c000066400000000000000000000016501505702741400214720ustar00rootroot00000000000000#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 } Genymobile-scrcpy-facefde/app/src/util/term.h000066400000000000000000000007001505702741400214720ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/util/thread.c000066400000000000000000000117201505702741400217710ustar00rootroot00000000000000#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 } Genymobile-scrcpy-facefde/app/src/util/thread.h000066400000000000000000000032711505702741400220000ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/util/tick.c000066400000000000000000000030171505702741400214540ustar00rootroot00000000000000#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 } Genymobile-scrcpy-facefde/app/src/util/tick.h000066400000000000000000000012311505702741400214550ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/util/timeout.c000066400000000000000000000033401505702741400222070ustar00rootroot00000000000000#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); } Genymobile-scrcpy-facefde/app/src/util/timeout.h000066400000000000000000000014301505702741400222120ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/util/vecdeque.h000066400000000000000000000224671505702741400223420ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/util/vector.h000066400000000000000000000337651505702741400220460ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/v4l2_sink.c000066400000000000000000000226761505702741400213740ustar00rootroot00000000000000#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); } Genymobile-scrcpy-facefde/app/src/v4l2_sink.h000066400000000000000000000013511505702741400213640ustar00rootroot00000000000000#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 Genymobile-scrcpy-facefde/app/src/version.c000066400000000000000000000041721505702741400212350ustar00rootroot00000000000000#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 } Genymobile-scrcpy-facefde/app/src/version.h000066400000000000000000000001511505702741400212330ustar00rootroot00000000000000#ifndef SC_VERSION_H #define SC_VERSION_H #include "common.h" void scrcpy_print_version(void); #endif Genymobile-scrcpy-facefde/app/tests/000077500000000000000000000000001505702741400177535ustar00rootroot00000000000000Genymobile-scrcpy-facefde/app/tests/test_adb_parser.c000066400000000000000000000213501505702741400232610ustar00rootroot00000000000000#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; } Genymobile-scrcpy-facefde/app/tests/test_audiobuf.c000066400000000000000000000061741505702741400227640ustar00rootroot00000000000000#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; } Genymobile-scrcpy-facefde/app/tests/test_binary.c000066400000000000000000000070331505702741400224450ustar00rootroot00000000000000#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; } Genymobile-scrcpy-facefde/app/tests/test_cli.c000066400000000000000000000104651505702741400217330ustar00rootroot00000000000000#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; } Genymobile-scrcpy-facefde/app/tests/test_control_msg_serialize.c000066400000000000000000000327221505702741400255610ustar00rootroot00000000000000#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, .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); const uint8_t expected[] = { SC_CONTROL_MSG_TYPE_INJECT_KEYCODE, 0x01, // AKEY_EVENT_ACTION_UP 0x00, 0x00, 0x00, 0x42, // AKEYCODE_ENTER 0x00, 0x00, 0x00, 0X05, // repeat 0x00, 0x00, 0x00, 0x41, // AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON }; assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_inject_text(void) { struct sc_control_msg msg = { .type = SC_CONTROL_MSG_TYPE_INJECT_TEXT, .inject_text = { .text = "hello, world!", }, }; uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 18); const uint8_t expected[] = { SC_CONTROL_MSG_TYPE_INJECT_TEXT, 0x00, 0x00, 0x00, 0x0d, // text length 'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text }; assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_inject_text_long(void) { struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_INJECT_TEXT; char text[SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH + 1]; memset(text, 'a', SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); text[SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH] = '\0'; msg.inject_text.text = text; 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); 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); assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_inject_touch_event(void) { struct sc_control_msg msg = { .type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, .inject_touch_event = { .action = AMOTION_EVENT_ACTION_DOWN, .pointer_id = UINT64_C(0x1234567887654321), .position = { .point = { .x = 100, .y = 200, }, .screen_size = { .width = 1080, .height = 1920, }, }, .pressure = 1.0f, .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); const uint8_t expected[] = { SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, 0x00, // AKEY_EVENT_ACTION_DOWN 0x12, 0x34, 0x56, 0x78, 0x87, 0x65, 0x43, 0x21, // pointer id 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0xc8, // 100 200 0x04, 0x38, 0x07, 0x80, // 1080 1920 0xff, 0xff, // pressure 0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY (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, .inject_scroll_event = { .position = { .point = { .x = 260, .y = 1026, }, .screen_size = { .width = 1080, .height = 1920, }, }, .hscroll = 16, .vscroll = -16, .buttons = 1, }, }; uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 21); const uint8_t expected[] = { SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026 0x04, 0x38, 0x07, 0x80, // 1080 1920 0x7F, 0xFF, // 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 }; 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, }, }; uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 2); const uint8_t expected[] = { SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, 0x01, // AKEY_EVENT_ACTION_UP }; assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_expand_notification_panel(void) { struct sc_control_msg msg = { .type = SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, }; 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_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, }; 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_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, }; 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, }, }; uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 2); const uint8_t expected[] = { SC_CONTROL_MSG_TYPE_GET_CLIPBOARD, SC_COPY_KEY_COPY, }; assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_set_clipboard(void) { struct sc_control_msg msg = { .type = SC_CONTROL_MSG_TYPE_SET_CLIPBOARD, .set_clipboard = { .sequence = UINT64_C(0x0102030405060708), .paste = true, .text = "hello, world!", }, }; uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 27); const uint8_t expected[] = { SC_CONTROL_MSG_TYPE_SET_CLIPBOARD, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // sequence 1, // paste 0x00, 0x00, 0x00, 0x0d, // text length 'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text }; assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_set_clipboard_long(void) { struct sc_control_msg msg = { .type = SC_CONTROL_MSG_TYPE_SET_CLIPBOARD, .set_clipboard = { .sequence = UINT64_C(0x0102030405060708), .paste = true, .text = NULL, }, }; char text[SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH + 1]; memset(text, 'a', SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH); text[SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH] = '\0'; msg.set_clipboard.text = text; uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == SC_CONTROL_MSG_MAX_SIZE); uint8_t expected[SC_CONTROL_MSG_MAX_SIZE] = { SC_CONTROL_MSG_TYPE_SET_CLIPBOARD, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // sequence 1, // paste // text length SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH >> 24, (SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH >> 16) & 0xff, (SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH >> 8) & 0xff, SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH & 0xff, }; memset(expected + 14, 'a', SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH); assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_set_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); assert(size == 2); const uint8_t expected[] = { SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER, 0x01, // true }; 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; test_serialize_inject_keycode(); test_serialize_inject_text(); test_serialize_inject_text_long(); test_serialize_inject_touch_event(); test_serialize_inject_scroll_event(); test_serialize_back_or_screen_on(); test_serialize_expand_notification_panel(); test_serialize_expand_settings_panel(); test_serialize_collapse_panels(); test_serialize_get_clipboard(); test_serialize_set_clipboard(); test_serialize_set_clipboard_long(); test_serialize_set_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(); return 0; } Genymobile-scrcpy-facefde/app/tests/test_device_msg_deserialize.c000066400000000000000000000052621505702741400256500ustar00rootroot00000000000000#include "common.h" #include #include #include #include #include "device_msg.h" static void test_deserialize_clipboard(void) { const uint8_t input[] = { DEVICE_MSG_TYPE_CLIPBOARD, 0x00, 0x00, 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); assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD); assert(msg.clipboard.text); assert(!strcmp("ABC", msg.clipboard.text)); sc_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; test_deserialize_clipboard(); test_deserialize_clipboard_big(); test_deserialize_ack_set_clipboard(); test_deserialize_uhid_output(); return 0; } Genymobile-scrcpy-facefde/app/tests/test_orientation.c000066400000000000000000000057761505702741400235300ustar00rootroot00000000000000#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; } Genymobile-scrcpy-facefde/app/tests/test_str.c000066400000000000000000000270641505702741400217770ustar00rootroot00000000000000#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; } Genymobile-scrcpy-facefde/app/tests/test_strbuf.c000066400000000000000000000017141505702741400224660ustar00rootroot00000000000000#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; } Genymobile-scrcpy-facefde/app/tests/test_vecdeque.c000066400000000000000000000106031505702741400227570ustar00rootroot00000000000000#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; } Genymobile-scrcpy-facefde/app/tests/test_vector.c000066400000000000000000000233521505702741400224650ustar00rootroot00000000000000#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; } Genymobile-scrcpy-facefde/assets/000077500000000000000000000000001505702741400173335ustar00rootroot00000000000000Genymobile-scrcpy-facefde/assets/screenshot-debian-600.jpg000066400000000000000000001276201505702741400237450ustar00rootroot00000000000000JFIFC     C   X  S A  @}ADH( lxN *Ǧۧ%@ c.'%JecT^f.`  F5JjHcAX XDJ"XD&C:c+]`5qN\32WwO=zh1`A@X"Eռ=>5JK^[9wN;^ΰZi18mϗZK:+r6@S%-DD ɻy6lu5sj)-n5 Fק`S c )`D++aQW:+CV׵ϯ0NR1c*""Fj$DAbD(nvܰ/qp©e#%0 @%DX""(Un [cW e9uk@c!,V$V$b+-@Q,H,rQ7 .{cV1N5nݼoF , 1 D%!j#7ԺrجcWS4cƍABK bDP#XXgQs].@̯=VH5uyh Aԅ-,ԩ`Ea"V$ DlI7 3+ˣp-5zh XcIA4Ԩ"E`#,+ c#s޸?ѲPՙN}@)Ɠێ1Ռz KNT%Q+bE`X%A%zzf2uQɣti4E)t^0># cV0K=JsP+$eĂX,"*VκvmϷgfN{̆64ItuGw.hSFT]}KNӤ.Foqo˾K;E5}`iXg~{/ =4罹k~$1cM_Vw|w^v7z<-a>z=_H^*D^g>H+@iW:ˆtn~Ⱥ5ss@ O7wƍ%Wz2zZ_|}&W;;.|ïc P\zz30ZJ> et+r%ݹ꾏+$˱Ƃ==a׊ ۼo̳X?y;m+_?ޏ%(Bhw2'ńrǷ=O4i$hU4O÷iFkNSxk9<797w+M6fUVz|Xi0P/.;Lv;1#٬t<ۧapڞ.]3\Cy~峰2^7+E.\kE|ա5EyWli;&˲TRѤo'-=xtN]^;&zu>sX}p߯ơ8D\grJ樜{B+\'q4,*P)Tv1 .sKt$Ȕ5b"}\);k3I;E`eHJ@T)Pj1@qw. 2Q2D'+=o>lw2lNZi5YTBPhX@9t32d!SRZ qNv+Y3D%n6IRJ!1:y$ԉfh f9g-)sG)\fZ̙s+a5FX,fƁ+P;;?Y-I*$HP(RW瞞2h;܈3Psbi,TF:.sExfDɎ%R/ylW iK=美-l;#55c)`Qj H v48pQ@fi2DU)nv^:phz˹MLcQV&9L4;B( 6fJ&2D-ykSjpN9׫v;pϣy~o!9>ssNX7 2L4h@)ӆ:!fdrJkM\W͡\;ӎuzF~~x~]OO侦/Z@d )۾}o>wl]MzO(Ye&dԆB5Euv<ۮIƀ2[v^=]MzW&̖i)e$V \/=/RWEY5G9OI\0.ѐ .Mʥ$8h(BpDa$H,65L?]q=smEa1QFWS~tɤ!A6 )hkӝ)FDE""5/~oXsIU7LN%RԡmkAxק:~QHDD$q̓ID)wy= $@cAo~.4"y2CHΒV\k=D)bE"XMAZbmO`4h v4h%ߢ<}2rH(5i!^5l#QHزEQMAH@Le@3^"D*=1VP8ߢH"E@Qi )I0%c@~= "7%dQAx{H32,"BT6.:VI ;]է|zǬRfR,QGcPE@Y߯9w{~o3o(|z~_>?wz=z{Ce-w[a%*翥sqνA+ʃ-)x}h}xz|fO73Ӝk"zz3ci_{EOG2WY|tO@g?t_-dCۯ͠~S^o ƒ]r>;:f5?_?ֿ~sY`$W~כ>>ɾvq}X2_}s|sYcZ]c|߷u7yHU*9i佷o׳>OqKnM?5n5-rkm;W [| տ;7Z?,վ׿kkoyyo5w3u/[#I,vV8kDu#3aOoO anbuYIM4lM`g Ⱦz=m[&>o??Fu~VƃTu_=YDcKtc,!}8k%21f1DHQbQ3T&  `s^k%2Ć%W:?S@7h"h3EH! FA `] k(Ծ܂11CMrs߼~+Pz'kz+/*:jwM亇P~ [߻^^edI#P!jK뮽yo;s@Ho_[HHy_N7>p ^to^~qx/>G_?%h^cO>-Ľ/u@+XX7Z{??uw59 ʿ/rΊ#yRNgl ~\?_?g`?[mP_RĽ{̼5YhLm֒v9]SK_uߩpW&yky|tf_=ܼ^O!5@S??x#?_~'=8חy8tƘ`>]QjW>~?/}^[-wB|t ^^j'_?4`)W0뚁W*R^)t~Ժx(t=+zs4%p"J`Yz5!]锤]~Mxt⯵Sh힦j>)5sV2E@^'8.~ʂ"(}c܆%+Z +z|(X)"Whٗ9Z$]kWA@Ssbr'7Z(}F1Y}o9fkZvI ЬS$U5 #0g"vTʜ5( uUW&z?&"LQv*DbsII*BP,UKDovz+_U_ku ҏnCkd+VdbbF9?%pzzDk]UkP+w|9ە)Z4RP#nVo)W]:Kw7k,j!+w|@+Vi;5TB")w|`줡%kGUs^oxddgz {]'3-B<2222(4w|~KC!_k֥P$oG;iNPlWk! S B4E )k~O Fqtc`rQ(猯?1FLumz%:RdJ骬rũ3-5lnWoj4Z~ڰ f"믦|y3<{] 2fQo_kG1kju߶-냏%kO"n_1rϗ<:MC['zڬrA̫R5 r%\dgB_E`^iIPhDURw"E3nH)˪&Փږt;^[E2,}Tu7 %Q~|i 8B]? XuL738Q)$ZJw\mitpՔF?v-oX%WuKX۾׉KW 5B9mQ@(5\23##!J.ƻ qGISZ 4]zI#&%c\epz&ET,MOIQw. [Uƫ]h)Gnζ|+]pdB,!HƱ###<22fN"swH? y:H>M^ZT`iA}cϝ cg Z9)9|~:2Nb-J_sd{g狟uHBPkAERbAEثԭK+#<30} tjHZC(q<ǔ8Py3&a<0FHy"aL<0@yO0aaQQQ5^FFFxFiFiFiFiFiFiFiFiFiᑞ9G<!1 0@AQ"2Pa3Rq#$BS5`?Ϣ`v))))))))))))))))))))))))))))))))))))+ Ga[L9-c߄VDҾF29^"E-*D>R HDHDHDHDHDHLIDHDHDHY,sd#$g=g=g=f \MɰCtpRZe A@'s+'nYS> D 7tzl.Kpז%9v繩D2B͓/Ub4UT˰3wG.7tzl>LͰ3wG67tz>Lψ,,swTR}ə9ۧŇQӟaf{d-Aڋ*/kq{X+½>'3~F@3ӡs7¡xwwkJ4ͰD]CaJ7SE+Zň$.."Pn桔 R<7znU,_>wTIDvÙMS]ںWby!E.[sNu:L2_]Cq˰ȉJ>kv 7f.w ˰ҩ\ '@- \Qin05n#E1n8du"; '#5T 4*V MTQF\T[MKyޔ)B_$\)hyf?'Lyy"FqATb?nĚۨٷpltԧ]H'e+tZU8*7U:z.aU苋;nc/r;nc;\wGLmwf;+Hvw;R|.NE wܷ~sTcsN0aB~,&̈́ 7Wtnn\4C^NQIeMv!sw$p A4To&8:poI4Z[XtMvV0eqů3w$~80䕱"q8(s2;w$u 7<a:AS뿳*mA[`WW SQ7~ݤQ5v ߰?&n3whz.{YEѼ489O;fTCiR+&r*U4fTmDˇ{Ch~C3-b5#CV\cuTT P'(.n[]X[\%{Wf#32Қ5gfI$ Ł #&F 6ad O-2Mu)ψR뛿df;Kj !qt'e'8JYaprlol1Pнd^>e!:hj+<чdՎlj;ښ8W0.~qy$G@t^0Chn>ʑo@Bk?G?0mO[L j-ܛcJlS4V/ 6u`~Q[ #(:j5l2>KlcIgh4Sp)F m?'.'}}}TEP)DX\7V0&QR2h$/l?+د+9JiE]Yjic_v eSl'K%e6YǼpA/X8#w#xې{B71,A#-Z/ooow';-6fbn3YyYl̖:ZlsOOUìN?f7&7bekpxPٲƉ|GЫ%e6R&;v H-M6POHBsZBe3V4,Y_![-hx NCcvETb*Nk=k+D&{E0 SZ-rM^o~L;ٿ6gB Xv;:tʃOnϛ5X ,Ab XBU T*PBU T*PZ*hZ-E=1 !0@AP"Qa#2q34BRSC`r?6VᲶV޼Vrg+1YVrf+1YVbg+9YVrg+9YVrMR+ā{ەvYՑ#VFd #V@5d Y5d #V@ Y@,d YPhYE,e (YG`pǚ^Kt>l?)߈/)ȋ{녹ͰsCMhnXj`F==3ю=GN?F:vycaF:-BOFv!V8m_~]N w^z Ku[榼;N@o#]FEo[]i7~k|+v]`Dg+#mq|G޺X)-pV (,䭙# ǔяn.)N Odf,L$6jY 1av(V^xMnh Ը#:0̭w#C;v^tcwd 6 9p0pQX8(Ǟ8\YS#HVLB.)Q C8(颚0HRQ M`n`qڭ2C (R:F,k;N :qNh^˘\;:Ua[;N>#giGӧ~;N/Fv^}vy )9g.ӰM/#F\.R*F[~vcFyNܮQ:=͢=8X)TԹtvcs'2߇}&\HCŔU- }:vNI=r<&\Fܺ-iw 0U>Ÿ\G !צcE8; tbBfҪ/UУ~jm;|髝y=ve`0HQF.k"%mkot"7̄`1SO77Y;tuw-U|-f*"SB{UaҖp|+eӰ:vvN^t#GUclscIY sl7.6DOp:vM(BbT`m4 X3*z&I͜sKWqN^؜cVik`dxR6<8j>Ԇo7~Kt6l)D쯆lQ)9}N<\s+jjcٟen/b*!ܤaO+)z]9iQ0=(955_\uRM^"'Z(&vߋFҖklpUPRUKk^VU+ِF̎_G[nCzEԷe쬇,M!<=Qxr melӟϊ-GY~l9od!~QNY L`t#超7ҾPU?f-!3lн[kj "{9H)̮oMY(bAmBt.\G6至1ߜ*Z(v{~RvU1G Gm/NNev,uxυ}SۓtFFDҺ{Æ5S x)5RS˾a70>+?^޸N?ԭ>ⱏ?41F Am/NLmf6YVUZa]_kUHԯC`m_bWGؕ%~G ͯ|Q *oB4J^0:v ;ˠxNwW3/\C@꾉GEk𷠝;0\ vm~jٝ;~3`=tDӂAdphTM]ʲ䷎6C]n>qN〛,7 ¸W ⧅I9}UdEJPrW-5\w7꣑ 6k'{ >8F.Qk1'i6iƠL5(a/ l-R#(hىR=ٕ5h8;](gy=j]U5Arv1ra" y~6 dQEiQ0ҋ\ڌ^l/o;:ZNtqG*za<f[HEc?tXcT-KhQ  .%/-J16679Jn5*'ϒ wg6`]NҚ疊9bg%t,S|aJܥEYSjN\ ["l4q3lTds –I{PPCfKOp:v-7ji=Sm2XbM+'o+,O'''P0򣉱7ՏB*W*qON_M!1"AQqr 02@a#PR34BbCc$Ss5`%6?#1c1c1cU1Q:G`EU'ac1c1c1c1c, ;k B\PP G᥏:ÞDZ*CS 0HX@>gom*K~HȖҚkWdއHQ ̩QJ\Hȫ **ibЮ(d1[ 3,PɌc ) I:-G3ca@Ǖ ͥn,BfVm(sX(zE閯hHeOzKD!S (6dͤeaRYHf1^X[nO)y?Ta_|;&5Mʶ5U:2+.B\';bC.VnڄjfO ч 8ĸo<պ Vٯ>bMci]? mԔ8E%Xs)55~I &:Q1i6m*v,΢:9zkȆmN10ҙy8bNˆC ,m,RH8ȆIZl#Y v(iVi,Wk/0C0֟H?(}i {>;!YlQydE 󘦴NŞtg/$-f\ #h ābݛ$k ,qwTYm p̬2yiG}ğMtPϭ(Sާ (bqPmaN:EYeAmK'5h`N#"$sA:|foVFN*4U I[Bpѥ{c [jnJ)Eeжᣲ8 6i4׼Nh+um0Ώq8/X,-H QM,kiKoL:Rn6ο!@jرܳ ڰxw%({[O*%(\#֯Gv'T)H4)ըy_3\CK~ks' f?-y1Ϸ%?c59Ƒ*p}@4Ӗc:hyxckᕮ1ϲ-y ZQ $aYlvx>;Ԃ䢡+U/1UCžOpLb g G#\cI'heQa){=XyᵶɊ*#kإjmIBQZeDQBUoYAdks'Rklǩ8 JU^el*ਅtE='[X h/.u%"S;7UN 3x—.+3=o^EhAKIJq_vUQ\ì[Uc[<$J8S[&>3L}ngOD. #\ccīWZe1}RΤ~;RHZ:>b yv(lS6Ф:א>q>fy#ŝZ\i? S:ʦEћlyH,yS 5LY))FoHU5P?ˑ11y䓵l=m7ΑJ] =eB8q#cAKGRA<՚}SHV! RC˜ #a1/6_Y 4 @Gҿu~yW?]GҿuCJ59'NufRd CHӤm_HQAJa4[Gtpd&:>LJW˩ +x00g:8 gE]JgKoo@U B_a}EI%8Ӫ R5h3JڈaTE͡x%i&:V_٥jw 9dks pmՁ F6p1q.{w@3t*aeDb"&'_GUZ{P*NaVe5JWTpTq0D[5myY9Y`~a0QRa6GҨ`UHF*_՗ᄯd:5Ur@!{,q~˸^EZMJʕL D.jS c=8wP.J7Ytz\*:Ia"1-h;-z;' yH7Zs/ }c>c[uۆʑvB/#` ;H!n'9P!})$nBRTqdBBeD\iY JI[Hj6E{t(( o6\=`8 CwR&,<_JL%j8 p @*¸eks &DĔ+moaPjCɯ90,AluZVNĦrYHzs:OԩN DS-fmSRp. '_ٿ[4q6I:IX*-(pLMK;͗iŅfȮiQ}-&8IPqC@f*vg*ӎ) Y]Ub7ŒęaexEVJNH:%/j1,V4eQEh $6+KҤI<\u "ե-'VK4o/$P'BaDg^ZRDr_2rv9WBCNz*7UeYߨ9v p:F0q~״wc\w#v n=jEϴfnq+}ޕؾ9(R˪l/m<P_AmoF1www;7'@9 Di *E.C26iX1 @ەoR~0@m % }/,3 ^ON+X֑Q EIHżO_<(S%sٟuҝ􉀑Ao"_uâC :GEeGڌ1<ԹeJdoxn@Zq`[5 :CMRL"b\6UeHQ2LtYf_N_$4H9*UUM]Q-O(eUHN}$,JJ5iBq%$o%9&w I)PF_`KKd$IA6j-af6Fmi%wA]E, FF'jCTz jmfU?i'x/ iո bͰ2l0W;/zKL,*6+}ݒ$ |M9rZl|⬕fBF8v@ol+-R;f m脦Щa\P#|C#wRӭ1mWjU[Ia,T p}6M!XBA`RUiDXZ&*M-r9,hg^ Xp71M9u$"u 4V᠁+)i=٭Mm԰o1[!.r0M[b@RUQ7-!v0T* jr7<7uj i^pR4S: G򔝝PA)P njaمmue][m,[Dzҏtum6*qUfNc7<wv+-YMl>ȸģ61R$APGq݃=%ΩW/q FnG!0.ֳgT{Z]N)g6CؗU65xO|K$|xiƔ0?GI#G]ǀNM&ʾ-6ONK&FI!U +^ر6?i8Uʥᮉy 90lGzyRX#pQ[..Qî>F)TjQ5F]Lc1c1c1c1c1a&*!1 AQaq0@P?! OOF<zDF 0`E0`F 0`TTTTTTTTTTTT`!AVdRT*G#ʑHy'uH9Df7sx7|g5sEF0'b1O/NX=hsm5,w%qH;gq ccc2~_6t5Oy$NH !>}}~P*]m؊pxmXMe%/ rxtKf$dd%DZR! p#Ss΋L$Rx]kIz=+jOұo۟UY;W-rʖu**vbii=X8G B!4BhN*1cF1aB`bM&1cяG B!BjA0a߆i?;Ռc1c!B`&-Wz=1c7TtZ.7x1cձYDD& <;+֔F1cձ)B>=R)t)JRF6666666661D!BpB >)JR)JRFCf(ccc 67qkiDta_<+^IpZ! ~RJR.XAoJRlllllllaţ#]>]79 <5wD! ~R)JRA)J0FlouPٮg3ѱT۽ GM~R)JSF{# PcR]O\;W6e 3uodҭMAέCbm[>OX* X666^=׃f-t2SJќ-sL!B U)JR)w40Mrl0B)aʖ{,ʏPj|ԭsPe|o*J!tycB^l49ݠcz^Ok:$>ſe<Է6y }qnK_,Ȯp-B\A"Q)2WXK. 6hq(Չ[=s}CʉWøtvf4ө͌{N0C!ǰbkj/plz[+A| [Fw5C&/D={y_Z!8cdХt)D L~+;A+y`åȢ{ WMdn(Й27wuT7t0oW7Oإq3?erݾ|W_)Apnq| JR)J(21EȞ͢75{w/>2G' /s.BwAU=١H6B4nϴH : %U\=۷\xOLD$!4y)JRD =c"|6# USIa\Arcs+-a[{`e,Q4K zCdNU(=$<`nuauBy"gy6+I# ;couАqB/\~l! td)KIr9(ӝXkyKmaq4^]QQt+ {܎}5 < 4.OM2Jxۣ}3grBqh )JRDJiu#DKTRK<i;ˈRj; B19JRL\N{)` &|Y:P.FIEL}+Z/BNd's&-n-rqN܆+aVCI-s} "(䵖fiXb]2vg {hJća_nd!4BHAFXRtAK?`Na)AQ#BՌe..7hHHAhMI-!c?bBBBBD 8B9 D-c=zR HHBBHPB=^_3.D A-!HJD- F>CьނO=DhBHc1>BBBދ'XTA!!,[",p <؉#DQۛ:zϸBhm#IXHA"!$SOAtWH=mlm ݘ1.5ēcG#m<@ cI,AH!4b^LQpuIEfKέجXbJt_+щ-h˦ !! h5aۙil!(K\16Tj6>5QDz-cMz BD.:l32llcz2,i WS*Iw5UYXi!8a?t 4{̈́!p`kWTw/ oЄ&&_pJ61lllce 6Qb:EeILOdpzyqz$ W4mBDƆv2ztL"oCe(1s1ߨY(zy F|@О)DRR)F0Y¹]+lݛOyށ&0J'.!J1-aMsNk~?>yПt]1QoFąfolĿ$- !JRBllcee>|kx +RҔl(FI8F1|' Q2`؍H{g8ɂyQ0пJ>|]ԤXӅz$[ Q2LBQ3i }azzݱfERUll<v]4 KJ}9W0#2$Fj2q>fh mp|?wjk蜨܁n+Ѣ.(zLizJRRBCq½ ?cڿZBw&"$$bd SdwND+~=& lVku ?+t 6?D\3.Ϩ?u^o흞')N>߰م*R%# J#؆sziE.#$h$g>kf~0ckp݆l7[rʞժg"M?GgK]DJЊaT4&оG"kp?+A/ :<^mV#$v]u =w4[.uiB# av>g/Uыb,s7oǑl'Q_mo-6+7>%]eru$bmJ;QI=9x#,<ӣ"3F,e;zOA$6|еyQxg PLdgcZkHM6os>0?lۜ/ wOGyq˵#3y}}:# n|% w;iNO9JRB  05@w==4CHYī~ȇHM!XgЌ'EӫFMuF<13 uvxk9F:mƌvHTIEC}øu.'r)V3XԳFi=fgH䥷T>LHyN):+ijtmh$e.ʾu++S%x-`[g3<Nv) JJa+~/p0|e탹&- F$l:)I|5n7:y739Pc7L7j4|oAf%BbW#n6T@22=Hq|$iIz"i4g|| 9&It_IlI-^]=E{JyKqJR2D)J]>4%J2k\QOsSOw 6'<{.ܐ]d潵טGTY$ij'R穟Xc=42L&݆mowInZҴyX)К6HOqt Jݕ}=觳IGnѣ} a;jf)J]lz #?+$g^'&;АӉܕ#/L`IK$%ͧkК6#f4L4/vW&"$A5/wtZ$IߡS50>h4[]z9燆7VBg\=[:&D3ĆtГxhZRl9r;,6^vE8JR)tRz 60S u6;mg#+_7ו~짢I{j9t@R|ųIY纔=BZ$s*K`R,6S 6a3m8qG`Jd-}Px žuL90h0c L@߁E\MؐW#)[(pE%ASpMpD߱ bU IzI{j5)_ċTzм ] d+O3 N̿h[{4)KG O#f1^33~v6<5#b`FˬF蚋فz]POw,! 6$pCl}T3WOne0Kܥ)JR~|OG8|Oa2)JR~şu *^P'R6C?Hy&#Didߢ?&S)JRK÷tGwy*0ex"oAlo"j.Ndc]dc ylqzRK(0 gk9.),M_Cd%mxǶĨvDkv%X4'KҔlCN9x'&R)xnLϑgsw>GpwGwGww_'w~Gu|w|pgxJR)xB_3XԥJR)JR)JR)JR)JR)JR)JR e-7-I%^D$ mlI- @i@%6mm$^I$I&$I$A!$N:גIdY%oK2mbeIl"mٿERz[m!$%,;mAI$Z->[Sd flPiVL~{N>GiDn~i%Z[MEpnlw(oL&[g:ݨF߷8`i][?l11[jUo)WP0oQhdmJ]޴K,LѤ/mat.۴4H?_/mLS$Ĕ_Oom}&fyg~'o3[XݩEg;'_}nN7k\t?@ά}VOk\ѷN yg^}-$!5?{$BI(|7{;>wsݩ>6)xeKaKn_ohb)/h:\"o;L%UӳBńD"z smekd-v;vͶQ!M*bmRݛ_&z@mܟmmrHeOz%mE/g rRzO5bdvmddnv.$] %'öۮ[h괲F4g)s!\[i‹M'%|}/3yEdղ xJ)&Rnyo4-RHi]vypM'7>{?ό ɅD:L/˶ͣJ _`l{]}ؼDe*%f{dmmn1-.-&dm- i4燺i?Ke~mIM?dv~yai%|߃' s-2Jo7)rB{}dQ-Q~I.{|'̏~S@wm$Ij)li{eDi 6.NHiT,M6{sMJ[S6H9 ms} i$fLSI`i&}bѵb#)OkMi,6ܒU-g'XI^II=刀 7;<)-ݨ D&I$ II I$I$I$I$HKmmmmmmd*!1 Aa0Qq@P?A C!C  dC!d2)p>N838N838N8^N/'qy8^N/'ΰ_߁CM4YuRA[xlvۺ*6IT)3>BKrx9_29̾u5:9<lG:G2G299<o7'a bd>}o?w<FyGI 9A_h8:/:f$ޫ\%`Te ^Ļ aY{y}y` AAX H¦LCYuƩ֔ ^{D4InHkyIS.\r?I$M%M&I4WQSE?uONIԺ (<ҹ$$k5] ю #ZGF,AV4`DR+@ <99Lh7 G}V$4"V)KDi:g8jDF H4$Z G(>Ȥ '+^Q)Vgj"5fHBMC&zPOr\ ?C'ܕiɜsX<-bBdWK$R5aE"sFd&#>ҩm%,y+3dC9e=;P`N䁉!b#b#Xi,H˂CEa-N0&nI?"Fu( E /ȍ Y[,˞X0CMaX2F$1?@ @[ؒ!q %*oHPXF*\7lQ4 T 5)Wh. ^?"SqMlCdӆ SфdM|\HAzH 4EQ(2Vvx$dM˂ up².ąc^rp@6E$%5H TW1MH$dHdnFԲHӽ,)i$" *441 ]eqbhP~p I5qF6c$UXGA- Gx!`fAbu#zER7eӁI#RPcA HH 1!$bF1T!TA(B 4CtSH`1b!DR Hv1 !B0Q" ttcD*! [Zp+H@b:1tBD!*\h3yaR"1cC!$$$$:>Oj==1tdD NL,$$%}\#&X @CEHI YAaOuG(HtKF.t¦BA M! 6& F**ERZ c#X $$!!(QќTB"(>$*cӂ¦u#:AR(;M9HƮ5,1C&lwՄz=tA D- $%D0.\Y)'f6qĴ`dkF"A K`TĜ#!Wh H   HKq jE&)0B1; #A @$! Rs=.\&*DE K2Cӊ0T9Ћ HZ0ayR a"/`B !H W/V:l6?G3pJf=qIЄmW8/F< FCFĶ)D[5{2 *S_Rs+k5KDs&E aV J#j%.2\"# GKRXf ! fac`P _$мև5W "RN;50<N㧐*=i(]_[ ӟ07nŵU0}ŷ~}W~rǦ{WI~ cK $&5Gյ_R.z:d(ЬFp(3tٙʈ?[r:~od>U=!!5r1y=ؑ#K<BYl?/Dq~.p*nlv1I$XсV"2upD$ٯ_owm[$N.6Hdڻ8Nu4N 8hK~%:KŨ!eLs-=y5~of!IYz("!b!d'iuF֥\jդagӫ1 I$(Əwa-K)vV$RiH!܅Jz0'CUcBKvo$(m HWo[G?3\vUCKJvYdibRԍ[ *mݷӺʒ<4LuQv!tNТ[ϳ3Y2$HI0u F*Hk~9ie=DRjͿ e;{Ps@$=;̒~ r0 '{GwXс?Flc%~=b(c'#9ɦ5(vYMPf%vsԂ]FLv#kK##\E鏝027XaF Ad}(Ӄ/fqVu`)l"pIBIG3dtpFaF( ZH#Yf'LI"2UPUUT"h$H"DRijMc$Ƌ.\r˗.\r˗.\r˗.\r˗.\r˗.\r˗/+!1 0AaQq@P`?(UQE (/(88b::9mQEQiqРGv;|֫w;gl;gl흳v;gl0 Wu1o?d@vtpŽ:A :TSuΩ:Tsuι:kӥ~GUQ:8fHZ 4 L`(rcDd!  ~, 1qmL@ aNaKeQlD KP$ 6sТֶ& q&db/?C yyyyyyx7 [MsA㖎8!Z t3#htfa(w/\Gq\z dz -C2 0m4PC˜qDZ^W*\sdEnazmKP̏М;=y1e!C][bUdC2HɨJy N=$Euq̎B@c P qA8` DNDad!(GGWC3T!h@` 810x T 0׹L=h90(LqBtR.6f_a 8 `,1SH`Lc[X#Eᰎ m9r`q6 ʀ00NJtr87-T'53#f/(xNL8D$0ho0@Am9(@pҩ8 ^=e #0 Y*(m繚 (D5z\Vw-UE a{j8ㆇ=K= K"ÆhA?'E ^P>`cC-^Tp=vfD4:yAqS{Y`52✠H1dztpUᆎਇH==]KġT =@cse sENaњ-N];8IǬS/CbJ+8 E^-!І<钣AҨiWL nY 1(E!^Bf*熪ՍY8 saDv`ǜ7 .61Шvpo,.a35dӌ=/pE BLڣt ,*`#΁sEE:=mY(mG5`sEǰ:8B `9 8(b rN F CLELh9|8WH% o@_"4 V[ vHDlK`x$$Qv3옵bhJ 0GkX'+W ibh$ 1E`PLs4<f^&H~@zаGF͛ WD}sh%6*4\  , O/Qc|roq^4C2 @/"\#L(N&Ccvp}{r!p} 10K @p$c'NeqBtfh''ģf&XGCAO?A#sRdbP0 ńE0aftN3d!/_A3 np2c;c^Ghp4B)〼Lc1uͰjZr mqX f3h석p4 "EsDYMq(pUBIzjTQl!Q  ([BwAE@^5GbEZBAs g:3Bu 7'CobAK &a f(-tw3 T!C+گAeE9bf`Ԑ @2?B| 3uPeb @8pM(d2E4h#Dcxظ̂~aL. Ef_Jo hBs.}xُ|@䐈bd(S&ŶL@q3GpxbG€sL@Qw ! w@?/`18 #aPK@no`$J=U?Q wBșHK|(P\ pUYj8 +2FЌ9} S8 vpޙ(=n r^ J7`ߴa47GFBBF4aPY:@ʎ ȱI0G/|0_zd*@"^T#NE]z bZZzF@OHD9$@&-@m`1ǪCVEo/u S:ތb`l1{KgPN Z2Yzp;j+K@\!Ȁ]^8KlF|@BzNqKBZZ-Up?)!1AQaq 0P?OOOOCWNw_NӸvӴ>u;i};Nw_NӺv~Z`(Ha@qJX k4XNwNӼvӴ?ンu;ߧi;NwNӽvӴ~{2կ ;FLeE)Gl8,2e&e a΢gp;sYx}~n0oFE25(ܯ̏A첵Q'؜$3m|(newZ+;&0s7AdAê0_>UG3 qCb^iڍݬ@P%a[mLR-zPD4.,PbWiۏ[bAv=ءuyPa NTNi0Jܽx\I_R e\i:p4U%4_@]17*fbb_4n(og@NL"ƕ$hBl¼ö *x  %)"7)0n.(W̳,ZkAkXv;$]|dV8 (7C`KHb!=o ^myF w}Q1oQA~%DԽ* Q_T6]wM;:#0.U-;Џ,*:!vAܼ1/5;Q N1;d-^m7A w,;CB`X&$^\RQN*)|n +յ}I[5I] 7zPa5!%kK qz60o. dC++ GȟZ I g^6|Ybvx-tzwzwAkvr>b jҊ]r"` $uBb5r(.+-j.*z4#䂧 . hR*M"&XvNֵ?4K{/ZR A];ϧv分!Dڷvliqz"Qt\(C _'?0&q^'î h_$HtrH'vѓäU|aaO*jg7Yv>p\ža{F)@`@41*z 8KmCV۰VĠ]}Rd: ϏH߭k uȿJC= YXmu<Xqn1T毤Fl3HWUǐR^Y[p\),%7.aǾ UliXa;K=xzX,v}X=/33M>Pns*J 0J"pO]ݝ=hGvvNqA9Ol-dHzCgb{CBHfnӈ`bK givK.v jTB:T1R\-CcQNcLb-{!$0)\e*R9*9\8.! &D@sP . (X]@ͫ BѩXe.2ZtR0Hg+p#37I^'#Dqs*qc MJԻ2F6[0Eh71K&/SħxӤ?[{J>fUWEzH7M 7cw .Λ2A]PfƋTph6h,*$fp }L5ͦNɺ;of0űj,`oAoE * ֲ_Xdc1>pլԤMڜ4nѲnv4[tA5ݣLۖC %F;ü&}Opߣk8MߦZ j7ЯΌf.appLwxs}!$i9MlSZ 7#+DXJ v Xm >h+}0 no#vE> 5 ރf^Cx1C8ų4wy 5C(-.JŊl/cns`x }S7UYAJ5f[KϢto҅=cT1}FONZAsAlCUށMvk[ΎmR{WPüwF#PZ\QE(贮N@{;"%AH{1n$UߤR.[``ރpkKstg¢EM#D#n4+&șBTs8Bt(Yo#7h?C# o)QUIK(*Zأ38-hVn˟ADEOruR˒԰C FvaP]zlup@PXc% 0Q49xZe92h@q!.uG2:Rn+#Z LUqLdAt=Uya2=c ݔb^j{|j/0EXM[2 oƃ|>Uh#z:ll0FfTc䴸%+Z zpSpӦ&a@3C-hI^n")]E*#|~e2Ki⍎%*WZ h7ߣm)0- BՉpA ` 3:O* T"gĴ-LR͇Ԭz iBgJI!2?R}i@?Y@]~ DZSn z`(\ؙ]1иP^~"{, VxC1asޣ^@ߦA@ߡC3]h']#&32hNTU/QuTOx4"sb,eHA;|Xp3 z!"P쉠փsrmxXwJ%ʦCtvWþxQwLS Ƨj:f1HFv:,$|n6-,;h0ewUUG_:j!:J ǥ2Gm(}!9xGeG7cJ R)Q`qHi GIר윌,UrU@4=6o9l^T+N8ͽ/%xH DaN`xPlvbaiY-(-=#ìBiĔߊ ! 0cu>WFNRQodSv6ǡr1߫}t[X-.J.ПZF4L F R ެ0pucӶ2q@ߣc:ܺ%1^qE9՟/F J5@} )u!P״$p7gkPDu[;C:;Gs:{̲ܝ߲W0M+2lvДcݾ)AlH' n (˯o^,э prOsE8NŃǣZP5fKP)h>ɇ[W&sk`ސ39A+X;rʵtz# =p(pdfdlɿIbPJ5Hhidf9!x" K(Vy ̭!>%}^,œ> :P N. i;ADƅ;tw8S-o5|ۦuL,u ]WR]cdke%1 Q*LBI7eGuTfЂ؃)e,dk@4ʪ@Ă'4W<xQVcO NjtUGu9H::f|*]-P/<" Aټkf}EgcĪh !Ȇ.1l;#DBZ]; -ew-lD8`j `gFH[r(- q*${Fi%3{'I8 `O gedV!@SB <o^@-@0 -ۡspJ7 ibh` *WG]Vgi)T0f4hnB*%07 Jy"x!N!6]2ú0:/6E`XUªULtQCuCYһ\a4$cs@gX@U&ټ2C;"hHԡ m2wvh@ϙ+ހ2whϙRT [pX؈/@if<o&IvQ_ Gh-p@^DHuА+7ת1Cm|!-:Ow{88$#+ys.J@b.jNOit\XV$a4#䋖6[!kaGF>&<1A#Bݡ9BTKP6ሲKѼhH $KnŠlZ3̍mU,"D ]E*^P2ctFY-̷YyvgfDމq# ;ĸ Z01C{5'AUeb IZ2RL()~%; [8G~0e9kpKVZ0Z"q7E6{zx UJzl ." D + I%9ZZz*9‚+΀i"8GCn?т3NTX#wKʁDY9cѣjڬ3. U#k@`A6Vz$a@&b<L?&v9EC7%vUb{aRr|NII]z! &W]ОPDhױJr?b'DV1cAT4C A3` /3E1.odi̊ Ip-xvN"^@WDv򚇝/Ba՛̨*h#*zzA%m rbTX 3Qڄ%b4c<.g3{8^ea-) s:)؍ ]APdbHqcscјDJG7C[6}0,a័hbz?QV֧-A sK-ncCFR+M!mpn9akپ(Qn6a z&)߸|BnhmfՊȤ6NӴ(i7I>N`T>` >%ĨNP“wúc܀'|.f1rp̴7g؇c#w$ZA19øv"YhA%8>ZNh(8Yw2a~M2Tw|&;CT̂|Z h%y^ CiLw#ͬĈ (w|Qn5ޢ@T75g[+L!tihHN+cا6(@̥vO:}D[8_t?0=e:.+v)U]N<<.VFVy+_-m a;3+fl LJ)~*B61od!Sɒm#8z1ܘAyw{AG6kCAa(5c=x(k7*[)܃IUo;AD0Vk?: @2{03kuP@n414BVBߘBn8rx8:n(FZ=qo #Kw;0.u+1*i6}֜s1_8HW ܞpg<:دJnnxLt:v= D/D4T0} s)cM?CZoRKGm^J|hVD}mh! _y}dfnfq`ݫD]Ibr〷ȝ]cDxh^K}͚D5rQ.9Us<ʽxD\[DWh֏[': ފ,Ė PU|K.NS]%xbN5$_uX#.D}F{˾i Zty͖ŶczcѦ(ûoB06 +fBz/mѷhOHp |4堝ĺܾ7+1+h u (MHK Q.0QiCB +%_y<މqz4C9F}Hc?͚-q׍ӣzzeh  @OY-e2"FC|DGĻqK~+~꿉}7?O?S+{7?_.oOS>Nd^4G$輴[m)SIN%:Jt)SIN%:Jt)SIN%:Jt)SIN%:Jt)S&<2Genymobile-scrcpy-facefde/build.gradle000066400000000000000000000010761505702741400203140ustar00rootroot00000000000000// Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { google() mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:8.7.1' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } allprojects { repositories { google() mavenCentral() } tasks.withType(JavaCompile) { options.compilerArgs << "-Xlint:deprecation" } } Genymobile-scrcpy-facefde/bump_version000077500000000000000000000016471505702741400204770ustar00rootroot00000000000000#!/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 Genymobile-scrcpy-facefde/config/000077500000000000000000000000001505702741400172765ustar00rootroot00000000000000Genymobile-scrcpy-facefde/config/android-checkstyle.gradle000066400000000000000000000012051505702741400242300ustar00rootroot00000000000000apply plugin: 'checkstyle' check.dependsOn 'checkstyle' checkstyle { toolVersion = '10.12.5' } task checkstyle(type: Checkstyle) { description = "Check Java style with Checkstyle" configFile = rootProject.file("config/checkstyle/checkstyle.xml") source = javaSources() classpath = files() ignoreFailures = true } def javaSources() { def files = [] android.sourceSets.each { sourceSet -> sourceSet.java.each { javaSource -> javaSource.getSrcDirs().each { if (it.exists()) { files.add(it) } } } } return files } Genymobile-scrcpy-facefde/config/checkstyle/000077500000000000000000000000001505702741400214345ustar00rootroot00000000000000Genymobile-scrcpy-facefde/config/checkstyle/checkstyle.xml000066400000000000000000000134701505702741400243210ustar00rootroot00000000000000 Genymobile-scrcpy-facefde/cross_win32.txt000066400000000000000000000006371505702741400207530ustar00rootroot00000000000000# apt install mingw-w64 mingw-w64-tools [binaries] name = 'mingw' c = 'i686-w64-mingw32-gcc' cpp = 'i686-w64-mingw32-g++' ar = 'i686-w64-mingw32-ar' strip = 'i686-w64-mingw32-strip' pkg-config = 'i686-w64-mingw32-pkg-config' # backward compatibility pkgconfig = 'i686-w64-mingw32-pkg-config' windres = 'i686-w64-mingw32-windres' [host_machine] system = 'windows' cpu_family = 'x86' cpu = 'i686' endian = 'little' Genymobile-scrcpy-facefde/cross_win64.txt000066400000000000000000000006571505702741400207620ustar00rootroot00000000000000# apt install mingw-w64 mingw-w64-tools [binaries] name = 'mingw' c = 'x86_64-w64-mingw32-gcc' cpp = 'x86_64-w64-mingw32-g++' ar = 'x86_64-w64-mingw32-ar' strip = 'x86_64-w64-mingw32-strip' pkg-config = 'x86_64-w64-mingw32-pkg-config' # backward compatibility pkgconfig = 'x86_64-w64-mingw32-pkg-config' windres = 'x86_64-w64-mingw32-windres' [host_machine] system = 'windows' cpu_family = 'x86' cpu = 'x86_64' endian = 'little' Genymobile-scrcpy-facefde/doc/000077500000000000000000000000001505702741400165765ustar00rootroot00000000000000Genymobile-scrcpy-facefde/doc/audio.md000066400000000000000000000153721505702741400202310ustar00rootroot00000000000000# 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 Genymobile-scrcpy-facefde/doc/build.md000066400000000000000000000153131505702741400202220ustar00rootroot00000000000000# Build scrcpy 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). ## 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. ## Requirements You need [adb]. It is available in the [Android SDK platform tools][platform-tools], or packaged in your distribution (`adb`). On Windows, download the [platform-tools][platform-tools-windows] and extract the following files to a directory accessible from your `PATH`: - `adb.exe` - `AdbWinApi.dll` - `AdbWinUsbApi.dll` 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 [platform-tools]: https://developer.android.com/studio/releases/platform-tools.html [platform-tools-windows]: https://dl.google.com/android/repository/platform-tools-latest-windows.zip [ffmpeg]: https://en.wikipedia.org/wiki/FFmpeg [LibSDL2]: https://en.wikipedia.org/wiki/Simple_DirectMedia_Layer ## System-specific steps ### Linux Install the required packages from your package manager. #### Debian/Ubuntu ```bash # runtime dependencies sudo apt install ffmpeg libsdl2-2.0-0 adb libusb-1.0-0 # client build dependencies sudo apt install gcc git pkg-config meson ninja-build libsdl2-dev \ libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \ libswresample-dev libusb-1.0-0-dev # server build dependencies sudo apt install openjdk-17-jdk ``` On old versions (like Ubuntu 16.04), `meson` is too old. In that case, install it from `pip3`: ```bash sudo apt install python3-pip pip3 install meson ``` #### Fedora ```bash # enable RPM fusion free sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm # client build dependencies sudo dnf install SDL2-devel ffms2-devel libusb1-devel libavdevice-free-devel meson gcc make # server build dependencies sudo dnf install java-devel ``` ### Windows #### Cross-compile from Linux This is the preferred method (and the way the release is built). From _Debian_, install _mingw_: ```bash sudo apt install mingw-w64 mingw-w64-tools libz-mingw-w64-dev ``` You also need the JDK to build the server: ```bash sudo apt install openjdk-17-jdk ``` Then generate the releases: ```bash ./release.sh ``` It will generate win32 and win64 releases into `dist/`. #### In MSYS2 From Windows, you need [MSYS2] to build the project. From an MSYS2 terminal, install the required packages: [MSYS2]: http://www.msys2.org/ ```bash # runtime dependencies pacman -S mingw-w64-x86_64-SDL2 \ mingw-w64-x86_64-ffmpeg \ mingw-w64-x86_64-libusb # client build dependencies pacman -S mingw-w64-x86_64-make \ mingw-w64-x86_64-gcc \ mingw-w64-x86_64-pkg-config \ mingw-w64-x86_64-meson ``` For a 32 bits version, replace `x86_64` by `i686`: ```bash # runtime dependencies pacman -S mingw-w64-i686-SDL2 \ mingw-w64-i686-ffmpeg \ mingw-w64-i686-libusb # client build dependencies pacman -S mingw-w64-i686-make \ mingw-w64-i686-gcc \ mingw-w64-i686-pkg-config \ mingw-w64-i686-meson ``` Java (>= 7) is not available in MSYS2, so if you plan to build the server, install it manually and make it available from the `PATH`: ```bash export PATH="$JAVA_HOME/bin:$PATH" ``` ### Mac OS Install the packages with [Homebrew]: [Homebrew]: https://brew.sh/ ```bash # runtime dependencies brew install sdl2 ffmpeg libusb # client build dependencies brew install pkg-config meson ``` Additionally, if you want to build the server, install Java 17 from Caskroom, and make it available from the `PATH`: ```bash brew tap homebrew/cask-versions brew install adoptopenjdk/openjdk/adoptopenjdk17 export JAVA_HOME="$(/usr/libexec/java_home --version 1.17)" export PATH="$JAVA_HOME/bin:$PATH" ``` ### Docker See [pierlon/scrcpy-docker](https://github.com/pierlon/scrcpy-docker). ## Common steps **As a non-root user**, clone the project: ```bash git clone https://github.com/Genymobile/scrcpy cd scrcpy ``` ### Build You may want to build only the client: the server binary, which will be pushed to the Android device, does not depend on your system and architecture. In that case, use the [prebuilt server] (so you will not need Java or the Android SDK). [prebuilt server]: #option-2-use-prebuilt-server #### Option 1: Build everything from sources Install the [Android SDK] (_Android Studio_), and set `ANDROID_SDK_ROOT` to its directory. For example: [Android SDK]: https://developer.android.com/studio/index.html ```bash # Linux export ANDROID_SDK_ROOT=~/Android/Sdk # Mac export ANDROID_SDK_ROOT=~/Library/Android/sdk # Windows set ANDROID_SDK_ROOT=%LOCALAPPDATA%\Android\sdk ``` Then, build: ```bash meson setup x --buildtype=release --strip -Db_lto=true ninja -Cx # DO NOT RUN AS ROOT ``` _Note: `ninja` [must][ninja-user] be run as a non-root user (only `ninja install` must be run as root)._ [ninja-user]: https://github.com/Genymobile/scrcpy/commit/4c49b27e9f6be02b8e63b508b60535426bd0291a #### Option 2: Use prebuilt server - [`scrcpy-server-v3.3.2`][direct-scrcpy-server] SHA-256: `2ee5ca0863ef440f5b7c75856bb475c5283d0a8359cb370b1c161314fd29dfd9` [direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.2/scrcpy-server-v3.3.2 Download the prebuilt server somewhere, and specify its path during the Meson configuration: ```bash meson setup x --buildtype=release --strip -Db_lto=true \ -Dprebuilt_server=/path/to/scrcpy-server ninja -Cx # DO NOT RUN AS ROOT ``` The server only works with a matching client version (this server works with the `master` branch). ### Run without installing: ```bash ./run x [options] ``` ### Install After a successful build, you can install _scrcpy_ on the system: ```bash sudo ninja -Cx install # without sudo on Windows ``` This installs several files: - `/usr/local/bin/scrcpy` (main app) - `/usr/local/share/scrcpy/scrcpy-server` (server to push to the device) - `/usr/local/share/man/man1/scrcpy.1` (manpage) - `/usr/local/share/icons/hicolor/256x256/apps/icon.png` (app icon) - `/usr/local/share/zsh/site-functions/_scrcpy` (zsh completion) - `/usr/local/share/bash-completion/completions/scrcpy` (bash completion) You can then run `scrcpy`. ### Uninstall ```bash sudo ninja -Cx uninstall # without sudo on Windows ``` Genymobile-scrcpy-facefde/doc/camera.md000066400000000000000000000116711505702741400203560ustar00rootroot00000000000000# 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. Genymobile-scrcpy-facefde/doc/connection.md000066400000000000000000000065521505702741400212670ustar00rootroot00000000000000# 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 Genymobile-scrcpy-facefde/doc/control.md000066400000000000000000000104171505702741400206030ustar00rootroot00000000000000# 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/ ``` Genymobile-scrcpy-facefde/doc/develop.md000066400000000000000000000510261505702741400205620ustar00rootroot00000000000000# 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_. Genymobile-scrcpy-facefde/doc/device.md000066400000000000000000000102771505702741400203660ustar00rootroot00000000000000# 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 ``` Genymobile-scrcpy-facefde/doc/gamepad.md000066400000000000000000000031041505702741400205140ustar00rootroot00000000000000# 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_). Genymobile-scrcpy-facefde/doc/keyboard.md000066400000000000000000000102631505702741400207220ustar00rootroot00000000000000# 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_). Genymobile-scrcpy-facefde/doc/linux.md000066400000000000000000000047721505702741400202710ustar00rootroot00000000000000# On Linux ## Install ### From the official release Download a static build of the [latest release]: - [`scrcpy-linux-x86_64-v3.3.2.tar.gz`][direct-linux-x86_64] (x86_64) SHA-256: `92bed0fa274b9165eb8740e07cf2e2692ebe09ad6911175b0ee42e08799dc51c` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest [direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.2/scrcpy-linux-x86_64-v3.3.2.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) Genymobile-scrcpy-facefde/doc/macos.md000066400000000000000000000032401505702741400202210ustar00rootroot00000000000000# On macOS ## Install ### From the official release Download a static build of the [latest release]: - [`scrcpy-macos-aarch64-v3.3.2.tar.gz`][direct-macos-aarch64] (aarch64) SHA-256: `a213eeff8ac95893e69c4bc6a001a402c6680dbfcb74cb353c0124184ed88e8d` - [`scrcpy-macos-x86_64-v3.3.2.tar.gz`][direct-macos-x86_64] (x86_64) SHA-256: `2a1b27fbb67821a886c7e8dea641899836c0abbe7afd37905584b99bcd21bc04` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest [direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.2/scrcpy-macos-aarch64-v3.3.2.tar.gz [direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.2/scrcpy-macos-x86_64-v3.3.2.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) Genymobile-scrcpy-facefde/doc/mouse.md000066400000000000000000000105031505702741400202470ustar00rootroot00000000000000# 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 ``` Genymobile-scrcpy-facefde/doc/otg.md000066400000000000000000000040151505702741400177110ustar00rootroot00000000000000# 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. Genymobile-scrcpy-facefde/doc/recording.md000066400000000000000000000040451505702741400210770ustar00rootroot00000000000000# 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 ``` Genymobile-scrcpy-facefde/doc/shortcuts.md000066400000000000000000000111071505702741400211560ustar00rootroot00000000000000# 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. Genymobile-scrcpy-facefde/doc/tunnels.md000066400000000000000000000050071505702741400206120ustar00rootroot00000000000000# 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 ``` Genymobile-scrcpy-facefde/doc/v4l2.md000066400000000000000000000032431505702741400177110ustar00rootroot00000000000000# 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 ``` Genymobile-scrcpy-facefde/doc/video.md000066400000000000000000000173331505702741400202350ustar00rootroot00000000000000# 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. Genymobile-scrcpy-facefde/doc/virtual_display.md000066400000000000000000000036331505702741400223400ustar00rootroot00000000000000# 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 ``` Genymobile-scrcpy-facefde/doc/window.md000066400000000000000000000020341505702741400204260ustar00rootroot00000000000000# 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 ``` Genymobile-scrcpy-facefde/doc/windows.md000066400000000000000000000054561505702741400206240ustar00rootroot00000000000000# On Windows ## Install ### From the official release Download the [latest release]: - [`scrcpy-win64-v3.3.2.zip`][direct-win64] (64-bit) SHA-256: `8f7b19371657b872e271e6b02a0c758c61c6e31e032e9df55a83aa3aab960bfa` - [`scrcpy-win32-v3.3.2.zip`][direct-win32] (32-bit) SHA-256: `cff2bbebdcfe14a023b77cd601fc4420b5631b19bd4b09ce4dcd4e5bf8e63244` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest [direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.2/scrcpy-win64-v3.3.2.zip [direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.2/scrcpy-win32-v3.3.2.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. Genymobile-scrcpy-facefde/gradle.properties000066400000000000000000000013331505702741400214050ustar00rootroot00000000000000# Project-wide Gradle settings. # IDE (e.g. Android Studio) users: # Gradle settings configured through the IDE *will override* # any settings specified in this file. # For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. org.gradle.jvmargs=-Xmx1536m # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true Genymobile-scrcpy-facefde/gradle/000077500000000000000000000000001505702741400172675ustar00rootroot00000000000000Genymobile-scrcpy-facefde/gradle/wrapper/000077500000000000000000000000001505702741400207475ustar00rootroot00000000000000Genymobile-scrcpy-facefde/gradle/wrapper/gradle-wrapper.jar000066400000000000000000001625061505702741400243730ustar00rootroot00000000000000PK A META-INF/PK Am>=@?META-INF/MANIFEST.MFMLK-. K-*ϳR03-IM+I, dZ)%*%rrPK Aorg/PK A org/gradle/PK Aorg/gradle/wrapper/PK A%Ӧ/org/gradle/wrapper/BootstrapMainStarter$1.classRn@=Ӹu1Ey@_Iik6U@.,RuL!G 6 (ı ]t1ssչ_x󸳈ᾏM|4isZ}.kތ=ed/cΥei0c =<Ob+7.PmB%M -wźl6i<Z293#ұwںl hx8FEv~ė9P%pw8.R3S ѵAc+jgQEoN SzBzj\: zQ^oPK Ai,$ -org/gradle/wrapper/BootstrapMainStarter.classVSWsٰ,1*BI/VV(-m.!nvfjﭽ< %2ӗv;IIbٳ]7>1IF2^ C0%cA0#aVF d\{2Ǽd\UqBNPp]R2"!c7%|؉^|$A aQʋ .aI~ ee41rtSwN3+ U U䳕"E(ak{wuT-.Z黶9rhu48C, -펖64:n=n/whviX iFZ$K]hO&VoFWc-MDqhƛ:UTT;#RNvRzH$['tk=e&śoh86lsܕP=֣ ed,X: ׶`Զl"zϼNE/wۼ-j=𒶨y1/ [\f UevS/7,xBY|͙ZI,¶Cw\o4 Hn"#RGw Nb(Utm{ۣ*z*ch #9_xWchyG}ը/CCASs/A݇~8# ER7q%B# ܤ@-8q;Dv{K0*GcM%8NMUK;I1D %ĞↄDvNަDgy|px#UcU]@}iP8T(j@ 1*B qg)&r⼁1 3D\>CGKH/|aA2fJe\㥋WE+p_,^qFE ,G'mN3OB {C^6mW\ :i{ELqߖ5e8+ :NK+Avu?QiZnYZ|]/M22'J2Cfhs)hn9ʎ2e(!Z ]o/唠\j-V},OL:^:7>r(sEUXuhJpSq pgfl,xUڒet=N-b6[؇ GO aa qĢE.aIe݆6>u鉪b(g em\.x!?NIo^ENS7jmj jhx*ܢl1U`dW|--r*ƯP瓹k)]0m+9[RZaˡu9C!mަuR3tmf91t5ˡ5$0Ƀ4Kzxj,ջPoZJ[F$ xF|EH_эC꺍+\v9D{Lj쪿!>f5[2 ƴ4?@L.d$+M:7tKXQvÏ8q& vxIn=d ]:G{ZSP'Ovciǔ΁}OEǻ!s+UzXb|\;oOڣ1}:iOM9nH5{IlI~j3伙7KXŔ̓Ϟ(Ɲ䰓 ܩFQ J, ]J΢Paĥa;l=L핃qnJv܉s/Ũ=F~y^#,ܤBMvXTq,*fQeɪvv`~$]g3V궎\\c9EӉ$ ,XXV_K8/hD* UT1qwrU$G5![?0mJa!1i&;@۾Da<}x30SmS ^(h[JM ϐ]/X[:-#{-\;->dI# {S#3Zܱlb`*^R%a9hQ : oit[rXx]Kzϒ#oHĒ2`ɠcZ ~)5m qJX̋ M۩P6eqCNX򠜴!9%![bqKU⒵d-USq5Lp8% =<_S|͠DhNI-YWBʒ3BxlwOgh 8z/Q,T cY't=p #Ҕ7lFYpZb1LmKEtAa]%GsjaOv1yLJ% )yĒMmX[MK\IܱL xmY0<S <z%K[IAHQϵ Gm{mvO9<$}}T.7kZ'y|m}*4u'm4'P*=5i Y@rݥڐ])/*-[jX mgs jrX}T[m k77BS;ESK.ȓ2`[[ݼ/w{5M<]9~C>E]tQ(8vUWjZW~?̝?¹c_+OϮo52XŊY 2XijzfdtAO`mঠ';)TolWQ BC۫*s fY| eqϠzCʪiY͓Uϼ5i$f_AAreU-/̽K ڂ0 [o;^Z]ʠxq3TkMbǽ\G {v%u";#;eF'9K3p?w$> 8atEK/#4;:i 훤".hjb6$?8x>=g3 $i~zq?gU7_BJl UH%.gP);dYQQY(:[ ߇*G"$vxZD:AO  pΡҝAtW|U0}c'\LjvM}`\s:^2q\6UۙہA9| gtf,&ڷσ_ҾyZ8E™rW99:xd=5دxPK AyL1org/gradle/wrapper/DownloadProgressListener.classu @E+jDE@ۖEERI#3c[>4v9OĪ.r%J[M DŽ,]8kߟ_PU:]3aG\^&at"-EeY˛8\3K tЎBNk؁PK A!9| 3org/gradle/wrapper/ExclusiveFileAccessManager.classWsWYګu|iVMNZ*;JlBNKƎ[t-vjSB[(P`x 3L$x0 }0+9%L=sݿOt]o3;6lKqoF3VB>%lW !8$` Dղ3"x D]AمEU WxcLQV$Cn.O#&A7zx N5@T3(zY4} ] ҳ4?Evqݏ<̏ LNp8>K.1Dg[A|4nkhLpSESv\c~]+蘭vvR5dw=#r<\KT8lo7`.ՐS WĮT U4]~T76v7BZ H ҫ@'c Klϓ31cDQDDwp>Pzp}@8B gmγ*& _$W +kr}FA?~(AZ8:ưq8 ӛ"؅Qc? z5)qjsDPh hF9҈u>c~iMȈvz5HM $?b)nIP$)SݳhѠǧB<q0.Ҷxd(WWw@M߉߭5S< a-sc-gONQM} (1#j R`뛿̀imP/QWM:;"+) Qgc@'Q)f! Ip @"pHwbo8=N%Ťډ;RN 8C?ӕ7oWy+V b3Bųl9ܭ9**$U<~PΫx^Kx{TJZ\/SrB+UtɯRj+akZu ^x 7)xxxX, *xw]rxE?.*>`XR0F|HeqE]LJ冏QPI| g<"9?+!I>//"؎ i?}GwvuDNh걭Xr`V3_ PޖH{[.˲g_W{o:Zbc,7  Z~JĒƮtaTҌ,gRK"54`q錥 VS3=v$%+03|dGn cY@"o-j#f6m VY&협2M-Fފ!JXg4kj5 =/2j&OHnX[\6sZǶC1!ґӟREtk_ڎśNk4"n HezE!,r_M~ֈmrI9 n)\™Jr^PPڬB2Kqf-Inڷkr{%mlmS663o,==Srr^7V'j[&F!T{̴5YrVHL6L2nG`#(DB7-P2&s Nsٖ |YjOS1X; ?5?V ̢CԩD ?B/+ 1kYb63R1Gs%qB{(,2\;Nxau%䋂g^W4Ox l؏!!vB`gSPyw\|w޼=yi7u4WqS.9,8V8s\[>$7{TiWGPY:NFQ7 e?_Fe_Yz2A8FIQrgA(uN#p$9&F9J)2:r@0G>̒RMŀmC|iL u1J/ٴb.%׎rB9,,G oE)2[;nV,ܣtԻ7f:5`jdؐ0[|g[qU:!lɪ6r g6[ aaO<0ùuPNYpiᬎ;Ug$~cO_-|{{ u)Gg>' ݋n9ȠS=.ƲPS9̵qw.prZ1b͊YS=Da(<,`*KPK A"org/gradle/wrapper/IDownload.classE 0  ^b* A{(l|JTf $_uX! ai'$|T:/ ?&NJ@XaXQГ7&zDVʙ&6X`-6L͉veLCz~@F@Y%H')9~<~WxL߰"Ni avyܲ'; WQ?/D\V+mĪM˩NjJjǢN4 55e-$| ¤iuMGOhSBWt%+i/ƌFKL\гva:Pљ^hw4*\kQ^\)6jkdQ|5\$5gʧ@yЊp6`&ƚewRr,oIښRe~멶\sngBs`2ҎKu1j4ZIWU|WP1PjYmlj 949U[:ycc)GܧvN45gjּyΚ !BRgyq)P1.(e%OWUNqMm;֚guhwX pg/0aŧM[˦mi9lHvX(65c ()X,J|{vg>BT᫪DTvjt7s1`t.Uz{̈́ze%ؒK)y9X튢u,5N V>]E#+K%TӗN >+*)P%ӃM,r{z77~;|n Q]]D]  Qfj-VI@wĚAO#=[`=eYDYBG7S2_ ӻV}\6T/z&pyg"D]թXB~i'>H6?e]G1< ˧LhYQbWfI+" & kX|!]ۗx+Z 3N`yte%XtLϚpp}';Ƞ.zG?lnyjOඖ@,d+/;m.PǶ-">G=dk-&ⱶϝ:ul ?g5 t%R}u2ei=[Ȳ"!&n▉Q\3a"e@XgX#-Ctۛ#a&QT Q z"H4H;4&('wJ:4OwYJ5N1. \ 0dy60uC.eQǨH~Zp&C%$,FٜB2LfL6=sT+еOТ't!uk*ߣ 34U xVȋI_=f%s &3lxѫwv+e/u6W.U1}d6PK Avp.- org/gradle/wrapper/Install.classY |T?'ax,0lH +aGb$%, K߼1 ]ZjV]肭$h*Z[jmk}~~M羙$y޻s?{-i/KS Lp rH< ϐ l>Sp ೤qq|_qIKti\\Rz[-xq ^:^&˥Y!t*exukA~VFDuBX"^A UXlIl[N٪q=;cgj]Ҵyޥn-|~O6_}#n6iVjljN&sqvs({fecp"|88)*NRG uUo<{y Ry~@4A H'I#X97씅k(IӮl1ӊEIhnP4O8-ljN2M퉔ūZz}ŠQ$?nE"1+!i6:c dQ:5!%T7K2R]"3?D +u_ F;#qlcSR j%Mo\5%Z.LR+u,>c[.NFq1 Y^F$!a9_2rS13=J,o)7*Ʈ@?'նDw_NbZXesy˿CIZgGM$YWzi[sq[Npe%zđ j11Q9.RT.ihnEPwH'6(aJBdvgt֝J(y(Kt+v6j{7'׳vW7 F\86h$;^]𵻶TnR-:ڢ$zuR,?ۉ ÷@%  ^CE1|]^< иA ϧGH?BʏVVqeGhaN'?ڹ)tdGoZ9<I"&2zԛAut=zD$ ϵ4iMeDW{C~*j?'4,*+>HEw:sd/RrnpVʹnOA"ip 2`n}nf3gsK3Vbm㸣ߍFL;JM4|f4yVx'ӷվBoޭ42맙+[Zy 䬃CIK@E?YpͩyV !iQ> 8JsOgyo!E4og'];'dwBIt6 ϥ(BP@bxIQZ@12y$fYTJQNVn @녉/W&I /ax~ Sv .ZN_n504 =_+ٯCtdnkt' X6~jT]C&^ё(8רM0•4xD2xTy/-`:@S JSi(;1Rs+*Gyy RAh ,S O~z_ != {\V$ڬCQ6\.{>L5e[)!&Z .Y\S 7xѱWԊվAZ y?#!If)g3ȵ*dҷ+;xSC=RI橷練>( s5=u|ፃ QW-iiZǚ&hRyli'9@zEH|/Vv ~M« Z-7.|((ɲnL;<漬s# gIL1~?f =bIcm~%C6=@ÃTR^MT⿟Z<z_<@9y|8rPZ?H[nMT-Ձ(lub%hȏbmMiv^SwStWh]pA?G`G?]O6hzހ$pME{=ݏ(MՀY-`0Xo\F?'Qcɓ8a_x 1fDyp݉ I)E@u\J[iP{S|s ho?< =PFA Bz. z n ciTmԐP8FKDB$;'z1]DtKbjzqZ~jo,/0#vsY9e:MT̊Bz&+UV0[G&Y)@VZDW֛9t!H 4WKX֫/fZBF`1^ŘS۔sJY?(?ooy֛`'KD%AAŲ&(v1'fHnӃt j16@Q-ҕ R4OɢPo.oA:l 'RL./k&/MA~O:A%X >$CWy-ױO{+2՟5x|2j?c-I|g}=5]ˡ " i8;NUņV5zP,y , Q|fb>b&pjrMfL+*Uwp5s6S^JXS>4'ZL8?g/ZCYUkjwj, [D*Kmtq*a'i1K,co W~ %B!4O-eGvg`ӝ"VT>uayq?^@˙\NX~U̹v? | qi3ou$-`fc<]99@S&;;eM*Z|<`?4dckqǐ{0#bvtUwxQ; 6m!W\5]1,)@F\s ˵w/'.Hqϰ3:NWfu-Ce"!lU#;m"x*@obw=Cj[$B# %qcI![3̔JgX&ovHIFi QV(Iؔ)r /,*ꊧP*qw(P&n_ݖ7ۛPK AJJ'8org/gradle/wrapper/PathAssembler$LocalDistribution.classQJ@=wj7EA(*(mۥnI٦`|(qv[Ī ̙3sf@Y0mf]̻X`pk\&poy @bW*IŐޕLsMh)50R8jT:㕐2~9+q/$W0fj%Zd8bHb[-A-XՃPw7B'<ڧ Sk? ~]$_S[GӸ %[!Ep=Xb(_ 1*BՃJCT;_qiXOLZ|- Fd ]PM~$ ov`?ghYIAk #eV4cxf"ТxoM@^R'X&@0+Nla)ק:#]PK Atx7&org/gradle/wrapper/PathAssembler.classV=cKy<^ 6vP@dIM2]o `FDD1$viKiBiBW˷M-;3l!{sw7p p?;0 MU\H*PAa/aJ;Kv%ۛ!5Am%-lce:i=(:BbW]S }B]6K& *? $%Mj\.WyOY xH#iXƓxJ*e<,2~WdQ[bJՔS~NAI&KbU"/z-~'20O>~PdO2ʄԊVL V@ilQ nkeuןCSS*DuÎSF5}ñ^Ɍ3mrMl.nn m2)ҝu4Se5RrRF5+tm83=[~xF `Stk/ֲ"ϦttRff;>k-VD/<<"JirPh)WeSpoDL޽&ӹqy|r?r 9&`$*B˴L`gn7_7/sWwrr;MP,."bQqՄUK{ |˲ղyӠ[Y7(=ހv 8 Uu~3IP~\Տ( ##x$SyLS>#ɺoM(EF\CbiLo%D5d XЙ6U+\C6:i%Wa @C=5 أ٣Jơ6B5U "Dc PF&\.zݤ\ :,wPz CR;-ӷ4Ef G I!Ҥ VOĉ`mWqó _g:ƆVK9#]Q۶͔nv-mνv^g-*6`Ja#Y" GAN l-|,g+[ *DŧL縥 |+||+Xyvy)'1MnBʶ6w{/(X,(~> Wٹ## I=(DWF$v t$0;/~*xBDV` -n|/#)yeǤ2FIi&^ʫ,=A5ٶTmMfRص/}6-ƷbU $egRevX|3%_ ๡~cg5hOW -a.5T cO_ {LP; e3y؆:J{3Pw@y42ESp~Gw"03V;LM`q=4/Z>u`iN]xG7Y κcA=#7? I4lv14$ ĨnqR!9KM-z9BԯrW)5" ,9z|~_=/!~L08WIH-_{M F1WU> =SpL<`ݖ_bqWPK A=?-org/gradle/wrapper/WrapperConfiguration.classmOAgJK|i " HULCߐ-H&~*M$&3w e&33_:a`@EG xL<2O<%xN8mPk35 QOM5)!Bw~+ 1sM(/;0/w^9sܰ nZd,8t|ZkZMlONlڍiv,.smns =NN8u ѽq$&L*A=HL)BCO*A4dF r_ s )%Ƞ 2)AeE, $!X yw y%^}93 2y~Oȸ`;)H^V0YW^а;98147243Ot KNj-;ޞ-#;[N3}Ht e]rbbAqy1ypȖmGCN$*= 8%g9ݲ =/az629uȐ Nl=qԤ1.aCi[Z:k?̝`iafRݘizoX4֔r!vSF`N3M-1z_arTQ5c!2xݮ\)&* ԉ^-ޫcd {Zn! ]$E锞s=' r|˝4g9yJ YE n ts@fJBU(E2DK2t!cj'-BFY&v\vwdLmz^B6i5~g#M ;{,5;UkUD u=*$$)-^QI?dIe ͞P#1F/Q0'i?e6!#(莄1eL%|%y,dKܢ"|j¡SRqm5U;^GQ˚h&}#Y܎I7w<}EA'LQQWMUόD2 }ʉx>1COG)7raWTN{Rӓ|88aAXTYoe݂zfuytR{:v:Z|`G_8檫x8󨊣~̕7:+x@-I :bqJ޻q(Kq6۷bj &};dm?JpE)@a ڦŕfhr%Q2&xglL->lDM6uFgºߏ C!̣Bh^GЮ hϳ_F~;=U7Ӿ˳_0kZcz8Ivzv0ixUñp,sϢfa\s oе(C BbjJ%%@,,V.ZX@3#8ꚑRM`:BBNcX@ xe,^F88isO[`IQfQ?b^ Ϣa ItV9gw"3X6;kcs9['_@cp qvC,bW/\ťl 9OC(.H']T !ٞ ތīS]hDLyW!p 5 kN=NRnp؜ch⟐ϔ4=c/-ePK A׃X ;org/gradle/cli/AbstractPropertiesCommandLineConverter.classV[WUN20 HԄKKIQJ;L0fOZZ_tA}N.i2Y9};vO6Q0e|`|`w,'? H` = V>Bx&c.cC i?]cЗ1-ci8"qB[׊kcrƔmeBhЋfb~MVd8i ޞf'$ F]p䙶0fv}-QԬB"9U ёѻE#0ff[-PvT4V5%\X3LXQgIWW]p0?:ĒVJFOMA`i ҮqA1\W@ \A܊A)5"ɓ48,FhJ+g ^zKŽa@7e2ZfYTRQxuWx8R E10ogm74Ӂr_q9\WOw/jhz"#z,Zi.ZFkP*؏$}iB+ c na]bᶂ1|( 2>R1>aʠSz5 xA$>UDP~.*8 %>Ä;g+ZEkߌM' g6-0 : NVϟ=!!{LX`e2zFI3U\%Ne5CC]Z]AၺmѸYCYbZ.+&%b `)s4 "(({j^<Q*>\{#%N׃\&9I~r)C09ymjV!:|k"%*5&@TZ[AkI )Zb=Nʆr2j s噜(B`4JY:TCaecU*b2vTzCɘgKEԷNR9r,m:BM}V{"O.6u1᳠~-g~hWKt/Ovs }+ =]hY,l$ ֊7 4O? gԻ h;_!ѸwiPCEӞEVh`ll ,[m'GT&p DAnE!i/8<~[$"V8zb3%+>g*h/V!r'em3=:Irn wN0!؊u2aIPK AD&3org/gradle/cli/CommandLineParser$AfterOptions.classmOA{-r- >W+p"D!$MFxag9.D%2^rҒcffwn׷fqۀQiu4h IStLaTzj6Pɓ5&yյXOlե}G8]x2Crɫ< +M[>.YN w\:JIi0dmxBBr卆Mu0r$fu~^F+[.5k՗-o0$z<AekͨS bVgcg>v@]7|- ƪ-+CG@ϔ0 jE7 7ckމW7xؒ1f00yd835?UѶ= q~ :5hhkmq_2wC^2 \ G@I]TGi4gL,~AbT 4"A;`ȑm:C٤3qhboPK AMu <org/gradle/cli/CommandLineParser$BeforeFirstSubCommand.classVrT串:I I|skiK^rը,Ӿ[2xHnbN0Ϲٳ=_~02Nc K2ȑ!aYFø ǪP&Cu!%6>1u(68 woz|a,oٕLVzF5VSawU9^6Lùps_u Wݢd֬ml4jeݾ$-Mn!M$2 -[au(7m2(MSתjڢ/xWCiX;PBNg, YtT)}3%l1T3,zoKf=H9 T_1$wbW5t?fk5s$]%4C Y Gp|@EYx.~H@]*[qrj;;kG柮G&İSEQɪz|q|ypj( mja8 ˔Ww[E^lq6v(=WN##rNYS.8~rWßK*b; Q"QiD@r0M[Ҭ$?!HvCIqf.Ɇ1JᮨoZ }4{JEFG?3x/J#'Gtr)Ipg{(EOJT).>Gd/-GWZo=sP#`K3Q[F]F]pՅڇbYh@~R (!ijJ3n8B# gHWܱa6:Nx*9g\.{b ׹ڸi^E` N$,SDgqc)/hRPK A*ZMForg/gradle/cli/CommandLineParser$CaseInsensitiveStringComparator.classS]OA=wPRˇ"UhAx1MLԏvn/`|EkC9{Ng?&6 Q)`,95=K&lfc*!|m2]Bt%a||ߓzGy)vV&+"M?~"u$ۑV~Q Nnx" %6m܏#nԍ֛uB_Є"[f)ְm*ȧHjfp\p ߩLbs E"Ld'h@Kӟ0}1/d MXEnqsYX'>9|r9\=wP5oy$L̑iftcye}ly:E1k-LYn3q2\uM&:c#2Oȭ㿈(&J`R؁p6k0,X\*wM/PK A|R&=org/gradle/cli/CommandLineParser$KnownOptionParserState.classXwU6ͤK*EqDt;VkEE+. ɐ3uf҂HQpCqP![>u\|I\@-V< gZY͓5ͨ-4Τjת 窬m͵l Znpi S5꿗$뻦 b^)Std%fsTCP`g^ t H@'؄Nr"#^hNoGQ7AUߑt$ YFRk|yaC@gpPn'I<:\(=Ěp,᝛aұV6dhd F^wމ& aࠑW~qpN߽̾2BI 7!?PK A$ľ<org/gradle/cli/CommandLineParser$MissingOptionArgState.classmOPw*LD ! ·'`0W̚o;oB~?ܶK˒s~퟿~Œr:'I*(uZG 7ٌqeМ-rD_K.]Sz3Bjs5]#ņu,nV4\k0#%rw[cU:3 WZ S69^RzxZsu-˲V;TJ?nI/GQw|m.UORn!eCw&à# ֻ @~+:0r`#:L:՞ͭ1𦯉)[PR2q\U%7ǖڒ#63Vt201,K`=org/gradle/cli/CommandLineParser$OptionAwareParserState.classUn@=q$6\%)MK@)p)E E*oĮO x$ J|Mݒ*A./ޝgf퟿PĽ8ȩ0*wTzc\ *L+(cVAQu4ܮ,dȖQ/5̂Ѱ OpC{i\e˶; ` CdթQIh5xƫ 7*\X.Cɶg9Zŵu6jI,s]SMj%} P녲',4!E]&8c$${]pG짍oRNK%ti!ӘЏSnbX[XPa sLf +~a[f:~Z)9hOS csmqr |FdUԩ0L-=ƽ_qkޥvrl0.ϥF|x!JӚ'vt!}rOYY,T6$a|iZ ̋dKTTȇC(Z7D!eSZ2B#oo+5UcCݢ{ B*^v#8GNi$y6+1E83$r]oPK A%̻7org/gradle/cli/CommandLineParser$OptionComparator.classTmOP~Q: 2@.,Ydďe4kI?/~#ƨ_ N6).]}s=~ E%d0#0+bNUJ"E,IHs;'"V8|USk"z6L7eA(;íi;- Vn)L!{䛎]vG Wlp˖y!P~`j2U6uyN_z`QWkH-T T|%|4y`P?UKjwMkwuz-֤$/0L[3xѪM7-O7 IRiu;?ڑ~Sb@ZaBhΟ\ia+6eYgљ{"!c /&q va[hǂZ00 }m j]{a=eўB$>P}4s2|=ѧQ*te躣K.@ >rd d$S3gH|$%!zp'a!xD"2{9 O蹀Sp HswnL8trI"!y3Ⱥ2GYv>i<2<l4!M{nV  #fdY"5>{}FCNS#ZOqϣHINQ2(R8˦Sqv[lB|'9乎OE;McBһ-!%."{N'MzzY%B65[m~kETBZƓq5<]/h}LѾ>McGMHx)PK A`M~U2org/gradle/cli/CommandLineParser$ParserState.classSoPN) s97ս&MLf.а.vF'_|(- AGmdQC-6xbaB{I/zI4\jϓdbs ?4iF^H(*tU?'tUav/Pcgw9`2r;$uTyP/#}@3I8c)O;B / Z &Wti2qfYC!zPڕh&ߗP2egvnӁ$$T@|*>A%W|):T Џ3_qI_q3!Źt^= xc @C!:Z_ eYq>8YMHEFw@@9\"!vCz1v񫮥x&҃k$}ˌf!uTsj]p99+1:*4*4fE!./1Y ?PK A=l)&org/gradle/cli/CommandLineParser.classYi`\őJ7zzeٲ=`l1S>K6XzFψ8B``d MͲaك= !l~ޛPwOwUuUuO{YT:jW5ױh/>)s3 O >ס yRa#||1ׄW%GģcxS5E]%_fFeu֤Pt|e&cuq8&&䞬JO|7#P"tM$(L8?=cq3֔&LMssOA0eAQC(j]Z-um 3f5if ≦mkG0JKͶh̚dcu;13cM0[1׬_ݸa5u֭X( ^N1U&VQQ[٥=IҨ]k G-\FګmWEc`kجn V"E9:Gw6v1|yqe41P<-l҉H(H[Rs,Jؽc[-`,~;DG|Kglloi5M% 9.bI+v"[FURo;R`FN3#mL= 4P%$lu * cLate+r"nuUCiظ̀J]B\ kX5Pj:)Bp(^Hi%n Zkn ݒ_>N &3A32g R4d Z8?lyOm'ˆf@hՌ%sƞUu63iʭ ŕ%p77DL^aE9͚V\EW p vX8)4iFf(Ma}.l?F,N@6 7w3§E5Fp J-fn1R^:t[BYԙd3R÷M΄A-Vvmϼ.(SYnZQ_;S[Zx|ڜ3KFR^9fpUg sPY1O/ }MX k#IRr ܊[ ܀ ܬFD- ;&-i{OP<1Mڹt\͐׍ku;5nHXv!]r1% 0Ґt *1T]Bحt-씸!ǐK2C.+%ª2C!W*or\MxղZmU3Wq\lE,rRz[UV3[r!7(G(Wr|ېjCn*%*Ų!wYX2u]4yӇY <'kP͝%w't3luY9LُC'dR{23$* 0d\/!{^C/hr7ܟŞkCPɈ: \lȃnяEb`*ƹc&{>Vi%=f ٽwbeic^Rĉ^Éww` OF3<,dn>g?_y2|% zX>"Nd1VfY72pLbh1s}6=JqŴ*9DuXUǻ?}e[ւeN(q%B=:FsWz~-)u+Z v{p,|{`8E< r_n۷Cc^{[܉բzv 46<[]kt6SNo;:ԃCqֻ[c`|ݽ(~zvV7\e"z>LP_kp6#+Lh1I`E4c/]AinعnV0=tziH_r3 %?~u'\vB A 7o}mh~ ǡWs _&%@,yő<ހ M3y⼃N wnW})m^YAhO YL+,l"G˱7<7hb>xi!_ph 9Q}( a`F˜Zoeۋ局v}r8i7N"eK{1\NIAU[k|6e)=LgitE:Rt=("4RA~?(N{,$)>ȡ)Y>gGK;Ȯ,J͡%TZX֋rE\ދ O܍€?i.UMj[W\V *Rv"uGYxm;>,.ӇK֑lwlfe/NoE'QbWb#`cm3< ؊_"K *#`x)hbtH):Ll&evH'҅.M^Q~;9˸T7C\)5?z9sr*hp?ۻ0=Q֜F {twtc1~DkUa!t0F!SE/wXS'Md_ø8缁羑gM'9K (=SX-,3,-N=uS9Pi Qc"%p;°s`qx<6V~,z*-wFG|:%f)XJXBV4[J^ ϑ^lT{ pg9܅*+/#ߒZJNxy*uìR6+|*,&VUV} 5^ƇbiҕbYЇ5y\$h:4"i4hpm7H8T ^Rx 0K%kAg鵓r7s~!o_ǜ9`9_K_$,+ J;~);܆%!Myxw#){9%qG2wR0 /r5{,5qBH\ d^pKPΓ7xY+b܋7X:yxY絲F园ɯS-18gO{zSe yNva`aZx 5mCd"QLtLq:ɛ ec%,3w]}sS}~e?2<_p媲>lJ0ݩjA^i uoQEpê8~?oSw/-Oijwg޶K/UGy^}B,ZJ,;;ɢxbf9rKa2J|ߋs=ijS8TPqhQnsʃYQU@O~lm.I=oA`2/-'a*o}CӑMswōiV*,K:&U@cHb{V5kyE=!#&P== kOц?r]?1>gAy{8u ca*^H*R!Vg-yh.#fN>_{EV43$ffN8Ѭ;ldO sN]wai ;{g>.a.=dzA#_;q eV)^?PK A>&org/gradle/cli/ParsedCommandLine.classWiwg~Fċ%vTMc[R8S)-LT(.Ph١li7 `rX;Ɵ}g$Kָ1G}.3ǽ?.v*`!*΢ .bufT,.+*WBM0#h'".T=^a}:|Fkt 0^7/㋸$__V0AVMa q[ ͼ1s#+3ɥ/E; f2n-C#2iҖ U0RҒKMfr)y(Q0r*T>c4yZq%sH˶YhM-C#\U08a~.k *)CxK )LAt\гkWYp#A͙ FA|Wݳw/뀽)xV5KԶx:ԕ|)#wOPccI(V^yU6KbѴ\j3TpUG(|VQH1M 7S8 RΊ|{Y#۶nQ/5^kKouO;=Fm,Rhq35$bwcLOFܙm Ln"9mG7Mhz*gJwrv詔DճEc YeyK?a{?՜qZ:fyȌYcm5 ^Xdkޏဂ6Y"|P[x[w= 1s=>4?RcfUDO38*$c/GW*S.5“~Un |ɘ_=0a 4 WU\0~pWLHZϖ:zypy*rK}jc븪֒[v.2}Ric9SVUPPIΝir+9gS QZyۇOBMkI'ƠG> =7rW=¶~̜gD1[f(O :6yP=𸟳J8Je2msQP0-^^Ц5[y?AC2?TjH܆mo"@1H1tXG1r}kL%װCРh&4o! ;]LHҰe(=Z[];7d?k'Wo A؅속=x5:ICab!Zcx(Dp1')1a8!N**&T=thT_P;='X/ FΙbX@u #Bwvp\}b9ûx?}dT}HH.o!syVOCI/a$qaxA1cx)R_J@{& (+}?Av3xG f97CTEB"]tF.'ޜ軅؜t<õ2i 1i_3%6 ;Aؓq6A-V4RgZlG orN$gdyE:+N:$̕ųxU '\5B%ǻ6 Z3]^ᜐk>lJ ;k>e'*6zEGe)A<],;[S;\Or=l8>Y ߀^9ي NLvczͭ&@S܇Қg\HVvWc .97{XŘ㰗Go )ȑ,I8sQHI 0$Abs9{`_i*^QUt  9%<' tEB^Krvs?`A^EAEXP QP dȹPK AytE,org/gradle/cli/ParsedCommandLineOption.classS]O@=ݯGePaeQ"hHV1YawRlIgN[`Y$ә{=?“4(H#b 冊$4-L%q[器+r⾊ [lpWAɶ(Ha1p#-g,C/{9*I Kxs UEJ%a:w^u"]a*s!"k~E쳟}V_*bTG;-%bXo"p(=@&_2w%^*$APPS =1D:!䉩If%ޱn05W(vbRD $"xj2Dϧxr7k> !V.p].GӱD\8"y RPK A\vB| :org/gradle/cli/ProjectPropertiesCommandLineConverter.classKO@D|?Pâu#Q+$C;1m  JW&.(1D,9vo/[@yl汕G)v }FHWkwLS!]nY7ZK:̿cJDZRysV;H+-)nkS#cruLXgh|BjFYDΏ%L%񎅎*_?ֈ:("<ڄbJՍ ؊tf^*K ߵ XUVi01k p8wZ8T0g?PaΛm=C Ss | 1\Zq-}C_JEˉjE+ w'PK A 8=|9org/gradle/cli/SystemPropertiesCommandLineConverter.classJ@ثmjE5BPąR/P~ӑ$&BJW 'iAY3͜l "lYlE <& d@HgL{:rRs:C*X4NĬQ ۴;hZ3a ѽG!]Gv7S"5eb o}ɸGtFMz9y~X{()spL`7e.KV, TXxɢfDTEGPWJmh~49AjxѰ sh gԙn85].FԒs9Q΢*s/@Ug J*ce+s+1 $p6/t-,;h-.Z >kZPK A?gradle-cli-classpath.properties+(JM.)**+MPK A%gradle-cli-parameter-names.propertiesPK A AMETA-INF/PK Am>=@?)META-INF/MANIFEST.MFPK AAorg/PK A Aorg/gradle/PK AAorg/gradle/wrapper/PK A%Ӧ/org/gradle/wrapper/BootstrapMainStarter$1.classPK Ai,$ -#org/gradle/wrapper/BootstrapMainStarter.classPK AhQ}#org/gradle/wrapper/Download$1.classPK Az`9Ap org/gradle/wrapper/Download$DefaultDownloadProgressListener.classPK AVUPL4org/gradle/wrapper/Download$ProxyAuthenticator.classPK A䣏x!rorg/gradle/wrapper/Download.classPK AyL1org/gradle/wrapper/DownloadProgressListener.classPK A!9| 3 org/gradle/wrapper/ExclusiveFileAccessManager.classPK A,y-'org/gradle/wrapper/GradleUserHomeLookup.classPK Ac67 *h*org/gradle/wrapper/GradleWrapperMain.classPK A"W4org/gradle/wrapper/IDownload.classPK A9lV"A5org/gradle/wrapper/Install$1.classPK A $|-=org/gradle/wrapper/Install$InstallCheck.classPK Avp.- @org/gradle/wrapper/Install.classPK A:o4Uorg/gradle/wrapper/Logger.classPK AJJ'8+Xorg/gradle/wrapper/PathAssembler$LocalDistribution.classPK Atx7&Zorg/gradle/wrapper/PathAssembler.classPK A| 0aorg/gradle/wrapper/SystemPropertiesHandler.classPK A=?-forg/gradle/wrapper/WrapperConfiguration.classPK AG (iorg/gradle/wrapper/WrapperExecutor.classPK Ae #rgradle-wrapper-classpath.propertiesPK A)Osgradle-wrapper-parameter-names.propertiesPK AAsorg/gradle/cli/PK A?<S1sorg/gradle/cli/AbstractCommandLineConverter.classPK A׃X ;Rvorg/gradle/cli/AbstractPropertiesCommandLineConverter.classPK A}yGK1{org/gradle/cli/CommandLineArgumentException.classPK Ag)|org/gradle/cli/CommandLineConverter.classPK ASf g&}org/gradle/cli/CommandLineOption.classPK A튯(?org/gradle/cli/CommandLineParser$1.classPK A$f{K ;*org/gradle/cli/CommandLineParser$AfterFirstSubCommand.classPK AD&3Έorg/gradle/cli/CommandLineParser$AfterOptions.classPK AMu <org/gradle/cli/CommandLineParser$BeforeFirstSubCommand.classPK A*ZMForg/gradle/cli/CommandLineParser$CaseInsensitiveStringComparator.classPK A|R&=Ȓorg/gradle/cli/CommandLineParser$KnownOptionParserState.classPK A$ľ<org/gradle/cli/CommandLineParser$MissingOptionArgState.classPK ATK>=org/gradle/cli/CommandLineParser$OptionAwareParserState.classPK A%̻7org/gradle/cli/CommandLineParser$OptionComparator.classPK AfC8org/gradle/cli/CommandLineParser$OptionParserState.classPK AE3org/gradle/cli/CommandLineParser$OptionString.classPK AgAqx=org/gradle/cli/CommandLineParser$OptionStringComparator.classPK A`M~U2org/gradle/cli/CommandLineParser$ParserState.classPK ApX k?Corg/gradle/cli/CommandLineParser$UnknownOptionParserState.classPK A=l)&}org/gradle/cli/CommandLineParser.classPK A>&org/gradle/cli/ParsedCommandLine.classPK AytE,org/gradle/cli/ParsedCommandLineOption.classPK A\vB| :org/gradle/cli/ProjectPropertiesCommandLineConverter.classPK A 8=|9org/gradle/cli/SystemPropertiesCommandLineConverter.classPK A?gradle-cli-classpath.propertiesPK A%gradle-cli-parameter-names.propertiesPK66FGenymobile-scrcpy-facefde/gradle/wrapper/gradle-wrapper.properties000066400000000000000000000005071505702741400260030ustar00rootroot00000000000000distributionBase=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 Genymobile-scrcpy-facefde/gradlew000077500000000000000000000132041505702741400174040ustar00rootroot00000000000000#!/usr/bin/env sh # # Copyright 2015 the original author or authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn () { echo "$*" } die () { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin or MSYS, switch paths to Windows format before running java if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=`expr $i + 1` done case $i in 0) set -- ;; 1) set -- "$args0" ;; 2) set -- "$args0" "$args1" ;; 3) set -- "$args0" "$args1" "$args2" ;; 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Escape application args save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } APP_ARGS=`save "$@"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" exec "$JAVACMD" "$@" Genymobile-scrcpy-facefde/gradlew.bat000066400000000000000000000057601505702741400201560ustar00rootroot00000000000000@rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega Genymobile-scrcpy-facefde/install_release.sh000077500000000000000000000012471505702741400215420ustar00rootroot00000000000000#!/usr/bin/env bash set -e BUILDDIR=build-auto PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.3.2/scrcpy-server-v3.3.2 PREBUILT_SERVER_SHA256=2ee5ca0863ef440f5b7c75856bb475c5283d0a8359cb370b1c161314fd29dfd9 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 Genymobile-scrcpy-facefde/meson.build000066400000000000000000000006041505702741400201730ustar00rootroot00000000000000project('scrcpy', 'c', version: '3.3.2', meson_version: '>= 0.49', default_options: [ 'c_std=c11', 'warning_level=2', 'b_ndebug=if-release', ]) add_project_arguments('-Wmissing-prototypes', language: 'c') if get_option('compile_app') subdir('app') endif if get_option('compile_server') subdir('server') endif Genymobile-scrcpy-facefde/meson_options.txt000066400000000000000000000014471505702741400214740ustar00rootroot00000000000000option('compile_app', type: 'boolean', value: true, description: 'Build the client') option('compile_server', type: 'boolean', value: true, description: 'Build the server') option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server') option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server from the same directory as the scrcpy executable') option('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') Genymobile-scrcpy-facefde/release/000077500000000000000000000000001505702741400174515ustar00rootroot00000000000000Genymobile-scrcpy-facefde/release/.gitignore000066400000000000000000000000161505702741400214360ustar00rootroot00000000000000/work /output Genymobile-scrcpy-facefde/release/build_common000066400000000000000000000002441505702741400220430ustar00rootroot00000000000000# This file must be sourced from the release scripts directory WORK_DIR="$PWD/work" OUTPUT_DIR="$PWD/output" VERSION="${VERSION:-$(git describe --tags --always)}" Genymobile-scrcpy-facefde/release/build_linux.sh000077500000000000000000000022171505702741400223300ustar00rootroot00000000000000#!/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/" Genymobile-scrcpy-facefde/release/build_macos.sh000077500000000000000000000022171505702741400222730ustar00rootroot00000000000000#!/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/" Genymobile-scrcpy-facefde/release/build_server.sh000077500000000000000000000005721505702741400225010ustar00rootroot00000000000000#!/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" Genymobile-scrcpy-facefde/release/build_windows.sh000077500000000000000000000027361505702741400226710ustar00rootroot00000000000000#!/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/" Genymobile-scrcpy-facefde/release/generate_checksums.sh000077500000000000000000000006341505702741400236520ustar00rootroot00000000000000#!/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" Genymobile-scrcpy-facefde/release/package_client.sh000077500000000000000000000022411505702741400227400ustar00rootroot00000000000000#!/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'" Genymobile-scrcpy-facefde/release/package_server.sh000077500000000000000000000004331505702741400227710ustar00rootroot00000000000000#!/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'" Genymobile-scrcpy-facefde/release/release.sh000077500000000000000000000006771505702741400214420ustar00rootroot00000000000000#!/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" Genymobile-scrcpy-facefde/release/test_client.sh000077500000000000000000000004311505702741400223230ustar00rootroot00000000000000#!/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 Genymobile-scrcpy-facefde/release/test_server.sh000077500000000000000000000002301505702741400223500ustar00rootroot00000000000000#!/bin/bash set -ex cd "$(dirname ${BASH_SOURCE[0]})" . build_common cd .. # root project dir GRADLE="${GRADLE:-./gradlew}" "$GRADLE" -p server check Genymobile-scrcpy-facefde/run000077500000000000000000000010621505702741400165620ustar00rootroot00000000000000#!/usr/bin/env bash # Run scrcpy generated in the specified BUILDDIR. # # This provides the same feature as "ninja run", except that it is possible to # pass arguments to scrcpy. # # Syntax: ./run BUILDDIR if [[ $# = 0 ]] then echo "Syntax: $0 BUILDDIR " >&2 exit 1 fi BUILDDIR="$1" shift if [[ ! -d "$BUILDDIR" ]] then echo "The build dir \"$BUILDDIR\" does not exist." >&2 exit 1 fi SCRCPY_ICON_PATH="app/data/icon.png" \ SCRCPY_SERVER_PATH="$BUILDDIR/server/scrcpy-server" \ "$BUILDDIR/app/scrcpy" "$@" Genymobile-scrcpy-facefde/server/000077500000000000000000000000001505702741400173375ustar00rootroot00000000000000Genymobile-scrcpy-facefde/server/.gitignore000066400000000000000000000001301505702741400213210ustar00rootroot00000000000000*.iml .gradle /local.properties /.idea/ .DS_Store /build /captures .externalNativeBuild Genymobile-scrcpy-facefde/server/build.gradle000066400000000000000000000013511505702741400216160ustar00rootroot00000000000000apply plugin: 'com.android.application' android { namespace 'com.genymobile.scrcpy' compileSdk 35 defaultConfig { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 35 versionCode 30302 versionName "3.3.2" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } buildFeatures { buildConfig true aidl true } } dependencies { testImplementation 'junit:junit:4.13.2' } apply from: "$project.rootDir/config/android-checkstyle.gradle" Genymobile-scrcpy-facefde/server/build_without_gradle.sh000077500000000000000000000057541505702741400241110ustar00rootroot00000000000000#!/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.2 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" Genymobile-scrcpy-facefde/server/meson.build000066400000000000000000000025331505702741400215040ustar00rootroot00000000000000# It may be useful to use a prebuilt server, so that no Android SDK is required # to build. If the 'prebuilt_server' option is set, just copy the file as is. prebuilt_server = get_option('prebuilt_server') if prebuilt_server == '' custom_target('scrcpy-server', # gradle is responsible for tracking source changes build_by_default: true, build_always_stale: true, output: 'scrcpy-server', command: [find_program('./scripts/build-wrapper.sh'), meson.current_source_dir(), '@OUTPUT@', get_option('buildtype')], console: true, install: true, install_dir: 'share/scrcpy') else if not prebuilt_server.startswith('/') # prebuilt server path is relative to the root scrcpy directory prebuilt_server = '../' + prebuilt_server endif custom_target('scrcpy-server-prebuilt', input: prebuilt_server, output: 'scrcpy-server', command: ['cp', '@INPUT@', '@OUTPUT@'], install: true, install_dir: 'share/scrcpy') endif 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 Genymobile-scrcpy-facefde/server/proguard-rules.pro000066400000000000000000000013571505702741400230420ustar00rootroot00000000000000# Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile Genymobile-scrcpy-facefde/server/scripts/000077500000000000000000000000001505702741400210265ustar00rootroot00000000000000Genymobile-scrcpy-facefde/server/scripts/build-wrapper.sh000077500000000000000000000014571505702741400241510ustar00rootroot00000000000000#!/usr/bin/env bash # Wrapper script to invoke gradle from meson set -e # Do not execute gradle when ninja is called as root (it would download the # whole gradle world in /root/.gradle). # This is typically useful for calling "sudo ninja install" after a "ninja # install" if [[ "$EUID" == 0 ]] then echo "(not invoking gradle, since we are root)" >&2 exit 0 fi PROJECT_ROOT="$1" OUTPUT="$2" BUILDTYPE="$3" # gradlew is in the parent of the server directory GRADLE=${GRADLE:-$PROJECT_ROOT/../gradlew} if [[ "$BUILDTYPE" == debug ]] then "$GRADLE" -p "$PROJECT_ROOT" assembleDebug cp "$PROJECT_ROOT/build/outputs/apk/debug/server-debug.apk" "$OUTPUT" else "$GRADLE" -p "$PROJECT_ROOT" assembleRelease cp "$PROJECT_ROOT/build/outputs/apk/release/server-release-unsigned.apk" "$OUTPUT" fi Genymobile-scrcpy-facefde/server/src/000077500000000000000000000000001505702741400201265ustar00rootroot00000000000000Genymobile-scrcpy-facefde/server/src/main/000077500000000000000000000000001505702741400210525ustar00rootroot00000000000000Genymobile-scrcpy-facefde/server/src/main/AndroidManifest.xml000066400000000000000000000001301505702741400246350ustar00rootroot00000000000000 Genymobile-scrcpy-facefde/server/src/main/aidl/000077500000000000000000000000001505702741400217635ustar00rootroot00000000000000Genymobile-scrcpy-facefde/server/src/main/aidl/android/000077500000000000000000000000001505702741400234035ustar00rootroot00000000000000Genymobile-scrcpy-facefde/server/src/main/aidl/android/content/000077500000000000000000000000001505702741400250555ustar00rootroot00000000000000Genymobile-scrcpy-facefde/server/src/main/aidl/android/content/IOnPrimaryClipChangedListener.aidl000066400000000000000000000013651505702741400335360ustar00rootroot00000000000000/** * 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(); } Genymobile-scrcpy-facefde/server/src/main/aidl/android/view/000077500000000000000000000000001505702741400243555ustar00rootroot00000000000000Genymobile-scrcpy-facefde/server/src/main/aidl/android/view/IDisplayWindowListener.aidl000066400000000000000000000047471505702741400316400ustar00rootroot00000000000000/* * 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); /** * Called when the eligibility of the desktop mode for a display have changed. */ void onDesktopModeEligibleChanged(int displayId); void onDisplayAddSystemDecorations(int displayId); void onDisplayRemoveSystemDecorations(int displayId); } Genymobile-scrcpy-facefde/server/src/main/java/000077500000000000000000000000001505702741400217735ustar00rootroot00000000000000Genymobile-scrcpy-facefde/server/src/main/java/android/000077500000000000000000000000001505702741400234135ustar00rootroot00000000000000Genymobile-scrcpy-facefde/server/src/main/java/android/content/000077500000000000000000000000001505702741400250655ustar00rootroot00000000000000Genymobile-scrcpy-facefde/server/src/main/java/android/content/IContentProvider.java000066400000000000000000000002501505702741400311630ustar00rootroot00000000000000package android.content; public interface IContentProvider { // android.content.IContentProvider is hidden, this is a fake one to expose the type to the project } Genymobile-scrcpy-facefde/server/src/main/java/com/000077500000000000000000000000001505702741400225515ustar00rootroot00000000000000Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/000077500000000000000000000000001505702741400247035ustar00rootroot00000000000000Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/000077500000000000000000000000001505702741400262065ustar00rootroot00000000000000Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/AndroidVersions.java000066400000000000000000000026131505702741400321640ustar00rootroot00000000000000package 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; } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/AsyncProcessor.java000066400000000000000000000007001505702741400320230ustar00rootroot00000000000000package 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; } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/CleanUp.java000066400000000000000000000235101505702741400304010ustar00rootroot00000000000000package 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); } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/FakeContext.java000066400000000000000000000073661505702741400313000ustar00rootroot00000000000000package com.genymobile.scrcpy; import com.genymobile.scrcpy.wrappers.ServiceManager; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.AttributionSource; 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 Context createPackageContext(String packageName, int flags) { 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; } // "semclipboard" is a Samsung-internal service // See if (Context.CLIPBOARD_SERVICE.equals(name) || "semclipboard".equals(name)) { try { Field field = service.getClass().getDeclaredField("mContext"); field.setAccessible(true); field.set(service, this); } catch (ReflectiveOperationException e) { throw new RuntimeException(e); } } return service; } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/Options.java000066400000000000000000000527561505702741400305230ustar00rootroot00000000000000package 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 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; } public int getMaxSize() { return maxSize; } public VideoCodec getVideoCodec() { return videoCodec; } public AudioCodec getAudioCodec() { return audioCodec; } 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 boolean isTunnelForward() { return 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 boolean getSendFrameMeta() { return sendFrameMeta; } public boolean getSendDummyByte() { return sendDummyByte; } public boolean getSendCodecMeta() { return sendCodecMeta; } @SuppressWarnings("MethodLength") public static Options parse(String... args) { if (args.length < 1) { throw new IllegalArgumentException("Missing client version"); } String clientVersion = args[0]; if (!clientVersion.equals(BuildConfig.VERSION_NAME)) { throw new IllegalArgumentException( "The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")"); } Options options = new Options(); for (int i = 1; i < args.length; ++i) { String arg = args[i]; int equalIndex = arg.indexOf('='); if (equalIndex == -1) { throw new IllegalArgumentException("Invalid key=value pair: \"" + arg + "\""); } String key = arg.substring(0, equalIndex); String value = arg.substring(equalIndex + 1); switch (key) { case "scid": int scid = Integer.parseInt(value, 0x10); if (scid < -1) { throw new IllegalArgumentException("scid may not be negative (except -1 for 'none'): " + scid); } options.scid = scid; break; case "log_level": options.logLevel = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH)); break; case "video": options.video = Boolean.parseBoolean(value); break; case "audio": options.audio = Boolean.parseBoolean(value); break; case "video_codec": VideoCodec videoCodec = VideoCodec.findByName(value); if (videoCodec == null) { throw new IllegalArgumentException("Video codec " + value + " not supported"); } options.videoCodec = videoCodec; break; case "audio_codec": AudioCodec audioCodec = AudioCodec.findByName(value); if (audioCodec == null) { throw new IllegalArgumentException("Audio codec " + value + " not supported"); } options.audioCodec = audioCodec; break; case "video_source": VideoSource videoSource = VideoSource.findByName(value); if (videoSource == null) { throw new IllegalArgumentException("Video source " + value + " not supported"); } options.videoSource = videoSource; break; case "audio_source": AudioSource audioSource = AudioSource.findByName(value); if (audioSource == null) { throw new IllegalArgumentException("Audio source " + value + " not supported"); } options.audioSource = audioSource; break; case "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); } } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/Server.java000066400000000000000000000241331505702741400303220ustar00rootroot00000000000000package 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 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 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(); boolean tunnelForward = options.isTunnelForward(); boolean control = options.getControl(); boolean video = options.getVideo(); boolean audio = options.getAudio(); boolean sendDummyByte = options.getSendDummyByte(); Workarounds.apply(); List asyncProcessors = new ArrayList<>(); DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, video, audio, control, sendDummyByte); try { if (options.getSendDeviceMeta()) { connection.sendDeviceMeta(Device.getDeviceName()); } 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); } } } public static void main(String... args) { int status = 0; try { internalMain(args); } catch (Throwable t) { Ln.e(t.getMessage(), t); status = 1; } finally { // By default, the Java process exits when all non-daemon threads are terminated. // The Android SDK might start some non-daemon threads internally, preventing the scrcpy server to exit. // So force the process to exit explicitly. System.exit(status); } } private static void internalMain(String... args) throws Exception { Thread.setDefaultUncaughtExceptionHandler((t, e) -> { Ln.e("Exception on thread " + t, e); }); 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 } } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/Workarounds.java000066400000000000000000000375751505702741400314100ustar00rootroot00000000000000package 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(); } } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/audio/000077500000000000000000000000001505702741400273075ustar00rootroot00000000000000Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java000066400000000000000000000011141505702741400325340ustar00rootroot00000000000000package 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); } AudioCaptureException.java000066400000000000000000000007211505702741400343370ustar00rootroot00000000000000Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/audiopackage 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 { } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/audio/AudioCodec.java000066400000000000000000000022761505702741400321600ustar00rootroot00000000000000package 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; } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/audio/AudioConfig.java000066400000000000000000000023621505702741400323440ustar00rootroot00000000000000package 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(); } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java000066400000000000000000000131731505702741400336770ustar00rootroot00000000000000package 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); } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java000066400000000000000000000334301505702741400325160ustar00rootroot00000000000000package 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 } } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java000066400000000000000000000152601505702741400342120ustar00rootroot00000000000000package 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); } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java000066400000000000000000000057361505702741400333660ustar00rootroot00000000000000package 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(); } } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/audio/AudioRecordReader.java000066400000000000000000000045321505702741400335010ustar00rootroot00000000000000package 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; } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java000066400000000000000000000031201505702741400323700ustar00rootroot00000000000000package 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; } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/control/000077500000000000000000000000001505702741400276665ustar00rootroot00000000000000Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/control/ControlChannel.java000066400000000000000000000012131505702741400334370ustar00rootroot00000000000000package 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); } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java000066400000000000000000000154121505702741400334610ustar00rootroot00000000000000package 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; } } ControlMessageReader.java000066400000000000000000000154321505702741400345270ustar00rootroot00000000000000Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/controlpackage 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); } } ControlProtocolException.java000066400000000000000000000003271505702741400354750ustar00rootroot00000000000000Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/controlpackage com.genymobile.scrcpy.control; import java.io.IOException; public class ControlProtocolException extends IOException { public ControlProtocolException(String message) { super(message); } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/control/Controller.java000066400000000000000000000750061505702741400326640ustar00rootroot00000000000000package 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(); } } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessage.java000066400000000000000000000024771505702741400332470ustar00rootroot00000000000000package 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; } } DeviceMessageSender.java000066400000000000000000000027271505702741400343270ustar00rootroot00000000000000Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/controlpackage 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(); } } } DeviceMessageWriter.java000066400000000000000000000032051505702741400343530ustar00rootroot00000000000000Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/controlpackage 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(); } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/control/KeyComposition.java000066400000000000000000000133411505702741400335070ustar00rootroot00000000000000package com.genymobile.scrcpy.control; import java.util.HashMap; import java.util.Map; /** * Decompose accented characters. *

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

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

* See diacritical dead key characters. */ public final class KeyComposition { private static final String KEY_DEAD_GRAVE = "\u0300"; private static final String KEY_DEAD_ACUTE = "\u0301"; private static final String KEY_DEAD_CIRCUMFLEX = "\u0302"; private static final String KEY_DEAD_TILDE = "\u0303"; private static final String KEY_DEAD_UMLAUT = "\u0308"; private static final Map COMPOSITION_MAP = createDecompositionMap(); private KeyComposition() { // not instantiable } public static String decompose(char c) { return COMPOSITION_MAP.get(c); } private static String grave(char c) { return KEY_DEAD_GRAVE + c; } private static String acute(char c) { return KEY_DEAD_ACUTE + c; } private static String circumflex(char c) { return KEY_DEAD_CIRCUMFLEX + c; } private static String tilde(char c) { return KEY_DEAD_TILDE + c; } private static String umlaut(char c) { return KEY_DEAD_UMLAUT + c; } private static Map createDecompositionMap() { Map map = new HashMap<>(); map.put('À', grave('A')); map.put('È', grave('E')); map.put('Ì', grave('I')); map.put('Ò', grave('O')); map.put('Ù', grave('U')); map.put('à', grave('a')); map.put('è', grave('e')); map.put('ì', grave('i')); map.put('ò', grave('o')); map.put('ù', grave('u')); map.put('Ǹ', grave('N')); map.put('ǹ', grave('n')); map.put('Ẁ', grave('W')); map.put('ẁ', grave('w')); map.put('Ỳ', grave('Y')); map.put('ỳ', grave('y')); map.put('Á', acute('A')); map.put('É', acute('E')); map.put('Í', acute('I')); map.put('Ó', acute('O')); map.put('Ú', acute('U')); map.put('Ý', acute('Y')); map.put('á', acute('a')); map.put('é', acute('e')); map.put('í', acute('i')); map.put('ó', acute('o')); map.put('ú', acute('u')); map.put('ý', acute('y')); map.put('Ć', acute('C')); map.put('ć', acute('c')); map.put('Ĺ', acute('L')); map.put('ĺ', acute('l')); map.put('Ń', acute('N')); map.put('ń', acute('n')); map.put('Ŕ', acute('R')); map.put('ŕ', acute('r')); map.put('Ś', acute('S')); map.put('ś', acute('s')); map.put('Ź', acute('Z')); map.put('ź', acute('z')); map.put('Ǵ', acute('G')); map.put('ǵ', acute('g')); map.put('Ḉ', acute('Ç')); map.put('ḉ', acute('ç')); map.put('Ḱ', acute('K')); map.put('ḱ', acute('k')); map.put('Ḿ', acute('M')); map.put('ḿ', acute('m')); map.put('Ṕ', acute('P')); map.put('ṕ', acute('p')); map.put('Ẃ', acute('W')); map.put('ẃ', acute('w')); map.put('Â', circumflex('A')); map.put('Ê', circumflex('E')); map.put('Î', circumflex('I')); map.put('Ô', circumflex('O')); map.put('Û', circumflex('U')); map.put('â', circumflex('a')); map.put('ê', circumflex('e')); map.put('î', circumflex('i')); map.put('ô', circumflex('o')); map.put('û', circumflex('u')); map.put('Ĉ', circumflex('C')); map.put('ĉ', circumflex('c')); map.put('Ĝ', circumflex('G')); map.put('ĝ', circumflex('g')); map.put('Ĥ', circumflex('H')); map.put('ĥ', circumflex('h')); map.put('Ĵ', circumflex('J')); map.put('ĵ', circumflex('j')); map.put('Ŝ', circumflex('S')); map.put('ŝ', circumflex('s')); map.put('Ŵ', circumflex('W')); map.put('ŵ', circumflex('w')); map.put('Ŷ', circumflex('Y')); map.put('ŷ', circumflex('y')); map.put('Ẑ', circumflex('Z')); map.put('ẑ', circumflex('z')); map.put('Ã', tilde('A')); map.put('Ñ', tilde('N')); map.put('Õ', tilde('O')); map.put('ã', tilde('a')); map.put('ñ', tilde('n')); map.put('õ', tilde('o')); map.put('Ĩ', tilde('I')); map.put('ĩ', tilde('i')); map.put('Ũ', tilde('U')); map.put('ũ', tilde('u')); map.put('Ẽ', tilde('E')); map.put('ẽ', tilde('e')); map.put('Ỹ', tilde('Y')); map.put('ỹ', tilde('y')); map.put('Ä', umlaut('A')); map.put('Ë', umlaut('E')); map.put('Ï', umlaut('I')); map.put('Ö', umlaut('O')); map.put('Ü', umlaut('U')); map.put('ä', umlaut('a')); map.put('ë', umlaut('e')); map.put('ï', umlaut('i')); map.put('ö', umlaut('o')); map.put('ü', umlaut('u')); map.put('ÿ', umlaut('y')); map.put('Ÿ', umlaut('Y')); map.put('Ḧ', umlaut('H')); map.put('ḧ', umlaut('h')); map.put('Ẅ', umlaut('W')); map.put('ẅ', umlaut('w')); map.put('Ẍ', umlaut('X')); map.put('ẍ', umlaut('x')); map.put('ẗ', umlaut('t')); return map; } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/control/Pointer.java000066400000000000000000000021001505702741400321420ustar00rootroot00000000000000package 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; } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/control/PointersState.java000066400000000000000000000055701505702741400333440ustar00rootroot00000000000000package 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); } } } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java000066400000000000000000000033071505702741400335050ustar00rootroot00000000000000package 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; } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java000066400000000000000000000224451505702741400327240ustar00rootroot00000000000000package 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); } } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/device/000077500000000000000000000000001505702741400274455ustar00rootroot00000000000000ConfigurationException.java000066400000000000000000000002631505702741400347200ustar00rootroot00000000000000Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/devicepackage com.genymobile.scrcpy.device; public class ConfigurationException extends Exception { public ConfigurationException(String message) { super(message); } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/device/DesktopConnection.java000066400000000000000000000141151505702741400337430ustar00rootroot00000000000000package 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; } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/device/Device.java000066400000000000000000000304131505702741400315100ustar00rootroot00000000000000package 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); } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/device/DeviceApp.java000066400000000000000000000010371505702741400321510ustar00rootroot00000000000000package 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; } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java000066400000000000000000000021641505702741400325340ustar00rootroot00000000000000package 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; } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java000066400000000000000000000010261505702741400323660ustar00rootroot00000000000000package 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; } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/device/Orientation.java000066400000000000000000000022331505702741400326030ustar00rootroot00000000000000package 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; } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/device/Point.java000066400000000000000000000014341505702741400314030ustar00rootroot00000000000000package com.genymobile.scrcpy.device; import java.util.Objects; public class Point { private final int x; private final int y; public Point(int x, int y) { this.x = x; this.y = y; } public int getX() { return x; } public int getY() { return y; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Point point = (Point) o; return x == point.x && y == point.y; } @Override public int hashCode() { return Objects.hash(x, y); } @Override public String toString() { return "Point{" + "x=" + x + ", y=" + y + '}'; } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/device/Position.java000066400000000000000000000032751505702741400321230ustar00rootroot00000000000000package 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 + '}'; } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/device/Size.java000066400000000000000000000051611505702741400312250ustar00rootroot00000000000000package 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; } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/device/Streamer.java000066400000000000000000000166551505702741400321070ustar00rootroot00000000000000package 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); } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/opengl/000077500000000000000000000000001505702741400274725ustar00rootroot00000000000000Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/opengl/AffineOpenGLFilter.java000066400000000000000000000110351505702741400337400ustar00rootroot00000000000000package 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(); } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/opengl/GLUtils.java000066400000000000000000000073741505702741400316730ustar00rootroot00000000000000package 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; } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLException.java000066400000000000000000000004501505702741400333370ustar00rootroot00000000000000package 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); } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLFilter.java000066400000000000000000000007241505702741400326320ustar00rootroot00000000000000package 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(); } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLRunner.java000066400000000000000000000212671505702741400326630ustar00rootroot00000000000000package 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(); } } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/util/000077500000000000000000000000001505702741400271635ustar00rootroot00000000000000Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/util/AffineMatrix.java000066400000000000000000000241571505702741400324140ustar00rootroot00000000000000package 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; } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/util/Binary.java000066400000000000000000000017771505702741400312660ustar00rootroot00000000000000package 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); } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/util/Codec.java000066400000000000000000000006371505702741400310510ustar00rootroot00000000000000package 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; } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/util/CodecOption.java000066400000000000000000000062671505702741400322470ustar00rootroot00000000000000package 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); } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/util/CodecUtils.java000066400000000000000000000023451505702741400320700ustar00rootroot00000000000000package 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()]); } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/util/Command.java000066400000000000000000000030011505702741400313760ustar00rootroot00000000000000package 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; } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/util/HandlerExecutor.java000066400000000000000000000011031505702741400331150ustar00rootroot00000000000000package 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"); } } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/util/IO.java000066400000000000000000000052631505702741400303430ustar00rootroot00000000000000package 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); } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/util/Ln.java000066400000000000000000000061751505702741400304100ustar00rootroot00000000000000package 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 } } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java000066400000000000000000000256511505702741400316010ustar00rootroot00000000000000package 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(); } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/util/Settings.java000066400000000000000000000026251505702741400316330ustar00rootroot00000000000000package com.genymobile.scrcpy.util; import com.genymobile.scrcpy.wrappers.ContentProvider; import com.genymobile.scrcpy.wrappers.ServiceManager; 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 */ } public static String getValue(String table, String key) throws SettingsException { try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { return provider.getValue(table, key); } } public static void putValue(String table, String key, String value) throws SettingsException { try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { provider.putValue(table, key, value); } } public static String getAndPutValue(String table, String key, String value) throws SettingsException { try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { String oldValue = provider.getValue(table, key); if (!value.equals(oldValue)) { provider.putValue(table, key, value); } return oldValue; } } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/util/SettingsException.java000066400000000000000000000007451505702741400335130ustar00rootroot00000000000000package 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); } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/util/StringUtils.java000066400000000000000000000012541505702741400323170ustar00rootroot00000000000000package com.genymobile.scrcpy.util; public final class StringUtils { private StringUtils() { // not instantiable } public static int getUtf8TruncationIndex(byte[] utf8, int maxLength) { int len = utf8.length; if (len <= maxLength) { return len; } len = maxLength; // see UTF-8 encoding while ((utf8[len] & 0x80) != 0 && (utf8[len] & 0xc0) != 0xc0) { // the next byte is not the start of a new UTF-8 codepoint // so if we would cut there, the character would be truncated len--; } return len; } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/video/000077500000000000000000000000001505702741400273145ustar00rootroot00000000000000Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/video/CameraAspectRatio.java000066400000000000000000000016431505702741400335120ustar00rootroot00000000000000package 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; } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java000066400000000000000000000377241505702741400327100ustar00rootroot00000000000000package 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) } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/video/CameraFacing.java000066400000000000000000000015411505702741400324600ustar00rootroot00000000000000package 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; } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/video/CaptureReset.java000066400000000000000000000016611505702741400325710ustar00rootroot00000000000000package 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(); } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/video/DisplaySizeMonitor.java000066400000000000000000000134471505702741400340000ustar00rootroot00000000000000package 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()"); } } } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java000066400000000000000000000255031505702741400335670ustar00rootroot00000000000000package 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(); } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java000066400000000000000000000204171505702741400327260ustar00rootroot00000000000000package 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(); } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java000066400000000000000000000045551505702741400331040ustar00rootroot00000000000000package 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(); } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java000066400000000000000000000320301505702741400330450ustar00rootroot00000000000000package 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(); } } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/video/VideoCodec.java000066400000000000000000000023361505702741400321670ustar00rootroot00000000000000package 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; } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java000066400000000000000000000074351505702741400324040ustar00rootroot00000000000000package 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; } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/video/VideoSource.java000066400000000000000000000007211505702741400324060ustar00rootroot00000000000000package 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; } } VirtualDisplayListener.java000066400000000000000000000003241505702741400345610ustar00rootroot00000000000000Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/videopackage com.genymobile.scrcpy.video; import com.genymobile.scrcpy.control.PositionMapper; public interface VirtualDisplayListener { void onNewVirtualDisplay(int displayId, PositionMapper positionMapper); } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/wrappers/000077500000000000000000000000001505702741400300515ustar00rootroot00000000000000Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java000066400000000000000000000151711505702741400340100ustar00rootroot00000000000000package 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); } } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java000066400000000000000000000026701505702741400341130ustar00rootroot00000000000000package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.FakeContext; import android.content.ClipData; import android.content.Context; public final class ClipboardManager { private final android.content.ClipboardManager manager; 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); } private ClipboardManager(android.content.ClipboardManager manager) { this.manager = manager; } public CharSequence getText() { ClipData clipData = manager.getPrimaryClip(); if (clipData == null || clipData.getItemCount() == 0) { return null; } return clipData.getItemAt(0).getText(); } public boolean 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); } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java000066400000000000000000000140761505702741400340510ustar00rootroot00000000000000package 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); } } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java000066400000000000000000000062251505702741400336670ustar00rootroot00000000000000package 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; } } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java000066400000000000000000000247621505702741400336270ustar00rootroot00000000000000package 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 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 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 { // 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) { 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) { 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); } 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); } 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); } } } DisplayWindowListener.java000066400000000000000000000026111505702741400351400ustar00rootroot00000000000000Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/wrapperspackage 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 } @Override public void onDesktopModeEligibleChanged(int displayId) { // empty default implementation } @Override public void onDisplayAddSystemDecorations(int displayId) { // empty default implementation } @Override public void onDisplayRemoveSystemDecorations(int displayId) { // empty default implementation } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java000066400000000000000000000135661505702741400333210ustar00rootroot00000000000000package 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.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 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) { this.manager = manager; } private static Method getInjectInputEventMethod() throws NoSuchMethodException { if (injectInputEventMethod == null) { injectInputEventMethod = android.hardware.input.InputManager.class.getMethod("injectInputEvent", InputEvent.class, int.class); } return injectInputEventMethod; } public boolean injectInputEvent(InputEvent inputEvent, int mode) { try { Method method = getInjectInputEventMethod(); return (boolean) method.invoke(manager, inputEvent, mode); } catch (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); } } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java000066400000000000000000000027531505702741400333120ustar00rootroot00000000000000package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.util.Ln; import android.os.Build; import android.os.IInterface; import java.lang.reflect.Method; public final class PowerManager { private final IInterface manager; private Method isScreenOnMethod; static PowerManager create() { IInterface manager = ServiceManager.getService("power", "android.os.IPowerManager"); return new PowerManager(manager); } private PowerManager(IInterface manager) { this.manager = manager; } private Method getIsScreenOnMethod() throws NoSuchMethodException { if (isScreenOnMethod == null) { 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; } } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java000066400000000000000000000070001505702741400336040ustar00rootroot00000000000000package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.FakeContext; import android.annotation.SuppressLint; import android.content.Context; import android.hardware.camera2.CameraManager; import android.os.IBinder; import android.os.IInterface; import java.lang.reflect.Constructor; import java.lang.reflect.Method; @SuppressLint("PrivateApi,DiscouragedPrivateApi") public final class ServiceManager { private static final Method GET_SERVICE_METHOD; static { try { GET_SERVICE_METHOD = Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String.class); } catch (Exception e) { throw new AssertionError(e); } } private static WindowManager windowManager; private static DisplayManager displayManager; private static InputManager inputManager; private static PowerManager powerManager; private static StatusBarManager statusBarManager; private static ClipboardManager clipboardManager; private static ActivityManager activityManager; private static CameraManager cameraManager; private ServiceManager() { /* not instantiable */ } static IInterface getService(String service, String type) { try { IBinder binder = (IBinder) GET_SERVICE_METHOD.invoke(null, service); Method asInterfaceMethod = Class.forName(type + "$Stub").getMethod("asInterface", IBinder.class); return (IInterface) asInterfaceMethod.invoke(null, binder); } catch (Exception e) { throw new AssertionError(e); } } public static WindowManager getWindowManager() { if (windowManager == null) { windowManager = WindowManager.create(); } return windowManager; } // The DisplayManager may be used from both the Controller thread and the video (main) thread public static synchronized DisplayManager getDisplayManager() { if (displayManager == null) { displayManager = DisplayManager.create(); } return displayManager; } public static InputManager getInputManager() { if (inputManager == null) { inputManager = InputManager.create(); } return inputManager; } public static PowerManager getPowerManager() { if (powerManager == null) { powerManager = PowerManager.create(); } return powerManager; } public static StatusBarManager getStatusBarManager() { if (statusBarManager == null) { statusBarManager = StatusBarManager.create(); } return statusBarManager; } public static ClipboardManager getClipboardManager() { if (clipboardManager == null) { // May be null, some devices have no clipboard manager clipboardManager = ClipboardManager.create(); } 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; } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java000066400000000000000000000070101505702741400341150ustar00rootroot00000000000000package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.util.Ln; import android.os.IInterface; import java.lang.reflect.Method; public final 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) { this.manager = manager; } private Method getExpandNotificationsPanelMethod() throws NoSuchMethodException { if (expandNotificationsPanelMethod == null) { try { expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel"); } catch (NoSuchMethodException e) { // Custom version for custom vendor ROM: expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel", int.class); expandNotificationPanelMethodCustomVersion = true; } } return expandNotificationsPanelMethod; } private Method getExpandSettingsPanel() throws NoSuchMethodException { if (expandSettingsPanelMethod == null) { try { // Since Android 7: https://android.googlesource.com/platform/frameworks/base.git/+/a9927325eda025504d59bb6594fee8e240d95b01%5E%21/ expandSettingsPanelMethod = manager.getClass().getMethod("expandSettingsPanel", String.class); } catch (NoSuchMethodException e) { // old version expandSettingsPanelMethod = manager.getClass().getMethod("expandSettingsPanel"); expandSettingsPanelMethodNewVersion = false; } } return expandSettingsPanelMethod; } private Method getCollapsePanelsMethod() throws NoSuchMethodException { if (collapsePanelsMethod == null) { collapsePanelsMethod = manager.getClass().getMethod("collapsePanels"); } return collapsePanelsMethod; } public void expandNotificationsPanel() { try { Method method = getExpandNotificationsPanelMethod(); if (expandNotificationPanelMethodCustomVersion) { method.invoke(manager, 0); } else { method.invoke(manager); } } catch (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); } } public void collapsePanels() { try { Method method = getCollapsePanelsMethod(); method.invoke(manager); } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); } } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java000066400000000000000000000147321505702741400336540ustar00rootroot00000000000000package 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 { private static final Class CLASS; // see public static final int POWER_MODE_OFF = 0; public static final int POWER_MODE_NORMAL = 2; static { try { CLASS = Class.forName("android.view.SurfaceControl"); } catch (ClassNotFoundException e) { throw new AssertionError(e); } } private static Method getBuiltInDisplayMethod; private static Method setDisplayPowerModeMethod; private static Method getPhysicalDisplayTokenMethod; private static Method getPhysicalDisplayIdsMethod; private SurfaceControl() { // only static methods } public static void openTransaction() { try { CLASS.getMethod("openTransaction").invoke(null); } catch (Exception e) { throw new AssertionError(e); } } public static void closeTransaction() { try { CLASS.getMethod("closeTransaction").invoke(null); } catch (Exception e) { throw new AssertionError(e); } } public static void setDisplayProjection(IBinder displayToken, int orientation, Rect layerStackRect, Rect displayRect) { try { CLASS.getMethod("setDisplayProjection", IBinder.class, int.class, Rect.class, Rect.class) .invoke(null, displayToken, orientation, layerStackRect, displayRect); } catch (Exception e) { throw new AssertionError(e); } } public static void setDisplayLayerStack(IBinder displayToken, int layerStack) { try { CLASS.getMethod("setDisplayLayerStack", IBinder.class, int.class).invoke(null, displayToken, layerStack); } catch (Exception e) { throw new AssertionError(e); } } public static void setDisplaySurface(IBinder displayToken, Surface surface) { try { CLASS.getMethod("setDisplaySurface", IBinder.class, Surface.class).invoke(null, displayToken, surface); } catch (Exception e) { throw new AssertionError(e); } } public static IBinder createDisplay(String name, boolean secure) 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() { try { getGetBuiltInDisplayMethod(); return true; } catch (NoSuchMethodException e) { return false; } } public static IBinder getBuiltInDisplay() { 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; } } private static Method getGetPhysicalDisplayTokenMethod() throws NoSuchMethodException { if (getPhysicalDisplayTokenMethod == null) { getPhysicalDisplayTokenMethod = CLASS.getMethod("getPhysicalDisplayToken", long.class); } return getPhysicalDisplayTokenMethod; } public static IBinder getPhysicalDisplayToken(long physicalDisplayId) { try { Method method = getGetPhysicalDisplayTokenMethod(); return (IBinder) method.invoke(null, physicalDisplayId); } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); return null; } } private static Method getGetPhysicalDisplayIdsMethod() throws NoSuchMethodException { if (getPhysicalDisplayIdsMethod == null) { getPhysicalDisplayIdsMethod = CLASS.getMethod("getPhysicalDisplayIds"); } return getPhysicalDisplayIdsMethod; } public static boolean 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; } } public static void destroyDisplay(IBinder displayToken) { try { CLASS.getMethod("destroyDisplay", IBinder.class).invoke(null, displayToken); } catch (Exception e) { throw new AssertionError(e); } } } Genymobile-scrcpy-facefde/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java000066400000000000000000000265311505702741400334650ustar00rootroot00000000000000package 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; 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) { 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; } } 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); } return null; } @TargetApi(AndroidVersions.API_30_ANDROID_11) public void unregisterDisplayWindowListener(IDisplayWindowListener listener) { try { manager.getClass().getMethod("unregisterDisplayWindowListener", IDisplayWindowListener.class).invoke(manager, listener); } 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); } } } Genymobile-scrcpy-facefde/server/src/test/000077500000000000000000000000001505702741400211055ustar00rootroot00000000000000Genymobile-scrcpy-facefde/server/src/test/java/000077500000000000000000000000001505702741400220265ustar00rootroot00000000000000Genymobile-scrcpy-facefde/server/src/test/java/com/000077500000000000000000000000001505702741400226045ustar00rootroot00000000000000Genymobile-scrcpy-facefde/server/src/test/java/com/genymobile/000077500000000000000000000000001505702741400247365ustar00rootroot00000000000000Genymobile-scrcpy-facefde/server/src/test/java/com/genymobile/scrcpy/000077500000000000000000000000001505702741400262415ustar00rootroot00000000000000Genymobile-scrcpy-facefde/server/src/test/java/com/genymobile/scrcpy/control/000077500000000000000000000000001505702741400277215ustar00rootroot00000000000000ControlMessageReaderTest.java000066400000000000000000000477021505702741400354270ustar00rootroot00000000000000Genymobile-scrcpy-facefde/server/src/test/java/com/genymobile/scrcpy/controlpackage 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 } } } DeviceMessageWriterTest.java000066400000000000000000000046151505702741400352540ustar00rootroot00000000000000Genymobile-scrcpy-facefde/server/src/test/java/com/genymobile/scrcpy/controlpackage 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); } } Genymobile-scrcpy-facefde/server/src/test/java/com/genymobile/scrcpy/util/000077500000000000000000000000001505702741400272165ustar00rootroot00000000000000Genymobile-scrcpy-facefde/server/src/test/java/com/genymobile/scrcpy/util/BinaryTest.java000066400000000000000000000046151505702741400321530ustar00rootroot00000000000000package 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); } } Genymobile-scrcpy-facefde/server/src/test/java/com/genymobile/scrcpy/util/CodecOptionsTest.java000066400000000000000000000074531505702741400333230ustar00rootroot00000000000000package 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()); } } Genymobile-scrcpy-facefde/server/src/test/java/com/genymobile/scrcpy/util/CommandParserTest.java000066400000000000000000000471231505702741400334630ustar00rootroot00000000000000package 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()); } } Genymobile-scrcpy-facefde/server/src/test/java/com/genymobile/scrcpy/util/StringUtilsTest.java000066400000000000000000000023261505702741400332130ustar00rootroot00000000000000package com.genymobile.scrcpy.util; import org.junit.Assert; import org.junit.Test; import java.nio.charset.StandardCharsets; public class StringUtilsTest { @Test public void testUtf8Truncate() { String s = "aÉbÔc"; byte[] utf8 = s.getBytes(StandardCharsets.UTF_8); Assert.assertEquals(7, utf8.length); int count; count = StringUtils.getUtf8TruncationIndex(utf8, 1); Assert.assertEquals(1, count); count = StringUtils.getUtf8TruncationIndex(utf8, 2); Assert.assertEquals(1, count); // É is 2 bytes-wide count = StringUtils.getUtf8TruncationIndex(utf8, 3); Assert.assertEquals(3, count); count = StringUtils.getUtf8TruncationIndex(utf8, 4); Assert.assertEquals(4, count); count = StringUtils.getUtf8TruncationIndex(utf8, 5); Assert.assertEquals(4, count); // Ô is 2 bytes-wide count = StringUtils.getUtf8TruncationIndex(utf8, 6); Assert.assertEquals(6, count); count = StringUtils.getUtf8TruncationIndex(utf8, 7); Assert.assertEquals(7, count); count = StringUtils.getUtf8TruncationIndex(utf8, 8); Assert.assertEquals(7, count); // no more chars } } Genymobile-scrcpy-facefde/settings.gradle000066400000000000000000000000221505702741400210430ustar00rootroot00000000000000include ':server'