diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 49402a6e..00000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,514 +0,0 @@ -name: Build - -on: - workflow_dispatch: - inputs: - name: - description: 'Version name (default is ref name)' - -env: - # $VERSION is used by release scripts - VERSION: ${{ github.event.inputs.name || github.ref_name }} - -jobs: - test-scrcpy-server: - runs-on: ubuntu-latest - env: - GRADLE: gradle # use native gradle instead of ./gradlew in scripts - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup JDK - uses: actions/setup-java@v4 - with: - distribution: 'zulu' - java-version: '17' - - - name: Test scrcpy-server - run: release/test_server.sh - - build-scrcpy-server: - runs-on: ubuntu-latest - env: - GRADLE: gradle # use native gradle instead of ./gradlew in scripts - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup JDK - uses: actions/setup-java@v4 - with: - distribution: 'zulu' - java-version: '17' - - - name: Build - run: release/build_server.sh - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: scrcpy-server - path: release/work/build-server/server/scrcpy-server - - test-build-scrcpy-server-without-gradle: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup JDK - uses: actions/setup-java@v4 - with: - distribution: 'zulu' - java-version: '17' - - - name: Build without gradle - run: server/build_without_gradle.sh - - test-client: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install dependencies - run: | - sudo apt update - sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ - libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ - libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ - libv4l-dev - - - name: Test - run: release/test_client.sh - - build-linux-x86_64: - runs-on: ubuntu-22.04 - steps: - - name: Check architecture - run: | - arch=$(uname -m) - if [[ "$arch" != x86_64 ]] - then - echo "Unexpected architecture: $arch" >&2 - exit 1 - fi - - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install dependencies - run: | - sudo apt update - sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ - libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ - libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ - libv4l-dev - - - name: Build - run: release/build_linux.sh x86_64 - - # upload-artifact does not preserve permissions - - name: Tar - run: | - cd release/work/build-linux-x86_64 - mkdir dist-tar - cd dist-tar - tar -C .. -cvf dist.tar.gz dist/ - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: build-linux-x86_64-intermediate - path: release/work/build-linux-x86_64/dist-tar/ - - build-win32: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install dependencies - run: | - sudo apt update - sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ - libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ - libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ - mingw-w64 mingw-w64-tools libz-mingw-w64-dev - - - name: Build - run: release/build_windows.sh 32 - - # upload-artifact does not preserve permissions - - name: Tar - run: | - cd release/work/build-win32 - mkdir dist-tar - cd dist-tar - tar -C .. -cvf dist.tar.gz dist/ - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: build-win32-intermediate - path: release/work/build-win32/dist-tar/ - - build-win64: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install dependencies - run: | - sudo apt update - sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ - libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ - libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ - mingw-w64 mingw-w64-tools libz-mingw-w64-dev - - - name: Build - run: release/build_windows.sh 64 - - # upload-artifact does not preserve permissions - - name: Tar - run: | - cd release/work/build-win64 - mkdir dist-tar - cd dist-tar - tar -C .. -cvf dist.tar.gz dist/ - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: build-win64-intermediate - path: release/work/build-win64/dist-tar/ - - build-macos-aarch64: - runs-on: macos-latest - steps: - - name: Check architecture - run: | - arch=$(uname -m) - if [[ "$arch" != arm64 ]] - then - echo "Unexpected architecture: $arch" >&2 - exit 1 - fi - - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install dependencies - run: | - brew install meson ninja nasm libiconv zlib automake autoconf \ - libtool - - - name: Build - env: - # the default Xcode (and macOS SDK) version can be found at - # - # - # then the minimal supported deployment target of that macOS SDK can be found at - # - MACOSX_DEPLOYMENT_TARGET: 10.13 - run: release/build_macos.sh aarch64 - - # upload-artifact does not preserve permissions - - name: Tar - run: | - cd release/work/build-macos-aarch64 - mkdir dist-tar - cd dist-tar - tar -C .. -cvf dist.tar.gz dist/ - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: build-macos-aarch64-intermediate - path: release/work/build-macos-aarch64/dist-tar/ - - build-macos-x86_64: - runs-on: macos-13 - steps: - - name: Check architecture - run: | - arch=$(uname -m) - if [[ "$arch" != x86_64 ]] - then - echo "Unexpected architecture: $arch" >&2 - exit 1 - fi - - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install dependencies - run: brew install meson ninja nasm libiconv zlib automake - # autoconf and libtool are already installed on macos-13 - - - name: Build - env: - # the default Xcode (and macOS SDK) version can be found at - # - # - # then the minimal supported deployment target of that macOS SDK can be found at - # - MACOSX_DEPLOYMENT_TARGET: 10.13 - run: release/build_macos.sh x86_64 - - # upload-artifact does not preserve permissions - - name: Tar - run: | - cd release/work/build-macos-x86_64 - mkdir dist-tar - cd dist-tar - tar -C .. -cvf dist.tar.gz dist/ - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: build-macos-x86_64-intermediate - path: release/work/build-macos-x86_64/dist-tar/ - - package-linux-x86_64: - needs: - - build-scrcpy-server - - build-linux-x86_64 - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Download scrcpy-server - uses: actions/download-artifact@v4 - with: - name: scrcpy-server - path: release/work/build-server/server/ - - - name: Download build-linux-x86_64 - uses: actions/download-artifact@v4 - with: - name: build-linux-x86_64-intermediate - path: release/work/build-linux-x86_64/dist-tar/ - - # upload-artifact does not preserve permissions - - name: Detar - run: | - cd release/work/build-linux-x86_64 - tar xf dist-tar/dist.tar.gz - - - name: Package - run: release/package_client.sh linux-x86_64 tar.gz - - - name: Upload release - uses: actions/upload-artifact@v4 - with: - name: release-linux-x86_64 - path: release/output/ - - package-win32: - needs: - - build-scrcpy-server - - build-win32 - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Download scrcpy-server - uses: actions/download-artifact@v4 - with: - name: scrcpy-server - path: release/work/build-server/server/ - - - name: Download build-win32 - uses: actions/download-artifact@v4 - with: - name: build-win32-intermediate - path: release/work/build-win32/dist-tar/ - - # upload-artifact does not preserve permissions - - name: Detar - run: | - cd release/work/build-win32 - tar xf dist-tar/dist.tar.gz - - - name: Package - run: release/package_client.sh win32 zip - - - name: Upload release - uses: actions/upload-artifact@v4 - with: - name: release-win32 - path: release/output/ - - package-win64: - needs: - - build-scrcpy-server - - build-win64 - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Download scrcpy-server - uses: actions/download-artifact@v4 - with: - name: scrcpy-server - path: release/work/build-server/server/ - - - name: Download build-win64 - uses: actions/download-artifact@v4 - with: - name: build-win64-intermediate - path: release/work/build-win64/dist-tar/ - - # upload-artifact does not preserve permissions - - name: Detar - run: | - cd release/work/build-win64 - tar xf dist-tar/dist.tar.gz - - - name: Package - run: release/package_client.sh win64 zip - - - name: Upload release - uses: actions/upload-artifact@v4 - with: - name: release-win64 - path: release/output - - package-macos-aarch64: - needs: - - build-scrcpy-server - - build-macos-aarch64 - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Download scrcpy-server - uses: actions/download-artifact@v4 - with: - name: scrcpy-server - path: release/work/build-server/server/ - - - name: Download build-macos-aarch64 - uses: actions/download-artifact@v4 - with: - name: build-macos-aarch64-intermediate - path: release/work/build-macos-aarch64/dist-tar/ - - # upload-artifact does not preserve permissions - - name: Detar - run: | - cd release/work/build-macos-aarch64 - tar xf dist-tar/dist.tar.gz - - - name: Package - run: release/package_client.sh macos-aarch64 tar.gz - - - name: Upload release - uses: actions/upload-artifact@v4 - with: - name: release-macos-aarch64 - path: release/output/ - - package-macos-x86_64: - needs: - - build-scrcpy-server - - build-macos-x86_64 - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Download scrcpy-server - uses: actions/download-artifact@v4 - with: - name: scrcpy-server - path: release/work/build-server/server/ - - - name: Download build-macos - uses: actions/download-artifact@v4 - with: - name: build-macos-x86_64-intermediate - path: release/work/build-macos-x86_64/dist-tar/ - - # upload-artifact does not preserve permissions - - name: Detar - run: | - cd release/work/build-macos-x86_64 - tar xf dist-tar/dist.tar.gz - - - name: Package - run: release/package_client.sh macos-x86_64 tar.gz - - - name: Upload release - uses: actions/upload-artifact@v4 - with: - name: release-macos-x86_64 - path: release/output/ - - release: - needs: - - build-scrcpy-server - - package-linux-x86_64 - - package-win32 - - package-win64 - - package-macos-aarch64 - - package-macos-x86_64 - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Download scrcpy-server - uses: actions/download-artifact@v4 - with: - name: scrcpy-server - path: release/work/build-server/server/ - - - name: Download release-linux-x86_64 - uses: actions/download-artifact@v4 - with: - name: release-linux-x86_64 - path: release/output/ - - - name: Download release-win32 - uses: actions/download-artifact@v4 - with: - name: release-win32 - path: release/output/ - - - name: Download release-win64 - uses: actions/download-artifact@v4 - with: - name: release-win64 - path: release/output/ - - - name: Download release-macos-aarch64 - uses: actions/download-artifact@v4 - with: - name: release-macos-aarch64 - path: release/output/ - - - name: Download release-macos-x86_64 - uses: actions/download-artifact@v4 - with: - name: release-macos-x86_64 - path: release/output/ - - - name: Package server - run: release/package_server.sh - - - name: Generate checksums - run: release/generate_checksums.sh - - - name: Upload release artifact - uses: actions/upload-artifact@v4 - with: - name: scrcpy-release-${{ env.VERSION }} - path: release/output diff --git a/FAQ.md b/FAQ.md index 24722c74..5f089cd7 100644 --- a/FAQ.md +++ b/FAQ.md @@ -166,13 +166,14 @@ Rebooting the device is necessary once this option is set. ### 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], +The default text injection method is [limited to ASCII characters][text-input]. +A trick allows to also inject some [accented characters][accented-characters], but that's all. See [#37]. To avoid the problem, [change the keyboard mode to simulate a physical keyboard][hid]. +[text-input]: https://github.com/Genymobile/scrcpy/issues?q=is%3Aopen+is%3Aissue+label%3Aunicode [accented-characters]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-accented-characters [#37]: https://github.com/Genymobile/scrcpy/issues/37 [hid]: doc/keyboard.md#physical-keyboard-simulation diff --git a/LICENSE b/LICENSE index 1196b3da..d9326a74 100644 --- a/LICENSE +++ b/LICENSE @@ -188,7 +188,7 @@ identification within third-party archives. Copyright (C) 2018 Genymobile - Copyright (C) 2018-2025 Romain Vimont + Copyright (C) 2018-2024 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index d886d23c..44f3d740 100644 --- a/README.md +++ b/README.md @@ -2,16 +2,16 @@ source for the project. Do not download releases from random websites, even if their name contains `scrcpy`.** -# scrcpy (v3.3.1) +# scrcpy (v2.7) scrcpy _pronounced "**scr**een **c**o**py**"_ -This application mirrors Android devices (video and audio) connected via USB or -[TCP/IP](doc/connection.md#tcpip-wireless) and allows control using the -computer's keyboard and mouse. It does not require _root_ access or an app -installed on the device. It works on _Linux_, _Windows_, and _macOS_. +This application mirrors Android devices (video and audio) connected via +USB or [over TCP/IP](doc/connection.md#tcpip-wireless), and allows to control the +device with the keyboard and the mouse of the computer. It does not require any +_root_ access. It works on _Linux_, _Windows_ and _macOS_. ![screenshot](assets/screenshot-debian-600.jpg) @@ -31,7 +31,6 @@ It focuses on: 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) @@ -58,7 +57,7 @@ Make sure you [enabled USB debugging][enable-adb] on your device(s). 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. +java.lang.SecurityException: 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 @@ -74,20 +73,10 @@ 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)) + - [Windows](doc/windows.md) - [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. @@ -102,12 +91,6 @@ Here are just some common examples. 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: @@ -151,7 +134,6 @@ documented in the following pages: - [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) @@ -191,7 +173,6 @@ to your problem immediately. 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) @@ -207,10 +188,10 @@ work][donate]: [donate]: https://blog.rom1v.com/about/#support-my-open-source-work -## License +## Licence Copyright (C) 2018 Genymobile - Copyright (C) 2018-2025 Romain Vimont + Copyright (C) 2018-2024 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index a49da8ca..db825ecc 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -2,7 +2,6 @@ _scrcpy() { local cur prev words cword local opts=" --always-on-top - --angle --audio-bit-rate= --audio-buffer= --audio-codec= @@ -18,12 +17,11 @@ _scrcpy() { --camera-fps= --camera-high-speed --camera-size= - --capture-orientation= --crop= -d --select-usb --disable-screensaver + --display-buffer= --display-id= - --display-ime-policy= --display-orientation= -e --select-tcpip -f --fullscreen @@ -35,11 +33,12 @@ _scrcpy() { --keyboard= --kill-adb-on-close --legacy-paste - --list-apps --list-camera-sizes --list-cameras --list-displays --list-encoders + --lock-video-orientation + --lock-video-orientation= -m --max-size= -M --max-fps= @@ -47,8 +46,6 @@ _scrcpy() { --mouse-bind= -n --no-control -N --no-playback - --new-display - --new-display= --no-audio --no-audio-playback --no-cleanup @@ -58,8 +55,6 @@ _scrcpy() { --no-mipmaps --no-mouse-hover --no-power-on - --no-vd-destroy-content - --no-vd-system-decorations --no-video --no-video-playback --orientation= @@ -80,9 +75,7 @@ _scrcpy() { --rotation= -s --serial= -S --turn-screen-off - --screen-off-timeout= --shortcut-mod= - --start-app= -t --show-touches --tcpip --tcpip= @@ -93,7 +86,6 @@ _scrcpy() { --v4l2-sink= -v --version -V --verbosity= - --video-buffer= --video-codec= --video-codec-options= --video-encoder= @@ -122,7 +114,7 @@ _scrcpy() { 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")) + COMPREPLY=($(compgen -W 'output mic playback' -- "$cur")) return ;; --camera-facing) @@ -141,22 +133,18 @@ _scrcpy() { 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 ;; + --lock-video-orientation) + COMPREPLY=($(compgen -W 'unlocked initial 0 90 180 270' -- "$cur")) + return + ;; --pause-on-exit) COMPREPLY=($(compgen -W 'true false if-error' -- "$cur")) return @@ -199,18 +187,16 @@ _scrcpy() { |--camera-size \ |--crop \ |--display-id \ + |--display-buffer \ |--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 \ diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 04ffb8f1..fa0fa84f 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -1,4 +1,4 @@ -#compdef scrcpy scrcpy.exe +#compdef -N scrcpy -N scrcpy.exe # # name: scrcpy # auth: hltdev [hltdev8642@gmail.com] @@ -9,14 +9,13 @@ 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-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-source=[Select the audio source]:source:(output mic playback)' '--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]' @@ -25,36 +24,34 @@ arguments=( '--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-buffer=[Add a buffering delay \(in milliseconds\) before displaying]' '--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\)]' + '-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\)]' + '-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]' + '--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 90 180 270)' {-m,--max-size=}'[Limit both the width and height of the video to value]' - '-M[Use UHID/AOA mouse \(same as --mouse=uhid or --mouse=aoa, depending on OTG mode\)]' + '-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]' @@ -64,8 +61,6 @@ arguments=( '--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)' @@ -84,9 +79,7 @@ arguments=( '--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]' @@ -96,7 +89,6 @@ arguments=( '--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]' diff --git a/app/deps/adb_windows.sh b/app/deps/adb.sh similarity index 63% rename from app/deps/adb_windows.sh rename to app/deps/adb.sh index de37162c..58a54659 100755 --- a/app/deps/adb_windows.sh +++ b/app/deps/adb.sh @@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common -VERSION=36.0.0 -FILENAME=platform-tools_r$VERSION-win.zip -PROJECT_DIR=platform-tools-$VERSION-windows -SHA256SUM=24bd8bebbbb58b9870db202b5c6775c4a49992632021c60750d9d8ec8179d5f0 +VERSION=35.0.0 +FILENAME=platform-tools_r$VERSION-windows.zip +PROJECT_DIR=platform-tools-$VERSION +SHA256SUM=7ab78a8f8b305ae4d0de647d99c43599744de61a0838d3a47bda0cdffefee87e cd "$SOURCES_DIR" @@ -27,6 +27,6 @@ else 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/" +mkdir -p "$INSTALL_DIR/$HOST/bin" +cd "$INSTALL_DIR/$HOST/bin" +cp -r "$SOURCES_DIR/$PROJECT_DIR"/. "$INSTALL_DIR/$HOST/bin/" diff --git a/app/deps/adb_linux.sh b/app/deps/adb_linux.sh deleted file mode 100755 index a3e339ec..00000000 --- a/app/deps/adb_linux.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash -set -ex -DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) -cd "$DEPS_DIR" -. common - -VERSION=36.0.0 -FILENAME=platform-tools_r$VERSION-linux.zip -PROJECT_DIR=platform-tools-$VERSION-linux -SHA256SUM=0ead642c943ffe79701fccca8f5f1c69c4ce4f43df2eefee553f6ccb27cbfbe8 - -cd "$SOURCES_DIR" - -if [[ -d "$PROJECT_DIR" ]] -then - echo "$PWD/$PROJECT_DIR" found -else - get_file "https://dl.google.com/android/repository/$FILENAME" "$FILENAME" "$SHA256SUM" - mkdir -p "$PROJECT_DIR" - cd "$PROJECT_DIR" - ZIP_PREFIX=platform-tools - unzip "../$FILENAME" "$ZIP_PREFIX"/adb - mv "$ZIP_PREFIX"/* . - rmdir "$ZIP_PREFIX" -fi - -mkdir -p "$INSTALL_DIR/adb-linux" -cd "$INSTALL_DIR/adb-linux" -cp -r "$SOURCES_DIR/$PROJECT_DIR"/. "$INSTALL_DIR/adb-linux/" diff --git a/app/deps/adb_macos.sh b/app/deps/adb_macos.sh deleted file mode 100755 index 36f5df89..00000000 --- a/app/deps/adb_macos.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash -set -ex -DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) -cd "$DEPS_DIR" -. common - -VERSION=36.0.0 -FILENAME=platform-tools_r$VERSION-darwin.zip -PROJECT_DIR=platform-tools-$VERSION-darwin -SHA256SUM=b241878e6ec20650b041bf715ea05f7d5dc73bd24529464bd9cf68946e3132bd - -cd "$SOURCES_DIR" - -if [[ -d "$PROJECT_DIR" ]] -then - echo "$PWD/$PROJECT_DIR" found -else - get_file "https://dl.google.com/android/repository/$FILENAME" "$FILENAME" "$SHA256SUM" - mkdir -p "$PROJECT_DIR" - cd "$PROJECT_DIR" - ZIP_PREFIX=platform-tools - unzip "../$FILENAME" "$ZIP_PREFIX"/adb - mv "$ZIP_PREFIX"/* . - rmdir "$ZIP_PREFIX" -fi - -mkdir -p "$INSTALL_DIR/adb-macos" -cd "$INSTALL_DIR/adb-macos" -cp -r "$SOURCES_DIR/$PROJECT_DIR"/. "$INSTALL_DIR/adb-macos/" diff --git a/app/deps/common b/app/deps/common index daaa96c0..c1cc7729 100644 --- a/app/deps/common +++ b/app/deps/common @@ -1,47 +1,25 @@ #!/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 +if [[ $# != 1 ]] +then + # : win32 or win64 + 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" +HOST="$1" - 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 -} +if [[ "$HOST" = win32 ]] +then + HOST_TRIPLET=i686-w64-mingw32 +elif [[ "$HOST" = win64 ]] +then + HOST_TRIPLET=x86_64-w64-mingw32 +else + echo "Unsupported host: $HOST" >&2 + exit 1 +fi DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" @@ -59,7 +37,7 @@ checksum() { local file="$1" local sum="$2" echo "$file: verifying checksum..." - echo "$sum $file" | shasum -a256 -c + echo "$sum $file" | sha256sum -c } get_file() { diff --git a/app/deps/dav1d.sh b/app/deps/dav1d.sh deleted file mode 100755 index 3069b6fe..00000000 --- a/app/deps/dav1d.sh +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env bash -set -ex -DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) -cd "$DEPS_DIR" -. common -process_args "$@" - -VERSION=1.5.0 -FILENAME=dav1d-$VERSION.tar.gz -PROJECT_DIR=dav1d-$VERSION -SHA256SUM=78b15d9954b513ea92d27f39362535ded2243e1b0924fde39f37a31ebed5f76b - -cd "$SOURCES_DIR" - -if [[ -d "$PROJECT_DIR" ]] -then - echo "$PWD/$PROJECT_DIR" found -else - get_file "https://code.videolan.org/videolan/dav1d/-/archive/$VERSION/$FILENAME" "$FILENAME" "$SHA256SUM" - tar xf "$FILENAME" # First level directory is "$PROJECT_DIR" -fi - -mkdir -p "$BUILD_DIR/$PROJECT_DIR" -cd "$BUILD_DIR/$PROJECT_DIR" - -if [[ -d "$DIRNAME" ]] -then - echo "'$PWD/$DIRNAME' already exists, not reconfigured" - cd "$DIRNAME" -else - mkdir "$DIRNAME" - cd "$DIRNAME" - - conf=( - --prefix="$INSTALL_DIR/$DIRNAME" - --libdir=lib - -Denable_tests=false - -Denable_tools=false - # Always build dav1d statically - --default-library=static - ) - - if [[ "$BUILD_TYPE" == cross ]] - then - case "$HOST" in - win32) - conf+=( - --cross-file="$SOURCES_DIR/$PROJECT_DIR/package/crossfiles/i686-w64-mingw32.meson" - ) - ;; - - win64) - conf+=( - --cross-file="$SOURCES_DIR/$PROJECT_DIR/package/crossfiles/x86_64-w64-mingw32.meson" - ) - ;; - - *) - echo "Unsupported host: $HOST" >&2 - exit 1 - esac - fi - - meson setup . "$SOURCES_DIR/$PROJECT_DIR" "${conf[@]}" -fi - -ninja -ninja install diff --git a/app/deps/ffmpeg.sh b/app/deps/ffmpeg.sh index fb8b9a25..89431542 100755 --- a/app/deps/ffmpeg.sh +++ b/app/deps/ffmpeg.sh @@ -3,12 +3,11 @@ set -ex DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common -process_args "$@" -VERSION=7.1.1 +VERSION=7.0.2 FILENAME=ffmpeg-$VERSION.tar.xz PROJECT_DIR=ffmpeg-$VERSION -SHA256SUM=733984395e0dbbe5c046abda2dc49a5544e7e0e1e2366bba849222ae9e3a03b1 +SHA256SUM=8646515b638a3ad303e23af6a3587734447cb8fc0a0c064ecdb8e95c4fd8b389 cd "$SOURCES_DIR" @@ -23,121 +22,68 @@ fi mkdir -p "$BUILD_DIR/$PROJECT_DIR" cd "$BUILD_DIR/$PROJECT_DIR" -if [[ -d "$DIRNAME" ]] +if [[ "$HOST" = win32 ]] then - echo "'$PWD/$DIRNAME' already exists, not reconfigured" - cd "$DIRNAME" + ARCH=x86 +elif [[ "$HOST" = win64 ]] +then + ARCH=x86_64 else - mkdir "$DIRNAME" - cd "$DIRNAME" + echo "Unsupported host: $HOST" >&2 + exit 1 +fi - 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 +# -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' - export PKG_CONFIG_PATH="$INSTALL_DIR/$DIRNAME/lib/pkgconfig:$PKG_CONFIG_PATH" +if [[ -d "$HOST" ]] +then + echo "'$PWD/$HOST' already exists, not reconfigured" + cd "$HOST" +else + mkdir "$HOST" + cd "$HOST" - 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 + "$SOURCES_DIR/$PROJECT_DIR"/configure \ + --prefix="$INSTALL_DIR/$HOST" \ + --enable-cross-compile \ + --target-os=mingw32 \ + --arch="$ARCH" \ + --cross-prefix="${HOST_TRIPLET}-" \ + --cc="${HOST_TRIPLET}-gcc" \ + --extra-cflags="-O2 -fPIC" \ + --enable-shared \ + --disable-static \ + --disable-programs \ + --disable-doc \ + --disable-swscale \ + --disable-postproc \ + --disable-avfilter \ + --disable-avdevice \ + --disable-network \ + --disable-everything \ + --enable-swresample \ + --enable-decoder=h264 \ + --enable-decoder=hevc \ + --enable-decoder=av1 \ + --enable-decoder=pcm_s16le \ + --enable-decoder=opus \ + --enable-decoder=aac \ + --enable-decoder=flac \ + --enable-decoder=png \ + --enable-protocol=file \ + --enable-demuxer=image2 \ + --enable-parser=png \ + --enable-zlib \ + --enable-muxer=matroska \ + --enable-muxer=mp4 \ + --enable-muxer=opus \ + --enable-muxer=flac \ + --enable-muxer=wav \ --disable-vulkan - --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 diff --git a/app/deps/libusb.sh b/app/deps/libusb.sh index 887a2a77..26f0140b 100755 --- a/app/deps/libusb.sh +++ b/app/deps/libusb.sh @@ -3,12 +3,11 @@ set -ex DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common -process_args "$@" -VERSION=1.0.29 +VERSION=1.0.27 FILENAME=libusb-$VERSION.tar.gz PROJECT_DIR=libusb-$VERSION -SHA256SUM=7c2dd39c0b2589236e48c93247c986ae272e27570942b4163cb00a060fcf1b74 +SHA256SUM=e8f18a7a36ecbb11fb820bd71540350d8f61bcd9db0d2e8c18a6fb80b214a3de cd "$SOURCES_DIR" @@ -26,40 +25,20 @@ cd "$BUILD_DIR/$PROJECT_DIR" export CFLAGS='-O2' export CXXFLAGS="$CFLAGS" -if [[ -d "$DIRNAME" ]] +if [[ -d "$HOST" ]] then - echo "'$PWD/$DIRNAME' already exists, not reconfigured" - cd "$DIRNAME" + echo "'$PWD/$HOST' already exists, not reconfigured" + cd "$HOST" 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 + mkdir "$HOST" + cd "$HOST" "$SOURCES_DIR/$PROJECT_DIR"/bootstrap.sh - "$SOURCES_DIR/$PROJECT_DIR"/configure "${conf[@]}" + "$SOURCES_DIR/$PROJECT_DIR"/configure \ + --prefix="$INSTALL_DIR/$HOST" \ + --host="$HOST_TRIPLET" \ + --enable-shared \ + --disable-static fi make -j diff --git a/app/deps/sdl.sh b/app/deps/sdl.sh index 54fee12b..c8b62746 100755 --- a/app/deps/sdl.sh +++ b/app/deps/sdl.sh @@ -3,12 +3,11 @@ set -ex DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common -process_args "$@" -VERSION=2.32.8 +VERSION=2.30.7 FILENAME=SDL-$VERSION.tar.gz PROJECT_DIR=SDL-release-$VERSION -SHA256SUM=dd35e05644ae527848d02433bec24dd0ea65db59faecf1a0e5d1880c533dac2c +SHA256SUM=1578c96f62c9ae36b64e431b2aa0e0b0fd07c275dedbc694afc38e19056688f5 cd "$SOURCES_DIR" @@ -26,54 +25,23 @@ cd "$BUILD_DIR/$PROJECT_DIR" export CFLAGS='-O2' export CXXFLAGS="$CFLAGS" -if [[ -d "$DIRNAME" ]] +if [[ -d "$HOST" ]] then - echo "'$PWD/$HDIRNAME' already exists, not reconfigured" - cd "$DIRNAME" + echo "'$PWD/$HOST' already exists, not reconfigured" + cd "$HOST" else - mkdir "$DIRNAME" - cd "$DIRNAME" + mkdir "$HOST" + cd "$HOST" - 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[@]}" + "$SOURCES_DIR/$PROJECT_DIR"/configure \ + --prefix="$INSTALL_DIR/$HOST" \ + --host="$HOST_TRIPLET" \ + --enable-shared \ + --disable-static fi make -j # There is no "make install-strip" make install # Strip manually -if [[ "$LINK_TYPE" == shared && "$HOST" == win* ]] -then - ${HOST_TRIPLET}-strip "$INSTALL_DIR/$DIRNAME/bin/SDL2.dll" -fi +${HOST_TRIPLET}-strip "$INSTALL_DIR/$HOST/bin/SDL2.dll" diff --git a/app/meson.build b/app/meson.build index f7df69eb..fc752e86 100644 --- a/app/meson.build +++ b/app/meson.build @@ -5,7 +5,6 @@ src = [ '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', @@ -23,7 +22,6 @@ src = [ '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', @@ -46,7 +44,6 @@ src = [ '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', @@ -110,22 +107,20 @@ 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), + dependency('libavformat', version: '>= 57.33'), + dependency('libavcodec', version: '>= 57.37'), + dependency('libavutil'), + dependency('libswresample'), + dependency('sdl2', version: '>= 2.0.5'), ] if v4l2_support - dependencies += dependency('libavdevice', static: static) + dependencies += dependency('libavdevice') endif if usb_support - dependencies += dependency('libusb-1.0', static: static) + dependencies += dependency('libusb-1.0') endif if host_machine.system() == 'windows' @@ -170,6 +165,9 @@ 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')) +# select the debugger method ('old' for Android < 9, 'new' for Android >= 9) +conf.set('SERVER_DEBUGGER_METHOD_NEW', get_option('server_debugger_method') == 'new') + # enable V4L2 support (linux only) conf.set('HAVE_V4L2', v4l2_support) @@ -192,19 +190,19 @@ 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_dir: join_paths(datadir, 'icons/hicolor/256x256/apps')) install_data('data/zsh-completion/_scrcpy', - install_dir: datadir / 'zsh/site-functions') + install_dir: join_paths(datadir, 'zsh/site-functions')) install_data('data/bash-completion/scrcpy', - install_dir: datadir / 'bash-completion/completions') + install_dir: join_paths(datadir, 'bash-completion/completions')) # Desktop entry file for application launchers if host_machine.system() == 'linux' # Install a launcher (ex: /usr/local/share/applications/scrcpy.desktop) install_data('data/scrcpy.desktop', - install_dir: datadir / 'applications') + install_dir: join_paths(datadir, 'applications')) install_data('data/scrcpy-console.desktop', - install_dir: datadir / 'applications') + install_dir: join_paths(datadir, 'applications')) endif @@ -279,9 +277,3 @@ if get_option('buildtype') == 'debug' 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 diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index 9c5374ae..6454c88e 100644 --- a/app/scrcpy-windows.rc +++ b/app/scrcpy-windows.rc @@ -13,7 +13,7 @@ BEGIN VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "OriginalFilename", "scrcpy.exe" VALUE "ProductName", "scrcpy" - VALUE "ProductVersion", "3.3.1" + VALUE "ProductVersion", "2.7" END END BLOCK "VarFileInfo" diff --git a/app/scrcpy.1 b/app/scrcpy.1 index d72fda13..a256c40e 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -19,10 +19,6 @@ provides display and control of Android devices connected on USB (or over TCP/IP .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). @@ -67,19 +63,13 @@ The available encoders can be listed by \fB\-\-list\-encoders\fR. .TP .BI "\-\-audio\-source " source -Select the audio source. Possible values are: +Select the audio source (output, mic or playback). - - "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. +The "output" source forwards the whole audio output, and disables playback on the device. + +The "playback" source captures the audio playback (Android apps can opt-out, so the whole output is not necessarily captured). + +The "mic" source captures the microphone. Default is output. @@ -103,18 +93,6 @@ 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. @@ -128,26 +106,28 @@ 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. +.BI "\-\-camera\-facing " facing +Select the device camera by its facing direction. + +Possible values are "front", "back" and "external". .TP -.BI "\-\-capture\-orientation " value -Possible values are 0, 90, 180, 270, flip0, flip90, flip180 and flip270, possibly prefixed by '@'. +.BI "\-\-camera\-fps " fps +Specify the camera capture frame rate. -The number represents the clockwise rotation in degrees; the "flip" keyword applies a horizontal flip before the rotation. +If not specified, Android's default frame rate (30 fps) is used. -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 "\-\-camera\-size " width\fRx\fIheight +Specify an explicit camera capture size. .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). +The values are expressed in the device natural orientation (typically, portrait for a phone, landscape for a tablet). Any +.B \-\-max\-size +value is computed on the cropped size. .TP .B \-d, \-\-select\-usb @@ -159,6 +139,12 @@ Also see \fB\-e\fR (\fB\-\-select\-tcpip\fR). .BI "\-\-disable\-screensaver" Disable screensaver while scrcpy is running. +.TP +.BI "\-\-display\-buffer " ms +Add a buffering delay (in milliseconds) before displaying. This increases latency to compensate for jitter. + +Default is 0 (no buffering). + .TP .BI "\-\-display\-id " id Specify the device display id to mirror. @@ -167,19 +153,6 @@ 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. @@ -254,10 +227,6 @@ Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+S This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically. -.TP -.B \-\-list\-apps -List Android apps installed on the device. - .TP .B \-\-list\-camera\-sizes List the valid camera capture sizes. @@ -274,6 +243,16 @@ List video and audio encoders available on the device. .B \-\-list\-displays List displays available on the device. +.TP +\fB\-\-lock\-video\-orientation\fR[=\fIvalue\fR] +Lock capture video orientation to \fIvalue\fR. + +Possible values are "unlocked", "initial" (locked to the initial orientation), 0, 90, 180, and 270. The values represent the clockwise rotation from the natural device orientation, in degrees. + +Default is "unlocked". + +Passing the option without argument is equivalent to passing "initial". + .TP .BI "\-m, \-\-max\-size " value Limit both the width and height of the video to \fIvalue\fR. The other dimension is computed so that the device aspect\-ratio is preserved. @@ -335,17 +314,6 @@ Disable device control (mirror the device in read\-only). .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. @@ -388,16 +356,6 @@ Do not forward mouse hover (mouse motion without any clicks) events. .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. @@ -408,7 +366,7 @@ Disable video playback on the computer. .TP .B \-\-no\-window -Disable scrcpy window. Implies --no-video-playback. +Disable scrcpy window. Implies --no-video-playback and --no-control. .TP .BI "\-\-orientation " value @@ -510,10 +468,6 @@ The device serial number. Mandatory only if several devices are connected to adb .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". @@ -524,22 +478,6 @@ For example, to use either LCtrl or LSuper for scrcpy shortcuts, pass "lctrl,lsu 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. @@ -547,15 +485,13 @@ 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. +.BI "\-\-tcpip\fR[=\fIip\fR[:\fIport\fR]] +Configure and reconnect the device over TCP/IP. If a destination address is provided, then scrcpy connects to this address before starting. The device must listen on the given TCP port (default is 5555). If no destination address is provided, then scrcpy attempts to find the IP address and adb port of the current device (typically connected over USB), enables TCP/IP mode if necessary, then connects to this address before starting. -Prefix the address with a '+' to force a reconnection. - .TP .BI "\-\-time\-limit " seconds Set the maximum mirroring time, in seconds. @@ -586,19 +522,13 @@ Default is "info" for release builds, "debug" for debug builds. .BI "\-\-v4l2-sink " /dev/videoN Output to v4l2loopback device. +It requires to lock the video orientation (see \fB\-\-lock\-video\-orientation\fR). + .TP .BI "\-\-v4l2-buffer " ms Add a buffering delay (in milliseconds) before pushing frames. This increases latency to compensate for jitter. -This option is similar to \fB\-\-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. +This option is similar to \fB\-\-display\-buffer\fR, but specific to V4L2 sink. Default is 0 (no buffering). @@ -707,10 +637,6 @@ Pause or re-pause display .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) @@ -801,11 +727,7 @@ 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) +Tilt (slide vertically with two fingers) .TP .B Drag & drop APK file @@ -852,7 +774,7 @@ Report bugs to . .SH COPYRIGHT Copyright \(co 2018 Genymobile -Copyright \(co 2018\-2025 Romain Vimont +Copyright \(co 2018\-2024 Romain Vimont Licensed under the Apache License, Version 2.0. diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index 9e9cfd6b..15c9c85a 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -4,11 +4,9 @@ #include #include #include -#include -#include "adb/adb_device.h" -#include "adb/adb_parser.h" -#include "util/env.h" +#include "adb_device.h" +#include "adb_parser.h" #include "util/file.h" #include "util/log.h" #include "util/process_intr.h" @@ -26,45 +24,15 @@ */ #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); -} +static const char *adb_executable; const char * sc_adb_get_executable(void) { + if (!adb_executable) { + adb_executable = getenv("ADB"); + if (!adb_executable) + adb_executable = "adb"; + } return adb_executable; } @@ -110,7 +78,7 @@ show_adb_installation_msg(void) { } pkg_managers[] = { {"apt", "apt install adb"}, {"apt-get", "apt-get install adb"}, - {"brew", "brew install --cask android-platform-tools"}, + {"brew", "brew cask install android-platform-tools"}, {"dnf", "dnf install android-tools"}, {"emerge", "emerge dev-util/android-tools"}, {"pacman", "pacman -S android-tools"}, @@ -413,7 +381,7 @@ sc_adb_connect(struct sc_intr *intr, const char *ip_port, unsigned flags) { // "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". + // "connected". char buf[128]; ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf) - 1); sc_pipe_close(pout); @@ -430,8 +398,7 @@ sc_adb_connect(struct sc_intr *intr, const char *ip_port, unsigned flags) { assert((size_t) r < sizeof(buf)); buf[r] = '\0'; - ok = !strncmp("connected", buf, sizeof("connected") - 1) - || !strncmp("already connected", buf, sizeof("already connected") - 1); + ok = !strncmp("connected", buf, sizeof("connected") - 1); if (!ok && !(flags & SC_ADB_NO_STDERR)) { // "adb connect" also prints errors to stdout. Since we capture it, // re-print the error to stderr. @@ -772,21 +739,3 @@ sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags) { return sc_adb_parse_device_ip(buf); } - -uint16_t -sc_adb_get_device_sdk_version(struct sc_intr *intr, const char *serial) { - char *sdk_version = - sc_adb_getprop(intr, serial, "ro.build.version.sdk", SC_ADB_SILENT); - if (!sdk_version) { - return 0; - } - - long value; - bool ok = sc_str_parse_integer(sdk_version, &value); - free(sdk_version); - if (!ok || value < 0 || value > 0xFFFF) { - return 0; - } - - return value; -} diff --git a/app/src/adb/adb.h b/app/src/adb/adb.h index e4903902..ffd532ea 100644 --- a/app/src/adb/adb.h +++ b/app/src/adb/adb.h @@ -6,7 +6,7 @@ #include #include -#include "adb/adb_device.h" +#include "adb_device.h" #include "util/intr.h" #define SC_ADB_NO_STDOUT (1 << 0) @@ -15,12 +15,6 @@ #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); @@ -120,10 +114,4 @@ sc_adb_getprop(struct sc_intr *intr, const char *serial, const char *prop, char * sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags); -/** - * Return the device SDK version. - */ -uint16_t -sc_adb_get_device_sdk_version(struct sc_intr *intr, const char *serial); - #endif diff --git a/app/src/adb/adb_device.h b/app/src/adb/adb_device.h index 308663ef..56393bcf 100644 --- a/app/src/adb/adb_device.h +++ b/app/src/adb/adb_device.h @@ -4,6 +4,7 @@ #include "common.h" #include +#include #include "util/vector.h" diff --git a/app/src/adb/adb_parser.c b/app/src/adb/adb_parser.c index 90a1b30b..66bb1854 100644 --- a/app/src/adb/adb_parser.c +++ b/app/src/adb/adb_parser.c @@ -3,7 +3,6 @@ #include #include #include -#include #include "util/log.h" #include "util/str.h" diff --git a/app/src/adb/adb_parser.h b/app/src/adb/adb_parser.h index b8738a35..f20349f6 100644 --- a/app/src/adb/adb_parser.h +++ b/app/src/adb/adb_parser.h @@ -3,9 +3,9 @@ #include "common.h" -#include +#include -#include "adb/adb_device.h" +#include "adb_device.h" /** * Parse the available devices from the output of `adb devices` diff --git a/app/src/adb/adb_tunnel.c b/app/src/adb/adb_tunnel.c index 43e80e13..fa936e4b 100644 --- a/app/src/adb/adb_tunnel.c +++ b/app/src/adb/adb_tunnel.c @@ -1,11 +1,11 @@ #include "adb_tunnel.h" #include -#include -#include "adb/adb.h" +#include "adb.h" #include "util/log.h" #include "util/net_intr.h" +#include "util/process_intr.h" static bool listen_on_port(struct sc_intr *intr, sc_socket socket, uint16_t port) { diff --git a/app/src/audio_player.c b/app/src/audio_player.c index 9413c2ea..274b6948 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -1,23 +1,138 @@ #include "audio_player.h" +#include +#include + #include "util/log.h" +//#define SC_AUDIO_PLAYER_DEBUG // uncomment to debug + +/** + * Real-time audio player with configurable latency + * + * As input, the player regularly receives AVFrames of decoded audio samples. + * As output, an SDL callback regularly requests audio samples to be played. + * In the middle, an audio buffer stores the samples produced but not consumed + * yet. + * + * The goal of the player is to feed the audio output with a latency as low as + * possible while avoiding buffer underrun (i.e. not being able to provide + * samples when requested). + * + * The player aims to feed the audio output with as little latency as possible + * while avoiding buffer underrun. To achieve this, it attempts to maintain the + * average buffering (the number of samples present in the buffer) around a + * target value. If this target buffering is too low, then buffer underrun will + * occur frequently. If it is too high, then latency will become unacceptable. + * This target value is configured using the scrcpy option --audio-buffer. + * + * The player cannot adjust the sample input rate (it receives samples produced + * in real-time) or the sample output rate (it must provide samples as + * requested by the audio output callback). Therefore, it may only apply + * compensation by resampling (converting _m_ input samples to _n_ output + * samples). + * + * The compensation itself is applied by libswresample (FFmpeg). It is + * configured using swr_set_compensation(). An important work for the player + * is to estimate the compensation value regularly and apply it. + * + * The estimated buffering level is the result of averaging the "natural" + * buffering (samples are produced and consumed by blocks, so it must be + * smoothed), and making instant adjustments resulting of its own actions + * (explicit compensation and silence insertion on underflow), which are not + * smoothed. + * + * Buffer underflow events can occur when packets arrive too late. In that case, + * the player inserts silence. Once the packets finally arrive (late), one + * strategy could be to drop the samples that were replaced by silence, in + * order to keep a minimal latency. However, dropping samples in case of buffer + * underflow is inadvisable, as it would temporarily increase the underflow + * even more and cause very noticeable audio glitches. + * + * Therefore, the player doesn't drop any sample on underflow. The compensation + * mechanism will absorb the delay introduced by the inserted silence. + */ + /** Downcast frame_sink to sc_audio_player */ #define DOWNCAST(SINK) container_of(SINK, struct sc_audio_player, frame_sink) +#define SC_AV_SAMPLE_FMT AV_SAMPLE_FMT_FLT #define SC_SDL_SAMPLE_FMT AUDIO_F32 +#define TO_BYTES(SAMPLES) sc_audiobuf_to_bytes(&ap->buf, (SAMPLES)) +#define TO_SAMPLES(BYTES) sc_audiobuf_to_samples(&ap->buf, (BYTES)) + static void SDLCALL sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) { struct sc_audio_player *ap = userdata; + // This callback is called with the lock used by SDL_LockAudioDevice() + assert(len_int > 0); size_t len = len_int; + uint32_t count = TO_SAMPLES(len); - assert(len % ap->audioreg.sample_size == 0); - uint32_t out_samples = len / ap->audioreg.sample_size; +#ifdef SC_AUDIO_PLAYER_DEBUG + LOGD("[Audio] SDL callback requests %" PRIu32 " samples", count); +#endif - sc_audio_regulator_pull(&ap->audioreg, stream, out_samples); + bool played = atomic_load_explicit(&ap->played, memory_order_relaxed); + if (!played) { + uint32_t buffered_samples = sc_audiobuf_can_read(&ap->buf); + // Wait until the buffer is filled up to at least target_buffering + // before playing + if (buffered_samples < ap->target_buffering) { + LOGV("[Audio] Inserting initial buffering silence: %" PRIu32 + " samples", count); + // Delay playback starting to reach the target buffering. Fill the + // whole buffer with silence (len is small compared to the + // arbitrary margin value). + memset(stream, 0, len); + return; + } + } + + uint32_t read = sc_audiobuf_read(&ap->buf, stream, count); + + if (read < count) { + uint32_t silence = count - read; + // Insert silence. In theory, the inserted silent samples replace the + // missing real samples, which will arrive later, so they should be + // dropped to keep the latency minimal. However, this would cause very + // audible glitches, so let the clock compensation restore the target + // latency. + LOGD("[Audio] Buffer underflow, inserting silence: %" PRIu32 " samples", + silence); + memset(stream + TO_BYTES(read), 0, TO_BYTES(silence)); + + bool received = atomic_load_explicit(&ap->received, + memory_order_relaxed); + if (received) { + // Inserting additional samples immediately increases buffering + atomic_fetch_add_explicit(&ap->underflow, silence, + memory_order_relaxed); + } + } + + atomic_store_explicit(&ap->played, true, memory_order_relaxed); +} + +static uint8_t * +sc_audio_player_get_swr_buf(struct sc_audio_player *ap, uint32_t min_samples) { + size_t min_buf_size = TO_BYTES(min_samples); + if (min_buf_size > ap->swr_buf_alloc_size) { + size_t new_size = min_buf_size + 4096; + uint8_t *buf = realloc(ap->swr_buf, new_size); + if (!buf) { + LOG_OOM(); + // Could not realloc to the requested size + return NULL; + } + ap->swr_buf = buf; + ap->swr_buf_alloc_size = new_size; + } + + return ap->swr_buf; } static bool @@ -25,21 +140,209 @@ 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); + SwrContext *swr_ctx = ap->swr_ctx; + + int64_t swr_delay = swr_get_delay(swr_ctx, ap->sample_rate); + // No need to av_rescale_rnd(), input and output sample rates are the same. + // Add more space (256) for clock compensation. + int dst_nb_samples = swr_delay + frame->nb_samples + 256; + + uint8_t *swr_buf = sc_audio_player_get_swr_buf(ap, dst_nb_samples); + if (!swr_buf) { + return false; + } + + int ret = swr_convert(swr_ctx, &swr_buf, dst_nb_samples, + (const uint8_t **) frame->data, frame->nb_samples); + if (ret < 0) { + LOGE("Resampling failed: %d", ret); + return false; + } + + // swr_convert() returns the number of samples which would have been + // written if the buffer was big enough. + uint32_t samples = MIN(ret, dst_nb_samples); +#ifdef SC_AUDIO_PLAYER_DEBUG + LOGD("[Audio] %" PRIu32 " samples written to buffer", samples); +#endif + + uint32_t cap = sc_audiobuf_capacity(&ap->buf); + if (samples > cap) { + // Very very unlikely: a single resampled frame should never + // exceed the audio buffer size (or something is very wrong). + // Ignore the first bytes in swr_buf to avoid memory corruption anyway. + swr_buf += TO_BYTES(samples - cap); + samples = cap; + } + + uint32_t skipped_samples = 0; + + uint32_t written = sc_audiobuf_write(&ap->buf, swr_buf, samples); + if (written < samples) { + uint32_t remaining = samples - written; + + // All samples that could be written without locking have been written, + // now we need to lock to drop/consume old samples + SDL_LockAudioDevice(ap->device); + + // Retry with the lock + written += sc_audiobuf_write(&ap->buf, + swr_buf + TO_BYTES(written), + remaining); + if (written < samples) { + remaining = samples - written; + // Still insufficient, drop old samples to make space + skipped_samples = sc_audiobuf_read(&ap->buf, NULL, remaining); + assert(skipped_samples == remaining); + } + + SDL_UnlockAudioDevice(ap->device); + + if (written < samples) { + // Now there is enough space + uint32_t w = sc_audiobuf_write(&ap->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(&ap->played, memory_order_relaxed); + if (played) { + underflow = atomic_exchange_explicit(&ap->underflow, 0, + memory_order_relaxed); + + max_buffered_samples = ap->target_buffering + + 12 * ap->output_buffer + + ap->target_buffering / 10; + } else { + // SDL playback not started yet, do not accumulate more than + // max_initial_buffering samples, this would cause unnecessary delay + // (and glitches to compensate) on start. + max_buffered_samples = ap->target_buffering + 2 * ap->output_buffer; + } + + uint32_t can_read = sc_audiobuf_can_read(&ap->buf); + if (can_read > max_buffered_samples) { + uint32_t skip_samples = 0; + + SDL_LockAudioDevice(ap->device); + can_read = sc_audiobuf_can_read(&ap->buf); + if (can_read > max_buffered_samples) { + skip_samples = can_read - max_buffered_samples; + uint32_t r = sc_audiobuf_read(&ap->buf, NULL, skip_samples); + assert(r == skip_samples); + (void) r; + skipped_samples += skip_samples; + } + SDL_UnlockAudioDevice(ap->device); + + if (skip_samples) { + if (played) { + LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32 + " samples", skip_samples); +#ifdef SC_AUDIO_PLAYER_DEBUG + } else { + LOGD("[Audio] Playback not started, skipping %" PRIu32 + " samples", skip_samples); +#endif + } + } + } + + atomic_store_explicit(&ap->received, true, memory_order_relaxed); + if (!played) { + // Nothing more to do + return true; + } + + // Number of samples added (or removed, if negative) for compensation + int32_t instant_compensation = (int32_t) written - frame->nb_samples; + // Inserting silence instantly increases buffering + int32_t inserted_silence = (int32_t) underflow; + // Dropping input samples instantly decreases buffering + int32_t dropped = (int32_t) skipped_samples; + + // The compensation must apply instantly, it must not be smoothed + ap->avg_buffering.avg += instant_compensation + inserted_silence - dropped; + if (ap->avg_buffering.avg < 0) { + // Since dropping samples instantly reduces buffering, the difference + // is applied immediately to the average value, assuming that the delay + // between the producer and the consumer will be caught up. + // + // However, when this assumption is not valid, the average buffering + // may decrease indefinitely. Prevent it to become negative to limit + // the consequences. + ap->avg_buffering.avg = 0; + } + + // However, the buffering level must be smoothed + sc_average_push(&ap->avg_buffering, can_read); + +#ifdef SC_AUDIO_PLAYER_DEBUG + LOGD("[Audio] can_read=%" PRIu32 " avg_buffering=%f", + can_read, sc_average_get(&ap->avg_buffering)); +#endif + + ap->samples_since_resync += written; + if (ap->samples_since_resync >= ap->sample_rate) { + // Recompute compensation every second + ap->samples_since_resync = 0; + + float avg = sc_average_get(&ap->avg_buffering); + int diff = ap->target_buffering - avg; + + // Enable compensation when the difference exceeds +/- 4ms. + // Disable compensation when the difference is lower than +/- 1ms. + int threshold = ap->compensation != 0 + ? ap->sample_rate / 1000 /* 1ms */ + : ap->sample_rate * 4 / 1000; /* 4ms */ + + if (abs(diff) < threshold) { + // Do not compensate for small values, the error is just noise + diff = 0; + } else if (diff < 0 && can_read < ap->target_buffering) { + // Do not accelerate if the instant buffering level is below the + // target, this would increase underflow + diff = 0; + } + // Compensate the diff over 4 seconds (but will be recomputed after 1 + // second) + int distance = 4 * ap->sample_rate; + // Limit compensation rate to 2% + int abs_max_diff = distance / 50; + diff = CLAMP(diff, -abs_max_diff, abs_max_diff); + LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32 + " compensation=%d", ap->target_buffering, avg, can_read, diff); + + if (diff != ap->compensation) { + int ret = swr_set_compensation(swr_ctx, diff, distance); + if (ret < 0) { + LOGW("Resampling compensation failed: %d", ret); + // not fatal + } else { + ap->compensation = diff; + } + } + } + + return true; } static bool sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, const AVCodecContext *ctx) { struct sc_audio_player *ap = DOWNCAST(sink); - #ifdef SCRCPY_LAVU_HAS_CHLAYOUT - assert(ctx->ch_layout.nb_channels > 0 && ctx->ch_layout.nb_channels < 256); - uint8_t nb_channels = ctx->ch_layout.nb_channels; + assert(ctx->ch_layout.nb_channels > 0); + unsigned nb_channels = ctx->ch_layout.nb_channels; #else int tmp = av_get_channel_layout_nb_channels(ctx->channel_layout); - assert(tmp > 0 && tmp < 256); - uint8_t nb_channels = tmp; + assert(tmp > 0); + unsigned nb_channels = tmp; #endif assert(ctx->sample_rate > 0); @@ -47,19 +350,17 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, 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; + ap->sample_rate = ctx->sample_rate; + ap->nb_channels = nb_channels; + ap->out_bytes_per_sample = out_bytes_per_sample; - 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; - } + ap->target_buffering = ap->target_buffering_delay * ap->sample_rate + / SC_TICK_FREQ; - uint64_t aout_samples = ap->output_buffer_duration * ctx->sample_rate + uint64_t aout_samples = ap->output_buffer_duration * ap->sample_rate / SC_TICK_FREQ; assert(aout_samples <= 0xFFFF); + ap->output_buffer = (uint16_t) aout_samples; SDL_AudioSpec desired = { .freq = ctx->sample_rate, @@ -74,10 +375,69 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, 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; } + SwrContext *swr_ctx = swr_alloc(); + if (!swr_ctx) { + LOG_OOM(); + goto error_close_audio_device; + } + ap->swr_ctx = swr_ctx; + +#ifdef SCRCPY_LAVU_HAS_CHLAYOUT + av_opt_set_chlayout(swr_ctx, "in_chlayout", &ctx->ch_layout, 0); + av_opt_set_chlayout(swr_ctx, "out_chlayout", &ctx->ch_layout, 0); +#else + av_opt_set_channel_layout(swr_ctx, "in_channel_layout", + ctx->channel_layout, 0); + av_opt_set_channel_layout(swr_ctx, "out_channel_layout", + ctx->channel_layout, 0); +#endif + + av_opt_set_int(swr_ctx, "in_sample_rate", ctx->sample_rate, 0); + av_opt_set_int(swr_ctx, "out_sample_rate", ctx->sample_rate, 0); + + av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt", ctx->sample_fmt, 0); + av_opt_set_sample_fmt(swr_ctx, "out_sample_fmt", SC_AV_SAMPLE_FMT, 0); + + int ret = swr_init(swr_ctx); + if (ret) { + LOGE("Failed to initialize the resampling context"); + goto error_free_swr_ctx; + } + + // Use a ring-buffer of the target buffering size plus 1 second between the + // producer and the consumer. It's too big on purpose, to guarantee that + // the producer and the consumer will be able to access it in parallel + // without locking. + uint32_t audiobuf_samples = ap->target_buffering + ap->sample_rate; + + size_t sample_size = ap->nb_channels * ap->out_bytes_per_sample; + bool ok = sc_audiobuf_init(&ap->buf, sample_size, audiobuf_samples); + if (!ok) { + goto error_free_swr_ctx; + } + + size_t initial_swr_buf_size = TO_BYTES(4096); + ap->swr_buf = malloc(initial_swr_buf_size); + if (!ap->swr_buf) { + LOG_OOM(); + goto error_destroy_audiobuf; + } + ap->swr_buf_alloc_size = initial_swr_buf_size; + + // Samples are produced and consumed by blocks, so the buffering must be + // smoothed to get a relatively stable value. + sc_average_init(&ap->avg_buffering, 128); + ap->samples_since_resync = 0; + + ap->received = false; + atomic_init(&ap->played, false); + atomic_init(&ap->received, false); + atomic_init(&ap->underflow, 0); + ap->compensation = 0; + // The thread calling open() is the thread calling push(), which fills the // audio buffer consumed by the SDL audio thread. ok = sc_thread_set_priority(SC_THREAD_PRIORITY_TIME_CRITICAL); @@ -89,6 +449,15 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, SDL_PauseAudioDevice(ap->device, 0); return true; + +error_destroy_audiobuf: + sc_audiobuf_destroy(&ap->buf); +error_free_swr_ctx: + swr_free(&ap->swr_ctx); +error_close_audio_device: + SDL_CloseAudioDevice(ap->device); + + return false; } static void @@ -99,7 +468,9 @@ sc_audio_player_frame_sink_close(struct sc_frame_sink *sink) { SDL_PauseAudioDevice(ap->device, 1); SDL_CloseAudioDevice(ap->device); - sc_audio_regulator_destroy(&ap->audioreg); + free(ap->swr_buf); + sc_audiobuf_destroy(&ap->buf); + swr_free(&ap->swr_ctx); } void diff --git a/app/src/audio_player.h b/app/src/audio_player.h index 5a66d43b..0c677363 100644 --- a/app/src/audio_player.h +++ b/app/src/audio_player.h @@ -3,27 +3,78 @@ #include "common.h" -#include +#include +#include +#include +#include +#include -#include "audio_regulator.h" #include "trait/frame_sink.h" +#include "util/audiobuf.h" +#include "util/average.h" +#include "util/thread.h" #include "util/tick.h" struct sc_audio_player { struct sc_frame_sink frame_sink; + SDL_AudioDeviceID device; + // The target buffering between the producer and the consumer. This value // is directly use for compensation. // Since audio capture and/or encoding on the device typically produce // blocks of 960 samples (20ms) or 1024 samples (~21.3ms), this target // value should be higher. sc_tick target_buffering_delay; + uint32_t target_buffering; // in samples - // SDL audio output buffer size + // SDL audio output buffer size. sc_tick output_buffer_duration; + uint16_t output_buffer; - SDL_AudioDeviceID device; - struct sc_audio_regulator audioreg; + // Audio buffer to communicate between the receiver and the SDL audio + // callback + struct sc_audiobuf buf; + + // Resampler (only used from the receiver thread) + struct SwrContext *swr_ctx; + + // The sample rate is the same for input and output + unsigned sample_rate; + // The number of channels is the same for input and output + unsigned nb_channels; + // The number of bytes per sample for a single channel + size_t out_bytes_per_sample; + + // Target buffer for resampling (only used by the receiver thread) + uint8_t *swr_buf; + size_t swr_buf_alloc_size; + + // Number of buffered samples (may be negative on underflow) (only used by + // the receiver thread) + struct sc_average avg_buffering; + // Count the number of samples to trigger a compensation update regularly + // (only used by the receiver thread) + uint32_t samples_since_resync; + + // Number of silence samples inserted since the last received packet + atomic_uint_least32_t underflow; + + // Current applied compensation value (only used by the receiver thread) + int compensation; + + // Set to true the first time a sample is received + atomic_bool received; + + // Set to true the first time the SDL callback is called + atomic_bool played; + + const struct sc_audio_player_callbacks *cbs; + void *cbs_userdata; +}; + +struct sc_audio_player_callbacks { + void (*on_ended)(struct sc_audio_player *ap, bool success, void *userdata); }; void diff --git a/app/src/audio_regulator.c b/app/src/audio_regulator.c deleted file mode 100644 index 16fdd08b..00000000 --- a/app/src/audio_regulator.c +++ /dev/null @@ -1,456 +0,0 @@ -#include "audio_regulator.h" - -#include -#include -#include -#include -#include -#include - -#include "util/log.h" - -//#define SC_AUDIO_REGULATOR_DEBUG // uncomment to debug - -/** - * Real-time audio regulator with configurable latency - * - * As input, the regulator regularly receives AVFrames of decoded audio samples. - * As output, the audio player regularly requests audio samples to be played. - * In the middle, an audio buffer stores the samples produced but not consumed - * yet. - * - * The goal of the regulator is to feed the audio player with a latency as low - * as possible while avoiding buffer underrun (i.e. not being able to provide - * samples when requested). - * - * To achieve this, it attempts to maintain the average buffering (the number - * of samples present in the buffer) around a target value. If this target - * buffering is too low, then buffer underrun will occur frequently. If it is - * too high, then latency will become unacceptable. This target value is - * configured using the scrcpy option --audio-buffer. - * - * The regulator cannot adjust the sample input rate (it receives samples - * produced in real-time) or the sample output rate (it must provide samples as - * requested by the audio player). Therefore, it may only apply compensation by - * resampling (converting _m_ input samples to _n_ output samples). - * - * The compensation itself is applied by libswresample (FFmpeg). It is - * configured using swr_set_compensation(). An important work for the regulator - * is to estimate the compensation value regularly and apply it. - * - * The estimated buffering level is the result of averaging the "natural" - * buffering (samples are produced and consumed by blocks, so it must be - * smoothed), and making instant adjustments resulting of its own actions - * (explicit compensation and silence insertion on underflow), which are not - * smoothed. - * - * Buffer underflow events can occur when packets arrive too late. In that case, - * the regulator inserts silence. Once the packets finally arrive (late), one - * strategy could be to drop the samples that were replaced by silence, in - * order to keep a minimal latency. However, dropping samples in case of buffer - * underflow is inadvisable, as it would temporarily increase the underflow - * even more and cause very noticeable audio glitches. - * - * Therefore, the regulator doesn't drop any sample on underflow. The - * compensation mechanism will absorb the delay introduced by the inserted - * silence. - */ - -#define TO_BYTES(SAMPLES) sc_audiobuf_to_bytes(&ar->buf, (SAMPLES)) -#define TO_SAMPLES(BYTES) sc_audiobuf_to_samples(&ar->buf, (BYTES)) - -void -sc_audio_regulator_pull(struct sc_audio_regulator *ar, uint8_t *out, - uint32_t out_samples) { -#ifdef SC_AUDIO_REGULATOR_DEBUG - LOGD("[Audio] Audio regulator pulls %" PRIu32 " samples", out_samples); -#endif - - // A lock is necessary in the rare case where the producer needs to drop - // samples already pushed (when the buffer is full) - sc_mutex_lock(&ar->mutex); - - bool played = atomic_load_explicit(&ar->played, memory_order_relaxed); - if (!played) { - uint32_t buffered_samples = sc_audiobuf_can_read(&ar->buf); - // Wait until the buffer is filled up to at least target_buffering - // before playing - if (buffered_samples < ar->target_buffering) { -#ifdef SC_AUDIO_REGULATOR_DEBUG - LOGD("[Audio] Inserting initial buffering silence: %" PRIu32 - " samples", out_samples); -#endif - // Delay playback starting to reach the target buffering. Fill the - // whole buffer with silence (len is small compared to the - // arbitrary margin value). - memset(out, 0, out_samples * ar->sample_size); - sc_mutex_unlock(&ar->mutex); - return; - } - } - - uint32_t read = sc_audiobuf_read(&ar->buf, out, out_samples); - - sc_mutex_unlock(&ar->mutex); - - if (read < out_samples) { - uint32_t silence = out_samples - read; - // Insert silence. In theory, the inserted silent samples replace the - // missing real samples, which will arrive later, so they should be - // dropped to keep the latency minimal. However, this would cause very - // audible glitches, so let the clock compensation restore the target - // latency. -#ifdef SC_AUDIO_REGULATOR_DEBUG - LOGD("[Audio] Buffer underflow, inserting silence: %" PRIu32 " samples", - silence); -#endif - memset(out + TO_BYTES(read), 0, TO_BYTES(silence)); - - bool received = atomic_load_explicit(&ar->received, - memory_order_relaxed); - if (received) { - // Inserting additional samples immediately increases buffering - atomic_fetch_add_explicit(&ar->underflow, silence, - memory_order_relaxed); - } - } - - atomic_store_explicit(&ar->played, true, memory_order_relaxed); -} - -static uint8_t * -sc_audio_regulator_get_swr_buf(struct sc_audio_regulator *ar, - uint32_t min_samples) { - size_t min_buf_size = TO_BYTES(min_samples); - if (min_buf_size > ar->swr_buf_alloc_size) { - size_t new_size = min_buf_size + 4096; - uint8_t *buf = realloc(ar->swr_buf, new_size); - if (!buf) { - LOG_OOM(); - // Could not realloc to the requested size - return NULL; - } - ar->swr_buf = buf; - ar->swr_buf_alloc_size = new_size; - } - - return ar->swr_buf; -} - -bool -sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) { - SwrContext *swr_ctx = ar->swr_ctx; - - uint32_t input_samples = frame->nb_samples; - - assert(frame->pts >= 0); - int64_t pts = frame->pts; - if (ar->next_expected_pts && pts - ar->next_expected_pts > 100000) { - LOGV("[Audio] Discontinuity detected: %" PRIi64 "µs", - pts - ar->next_expected_pts); - // More than 100ms: consider it as a discontinuity - // (typically because silence packets were not captured) - uint32_t can_read = sc_audiobuf_can_read(&ar->buf); - if (input_samples + can_read < ar->target_buffering) { - // Adjust buffering to the target value directly - uint32_t silence = ar->target_buffering - can_read - input_samples; - sc_audiobuf_write_silence(&ar->buf, silence); - } - - // Reset state - ar->avg_buffering.avg = ar->target_buffering; - int ret = swr_set_compensation(swr_ctx, 0, 0); - (void) ret; - assert(!ret); // disabling compensation should never fail - ar->compensation_active = false; - ar->samples_since_resync = 0; - atomic_store_explicit(&ar->underflow, 0, memory_order_relaxed); - } - - int64_t packet_duration = input_samples * INT64_C(1000000) - / ar->sample_rate; - ar->next_expected_pts = pts + packet_duration; - - int64_t swr_delay = swr_get_delay(swr_ctx, ar->sample_rate); - // No need to av_rescale_rnd(), input and output sample rates are the same. - // Add more space (256) for clock compensation. - int dst_nb_samples = swr_delay + frame->nb_samples + 256; - - uint8_t *swr_buf = sc_audio_regulator_get_swr_buf(ar, dst_nb_samples); - if (!swr_buf) { - return false; - } - - int ret = swr_convert(swr_ctx, &swr_buf, dst_nb_samples, - (const uint8_t **) frame->data, frame->nb_samples); - if (ret < 0) { - LOGE("Resampling failed: %d", ret); - return false; - } - - // swr_convert() returns the number of samples which would have been - // written if the buffer was big enough. - uint32_t samples = MIN(ret, dst_nb_samples); -#ifdef SC_AUDIO_REGULATOR_DEBUG - LOGD("[Audio] %" PRIu32 " samples written to buffer", samples); -#endif - - uint32_t cap = sc_audiobuf_capacity(&ar->buf); - if (samples > cap) { - // Very very unlikely: a single resampled frame should never - // exceed the audio buffer size (or something is very wrong). - // Ignore the first bytes in swr_buf to avoid memory corruption anyway. - swr_buf += TO_BYTES(samples - cap); - samples = cap; - } - - uint32_t skipped_samples = 0; - - uint32_t written = sc_audiobuf_write(&ar->buf, swr_buf, samples); - if (written < samples) { - uint32_t remaining = samples - written; - - // All samples that could be written without locking have been written, - // now we need to lock to drop/consume old samples - sc_mutex_lock(&ar->mutex); - - // Retry with the lock - written += sc_audiobuf_write(&ar->buf, - swr_buf + TO_BYTES(written), - remaining); - if (written < samples) { - remaining = samples - written; - // Still insufficient, drop old samples to make space - skipped_samples = sc_audiobuf_read(&ar->buf, NULL, remaining); - assert(skipped_samples == remaining); - } - - sc_mutex_unlock(&ar->mutex); - - if (written < samples) { - // Now there is enough space - uint32_t w = sc_audiobuf_write(&ar->buf, - swr_buf + TO_BYTES(written), - remaining); - assert(w == remaining); - (void) w; - } - } - - uint32_t underflow = 0; - uint32_t max_buffered_samples; - bool played = atomic_load_explicit(&ar->played, memory_order_relaxed); - if (played) { - underflow = atomic_exchange_explicit(&ar->underflow, 0, - memory_order_relaxed); - ar->underflow_report += underflow; - - max_buffered_samples = ar->target_buffering * 11 / 10 - + 60 * ar->sample_rate / 1000 /* 60 ms */; - } else { - // Playback not started yet, do not accumulate more than - // max_initial_buffering samples, this would cause unnecessary delay - // (and glitches to compensate) on start. - max_buffered_samples = ar->target_buffering - + 10 * ar->sample_rate / 1000 /* 10 ms */; - } - - uint32_t can_read = sc_audiobuf_can_read(&ar->buf); - if (can_read > max_buffered_samples) { - uint32_t skip_samples = 0; - - sc_mutex_lock(&ar->mutex); - can_read = sc_audiobuf_can_read(&ar->buf); - if (can_read > max_buffered_samples) { - skip_samples = can_read - max_buffered_samples; - uint32_t r = sc_audiobuf_read(&ar->buf, NULL, skip_samples); - assert(r == skip_samples); - (void) r; - skipped_samples += skip_samples; - } - sc_mutex_unlock(&ar->mutex); - - if (skip_samples) { - if (played) { - LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32 - " samples", skip_samples); -#ifdef SC_AUDIO_REGULATOR_DEBUG - } else { - LOGD("[Audio] Playback not started, skipping %" PRIu32 - " samples", skip_samples); -#endif - } - } - } - - atomic_store_explicit(&ar->received, true, memory_order_relaxed); - if (!played) { - // Nothing more to do - return true; - } - - // Number of samples added (or removed, if negative) for compensation - int32_t instant_compensation = (int32_t) written - input_samples; - // Inserting silence instantly increases buffering - int32_t inserted_silence = (int32_t) underflow; - // Dropping input samples instantly decreases buffering - int32_t dropped = (int32_t) skipped_samples; - - // The compensation must apply instantly, it must not be smoothed - ar->avg_buffering.avg += instant_compensation + inserted_silence - dropped; - if (ar->avg_buffering.avg < 0) { - // Since dropping samples instantly reduces buffering, the difference - // is applied immediately to the average value, assuming that the delay - // between the producer and the consumer will be caught up. - // - // However, when this assumption is not valid, the average buffering - // may decrease indefinitely. Prevent it to become negative to limit - // the consequences. - ar->avg_buffering.avg = 0; - } - - // However, the buffering level must be smoothed - sc_average_push(&ar->avg_buffering, can_read); - -#ifdef SC_AUDIO_REGULATOR_DEBUG - LOGD("[Audio] can_read=%" PRIu32 " avg_buffering=%f", - can_read, sc_average_get(&ar->avg_buffering)); -#endif - - ar->samples_since_resync += written; - if (ar->samples_since_resync >= ar->sample_rate) { - // Recompute compensation every second - ar->samples_since_resync = 0; - - float avg = sc_average_get(&ar->avg_buffering); - int diff = ar->target_buffering - avg; - - // Enable compensation when the difference exceeds +/- 4ms. - // Disable compensation when the difference is lower than +/- 1ms. - int threshold = ar->compensation_active - ? ar->sample_rate / 1000 /* 1ms */ - : ar->sample_rate * 4 / 1000; /* 4ms */ - - if (abs(diff) < threshold) { - // Do not compensate for small values, the error is just noise - diff = 0; - } else if (diff < 0 && can_read < ar->target_buffering) { - // Do not accelerate if the instant buffering level is below the - // target, this would increase underflow - diff = 0; - } - // Compensate the diff over 4 seconds (but will be recomputed after 1 - // second) - int distance = 4 * ar->sample_rate; - // Limit compensation rate to 2% - int abs_max_diff = distance / 50; - diff = CLAMP(diff, -abs_max_diff, abs_max_diff); - LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32 - " compensation=%d (underflow=%" PRIu32 ")", - ar->target_buffering, avg, can_read, diff, ar->underflow_report); - ar->underflow_report = 0; - - int ret = swr_set_compensation(swr_ctx, diff, distance); - if (ret < 0) { - LOGW("Resampling compensation failed: %d", ret); - // not fatal - } else { - ar->compensation_active = diff != 0; - } - } - - return true; -} - -bool -sc_audio_regulator_init(struct sc_audio_regulator *ar, size_t sample_size, - const AVCodecContext *ctx, uint32_t target_buffering) { - SwrContext *swr_ctx = swr_alloc(); - if (!swr_ctx) { - LOG_OOM(); - return false; - } - ar->swr_ctx = swr_ctx; - -#ifdef SCRCPY_LAVU_HAS_CHLAYOUT - av_opt_set_chlayout(swr_ctx, "in_chlayout", &ctx->ch_layout, 0); - av_opt_set_chlayout(swr_ctx, "out_chlayout", &ctx->ch_layout, 0); -#else - av_opt_set_channel_layout(swr_ctx, "in_channel_layout", - ctx->channel_layout, 0); - av_opt_set_channel_layout(swr_ctx, "out_channel_layout", - ctx->channel_layout, 0); -#endif - - av_opt_set_int(swr_ctx, "in_sample_rate", ctx->sample_rate, 0); - av_opt_set_int(swr_ctx, "out_sample_rate", ctx->sample_rate, 0); - - av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt", ctx->sample_fmt, 0); - av_opt_set_sample_fmt(swr_ctx, "out_sample_fmt", SC_AV_SAMPLE_FMT, 0); - - int ret = swr_init(swr_ctx); - if (ret) { - LOGE("Failed to initialize the resampling context"); - goto error_free_swr_ctx; - } - - bool ok = sc_mutex_init(&ar->mutex); - if (!ok) { - goto error_free_swr_ctx; - } - - ar->target_buffering = target_buffering; - ar->sample_size = sample_size; - ar->sample_rate = ctx->sample_rate; - - // Use a ring-buffer of the target buffering size plus 1 second between the - // producer and the consumer. It's too big on purpose, to guarantee that - // the producer and the consumer will be able to access it in parallel - // without locking. - uint32_t audiobuf_samples = target_buffering + ar->sample_rate; - - ok = sc_audiobuf_init(&ar->buf, sample_size, audiobuf_samples); - if (!ok) { - goto error_destroy_mutex; - } - - size_t initial_swr_buf_size = TO_BYTES(4096); - ar->swr_buf = malloc(initial_swr_buf_size); - if (!ar->swr_buf) { - LOG_OOM(); - goto error_destroy_audiobuf; - } - ar->swr_buf_alloc_size = initial_swr_buf_size; - - // Samples are produced and consumed by blocks, so the buffering must be - // smoothed to get a relatively stable value. - sc_average_init(&ar->avg_buffering, 128); - ar->samples_since_resync = 0; - - ar->received = false; - atomic_init(&ar->played, false); - atomic_init(&ar->received, false); - atomic_init(&ar->underflow, 0); - ar->underflow_report = 0; - ar->compensation_active = false; - ar->next_expected_pts = 0; - - return true; - -error_destroy_audiobuf: - sc_audiobuf_destroy(&ar->buf); -error_destroy_mutex: - sc_mutex_destroy(&ar->mutex); -error_free_swr_ctx: - swr_free(&ar->swr_ctx); - - return false; -} - -void -sc_audio_regulator_destroy(struct sc_audio_regulator *ar) { - free(ar->swr_buf); - sc_audiobuf_destroy(&ar->buf); - sc_mutex_destroy(&ar->mutex); - swr_free(&ar->swr_ctx); -} diff --git a/app/src/audio_regulator.h b/app/src/audio_regulator.h deleted file mode 100644 index 4e18fe08..00000000 --- a/app/src/audio_regulator.h +++ /dev/null @@ -1,79 +0,0 @@ -#ifndef SC_AUDIO_REGULATOR_H -#define SC_AUDIO_REGULATOR_H - -#include "common.h" - -#include -#include -#include -#include -#include -#include -#include "util/audiobuf.h" -#include "util/average.h" -#include "util/thread.h" - -#define SC_AV_SAMPLE_FMT AV_SAMPLE_FMT_FLT - -struct sc_audio_regulator { - sc_mutex mutex; - - // Target buffering between the producer and the consumer (in samples) - uint32_t target_buffering; - - // Audio buffer to communicate between the receiver and the player - struct sc_audiobuf buf; - - // Resampler (only used from the receiver thread) - struct SwrContext *swr_ctx; - - // The sample rate is the same for input and output - uint32_t sample_rate; - // The number of bytes per sample (for all channels) - size_t sample_size; - - // Target buffer for resampling (only used by the receiver thread) - uint8_t *swr_buf; - size_t swr_buf_alloc_size; - - // Number of buffered samples (may be negative on underflow) (only used by - // the receiver thread) - struct sc_average avg_buffering; - // Count the number of samples to trigger a compensation update regularly - // (only used by the receiver thread) - uint32_t samples_since_resync; - - // Number of silence samples inserted since the last received packet - atomic_uint_least32_t underflow; - - // Number of silence samples inserted since the last log - uint32_t underflow_report; - - // Non-zero compensation applied (only used by the receiver thread) - bool compensation_active; - - // Set to true the first time a sample is received - atomic_bool received; - - // Set to true the first time samples are pulled by the player - atomic_bool played; - - // PTS of the next expected packet (useful to detect discontinuities) - int64_t next_expected_pts; -}; - -bool -sc_audio_regulator_init(struct sc_audio_regulator *ar, size_t sample_size, - const AVCodecContext *ctx, uint32_t target_buffering); - -void -sc_audio_regulator_destroy(struct sc_audio_regulator *ar); - -bool -sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame); - -void -sc_audio_regulator_pull(struct sc_audio_regulator *ar, uint8_t *out, - uint32_t samples); - -#endif diff --git a/app/src/cli.c b/app/src/cli.c index b2e3e30a..3c1f9a1b 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -5,7 +5,6 @@ #include #include #include -#include #include #include "options.h" @@ -14,7 +13,6 @@ #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) @@ -52,7 +50,6 @@ enum { OPT_POWER_OFF_ON_CLOSE, OPT_V4L2_SINK, OPT_DISPLAY_BUFFER, - OPT_VIDEO_BUFFER, OPT_V4L2_BUFFER, OPT_TUNNEL_HOST, OPT_TUNNEL_PORT, @@ -105,15 +102,6 @@ enum { 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 { @@ -155,13 +143,6 @@ static const struct sc_option options[] = { .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", @@ -217,31 +198,13 @@ static const struct sc_option options[] = { .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 " + .text = "Select the audio source (output, mic or playback).\n" + "The \"output\" source forwards the whole audio output, and " + "disables playback on the device.\n" + "The \"playback\" source 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" + "The \"mic\" source captures the microphone.\n" "Default is output.", }, { @@ -277,6 +240,14 @@ static const struct sc_option options[] = { "ratio), \":\" (e.g. \"4:3\") or \"\" (e.g. " "\"1.6\")." }, + { + .longopt_id = OPT_CAMERA_ID, + .longopt = "camera-id", + .argdesc = "id", + .text = "Specify the device camera id to mirror.\n" + "The available camera ids can be listed by:\n" + " scrcpy --list-cameras", + }, { .longopt_id = OPT_CAMERA_FACING, .longopt = "camera-facing", @@ -284,14 +255,6 @@ static const struct sc_option options[] = { .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", @@ -299,14 +262,6 @@ static const struct sc_option options[] = { "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", @@ -314,21 +269,12 @@ static const struct sc_option options[] = { .text = "Specify an explicit camera capture size.", }, { - .longopt_id = OPT_CAPTURE_ORIENTATION, - .longopt = "capture-orientation", + .longopt_id = OPT_CAMERA_FPS, + .longopt = "camera-fps", .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.", + .text = "Specify the camera capture frame rate.\n" + "If not specified, Android's default frame rate (30 fps) is " + "used.", }, { // Not really deprecated (--codec has never been released), but without @@ -351,7 +297,8 @@ static const struct sc_option options[] = { .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).", + "(typically, portrait for a phone, landscape for a tablet). " + "Any --max-size value is computed on the cropped size.", }, { .shortopt = 'd', @@ -371,10 +318,12 @@ static const struct sc_option options[] = { .argdesc = "id", }, { - // deprecated .longopt_id = OPT_DISPLAY_BUFFER, .longopt = "display-buffer", .argdesc = "ms", + .text = "Add a buffering delay (in milliseconds) before displaying. " + "This increases latency to compensate for jitter.\n" + "Default is 0 (no buffering).", }, { .longopt_id = OPT_DISPLAY_ID, @@ -385,19 +334,6 @@ static const struct sc_option options[] = { " 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", @@ -506,11 +442,6 @@ static const struct sc_option options[] = { "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", @@ -532,10 +463,18 @@ static const struct sc_option options[] = { .text = "List video and audio encoders available on the device.", }, { - // deprecated .longopt_id = OPT_LOCK_VIDEO_ORIENTATION, .longopt = "lock-video-orientation", .argdesc = "value", + .optional_arg = true, + .text = "Lock capture video orientation to value.\n" + "Possible values are \"unlocked\", \"initial\" (locked to the " + "initial orientation), 0, 90, 180 and 270. The values " + "represent the clockwise rotation from the natural device " + "orientation, in degrees.\n" + "Default is \"unlocked\".\n" + "Passing the option without argument is equivalent to passing " + "\"initial\".", }, { .shortopt = 'm', @@ -618,20 +557,6 @@ static const struct sc_option options[] = { .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", @@ -694,20 +619,6 @@ static const struct sc_option options[] = { .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", @@ -721,7 +632,8 @@ static const struct sc_option options[] = { { .longopt_id = OPT_NO_WINDOW, .longopt = "no-window", - .text = "Disable scrcpy window. Implies --no-video-playback.", + .text = "Disable scrcpy window. Implies --no-video-playback and " + "--no-control.", }, { .longopt_id = OPT_ORIENTATION, @@ -859,13 +771,6 @@ static const struct sc_option options[] = { .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", @@ -879,20 +784,6 @@ static const struct sc_option options[] = { "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", @@ -903,17 +794,16 @@ static const struct sc_option options[] = { { .longopt_id = OPT_TCPIP, .longopt = "tcpip", - .argdesc = "[+]ip[:port]", + .argdesc = "ip[:port]", .optional_arg = true, - .text = "Configure and connect the device over TCP/IP.\n" + .text = "Configure and reconnect the device over TCP/IP.\n" "If a destination address is provided, then scrcpy connects to " "this address before starting. The device must listen on the " "given TCP port (default is 5555).\n" "If no destination address is provided, then scrcpy attempts " "to find the IP address of the current device (typically " "connected over USB), enables TCP/IP mode, then connects to " - "this address before starting.\n" - "Prefix the address with a '+' to force a reconnection.", + "this address before starting.", }, { .longopt_id = OPT_TIME_LIMIT, @@ -961,6 +851,8 @@ static const struct sc_option options[] = { .longopt = "v4l2-sink", .argdesc = "/dev/videoN", .text = "Output to v4l2loopback device.\n" + "It requires to lock the video orientation (see " + "--lock-video-orientation).\n" "This feature is only available on Linux.", }, { @@ -969,20 +861,11 @@ static const struct sc_option options[] = { .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 " + "This option is similar to --display-buffer, but specific to " "V4L2 sink.\n" "Default is 0 (no buffering).\n" "This option is only available on Linux.", }, - { - .longopt_id = OPT_VIDEO_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", @@ -1094,10 +977,6 @@ static const struct sc_shortcut shortcuts[] = { .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)", @@ -1193,11 +1072,7 @@ static const struct sc_shortcut shortcuts[] = { }, { .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)", + .text = "Tilt (slide vertically with two fingers)", }, { .shortcuts = { "Drag & drop APK file" }, @@ -1647,24 +1522,77 @@ parse_audio_output_buffer(const char *s, sc_tick *tick) { } 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; +parse_lock_video_orientation(const char *s, + enum sc_lock_video_orientation *lock_mode) { + if (!s || !strcmp(s, "initial")) { + // Without argument, lock the initial orientation + *lock_mode = SC_LOCK_VIDEO_ORIENTATION_INITIAL; return true; } - if (!strcmp(s, "fallback")) { - *policy = SC_DISPLAY_IME_POLICY_FALLBACK; + + if (!strcmp(s, "unlocked")) { + *lock_mode = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED; return true; } - if (!strcmp(s, "hide")) { - *policy = SC_DISPLAY_IME_POLICY_HIDE; + + if (!strcmp(s, "0")) { + *lock_mode = SC_LOCK_VIDEO_ORIENTATION_0; return true; } - LOGE("Unsupported display IME policy: %s (expected local, fallback or " - "hide)", s); + + if (!strcmp(s, "90")) { + *lock_mode = SC_LOCK_VIDEO_ORIENTATION_90; + return true; + } + + if (!strcmp(s, "180")) { + *lock_mode = SC_LOCK_VIDEO_ORIENTATION_180; + return true; + } + + if (!strcmp(s, "270")) { + *lock_mode = SC_LOCK_VIDEO_ORIENTATION_270; + return true; + } + + if (!strcmp(s, "1")) { + LOGW("--lock-video-orientation=1 is deprecated, use " + "--lock-video-orientation=270 instead."); + *lock_mode = SC_LOCK_VIDEO_ORIENTATION_270; + return true; + } + + if (!strcmp(s, "2")) { + LOGW("--lock-video-orientation=2 is deprecated, use " + "--lock-video-orientation=180 instead."); + *lock_mode = SC_LOCK_VIDEO_ORIENTATION_180; + return true; + } + + if (!strcmp(s, "3")) { + LOGW("--lock-video-orientation=3 is deprecated, use " + "--lock-video-orientation=90 instead."); + *lock_mode = SC_LOCK_VIDEO_ORIENTATION_90; + return true; + } + + LOGE("Unsupported --lock-video-orientation value: %s (expected initial, " + "unlocked, 0, 90, 180 or 270).", s); return false; } +static bool +parse_rotation(const char *s, uint8_t *rotation) { + long value; + bool ok = parse_integer_arg(s, &value, false, 0, 3, "rotation"); + if (!ok) { + return false; + } + + *rotation = (uint8_t) value; + return true; +} + static bool parse_orientation(const char *s, enum sc_orientation *orientation) { if (!strcmp(s, "0")) { @@ -1704,32 +1632,6 @@ parse_orientation(const char *s, enum sc_orientation *orientation) { 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" @@ -2054,50 +1956,8 @@ parse_audio_source(const char *optarg, enum sc_audio_source *source) { 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); + LOGE("Unsupported audio source: %s (expected output, mic or playback)", + optarg); return false; } @@ -2242,20 +2102,6 @@ parse_time_limit(const char *s, sc_tick *tick) { 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")) { @@ -2381,8 +2227,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->crop = optarg; break; case OPT_DISPLAY: - LOGE("--display has been removed, use --display-id instead."); - return false; + LOGW("--display is deprecated, use --display-id instead."); + // fall through case OPT_DISPLAY_ID: if (!parse_display_id(optarg, &opts->display_id)) { return false; @@ -2446,13 +2292,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], "--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)) { + if (!parse_lock_video_orientation(optarg, + &opts->lock_video_orientation)) { return false; } break; @@ -2470,9 +2311,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->control = false; break; case OPT_NO_DISPLAY: - LOGE("--no-display has been removed, use --no-playback " - "instead."); - return false; + LOGW("--no-display is deprecated, use --no-playback instead."); + // fall through case 'N': opts->video_playback = false; opts->audio_playback = false; @@ -2558,9 +2398,32 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], 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; + LOGW("--rotation is deprecated, use --display-orientation " + "instead."); + uint8_t rotation; + if (!parse_rotation(optarg, &rotation)) { + return false; + } + assert(rotation <= 3); + switch (rotation) { + case 0: + opts->display_orientation = SC_ORIENTATION_0; + break; + case 1: + // rotation 1 was 90° counterclockwise, but orientation + // is expressed clockwise + opts->display_orientation = SC_ORIENTATION_270; + break; + case 2: + opts->display_orientation = SC_ORIENTATION_180; + break; + case 3: + // rotation 3 was 270° counterclockwise, but orientation + // is expressed clockwise + opts->display_orientation = SC_ORIENTATION_90; + break; + } + break; case OPT_DISPLAY_ORIENTATION: if (!parse_orientation(optarg, &opts->display_orientation)) { return false; @@ -2621,9 +2484,23 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } break; case OPT_FORWARD_ALL_CLICKS: - LOGE("--forward-all-clicks has been removed, " + LOGW("--forward-all-clicks is deprecated, " "use --mouse-bind=++++ instead."); - return false; + opts->mouse_bindings = (struct sc_mouse_bindings) { + .pri = { + .right_click = SC_MOUSE_BINDING_CLICK, + .middle_click = SC_MOUSE_BINDING_CLICK, + .click4 = SC_MOUSE_BINDING_CLICK, + .click5 = SC_MOUSE_BINDING_CLICK, + }, + .sec = { + .right_click = SC_MOUSE_BINDING_CLICK, + .middle_click = SC_MOUSE_BINDING_CLICK, + .click4 = SC_MOUSE_BINDING_CLICK, + .click5 = SC_MOUSE_BINDING_CLICK, + }, + }; + break; case OPT_LEGACY_PASTE: opts->legacy_paste = true; break; @@ -2631,11 +2508,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], 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)) { + if (!parse_buffering_time(optarg, &opts->display_buffer)) { return false; } break; @@ -2718,9 +2591,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], 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; @@ -2794,33 +2664,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], 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; @@ -2859,10 +2702,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], #endif if (!opts->window) { - // Without window, there cannot be any video playback + // Without window, there cannot be any video playback or control opts->video_playback = false; - // Controls are still possible, allowing for options like - // --turn-screen-off + opts->control = false; } if (!opts->video) { @@ -2916,6 +2758,13 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } + if (opts->lock_video_orientation == + SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) { + LOGI("Video orientation is locked for v4l2 sink. " + "See --lock-video-orientation."); + opts->lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_INITIAL; + } + // V4L2 could not handle size change. // Do not log because downsizing on error is the default behavior, // not an explicit request from the user. @@ -2923,7 +2772,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } if (opts->v4l2_buffer && !opts->v4l2_device) { - LOGE("V4L2 buffer value without V4L2 sink"); + LOGE("V4L2 buffer value without V4L2 sink\n"); return false; } #endif @@ -2942,8 +2791,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], 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; + LOGI("No video mirroring, mouse mode switched to UHID"); + opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_UHID; } else { opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_SDK; } @@ -2995,18 +2844,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } } - 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"); @@ -3077,12 +2914,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], 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; @@ -3119,17 +2950,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], 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) { @@ -3262,10 +3082,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], 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 diff --git a/app/src/compat.h b/app/src/compat.h index 296d1a9f..1995d384 100644 --- a/app/src/compat.h +++ b/app/src/compat.h @@ -75,14 +75,6 @@ # 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 diff --git a/app/src/control_msg.c b/app/src/control_msg.c index e46c6165..d599b62d 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -22,6 +22,9 @@ #define MOTIONEVENT_ACTION_LABEL(value) \ ENUM_TO_LABEL(android_motionevent_action_labels, value) +#define SCREEN_POWER_MODE_LABEL(value) \ + ENUM_TO_LABEL(screen_power_mode_labels, value) + static const char *const android_keyevent_action_labels[] = { "down", "up", @@ -44,6 +47,14 @@ static const char *const android_motionevent_action_labels[] = { "btn-release", }; +static const char *const screen_power_mode_labels[] = { + "off", + "doze", + "normal", + "doze-suspend", + "suspend", +}; + static const char *const copy_key_labels[] = { "none", "copy", @@ -127,14 +138,10 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { 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); + int16_t hscroll = + sc_float_to_i16fp(msg->inject_scroll_event.hscroll); + int16_t vscroll = + sc_float_to_i16fp(msg->inject_scroll_event.vscroll); sc_write16be(&buf[13], (uint16_t) hscroll); sc_write16be(&buf[15], (uint16_t) vscroll); sc_write32be(&buf[17], msg->inject_scroll_event.buttons); @@ -151,15 +158,13 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { 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; + case SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: + buf[1] = msg->set_screen_power_mode.mode; 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; + size_t index = 3; index += write_string_tiny(&buf[index], msg->uhid_create.name, 127); sc_write16be(&buf[index], msg->uhid_create.report_desc_size); @@ -178,16 +183,11 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { 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: @@ -264,9 +264,9 @@ sc_control_msg_log(const struct sc_control_msg *msg) { 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"); + case SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: + LOG_CMSG("power mode %s", + SCREEN_POWER_MODE_LABEL(msg->set_screen_power_mode.mode)); break; case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: LOG_CMSG("expand notification panel"); @@ -284,13 +284,9 @@ sc_control_msg_log(const struct sc_control_msg *msg) { // 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); + LOG_CMSG("UHID create [%" PRIu16 "] name=%s%s%s " + "report_desc_size=%" PRIu16, msg->uhid_create.id, + quote, name, quote, msg->uhid_create.report_desc_size); break; } case SC_CONTROL_MSG_TYPE_UHID_INPUT: { @@ -312,12 +308,6 @@ sc_control_msg_log(const struct sc_control_msg *msg) { 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; @@ -343,9 +333,6 @@ sc_control_msg_destroy(struct sc_control_msg *msg) { 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; diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 74dbcba8..1ae8cae4 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -35,14 +35,18 @@ enum sc_control_msg_type { 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_SET_SCREEN_POWER_MODE, SC_CONTROL_MSG_TYPE_ROTATE_DEVICE, SC_CONTROL_MSG_TYPE_UHID_CREATE, SC_CONTROL_MSG_TYPE_UHID_INPUT, SC_CONTROL_MSG_TYPE_UHID_DESTROY, SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS, - SC_CONTROL_MSG_TYPE_START_APP, - SC_CONTROL_MSG_TYPE_RESET_VIDEO, +}; + +enum sc_screen_power_mode { + // see + SC_SCREEN_POWER_MODE_OFF = 0, + SC_SCREEN_POWER_MODE_NORMAL = 2, }; enum sc_copy_key { @@ -90,12 +94,10 @@ struct sc_control_msg { bool paste; } set_clipboard; struct { - bool on; - } set_display_power; + enum sc_screen_power_mode mode; + } set_screen_power_mode; 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 @@ -108,9 +110,6 @@ struct sc_control_msg { struct { uint16_t id; } uhid_destroy; - struct { - char *name; - } start_app; }; }; diff --git a/app/src/decoder.c b/app/src/decoder.c index 4d0a1daf..5d42b8b0 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -1,9 +1,11 @@ #include "decoder.h" -#include -#include -#include +#include +#include +#include +#include "events.h" +#include "trait/frame_sink.h" #include "util/log.h" /** Downcast packet_sink to decoder */ diff --git a/app/src/decoder.h b/app/src/decoder.h index 1f525fae..ba8903f4 100644 --- a/app/src/decoder.h +++ b/app/src/decoder.h @@ -3,11 +3,13 @@ #include "common.h" -#include - #include "trait/frame_source.h" #include "trait/packet_sink.h" +#include +#include +#include + struct sc_decoder { struct sc_packet_sink packet_sink; // packet sink trait struct sc_frame_source frame_source; // frame source trait diff --git a/app/src/delay_buffer.c b/app/src/delay_buffer.c index f75c6f72..e89a2092 100644 --- a/app/src/delay_buffer.c +++ b/app/src/delay_buffer.c @@ -2,7 +2,9 @@ #include #include -#include + +#include +#include #include "util/log.h" diff --git a/app/src/delay_buffer.h b/app/src/delay_buffer.h index 61cd77e4..18c1ce94 100644 --- a/app/src/delay_buffer.h +++ b/app/src/delay_buffer.h @@ -4,7 +4,6 @@ #include "common.h" #include -#include #include "clock.h" #include "trait/frame_source.h" diff --git a/app/src/demuxer.c b/app/src/demuxer.c index 885cd6ee..7223b553 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -1,11 +1,14 @@ #include "demuxer.h" #include -#include -#include #include +#include +#include +#include "decoder.h" +#include "events.h" #include "packet_merger.h" +#include "recorder.h" #include "util/binary.h" #include "util/log.h" diff --git a/app/src/demuxer.h b/app/src/demuxer.h index 2b7cb703..5587d12d 100644 --- a/app/src/demuxer.h +++ b/app/src/demuxer.h @@ -4,8 +4,12 @@ #include "common.h" #include +#include +#include +#include #include "trait/packet_source.h" +#include "trait/packet_sink.h" #include "util/net.h" #include "util/thread.h" diff --git a/app/src/device_msg.h b/app/src/device_msg.h index d6c701bb..86b2ccb7 100644 --- a/app/src/device_msg.h +++ b/app/src/device_msg.h @@ -3,9 +3,9 @@ #include "common.h" -#include +#include #include -#include +#include #define DEVICE_MSG_MAX_SIZE (1 << 18) // 256k // type: 1 byte; length: 4 bytes diff --git a/app/src/display.c b/app/src/display.c index aee8ef80..39018834 100644 --- a/app/src/display.c +++ b/app/src/display.c @@ -1,8 +1,6 @@ #include "display.h" #include -#include -#include #include #include "util/log.h" diff --git a/app/src/display.h b/app/src/display.h index 4de9b0a9..064bb7bf 100644 --- a/app/src/display.h +++ b/app/src/display.h @@ -4,8 +4,7 @@ #include "common.h" #include -#include -#include +#include #include #include "coords.h" diff --git a/app/src/events.c b/app/src/events.c index b4322d1b..ce885241 100644 --- a/app/src/events.c +++ b/app/src/events.c @@ -1,7 +1,5 @@ #include "events.h" -#include - #include "util/log.h" #include "util/thread.h" diff --git a/app/src/events.h b/app/src/events.h index 2fe4d3a7..59c55de4 100644 --- a/app/src/events.h +++ b/app/src/events.h @@ -5,7 +5,7 @@ #include #include -#include +#include enum { SC_EVENT_NEW_FRAME = SDL_USEREVENT, diff --git a/app/src/file_pusher.c b/app/src/file_pusher.c index 681fb5d6..06911052 100644 --- a/app/src/file_pusher.c +++ b/app/src/file_pusher.c @@ -1,11 +1,11 @@ #include "file_pusher.h" #include -#include #include #include "adb/adb.h" #include "util/log.h" +#include "util/process_intr.h" #define DEFAULT_PUSH_TARGET "/sdcard/Download/" diff --git a/app/src/fps_counter.c b/app/src/fps_counter.c index 1daa42ba..dd4ae1da 100644 --- a/app/src/fps_counter.c +++ b/app/src/fps_counter.c @@ -1,7 +1,6 @@ #include "fps_counter.h" #include -#include #include "util/log.h" diff --git a/app/src/fps_counter.h b/app/src/fps_counter.h index 3eab461c..e7619271 100644 --- a/app/src/fps_counter.h +++ b/app/src/fps_counter.h @@ -5,9 +5,9 @@ #include #include +#include #include "util/thread.h" -#include "util/tick.h" struct sc_fps_counter { sc_thread thread; diff --git a/app/src/frame_buffer.c b/app/src/frame_buffer.c index 9fd4cf6f..5699b58f 100644 --- a/app/src/frame_buffer.c +++ b/app/src/frame_buffer.c @@ -1,6 +1,8 @@ #include "frame_buffer.h" #include +#include +#include #include "util/log.h" diff --git a/app/src/frame_buffer.h b/app/src/frame_buffer.h index e748adfb..f97261cd 100644 --- a/app/src/frame_buffer.h +++ b/app/src/frame_buffer.h @@ -4,7 +4,6 @@ #include "common.h" #include -#include #include "util/thread.h" diff --git a/app/src/hid/hid_event.h b/app/src/hid/hid_event.h index b0d45ce8..37c3611b 100644 --- a/app/src/hid/hid_event.h +++ b/app/src/hid/hid_event.h @@ -3,7 +3,6 @@ #include "common.h" -#include #include #define SC_HID_MAX_SIZE 15 @@ -16,6 +15,7 @@ struct sc_hid_input { struct sc_hid_open { uint16_t hid_id; + const char *name; // pointer to static memory const uint8_t *report_desc; // pointer to static memory size_t report_desc_size; }; diff --git a/app/src/hid/hid_gamepad.c b/app/src/hid/hid_gamepad.c index 842eae9e..e2bf0616 100644 --- a/app/src/hid/hid_gamepad.c +++ b/app/src/hid/hid_gamepad.c @@ -2,8 +2,6 @@ #include #include -#include -#include #include "util/binary.h" #include "util/log.h" @@ -54,10 +52,10 @@ static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = { 0x09, 0x30, // Usage (Y) Left stick y 0x09, 0x31, - // Usage (Rx) Right stick x - 0x09, 0x33, - // Usage (Ry) Right stick y - 0x09, 0x34, + // Usage (Z) Right stick x + 0x09, 0x32, + // Usage (Rz) Right stick y + 0x09, 0x35, // Logical Minimum (0) 0x15, 0x00, // Logical Maximum (65535) @@ -67,15 +65,15 @@ static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = { 0x75, 0x10, // Report Count (4) 0x95, 0x04, - // Input (Data, Variable, Absolute): 4x2 bytes (X, Y, Z, Rz) + // Input (Data, Variable, Absolute): 4 bytes (X, Y, Z, Rz) 0x81, 0x02, - // Usage Page (Generic Desktop) - 0x05, 0x01, - // Usage (Z) - 0x09, 0x32, - // Usage (Rz) - 0x09, 0x35, + // Usage Page (Simulation Controls) + 0x05, 0x02, + // Usage (Brake) + 0x09, 0xC5, + // Usage (Accelerator) + 0x09, 0xC4, // Logical Minimum (0) 0x15, 0x00, // Logical Maximum (32767) @@ -84,7 +82,7 @@ static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = { 0x75, 0x10, // Report Count (2) 0x95, 0x02, - // Input (Data, Variable, Absolute): 2x2 bytes (L2, R2) + // Input (Data, Variable, Absolute): 2 bytes (L2, R2) 0x81, 0x02, // Usage Page (Buttons) @@ -184,7 +182,7 @@ static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = { * `------------- SC_GAMEPAD_BUTTON_RIGHT_STICK * * +---------------+ - * byte 14: |0 0 0 0 . . . .| hat switch (dpad) position (0-8) + * byte 14: |0 0 0 . . . . .| hat switch (dpad) position (0-8) * +---------------+ * 9 possible positions and their values: * 8 1 2 @@ -193,19 +191,16 @@ static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = { * (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_x = 0; + slot->axis_left_y = 0; + slot->axis_right_x = 0; + slot->axis_right_y = 0; slot->axis_left_trigger = 0; slot->axis_right_trigger = 0; } @@ -248,8 +243,14 @@ sc_hid_gamepad_generate_open(struct sc_hid_gamepad *hid, sc_hid_gamepad_slot_init(&hid->slots[slot_idx], gamepad_id); + SDL_GameController* game_controller = + SDL_GameControllerFromInstanceID(gamepad_id); + assert(game_controller); + const char *name = SDL_GameControllerName(game_controller); + uint16_t hid_id = sc_hid_gamepad_slot_get_id(slot_idx); hid_open->hid_id = hid_id; + hid_open->name = name; hid_open->report_desc = SC_HID_GAMEPAD_REPORT_DESC; hid_open->report_desc_size = sizeof(SC_HID_GAMEPAD_REPORT_DESC); @@ -422,6 +423,8 @@ sc_hid_gamepad_generate_input_from_axis(struct sc_hid_gamepad *hid, struct sc_hid_gamepad_slot *slot = &hid->slots[slot_idx]; +// [-32768 to 32767] -> [0 to 65535] +#define AXIS_RESCALE(V) (uint16_t) (((int32_t) V) + 0x8000) switch (event->axis) { case SC_GAMEPAD_AXIS_LEFTX: slot->axis_left_x = AXIS_RESCALE(event->value); diff --git a/app/src/hid/hid_gamepad.h b/app/src/hid/hid_gamepad.h index 8d939ac7..b532a703 100644 --- a/app/src/hid/hid_gamepad.h +++ b/app/src/hid/hid_gamepad.h @@ -4,7 +4,6 @@ #include "common.h" #include -#include #include "hid/hid_event.h" #include "input_events.h" diff --git a/app/src/hid/hid_keyboard.c b/app/src/hid/hid_keyboard.c index 6477396a..2109224a 100644 --- a/app/src/hid/hid_keyboard.c +++ b/app/src/hid/hid_keyboard.c @@ -1,6 +1,5 @@ #include "hid_keyboard.h" -#include #include #include "util/log.h" @@ -336,6 +335,7 @@ sc_hid_keyboard_generate_input_from_mods(struct sc_hid_input *hid_input, void sc_hid_keyboard_generate_open(struct sc_hid_open *hid_open) { hid_open->hid_id = SC_HID_ID_KEYBOARD; + hid_open->name = NULL; // No name specified after "scrcpy" hid_open->report_desc = SC_HID_KEYBOARD_REPORT_DESC; hid_open->report_desc_size = sizeof(SC_HID_KEYBOARD_REPORT_DESC); } diff --git a/app/src/hid/hid_keyboard.h b/app/src/hid/hid_keyboard.h index 5ecfd8cf..cde1ac52 100644 --- a/app/src/hid/hid_keyboard.h +++ b/app/src/hid/hid_keyboard.h @@ -4,7 +4,6 @@ #include "common.h" #include -#include #include "hid/hid_event.h" #include "input_events.h" diff --git a/app/src/hid/hid_mouse.c b/app/src/hid/hid_mouse.c index 33f0807e..ac215165 100644 --- a/app/src/hid/hid_mouse.c +++ b/app/src/hid/hid_mouse.c @@ -1,10 +1,8 @@ #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 +// 1 byte for wheel motion +#define SC_HID_MOUSE_INPUT_SIZE 4 /** * Mouse descriptor from the specification: @@ -75,21 +73,6 @@ static const uint8_t SC_HID_MOUSE_REPORT_DESC[] = { // 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, @@ -175,8 +158,7 @@ sc_hid_mouse_generate_input_from_motion(struct sc_hid_input *hid_input, 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 + data[3] = 0; // wheel coordinates only used for scrolling } void @@ -188,31 +170,27 @@ sc_hid_mouse_generate_input_from_click(struct sc_hid_input *hid_input, 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 + data[3] = 0; // wheel coordinates only used for scrolling } -bool +void 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; + // In practice, vscroll is always -1, 0 or 1, but in theory other values + // are possible + data[3] = CLAMP(event->vscroll, -127, 127); + // Horizontal scrolling ignored } void sc_hid_mouse_generate_open(struct sc_hid_open *hid_open) { hid_open->hid_id = SC_HID_ID_MOUSE; + hid_open->name = NULL; // No name specified after "scrcpy" hid_open->report_desc = SC_HID_MOUSE_REPORT_DESC; hid_open->report_desc_size = sizeof(SC_HID_MOUSE_REPORT_DESC); } diff --git a/app/src/hid/hid_mouse.h b/app/src/hid/hid_mouse.h index 4ae4bfd4..a9a54718 100644 --- a/app/src/hid/hid_mouse.h +++ b/app/src/hid/hid_mouse.h @@ -3,6 +3,8 @@ #include "common.h" +#include + #include "hid/hid_event.h" #include "input_events.h" @@ -22,7 +24,7 @@ void sc_hid_mouse_generate_input_from_click(struct sc_hid_input *hid_input, const struct sc_mouse_click_event *event); -bool +void sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input, const struct sc_mouse_scroll_event *event); diff --git a/app/src/icon.c b/app/src/icon.c index 797afc75..a76a85c9 100644 --- a/app/src/icon.c +++ b/app/src/icon.c @@ -2,22 +2,16 @@ #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 "compat.h" +#include "util/file.h" #include "util/log.h" +#include "util/str.h" #define SCRCPY_PORTABLE_ICON_FILENAME "icon.png" #define SCRCPY_DEFAULT_ICON_PATH \ @@ -25,22 +19,35 @@ static char * get_icon_path(void) { - char *icon_path = sc_get_env("SCRCPY_ICON_PATH"); - if (icon_path) { +#ifdef __WINDOWS__ + const wchar_t *icon_path_env = _wgetenv(L"SCRCPY_ICON_PATH"); +#else + const char *icon_path_env = getenv("SCRCPY_ICON_PATH"); +#endif + if (icon_path_env) { // if the envvar is set, use it +#ifdef __WINDOWS__ + char *icon_path = sc_str_from_wchars(icon_path_env); +#else + char *icon_path = strdup(icon_path_env); +#endif + if (!icon_path) { + LOG_OOM(); + return NULL; + } LOGD("Using SCRCPY_ICON_PATH: %s", icon_path); return icon_path; } #ifndef PORTABLE LOGD("Using icon: " SCRCPY_DEFAULT_ICON_PATH); - icon_path = strdup(SCRCPY_DEFAULT_ICON_PATH); + char *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); + char *icon_path = sc_file_get_local_path(SCRCPY_PORTABLE_ICON_FILENAME); if (!icon_path) { LOGE("Could not get icon path"); return NULL; diff --git a/app/src/icon.h b/app/src/icon.h index 6bcf46d2..3251e48f 100644 --- a/app/src/icon.h +++ b/app/src/icon.h @@ -3,7 +3,9 @@ #include "common.h" -#include +#include +#include +#include SDL_Surface * scrcpy_icon_load(void); diff --git a/app/src/input_events.h b/app/src/input_events.h index 1e34b50e..c8966a35 100644 --- a/app/src/input_events.h +++ b/app/src/input_events.h @@ -9,6 +9,7 @@ #include #include "coords.h" +#include "options.h" /* The representation of input events in scrcpy is very close to the SDL API, * for simplicity. @@ -393,8 +394,6 @@ 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 }; @@ -413,12 +412,18 @@ struct sc_touch_event { float pressure; }; +enum sc_gamepad_device_event_type { + SC_GAMEPAD_DEVICE_ADDED, + SC_GAMEPAD_DEVICE_REMOVED, +}; + // 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 { + enum sc_gamepad_device_event_type type; uint32_t gamepad_id; }; @@ -498,6 +503,16 @@ sc_mouse_buttons_state_from_sdl(uint32_t buttons_state) { return buttons_state; } +static inline enum sc_gamepad_device_event_type +sc_gamepad_device_event_type_from_sdl_type(uint32_t type) { + assert(type == SDL_CONTROLLERDEVICEADDED + || type == SDL_CONTROLLERDEVICEREMOVED); + if (type == SDL_CONTROLLERDEVICEADDED) { + return SC_GAMEPAD_DEVICE_ADDED; + } + return SC_GAMEPAD_DEVICE_REMOVED; +} + static inline enum sc_gamepad_axis sc_gamepad_axis_from_sdl(uint8_t axis) { if (axis <= SDL_CONTROLLER_AXIS_TRIGGERRIGHT) { diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 3e4dd0f3..77cb4f1d 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -1,17 +1,57 @@ #include "input_manager.h" #include -#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" +#define SC_SDL_SHORTCUT_MODS_MASK (KMOD_CTRL | KMOD_ALT | KMOD_GUI) + +static inline uint16_t +to_sdl_mod(uint8_t shortcut_mod) { + uint16_t sdl_mod = 0; + if (shortcut_mod & SC_SHORTCUT_MOD_LCTRL) { + sdl_mod |= KMOD_LCTRL; + } + if (shortcut_mod & SC_SHORTCUT_MOD_RCTRL) { + sdl_mod |= KMOD_RCTRL; + } + if (shortcut_mod & SC_SHORTCUT_MOD_LALT) { + sdl_mod |= KMOD_LALT; + } + if (shortcut_mod & SC_SHORTCUT_MOD_RALT) { + sdl_mod |= KMOD_RALT; + } + if (shortcut_mod & SC_SHORTCUT_MOD_LSUPER) { + sdl_mod |= KMOD_LGUI; + } + if (shortcut_mod & SC_SHORTCUT_MOD_RSUPER) { + sdl_mod |= KMOD_RGUI; + } + return sdl_mod; +} + +static bool +is_shortcut_mod(struct sc_input_manager *im, uint16_t sdl_mod) { + // keep only the relevant modifier keys + sdl_mod &= SC_SDL_SHORTCUT_MODS_MASK; + + // at least one shortcut mod pressed? + return sdl_mod & im->sdl_shortcut_mods; +} + +static bool +is_shortcut_key(struct sc_input_manager *im, SDL_Keycode keycode) { + return (im->sdl_shortcut_mods & KMOD_LCTRL && keycode == SDLK_LCTRL) + || (im->sdl_shortcut_mods & KMOD_RCTRL && keycode == SDLK_RCTRL) + || (im->sdl_shortcut_mods & KMOD_LALT && keycode == SDLK_LALT) + || (im->sdl_shortcut_mods & KMOD_RALT && keycode == SDLK_RALT) + || (im->sdl_shortcut_mods & KMOD_LGUI && keycode == SDLK_LGUI) + || (im->sdl_shortcut_mods & KMOD_RGUI && keycode == SDLK_RGUI); +} + void sc_input_manager_init(struct sc_input_manager *im, const struct sc_input_manager_params *params) { @@ -33,7 +73,7 @@ sc_input_manager_init(struct sc_input_manager *im, 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->sdl_shortcut_mods = to_sdl_mod(params->shortcut_mods); im->vfinger_down = false; im->vfinger_invert_x = false; @@ -207,12 +247,13 @@ set_device_clipboard(struct sc_input_manager *im, bool paste, } static void -set_display_power(struct sc_input_manager *im, bool on) { +set_screen_power_mode(struct sc_input_manager *im, + enum sc_screen_power_mode mode) { assert(im->controller); struct sc_control_msg msg; - msg.type = SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER; - msg.set_display_power.on = on; + msg.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; + msg.set_screen_power_mode.mode = mode; if (!sc_controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'set screen power mode'"); @@ -288,18 +329,6 @@ open_hard_keyboard_settings(struct sc_input_manager *im) { } } -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) { @@ -317,8 +346,7 @@ sc_input_manager_process_text_input(struct sc_input_manager *im, return; } - if (sc_shortcut_mods_is_shortcut_mod(im->sdl_shortcut_mods, - SDL_GetModState())) { + if (is_shortcut_mod(im, SDL_GetModState())) { // A shortcut must never generate text events return; } @@ -385,9 +413,8 @@ sc_input_manager_process_key(struct sc_input_manager *im, // 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); + bool is_shortcut = is_shortcut_mod(im, mod) + || is_shortcut_key(im, sdl_keycode); if (down && !repeat) { if (sdl_keycode == im->last_keycode && mod == im->last_mod) { @@ -430,8 +457,10 @@ sc_input_manager_process_key(struct sc_input_manager *im, return; case SDLK_o: if (control && !repeat && down && !paused) { - bool on = shift; - set_display_power(im, on); + enum sc_screen_power_mode mode = shift + ? SC_SCREEN_POWER_MODE_NORMAL + : SC_SCREEN_POWER_MODE_OFF; + set_screen_power_mode(im, mode); } return; case SDLK_z: @@ -507,7 +536,7 @@ sc_input_manager_process_key(struct sc_input_manager *im, return; case SDLK_f: if (video && !shift && !repeat && down) { - sc_screen_toggle_fullscreen(im->screen); + sc_screen_switch_fullscreen(im->screen); } return; case SDLK_w: @@ -537,12 +566,8 @@ sc_input_manager_process_key(struct sc_input_manager *im, } return; case SDLK_r: - if (control && !repeat && down && !paused) { - if (shift) { - reset_video(im); - } else { - rotate_device(im); - } + if (control && !shift && !repeat && down && !paused) { + rotate_device(im); } return; case SDLK_k: @@ -811,7 +836,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, } bool change_vfinger = event->button == SDL_BUTTON_LEFT && - ((down && !im->vfinger_down && (ctrl_pressed || shift_pressed)) || + ((down && !im->vfinger_down && (ctrl_pressed ^ shift_pressed)) || (!down && im->vfinger_down)); bool use_finger = im->vfinger_down || change_vfinger; @@ -843,28 +868,16 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, // 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 + // To simulate a tilt gesture (a vertical slide with two fingers), Shift + // can be used instead of Ctrl. The "virtual finger" has a position // inverted with respect to the vertical axis of symmetry in the middle of // the screen. - // - // 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_x = ctrl_pressed || shift_pressed; im->vfinger_invert_y = ctrl_pressed; } struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size, @@ -897,14 +910,12 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im, 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, + .hscroll = CLAMP(event->preciseX, -1.0f, 1.0f), + .vscroll = CLAMP(event->preciseY, -1.0f, 1.0f), #else - .hscroll = event->x, - .vscroll = event->y, + .hscroll = CLAMP(event->x, -1, 1), + .vscroll = CLAMP(event->y, -1, 1), #endif - .hscroll_int = event->x, - .vscroll_int = event->y, .buttons_state = im->mouse_buttons_state, }; @@ -914,6 +925,7 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im, static void sc_input_manager_process_gamepad_device(struct sc_input_manager *im, const SDL_ControllerDeviceEvent *event) { + SDL_JoystickID id; if (event->type == SDL_CONTROLLERDEVICEADDED) { SDL_GameController *gc = SDL_GameControllerOpen(event->which); if (!gc) { @@ -928,12 +940,9 @@ sc_input_manager_process_gamepad_device(struct sc_input_manager *im, return; } - struct sc_gamepad_device_event evt = { - .gamepad_id = SDL_JoystickInstanceID(joystick), - }; - im->gp->ops->process_gamepad_added(im->gp, &evt); + id = SDL_JoystickInstanceID(joystick); } else if (event->type == SDL_CONTROLLERDEVICEREMOVED) { - SDL_JoystickID id = event->which; + id = event->which; SDL_GameController *gc = SDL_GameControllerFromInstanceID(id); if (gc) { @@ -941,15 +950,16 @@ sc_input_manager_process_gamepad_device(struct sc_input_manager *im, } 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; } + + struct sc_gamepad_device_event evt = { + .type = sc_gamepad_device_event_type_from_sdl_type(event->type), + .gamepad_id = id, + }; + im->gp->ops->process_gamepad_device(im->gp, &evt); } static void diff --git a/app/src/input_manager.h b/app/src/input_manager.h index af4cbc69..8efd0153 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -4,12 +4,12 @@ #include "common.h" #include -#include -#include -#include + +#include #include "controller.h" #include "file_pusher.h" +#include "fps_counter.h" #include "options.h" #include "trait/gamepad_processor.h" #include "trait/key_processor.h" diff --git a/app/src/keyboard_sdk.c b/app/src/keyboard_sdk.c index 466a1aeb..00b7f92a 100644 --- a/app/src/keyboard_sdk.c +++ b/app/src/keyboard_sdk.c @@ -1,13 +1,8 @@ #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" @@ -50,10 +45,6 @@ convert_keycode(enum sc_keycode from, enum android_keycode *to, uint16_t mod, {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. @@ -175,7 +166,11 @@ convert_keycode(enum sc_keycode from, enum android_keycode *to, uint16_t mod, return false; } - // Handle letters and space + if (mod & (SC_MOD_LALT | SC_MOD_RALT | SC_MOD_LGUI | SC_MOD_RGUI)) { + return false; + } + + // if ALT and META are not pressed, also handle letters and space entry = SC_INTMAP_FIND_ENTRY(alphaspace_keys, from); if (entry) { *to = entry->value; diff --git a/app/src/main.c b/app/src/main.c index c58e0be7..8bbd074f 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -1,6 +1,9 @@ #include "common.h" +#include #include +#include +#include #ifdef HAVE_V4L2 # include #endif diff --git a/app/src/mouse_capture.c b/app/src/mouse_capture.c deleted file mode 100644 index 25345faa..00000000 --- a/app/src/mouse_capture.c +++ /dev/null @@ -1,123 +0,0 @@ -#include "mouse_capture.h" - -#include "shortcut_mod.h" -#include "util/log.h" - -void -sc_mouse_capture_init(struct sc_mouse_capture *mc, SDL_Window *window, - uint8_t shortcut_mods) { - mc->window = window; - mc->sdl_mouse_capture_keys = sc_shortcut_mods_to_sdl(shortcut_mods); - mc->mouse_capture_key_pressed = SDLK_UNKNOWN; -} - -static inline bool -sc_mouse_capture_is_capture_key(struct sc_mouse_capture *mc, SDL_Keycode key) { - return sc_shortcut_mods_is_shortcut_key(mc->sdl_mouse_capture_keys, key); -} - -bool -sc_mouse_capture_handle_event(struct sc_mouse_capture *mc, - const SDL_Event *event) { - switch (event->type) { - case SDL_WINDOWEVENT: - if (event->window.event == SDL_WINDOWEVENT_FOCUS_LOST) { - sc_mouse_capture_set_active(mc, false); - return true; - } - break; - case SDL_KEYDOWN: { - SDL_Keycode key = event->key.keysym.sym; - if (sc_mouse_capture_is_capture_key(mc, key)) { - if (!mc->mouse_capture_key_pressed) { - mc->mouse_capture_key_pressed = key; - } else { - // Another mouse capture key has been pressed, cancel - // mouse (un)capture - mc->mouse_capture_key_pressed = 0; - } - // Mouse capture keys are never forwarded to the device - return true; - } - break; - } - case SDL_KEYUP: { - SDL_Keycode key = event->key.keysym.sym; - SDL_Keycode cap = mc->mouse_capture_key_pressed; - mc->mouse_capture_key_pressed = 0; - if (sc_mouse_capture_is_capture_key(mc, key)) { - if (key == cap) { - // A mouse capture key has been pressed then released: - // toggle the capture mouse mode - sc_mouse_capture_toggle(mc); - } - // Mouse capture keys are never forwarded to the device - return true; - } - break; - } - case SDL_MOUSEWHEEL: - case SDL_MOUSEMOTION: - case SDL_MOUSEBUTTONDOWN: - if (!sc_mouse_capture_is_active(mc)) { - // The mouse will be captured on SDL_MOUSEBUTTONUP, so consume - // the event - return true; - } - break; - case SDL_MOUSEBUTTONUP: - if (!sc_mouse_capture_is_active(mc)) { - sc_mouse_capture_set_active(mc, true); - return true; - } - break; - case SDL_FINGERMOTION: - case SDL_FINGERDOWN: - case SDL_FINGERUP: - // Touch events are not compatible with relative mode - // (coordinates are not relative), so consume the event - return true; - } - - return false; -} - -void -sc_mouse_capture_set_active(struct sc_mouse_capture *mc, bool capture) { -#ifdef __APPLE__ - // Workaround for SDL bug on macOS: - // - if (capture) { - int mouse_x, mouse_y; - SDL_GetGlobalMouseState(&mouse_x, &mouse_y); - - int x, y, w, h; - SDL_GetWindowPosition(mc->window, &x, &y); - SDL_GetWindowSize(mc->window, &w, &h); - - bool outside_window = mouse_x < x || mouse_x >= x + w - || mouse_y < y || mouse_y >= y + h; - if (outside_window) { - SDL_WarpMouseInWindow(mc->window, w / 2, h / 2); - } - } -#else - (void) mc; -#endif - if (SDL_SetRelativeMouseMode(capture)) { - LOGE("Could not set relative mouse mode to %s: %s", - capture ? "true" : "false", SDL_GetError()); - } -} - -bool -sc_mouse_capture_is_active(struct sc_mouse_capture *mc) { - (void) mc; - return SDL_GetRelativeMouseMode(); -} - -void -sc_mouse_capture_toggle(struct sc_mouse_capture *mc) { - bool new_value = !sc_mouse_capture_is_active(mc); - sc_mouse_capture_set_active(mc, new_value); -} diff --git a/app/src/mouse_capture.h b/app/src/mouse_capture.h deleted file mode 100644 index f352cc13..00000000 --- a/app/src/mouse_capture.h +++ /dev/null @@ -1,38 +0,0 @@ -#ifndef SC_MOUSE_CAPTURE_H -#define SC_MOUSE_CAPTURE_H - -#include "common.h" - -#include - -#include - -struct sc_mouse_capture { - SDL_Window *window; - uint16_t sdl_mouse_capture_keys; - - // To enable/disable mouse capture, a mouse capture key (LALT, LGUI or - // RGUI) must be pressed. This variable tracks the pressed capture key. - SDL_Keycode mouse_capture_key_pressed; - -}; - -void -sc_mouse_capture_init(struct sc_mouse_capture *mc, SDL_Window *window, - uint8_t shortcut_mods); - -void -sc_mouse_capture_set_active(struct sc_mouse_capture *mc, bool capture); - -bool -sc_mouse_capture_is_active(struct sc_mouse_capture *mc); - -void -sc_mouse_capture_toggle(struct sc_mouse_capture *mc); - -// Return true if it consumed the event -bool -sc_mouse_capture_handle_event(struct sc_mouse_capture *mc, - const SDL_Event *event); - -#endif diff --git a/app/src/mouse_sdk.c b/app/src/mouse_sdk.c index 7eceffa7..a7998972 100644 --- a/app/src/mouse_sdk.c +++ b/app/src/mouse_sdk.c @@ -1,12 +1,12 @@ #include "mouse_sdk.h" #include -#include #include "android/input.h" #include "control_msg.h" #include "controller.h" #include "input_events.h" +#include "util/intmap.h" #include "util/log.h" /** Downcast mouse processor to sc_mouse_sdk */ diff --git a/app/src/mouse_sdk.h b/app/src/mouse_sdk.h index fe92a2d7..142b89bb 100644 --- a/app/src/mouse_sdk.h +++ b/app/src/mouse_sdk.h @@ -6,6 +6,7 @@ #include #include "controller.h" +#include "screen.h" #include "trait/mouse_processor.h" struct sc_mouse_sdk { diff --git a/app/src/opengl.c b/app/src/opengl.c index 0cb83ed7..376690af 100644 --- a/app/src/opengl.c +++ b/app/src/opengl.c @@ -2,8 +2,7 @@ #include #include -#include -#include +#include "SDL2/SDL.h" void sc_opengl_init(struct sc_opengl *gl) { diff --git a/app/src/options.c b/app/src/options.c index 0fe82d29..f8448792 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -1,7 +1,5 @@ #include "options.h" -#include - const struct scrcpy_options scrcpy_options_default = { .serial = NULL, .crop = NULL, @@ -52,21 +50,18 @@ const struct scrcpy_options scrcpy_options_default = { .video_bit_rate = 0, .audio_bit_rate = 0, .max_fps = NULL, - .capture_orientation = SC_ORIENTATION_0, - .capture_orientation_lock = SC_ORIENTATION_UNLOCKED, + .lock_video_orientation = SC_LOCK_VIDEO_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, + .display_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, @@ -108,11 +103,6 @@ const struct scrcpy_options scrcpy_options_default = { .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 diff --git a/app/src/options.h b/app/src/options.h index 03b42913..5f6726e0 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -5,6 +5,7 @@ #include #include +#include #include #include "util/tick.h" @@ -59,14 +60,6 @@ enum sc_audio_source { 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 { @@ -91,19 +84,6 @@ enum sc_orientation { // v v v 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)); @@ -150,6 +130,16 @@ sc_orientation_get_name(enum sc_orientation orientation) { } } +enum sc_lock_video_orientation { + SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1, + // lock the current orientation when scrcpy starts + SC_LOCK_VIDEO_ORIENTATION_INITIAL = -2, + SC_LOCK_VIDEO_ORIENTATION_0 = 0, + SC_LOCK_VIDEO_ORIENTATION_90 = 3, + SC_LOCK_VIDEO_ORIENTATION_180 = 2, + SC_LOCK_VIDEO_ORIENTATION_270 = 1, +}; + enum sc_keyboard_input_mode { SC_KEYBOARD_INPUT_MODE_AUTO, SC_KEYBOARD_INPUT_MODE_UHID_OR_AOA, // normal vs otg mode @@ -261,22 +251,18 @@ struct scrcpy_options { 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_lock_video_orientation lock_video_orientation; 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 display_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; @@ -318,15 +304,10 @@ struct scrcpy_options { #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; diff --git a/app/src/packet_merger.c b/app/src/packet_merger.c index dea038b6..81b02d2c 100644 --- a/app/src/packet_merger.c +++ b/app/src/packet_merger.c @@ -1,9 +1,5 @@ #include "packet_merger.h" -#include -#include -#include - #include "util/log.h" void diff --git a/app/src/packet_merger.h b/app/src/packet_merger.h index 3f9972ce..e1824c2c 100644 --- a/app/src/packet_merger.h +++ b/app/src/packet_merger.h @@ -5,7 +5,7 @@ #include #include -#include +#include /** * Config packets (containing the SPS/PPS) are sent in-band. A new config diff --git a/app/src/receiver.c b/app/src/receiver.c index 2ccb8a8b..b89b0c6e 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -2,6 +2,7 @@ #include #include +#include #include #include "device_msg.h" diff --git a/app/src/recorder.c b/app/src/recorder.c index c26f8f2d..9e0b3395 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -1,9 +1,6 @@ #include "recorder.h" #include -#include -#include -#include #include #include #include @@ -146,14 +143,8 @@ sc_recorder_open_output_file(struct sc_recorder *recorder) { 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); + int ret = avio_open(&recorder->ctx->pb, recorder->filename, + AVIO_FLAG_WRITE); if (ret < 0) { LOGE("Failed to open output file: %s", recorder->filename); avformat_free_context(recorder->ctx); diff --git a/app/src/recorder.h b/app/src/recorder.h index 70b73836..d096e79a 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -4,10 +4,9 @@ #include "common.h" #include -#include -#include #include +#include "coords.h" #include "options.h" #include "trait/packet_sink.h" #include "util/thread.h" diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index a4c8c340..854657fb 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -1,11 +1,10 @@ #include "scrcpy.h" -#include -#include -#include #include -#include #include +#include +#include +#include #include #ifdef _WIN32 @@ -38,9 +37,9 @@ #endif #include "util/acksync.h" #include "util/log.h" +#include "util/net.h" #include "util/rand.h" #include "util/timeout.h" -#include "util/tick.h" #ifdef HAVE_V4L2 # include "v4l2_sink.h" #endif @@ -54,7 +53,7 @@ struct scrcpy { struct sc_decoder video_decoder; struct sc_decoder audio_decoder; struct sc_recorder recorder; - struct sc_delay_buffer video_buffer; + struct sc_delay_buffer display_buffer; #ifdef HAVE_V4L2 struct sc_v4l2_sink v4l2_sink; struct sc_delay_buffer v4l2_buffer; @@ -107,17 +106,6 @@ sdl_set_hints(const char *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"); @@ -176,7 +164,7 @@ sdl_configure(bool video_playback, bool disable_screensaver) { } static enum scrcpy_exit_code -event_loop(struct scrcpy *s, bool has_screen) { +event_loop(struct scrcpy *s) { SDL_Event event; while (SDL_WaitEvent(&event)) { switch (event.type) { @@ -208,7 +196,7 @@ event_loop(struct scrcpy *s, bool has_screen) { break; } default: - if (has_screen && !sc_screen_handle_event(&s->screen, &event)) { + if (!sc_screen_handle_event(&s->screen, &event)) { return SCRCPY_EXIT_FAILURE; } break; @@ -440,14 +428,9 @@ scrcpy(struct scrcpy_options *options) { .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, + .lock_video_orientation = options->lock_video_orientation, .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, @@ -471,8 +454,6 @@ scrcpy(struct scrcpy_options *options) { .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, }; @@ -833,11 +814,11 @@ aoa_complete: 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; + if (options->display_buffer) { + sc_delay_buffer_init(&s->display_buffer, + options->display_buffer, true); + sc_frame_source_add_sink(src, &s->display_buffer.frame_sink); + src = &s->display_buffer.frame_source; } sc_frame_source_add_sink(src, &s->screen.frame_sink); @@ -891,11 +872,11 @@ aoa_complete: // 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; + msg.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; + msg.set_screen_power_mode.mode = SC_SCREEN_POWER_MODE_OFF; if (!sc_controller_push_msg(&s->controller, &msg)) { - LOGW("Could not request 'set display power'"); + LOGW("Could not request 'set screen power mode'"); } } @@ -925,26 +906,7 @@ aoa_complete: 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); + ret = event_loop(s); terminate_event_loop(); LOGD("quit..."); diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 7f6a0fb2..d4d494a3 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -3,6 +3,7 @@ #include "common.h" +#include #include "options.h" enum scrcpy_exit_code { diff --git a/app/src/screen.c b/app/src/screen.c index 1d694f12..cb455cb1 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -162,6 +162,47 @@ sc_screen_is_relative_mode(struct sc_screen *screen) { return screen->im.mp && screen->im.mp->relative_mode; } +static void +sc_screen_set_mouse_capture(struct sc_screen *screen, bool capture) { +#ifdef __APPLE__ + // Workaround for SDL bug on macOS: + // + if (capture) { + int mouse_x, mouse_y; + SDL_GetGlobalMouseState(&mouse_x, &mouse_y); + + int x, y, w, h; + SDL_GetWindowPosition(screen->window, &x, &y); + SDL_GetWindowSize(screen->window, &w, &h); + + bool outside_window = mouse_x < x || mouse_x >= x + w + || mouse_y < y || mouse_y >= y + h; + if (outside_window) { + SDL_WarpMouseInWindow(screen->window, w / 2, h / 2); + } + } +#else + (void) screen; +#endif + if (SDL_SetRelativeMouseMode(capture)) { + LOGE("Could not set relative mouse mode to %s: %s", + capture ? "true" : "false", SDL_GetError()); + } +} + +static inline bool +sc_screen_get_mouse_capture(struct sc_screen *screen) { + (void) screen; + return SDL_GetRelativeMouseMode(); +} + +static inline void +sc_screen_toggle_mouse_capture(struct sc_screen *screen) { + (void) screen; + bool new_value = !sc_screen_get_mouse_capture(screen); + sc_screen_set_mouse_capture(screen, new_value); +} + static void sc_screen_update_content_rect(struct sc_screen *screen) { assert(screen->video); @@ -330,6 +371,7 @@ sc_screen_init(struct sc_screen *screen, screen->fullscreen = false; screen->maximized = false; screen->minimized = false; + screen->mouse_capture_key_pressed = 0; screen->paused = false; screen->resume_frame = NULL; screen->orientation = SC_ORIENTATION_0; @@ -444,9 +486,6 @@ sc_screen_init(struct sc_screen *screen, 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); @@ -467,7 +506,7 @@ sc_screen_init(struct sc_screen *screen, 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); + sc_screen_set_mouse_capture(screen, true); } return true; @@ -499,7 +538,7 @@ sc_screen_show_initial_window(struct sc_screen *screen) { SDL_SetWindowPosition(screen->window, x, y); if (screen->req.fullscreen) { - sc_screen_toggle_fullscreen(screen); + sc_screen_switch_fullscreen(screen); } if (screen->req.start_fps_counter) { @@ -674,7 +713,7 @@ sc_screen_apply_frame(struct sc_screen *screen) { if (sc_screen_is_relative_mode(screen)) { // Capture mouse on start - sc_mouse_capture_set_active(&screen->mc, true); + sc_screen_set_mouse_capture(screen, true); } } @@ -735,7 +774,7 @@ sc_screen_set_paused(struct sc_screen *screen, bool paused) { } void -sc_screen_toggle_fullscreen(struct sc_screen *screen) { +sc_screen_switch_fullscreen(struct sc_screen *screen) { assert(screen->video); uint32_t new_mode = screen->fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP; @@ -798,8 +837,15 @@ sc_screen_resize_to_pixel_perfect(struct sc_screen *screen) { content_size.height); } +static inline bool +sc_screen_is_mouse_capture_key(SDL_Keycode key) { + return key == SDLK_LALT || key == SDLK_LGUI || key == SDLK_RGUI; +} + bool sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) { + bool relative_mode = sc_screen_is_relative_mode(screen); + switch (event->type) { case SC_EVENT_SCREEN_INIT_SIZE: { // The initial size is passed via screen->frame_size @@ -857,14 +903,69 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) { apply_pending_resize(screen); sc_screen_render(screen, true); break; + case SDL_WINDOWEVENT_FOCUS_LOST: + if (relative_mode) { + sc_screen_set_mouse_capture(screen, false); + } + break; } return 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; + case SDL_KEYDOWN: + if (relative_mode) { + SDL_Keycode key = event->key.keysym.sym; + if (sc_screen_is_mouse_capture_key(key)) { + if (!screen->mouse_capture_key_pressed) { + screen->mouse_capture_key_pressed = key; + } else { + // Another mouse capture key has been pressed, cancel + // mouse (un)capture + screen->mouse_capture_key_pressed = 0; + } + // Mouse capture keys are never forwarded to the device + return true; + } + } + break; + case SDL_KEYUP: + if (relative_mode) { + SDL_Keycode key = event->key.keysym.sym; + SDL_Keycode cap = screen->mouse_capture_key_pressed; + screen->mouse_capture_key_pressed = 0; + if (sc_screen_is_mouse_capture_key(key)) { + if (key == cap) { + // A mouse capture key has been pressed then released: + // toggle the capture mouse mode + sc_screen_toggle_mouse_capture(screen); + } + // Mouse capture keys are never forwarded to the device + return true; + } + } + break; + case SDL_MOUSEWHEEL: + case SDL_MOUSEMOTION: + case SDL_MOUSEBUTTONDOWN: + if (relative_mode && !sc_screen_get_mouse_capture(screen)) { + // Do not forward to input manager, the mouse will be captured + // on SDL_MOUSEBUTTONUP + return true; + } + break; + case SDL_FINGERMOTION: + case SDL_FINGERDOWN: + case SDL_FINGERUP: + if (relative_mode) { + // Touch events are not compatible with relative mode + // (coordinates are not relative) + return true; + } + break; + case SDL_MOUSEBUTTONUP: + if (relative_mode && !sc_screen_get_mouse_capture(screen)) { + sc_screen_set_mouse_capture(screen, true); + return true; + } + break; } sc_input_manager_handle_event(&screen->im, event); diff --git a/app/src/screen.h b/app/src/screen.h index 6621b2d2..7e1f7e6e 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -1,14 +1,11 @@ -#ifndef SC_SCREEN_H -#define SC_SCREEN_H +#ifndef SCREEN_H +#define SCREEN_H #include "common.h" #include -#include #include -#include -#include -#include +#include #include "controller.h" #include "coords.h" @@ -16,7 +13,7 @@ #include "fps_counter.h" #include "frame_buffer.h" #include "input_manager.h" -#include "mouse_capture.h" +#include "opengl.h" #include "options.h" #include "trait/key_processor.h" #include "trait/frame_sink.h" @@ -33,7 +30,6 @@ struct sc_screen { 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; @@ -65,6 +61,10 @@ struct sc_screen { bool maximized; bool minimized; + // To enable/disable mouse capture, a mouse capture key (LALT, LGUI or + // RGUI) must be pressed. This variable tracks the pressed capture key. + SDL_Keycode mouse_capture_key_pressed; + AVFrame *frame; bool paused; @@ -126,9 +126,9 @@ sc_screen_destroy(struct sc_screen *screen); void sc_screen_hide_window(struct sc_screen *screen); -// toggle the fullscreen mode +// switch the fullscreen mode void -sc_screen_toggle_fullscreen(struct sc_screen *screen); +sc_screen_switch_fullscreen(struct sc_screen *screen); // resize window to optimal size (remove black borders) void diff --git a/app/src/server.c b/app/src/server.c index 153219c3..90a0ac5d 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -1,18 +1,18 @@ #include "server.h" #include +#include #include #include -#include -#include -#include +#include +#include #include "adb/adb.h" -#include "util/env.h" +#include "util/binary.h" #include "util/file.h" #include "util/log.h" #include "util/net_intr.h" -#include "util/process.h" +#include "util/process_intr.h" #include "util/str.h" #define SC_SERVER_FILENAME "scrcpy-server" @@ -25,22 +25,35 @@ static char * get_server_path(void) { - char *server_path = sc_get_env("SCRCPY_SERVER_PATH"); - if (server_path) { +#ifdef __WINDOWS__ + const wchar_t *server_path_env = _wgetenv(L"SCRCPY_SERVER_PATH"); +#else + const char *server_path_env = getenv("SCRCPY_SERVER_PATH"); +#endif + if (server_path_env) { // if the envvar is set, use it +#ifdef __WINDOWS__ + char *server_path = sc_str_from_wchars(server_path_env); +#else + char *server_path = strdup(server_path_env); +#endif + if (!server_path) { + LOG_OOM(); + return NULL; + } LOGD("Using SCRCPY_SERVER_PATH: %s", server_path); return server_path; } #ifndef PORTABLE LOGD("Using server: " SC_SERVER_PATH_DEFAULT); - server_path = strdup(SC_SERVER_PATH_DEFAULT); + char *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); + char *server_path = sc_file_get_local_path(SC_SERVER_FILENAME); if (!server_path) { LOGE("Could not get local file path, " "using " SC_SERVER_FILENAME " from current directory"); @@ -53,6 +66,56 @@ get_server_path(void) { return server_path; } +static void +sc_server_params_destroy(struct sc_server_params *params) { + // The server stores a copy of the params provided by the user + free((char *) params->req_serial); + free((char *) params->crop); + free((char *) params->video_codec_options); + free((char *) params->audio_codec_options); + free((char *) params->video_encoder); + free((char *) params->audio_encoder); + free((char *) params->tcpip_dst); + free((char *) params->camera_id); + free((char *) params->camera_ar); +} + +static bool +sc_server_params_copy(struct sc_server_params *dst, + const struct sc_server_params *src) { + *dst = *src; + + // The params reference user-allocated memory, so we must copy them to + // handle them from another thread + +#define COPY(FIELD) do { \ + dst->FIELD = NULL; \ + if (src->FIELD) { \ + dst->FIELD = strdup(src->FIELD); \ + if (!dst->FIELD) { \ + goto error; \ + } \ + } \ +} while(0) + + COPY(req_serial); + COPY(crop); + COPY(video_codec_options); + COPY(audio_codec_options); + COPY(video_encoder); + COPY(audio_encoder); + COPY(tcpip_dst); + COPY(camera_id); + COPY(camera_ar); +#undef COPY + + return true; + +error: + sc_server_params_destroy(dst); + return false; +} + static bool push_server(struct sc_intr *intr, const char *serial) { char *server_path = get_server_path(); @@ -149,43 +212,12 @@ sc_server_get_audio_source_name(enum sc_audio_source audio_source) { 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 @@ -219,31 +251,18 @@ execute_server(struct sc_server *server, 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; + cmd[count++] = +# ifdef SERVER_DEBUGGER_METHOD_NEW + /* Android 9 and above */ + "-XjdwpProvider:internal -XjdwpOptions:transport=dt_socket,suspend=y," + "server=y,address=" +# else + /* Android 8 and below */ + "-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address=" +# endif + SERVER_DEBUGGER_PORT; #endif - cmd[count++] = "/"; // unused cmd[count++] = "com.genymobile.scrcpy.Server"; cmd[count++] = SCRCPY_VERSION; @@ -305,21 +324,9 @@ execute_server(struct sc_server *server, 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 (params->lock_video_orientation != SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) { + ADD_PARAM("lock_video_orientation=%" PRIi8, + params->lock_video_orientation); } if (server->tunnel.forward) { ADD_PARAM("tunnel_forward=true"); @@ -363,11 +370,6 @@ execute_server(struct sc_server *server, 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); @@ -403,20 +405,6 @@ execute_server(struct sc_server *server, // 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"); } @@ -429,23 +417,16 @@ execute_server(struct sc_server *server, 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) - // + LOGI("Server debugger waiting for a client on device port " + SERVER_DEBUGGER_PORT "..."); + // From the computer, run + // adb forward tcp:5005 tcp:5005 // Then, from Android Studio: Run > Debug > Edit configurations... // On the left, click on '+', "Remote", with: // Host: localhost @@ -518,25 +499,22 @@ connect_to_server(struct sc_server *server, unsigned attempts, sc_tick delay, 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(); + bool ok = sc_server_params_copy(&server->params, params); if (!ok) { + LOG_OOM(); return false; } ok = sc_mutex_init(&server->mutex); if (!ok) { - sc_adb_destroy(); + sc_server_params_destroy(&server->params); return false; } ok = sc_cond_init(&server->cond_stopped); if (!ok) { sc_mutex_destroy(&server->mutex); - sc_adb_destroy(); + sc_server_params_destroy(&server->params); return false; } @@ -544,7 +522,7 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params, if (!ok) { sc_cond_destroy(&server->cond_stopped); sc_mutex_destroy(&server->mutex); - sc_adb_destroy(); + sc_server_params_destroy(&server->params); return false; } @@ -866,14 +844,11 @@ sc_server_switch_to_tcpip(struct sc_server *server, const char *serial) { } static bool -sc_server_connect_to_tcpip(struct sc_server *server, const char *ip_port, - bool disconnect) { +sc_server_connect_to_tcpip(struct sc_server *server, const char *ip_port) { 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); - } + // 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); @@ -889,7 +864,7 @@ sc_server_connect_to_tcpip(struct sc_server *server, const char *ip_port, static bool sc_server_configure_tcpip_known_address(struct sc_server *server, - const char *addr, bool disconnect) { + const char *addr) { // Append ":5555" if no port is present bool contains_port = strchr(addr, ':'); char *ip_port = contains_port ? strdup(addr) @@ -900,7 +875,7 @@ sc_server_configure_tcpip_known_address(struct sc_server *server, } server->serial = ip_port; - return sc_server_connect_to_tcpip(server, ip_port, disconnect); + return sc_server_connect_to_tcpip(server, ip_port); } static bool @@ -925,7 +900,7 @@ sc_server_configure_tcpip_unknown_address(struct sc_server *server, } server->serial = ip_port; - return sc_server_connect_to_tcpip(server, ip_port, false); + return sc_server_connect_to_tcpip(server, ip_port); } static void @@ -1012,13 +987,7 @@ run_server(void *data) { 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); + ok = sc_server_configure_tcpip_known_address(server, params->tcpip_dst); if (!ok) { goto error_connection_failed; } @@ -1192,9 +1161,8 @@ sc_server_destroy(struct sc_server *server) { free(server->serial); free(server->device_socket_name); + sc_server_params_destroy(&server->params); sc_intr_destroy(&server->intr); sc_cond_destroy(&server->cond_stopped); sc_mutex_destroy(&server->mutex); - - sc_adb_destroy(); } diff --git a/app/src/server.h b/app/src/server.h index 5f4592de..d9d42582 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -1,17 +1,19 @@ -#ifndef SC_SERVER_H -#define SC_SERVER_H +#ifndef SERVER_H +#define SERVER_H #include "common.h" +#include #include #include #include "adb/adb_tunnel.h" +#include "coords.h" #include "options.h" #include "util/intr.h" +#include "util/log.h" #include "util/net.h" #include "util/thread.h" -#include "util/tick.h" #define SC_DEVICE_NAME_FIELD_LENGTH 64 struct sc_server_info { @@ -43,14 +45,9 @@ struct sc_server_params { 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; + int8_t lock_video_orientation; 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; @@ -68,8 +65,6 @@ struct sc_server_params { bool power_on; bool kill_adb_on_close; bool camera_high_speed; - bool vd_destroy_content; - bool vd_system_decorations; uint8_t list; }; diff --git a/app/src/shortcut_mod.h b/app/src/shortcut_mod.h deleted file mode 100644 index f6c13f03..00000000 --- a/app/src/shortcut_mod.h +++ /dev/null @@ -1,61 +0,0 @@ -#ifndef SC_SHORTCUT_MOD_H -#define SC_SHORTCUT_MOD_H - -#include "common.h" - -#include -#include -#include -#include - -#include "options.h" - -#define SC_SDL_SHORTCUT_MODS_MASK (KMOD_CTRL | KMOD_ALT | KMOD_GUI) - -// input: OR of enum sc_shortcut_mod -// output: OR of SDL_Keymod -static inline uint16_t -sc_shortcut_mods_to_sdl(uint8_t shortcut_mods) { - uint16_t sdl_mod = 0; - if (shortcut_mods & SC_SHORTCUT_MOD_LCTRL) { - sdl_mod |= KMOD_LCTRL; - } - if (shortcut_mods & SC_SHORTCUT_MOD_RCTRL) { - sdl_mod |= KMOD_RCTRL; - } - if (shortcut_mods & SC_SHORTCUT_MOD_LALT) { - sdl_mod |= KMOD_LALT; - } - if (shortcut_mods & SC_SHORTCUT_MOD_RALT) { - sdl_mod |= KMOD_RALT; - } - if (shortcut_mods & SC_SHORTCUT_MOD_LSUPER) { - sdl_mod |= KMOD_LGUI; - } - if (shortcut_mods & SC_SHORTCUT_MOD_RSUPER) { - sdl_mod |= KMOD_RGUI; - } - return sdl_mod; -} - -static inline bool -sc_shortcut_mods_is_shortcut_mod(uint16_t sdl_shortcut_mods, uint16_t sdl_mod) { - // sdl_shortcut_mods must be within the mask - assert(!(sdl_shortcut_mods & ~SC_SDL_SHORTCUT_MODS_MASK)); - - // at least one shortcut mod pressed? - return sdl_mod & sdl_shortcut_mods; -} - -static inline bool -sc_shortcut_mods_is_shortcut_key(uint16_t sdl_shortcut_mods, - SDL_Keycode keycode) { - return (sdl_shortcut_mods & KMOD_LCTRL && keycode == SDLK_LCTRL) - || (sdl_shortcut_mods & KMOD_RCTRL && keycode == SDLK_RCTRL) - || (sdl_shortcut_mods & KMOD_LALT && keycode == SDLK_LALT) - || (sdl_shortcut_mods & KMOD_RALT && keycode == SDLK_RALT) - || (sdl_shortcut_mods & KMOD_LGUI && keycode == SDLK_LGUI) - || (sdl_shortcut_mods & KMOD_RGUI && keycode == SDLK_RGUI); -} - -#endif diff --git a/app/src/sys/unix/file.c b/app/src/sys/unix/file.c index 8f7fb074..9c3f7333 100644 --- a/app/src/sys/unix/file.c +++ b/app/src/sys/unix/file.c @@ -1,15 +1,11 @@ #include "util/file.h" #include -#include #include +#include #include #include -#include #include -#ifdef __APPLE__ -# include // for _NSGetExecutablePath() -#endif #include "util/log.h" @@ -64,22 +60,11 @@ sc_file_get_executable_path(void) { } 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); + // in practice, we only need this feature for portable builds, only used on + // Windows, so we don't care implementing it for every platform + // (it's useful to have a working version on Linux for debugging though) + return NULL; #endif } diff --git a/app/src/sys/unix/process.c b/app/src/sys/unix/process.c index 36d1ff7d..8c4a53c3 100644 --- a/app/src/sys/unix/process.c +++ b/app/src/sys/unix/process.c @@ -4,8 +4,6 @@ #include #include #include -#include -#include #include #include #include diff --git a/app/src/trait/frame_sink.h b/app/src/trait/frame_sink.h index 67be4d46..8ef248b6 100644 --- a/app/src/trait/frame_sink.h +++ b/app/src/trait/frame_sink.h @@ -3,6 +3,7 @@ #include "common.h" +#include #include #include diff --git a/app/src/trait/frame_source.c b/app/src/trait/frame_source.c index 56848309..416eccd9 100644 --- a/app/src/trait/frame_source.c +++ b/app/src/trait/frame_source.c @@ -1,7 +1,5 @@ #include "frame_source.h" -#include - void sc_frame_source_init(struct sc_frame_source *source) { source->sink_count = 0; diff --git a/app/src/trait/frame_source.h b/app/src/trait/frame_source.h index cb1ef905..94222af0 100644 --- a/app/src/trait/frame_source.h +++ b/app/src/trait/frame_source.h @@ -3,9 +3,7 @@ #include "common.h" -#include - -#include "trait/frame_sink.h" +#include "frame_sink.h" #define SC_FRAME_SOURCE_MAX_SINKS 2 diff --git a/app/src/trait/gamepad_processor.h b/app/src/trait/gamepad_processor.h index 5e8dc2a4..72479783 100644 --- a/app/src/trait/gamepad_processor.h +++ b/app/src/trait/gamepad_processor.h @@ -3,6 +3,9 @@ #include "common.h" +#include +#include + #include "input_events.h" /** @@ -17,22 +20,13 @@ struct sc_gamepad_processor { struct sc_gamepad_processor_ops { /** - * Process a gamepad device added event + * Process a gamepad device added or removed * * 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_gamepad_device)(struct sc_gamepad_processor *gp, + const struct sc_gamepad_device_event *event); /** * Process a gamepad axis event diff --git a/app/src/trait/key_processor.h b/app/src/trait/key_processor.h index 9e9bb86e..96374413 100644 --- a/app/src/trait/key_processor.h +++ b/app/src/trait/key_processor.h @@ -3,6 +3,7 @@ #include "common.h" +#include #include #include "input_events.h" diff --git a/app/src/trait/mouse_processor.h b/app/src/trait/mouse_processor.h index d0a96e7c..6e0b596e 100644 --- a/app/src/trait/mouse_processor.h +++ b/app/src/trait/mouse_processor.h @@ -3,6 +3,7 @@ #include "common.h" +#include #include #include "input_events.h" diff --git a/app/src/trait/packet_sink.h b/app/src/trait/packet_sink.h index e12dea12..84cfe814 100644 --- a/app/src/trait/packet_sink.h +++ b/app/src/trait/packet_sink.h @@ -3,6 +3,7 @@ #include "common.h" +#include #include #include diff --git a/app/src/trait/packet_source.c b/app/src/trait/packet_source.c index 0a2c6c4d..c0836f1d 100644 --- a/app/src/trait/packet_source.c +++ b/app/src/trait/packet_source.c @@ -1,7 +1,5 @@ #include "packet_source.h" -#include - void sc_packet_source_init(struct sc_packet_source *source) { source->sink_count = 0; diff --git a/app/src/trait/packet_source.h b/app/src/trait/packet_source.h index 8788021a..16d56e86 100644 --- a/app/src/trait/packet_source.h +++ b/app/src/trait/packet_source.h @@ -3,9 +3,7 @@ #include "common.h" -#include - -#include "trait/packet_sink.h" +#include "packet_sink.h" #define SC_PACKET_SOURCE_MAX_SINKS 2 diff --git a/app/src/uhid/gamepad_uhid.c b/app/src/uhid/gamepad_uhid.c index c64feb18..62b0f653 100644 --- a/app/src/uhid/gamepad_uhid.c +++ b/app/src/uhid/gamepad_uhid.c @@ -1,10 +1,5 @@ #include "gamepad_uhid.h" -#include -#include -#include -#include - #include "hid/hid_gamepad.h" #include "input_events.h" #include "util/log.h" @@ -12,11 +7,6 @@ /** 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, @@ -40,9 +30,7 @@ sc_gamepad_uhid_send_open(struct sc_gamepad_uhid *gamepad, 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.name = hid_open->name; msg.uhid_create.report_desc = hid_open->report_desc; msg.uhid_create.report_desc_size = hid_open->report_desc_size; @@ -64,39 +52,29 @@ sc_gamepad_uhid_send_close(struct sc_gamepad_uhid *gamepad, } static void -sc_gamepad_processor_process_gamepad_added(struct sc_gamepad_processor *gp, +sc_gamepad_processor_process_gamepad_device(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; + if (event->type == SC_GAMEPAD_DEVICE_ADDED) { + struct sc_hid_open hid_open; + if (!sc_hid_gamepad_generate_open(&gamepad->hid, &hid_open, + event->gamepad_id)) { + return; + } + + sc_gamepad_uhid_send_open(gamepad, &hid_open); + } else { + assert(event->type == SC_GAMEPAD_DEVICE_REMOVED); + + struct sc_hid_close hid_close; + if (!sc_hid_gamepad_generate_close(&gamepad->hid, &hid_close, + event->gamepad_id)) { + return; + } + + sc_gamepad_uhid_send_close(gamepad, &hid_close); } - - 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 @@ -136,8 +114,7 @@ sc_gamepad_uhid_init(struct sc_gamepad_uhid *gamepad, 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_device = sc_gamepad_processor_process_gamepad_device, .process_gamepad_axis = sc_gamepad_processor_process_gamepad_axis, .process_gamepad_button = sc_gamepad_processor_process_gamepad_button, }; diff --git a/app/src/uhid/gamepad_uhid.h b/app/src/uhid/gamepad_uhid.h index ad747604..07d03099 100644 --- a/app/src/uhid/gamepad_uhid.h +++ b/app/src/uhid/gamepad_uhid.h @@ -3,6 +3,8 @@ #include "common.h" +#include + #include "controller.h" #include "hid/hid_gamepad.h" #include "trait/gamepad_processor.h" diff --git a/app/src/uhid/keyboard_uhid.c b/app/src/uhid/keyboard_uhid.c index 70082990..496da23d 100644 --- a/app/src/uhid/keyboard_uhid.c +++ b/app/src/uhid/keyboard_uhid.c @@ -1,12 +1,6 @@ #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) @@ -147,9 +141,7 @@ sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb, 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.name = hid_open.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(controller, &msg)) { diff --git a/app/src/uhid/mouse_uhid.c b/app/src/uhid/mouse_uhid.c index 869e48a4..1dc02777 100644 --- a/app/src/uhid/mouse_uhid.c +++ b/app/src/uhid/mouse_uhid.c @@ -1,8 +1,5 @@ #include "mouse_uhid.h" -#include -#include - #include "hid/hid_mouse.h" #include "input_events.h" #include "util/log.h" @@ -55,9 +52,7 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, 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_hid_mouse_generate_input_from_scroll(&hid_input, event); sc_mouse_uhid_send_input(mouse, &hid_input, "mouse scroll"); } @@ -86,9 +81,7 @@ sc_mouse_uhid_init(struct sc_mouse_uhid *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.name = hid_open.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(controller, &msg)) { diff --git a/app/src/uhid/uhid_output.c b/app/src/uhid/uhid_output.c index e743a73c..05e691da 100644 --- a/app/src/uhid/uhid_output.c +++ b/app/src/uhid/uhid_output.c @@ -1,5 +1,6 @@ #include "uhid_output.h" +#include #include #include "uhid/keyboard_uhid.h" diff --git a/app/src/uhid/uhid_output.h b/app/src/uhid/uhid_output.h index ed028b58..cd6a800f 100644 --- a/app/src/uhid/uhid_output.h +++ b/app/src/uhid/uhid_output.h @@ -3,7 +3,7 @@ #include "common.h" -#include +#include #include /** diff --git a/app/src/usb/aoa_hid.c b/app/src/usb/aoa_hid.c index 8cb62bfd..236a78ed 100644 --- a/app/src/usb/aoa_hid.c +++ b/app/src/usb/aoa_hid.c @@ -1,16 +1,13 @@ -#include "aoa_hid.h" +#include "util/log.h" #include #include #include -#include -#include -#include +#include "aoa_hid.h" #include "events.h" #include "util/log.h" #include "util/str.h" -#include "util/tick.h" #include "util/vector.h" // See . diff --git a/app/src/usb/aoa_hid.h b/app/src/usb/aoa_hid.h index 2755c957..00961c28 100644 --- a/app/src/usb/aoa_hid.h +++ b/app/src/usb/aoa_hid.h @@ -1,15 +1,16 @@ #ifndef SC_AOA_HID_H #define SC_AOA_HID_H -#include "common.h" - -#include #include +#include + +#include #include "hid/hid_event.h" -#include "usb/usb.h" +#include "usb.h" #include "util/acksync.h" #include "util/thread.h" +#include "util/tick.h" #include "util/vecdeque.h" enum sc_aoa_event_type { diff --git a/app/src/usb/gamepad_aoa.c b/app/src/usb/gamepad_aoa.c index d29b1a78..37587532 100644 --- a/app/src/usb/gamepad_aoa.c +++ b/app/src/usb/gamepad_aoa.c @@ -1,7 +1,5 @@ #include "gamepad_aoa.h" -#include - #include "input_events.h" #include "util/log.h" @@ -9,35 +7,33 @@ #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, +sc_gamepad_processor_process_gamepad_device(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; - } + if (event->type == SC_GAMEPAD_DEVICE_ADDED) { + 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)"); - } -} + // 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)"); + } + } else { + assert(event->type == SC_GAMEPAD_DEVICE_REMOVED); -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; + } - 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)"); + if (!sc_aoa_push_close(gamepad->aoa, &hid_close)) { + LOGW("Could not push AOA HID close (gamepad)"); + } } } @@ -80,8 +76,7 @@ sc_gamepad_aoa_init(struct sc_gamepad_aoa *gamepad, struct sc_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_device = sc_gamepad_processor_process_gamepad_device, .process_gamepad_axis = sc_gamepad_processor_process_gamepad_axis, .process_gamepad_button = sc_gamepad_processor_process_gamepad_button, }; diff --git a/app/src/usb/gamepad_aoa.h b/app/src/usb/gamepad_aoa.h index 0297a365..b2dfbe5e 100644 --- a/app/src/usb/gamepad_aoa.h +++ b/app/src/usb/gamepad_aoa.h @@ -3,8 +3,10 @@ #include "common.h" +#include + +#include "aoa_hid.h" #include "hid/hid_gamepad.h" -#include "usb/aoa_hid.h" #include "trait/gamepad_processor.h" struct sc_gamepad_aoa { diff --git a/app/src/usb/keyboard_aoa.h b/app/src/usb/keyboard_aoa.h index 9e9500a3..565b9177 100644 --- a/app/src/usb/keyboard_aoa.h +++ b/app/src/usb/keyboard_aoa.h @@ -5,8 +5,8 @@ #include +#include "aoa_hid.h" #include "hid/hid_keyboard.h" -#include "usb/aoa_hid.h" #include "trait/key_processor.h" struct sc_keyboard_aoa { diff --git a/app/src/usb/mouse_aoa.c b/app/src/usb/mouse_aoa.c index fd5fa5e0..cb566cc0 100644 --- a/app/src/usb/mouse_aoa.c +++ b/app/src/usb/mouse_aoa.c @@ -1,7 +1,6 @@ #include "mouse_aoa.h" #include -#include #include "hid/hid_mouse.h" #include "input_events.h" @@ -42,9 +41,7 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, 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; - } + sc_hid_mouse_generate_input_from_scroll(&hid_input, event); if (!sc_aoa_push_input(mouse->aoa, &hid_input)) { LOGW("Could not push AOA HID input (mouse scroll)"); diff --git a/app/src/usb/mouse_aoa.h b/app/src/usb/mouse_aoa.h index 506286ba..afaed761 100644 --- a/app/src/usb/mouse_aoa.h +++ b/app/src/usb/mouse_aoa.h @@ -5,7 +5,7 @@ #include -#include "usb/aoa_hid.h" +#include "aoa_hid.h" #include "trait/mouse_processor.h" struct sc_mouse_aoa { diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c index 1a9cc46e..9595face 100644 --- a/app/src/usb/scrcpy_otg.c +++ b/app/src/usb/scrcpy_otg.c @@ -1,19 +1,10 @@ #include "scrcpy_otg.h" -#include -#include -#include #include -#ifdef _WIN32 -# include "adb/adb.h" -#endif +#include "adb/adb.h" #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 "screen_otg.h" #include "util/log.h" struct scrcpy_otg { @@ -104,14 +95,9 @@ scrcpy_otg(struct scrcpy_options *options) { // 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"); - } + unsigned flags = SC_ADB_NO_STDOUT | SC_ADB_NO_STDERR | SC_ADB_NO_LOGERR; + // uninterruptible (intr == NULL), but in practice it's very quick + sc_adb_kill_server(NULL, flags); #endif static const struct sc_usb_callbacks cbs = { @@ -199,7 +185,6 @@ scrcpy_otg(struct scrcpy_options *options) { .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); diff --git a/app/src/usb/screen_otg.c b/app/src/usb/screen_otg.c index 5c580df9..b13f8d04 100644 --- a/app/src/usb/screen_otg.c +++ b/app/src/usb/screen_otg.c @@ -1,13 +1,50 @@ #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_set_mouse_capture(struct sc_screen_otg *screen, bool capture) { +#ifdef __APPLE__ + // Workaround for SDL bug on macOS: + // + if (capture) { + int mouse_x, mouse_y; + SDL_GetGlobalMouseState(&mouse_x, &mouse_y); + + int x, y, w, h; + SDL_GetWindowPosition(screen->window, &x, &y); + SDL_GetWindowSize(screen->window, &w, &h); + + bool outside_window = mouse_x < x || mouse_x >= x + w + || mouse_y < y || mouse_y >= y + h; + if (outside_window) { + SDL_WarpMouseInWindow(screen->window, w / 2, h / 2); + } + } +#else + (void) screen; +#endif + if (SDL_SetRelativeMouseMode(capture)) { + LOGE("Could not set relative mouse mode to %s: %s", + capture ? "true" : "false", SDL_GetError()); + } +} + +static inline bool +sc_screen_otg_get_mouse_capture(struct sc_screen_otg *screen) { + (void) screen; + return SDL_GetRelativeMouseMode(); +} + +static inline void +sc_screen_otg_toggle_mouse_capture(struct sc_screen_otg *screen) { + (void) screen; + bool new_value = !sc_screen_otg_get_mouse_capture(screen); + sc_screen_otg_set_mouse_capture(screen, new_value); +} + static void sc_screen_otg_render(struct sc_screen_otg *screen) { SDL_RenderClear(screen->renderer); @@ -24,6 +61,8 @@ sc_screen_otg_init(struct sc_screen_otg *screen, screen->mouse = params->mouse; screen->gamepad = params->gamepad; + screen->mouse_capture_key_pressed = 0; + const char *title = params->window_title; assert(title); @@ -74,11 +113,9 @@ sc_screen_otg_init(struct sc_screen_otg *screen, 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); + sc_screen_otg_set_mouse_capture(screen, true); } return true; @@ -100,6 +137,11 @@ sc_screen_otg_destroy(struct sc_screen_otg *screen) { SDL_DestroyWindow(screen->window); } +static inline bool +sc_screen_otg_is_mouse_capture_key(SDL_Keycode key) { + return key == SDLK_LALT || key == SDLK_LGUI || key == SDLK_RGUI; +} + static void sc_screen_otg_process_key(struct sc_screen_otg *screen, const SDL_KeyboardEvent *event) { @@ -164,15 +206,8 @@ sc_screen_otg_process_mouse_wheel(struct sc_screen_otg *screen, 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), }; @@ -186,6 +221,7 @@ sc_screen_otg_process_gamepad_device(struct sc_screen_otg *screen, assert(screen->gamepad); struct sc_gamepad_processor *gp = &screen->gamepad->gamepad_processor; + SDL_JoystickID id; if (event->type == SDL_CONTROLLERDEVICEADDED) { SDL_GameController *gc = SDL_GameControllerOpen(event->which); if (!gc) { @@ -200,12 +236,9 @@ sc_screen_otg_process_gamepad_device(struct sc_screen_otg *screen, return; } - struct sc_gamepad_device_event evt = { - .gamepad_id = SDL_JoystickInstanceID(joystick), - }; - gp->ops->process_gamepad_added(gp, &evt); + id = SDL_JoystickInstanceID(joystick); } else if (event->type == SDL_CONTROLLERDEVICEREMOVED) { - SDL_JoystickID id = event->which; + id = event->which; SDL_GameController *gc = SDL_GameControllerFromInstanceID(id); if (gc) { @@ -213,12 +246,16 @@ sc_screen_otg_process_gamepad_device(struct sc_screen_otg *screen, } else { LOGW("Unknown gamepad device removed"); } - - struct sc_gamepad_device_event evt = { - .gamepad_id = id, - }; - gp->ops->process_gamepad_removed(gp, &evt); + } else { + // Nothing to do + return; } + + struct sc_gamepad_device_event evt = { + .type = sc_gamepad_device_event_type_from_sdl_type(event->type), + .gamepad_id = id, + }; + gp->ops->process_gamepad_device(gp, &evt); } static void @@ -261,46 +298,80 @@ sc_screen_otg_process_gamepad_button(struct sc_screen_otg *screen, 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; + case SDL_WINDOWEVENT_FOCUS_LOST: + if (screen->mouse) { + sc_screen_otg_set_mouse_capture(screen, false); + } + break; } return; case SDL_KEYDOWN: + if (screen->mouse) { + SDL_Keycode key = event->key.keysym.sym; + if (sc_screen_otg_is_mouse_capture_key(key)) { + if (!screen->mouse_capture_key_pressed) { + screen->mouse_capture_key_pressed = key; + } else { + // Another mouse capture key has been pressed, cancel + // mouse (un)capture + screen->mouse_capture_key_pressed = 0; + } + // Mouse capture keys are never forwarded to the device + return; + } + } + if (screen->keyboard) { sc_screen_otg_process_key(screen, &event->key); } break; case SDL_KEYUP: + if (screen->mouse) { + SDL_Keycode key = event->key.keysym.sym; + SDL_Keycode cap = screen->mouse_capture_key_pressed; + screen->mouse_capture_key_pressed = 0; + if (sc_screen_otg_is_mouse_capture_key(key)) { + if (key == cap) { + // A mouse capture key has been pressed then released: + // toggle the capture mouse mode + sc_screen_otg_toggle_mouse_capture(screen); + } + // Mouse capture keys are never forwarded to the device + return; + } + } + if (screen->keyboard) { sc_screen_otg_process_key(screen, &event->key); } break; case SDL_MOUSEMOTION: - if (screen->mouse) { + if (screen->mouse && sc_screen_otg_get_mouse_capture(screen)) { sc_screen_otg_process_mouse_motion(screen, &event->motion); } break; case SDL_MOUSEBUTTONDOWN: - if (screen->mouse) { + if (screen->mouse && sc_screen_otg_get_mouse_capture(screen)) { sc_screen_otg_process_mouse_button(screen, &event->button); } break; case SDL_MOUSEBUTTONUP: if (screen->mouse) { - sc_screen_otg_process_mouse_button(screen, &event->button); + if (sc_screen_otg_get_mouse_capture(screen)) { + sc_screen_otg_process_mouse_button(screen, &event->button); + } else { + sc_screen_otg_set_mouse_capture(screen, true); + } } break; case SDL_MOUSEWHEEL: - if (screen->mouse) { + if (screen->mouse && sc_screen_otg_get_mouse_capture(screen)) { sc_screen_otg_process_mouse_wheel(screen, &event->wheel); } break; diff --git a/app/src/usb/screen_otg.h b/app/src/usb/screen_otg.h index 08b76ae7..2ea76eda 100644 --- a/app/src/usb/screen_otg.h +++ b/app/src/usb/screen_otg.h @@ -4,13 +4,11 @@ #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" +#include "keyboard_aoa.h" +#include "mouse_aoa.h" +#include "gamepad_aoa.h" struct sc_screen_otg { struct sc_keyboard_aoa *keyboard; @@ -21,7 +19,8 @@ struct sc_screen_otg { SDL_Renderer *renderer; SDL_Texture *texture; - struct sc_mouse_capture mc; + // See equivalent mechanism in screen.h + SDL_Keycode mouse_capture_key_pressed; }; struct sc_screen_otg_params { @@ -36,7 +35,6 @@ struct sc_screen_otg_params { uint16_t window_width; uint16_t window_height; bool window_borderless; - uint8_t shortcut_mods; // OR of enum sc_shortcut_mod values }; bool diff --git a/app/src/util/acksync.c b/app/src/util/acksync.c index 76ecee0d..2899cdcb 100644 --- a/app/src/util/acksync.c +++ b/app/src/util/acksync.c @@ -1,6 +1,7 @@ #include "acksync.h" #include +#include "util/log.h" bool sc_acksync_init(struct sc_acksync *as) { diff --git a/app/src/util/acksync.h b/app/src/util/acksync.h index 3d9c9b2f..58ab1b35 100644 --- a/app/src/util/acksync.h +++ b/app/src/util/acksync.h @@ -3,10 +3,7 @@ #include "common.h" -#include -#include -#include "util/thread.h" -#include "util/tick.h" +#include "thread.h" #define SC_SEQUENCE_INVALID 0 diff --git a/app/src/util/audiobuf.c b/app/src/util/audiobuf.c index eeb27514..3cc5cad1 100644 --- a/app/src/util/audiobuf.c +++ b/app/src/util/audiobuf.c @@ -116,38 +116,3 @@ sc_audiobuf_write(struct sc_audiobuf *buf, const void *from_, return samples_count; } - -uint32_t -sc_audiobuf_write_silence(struct sc_audiobuf *buf, uint32_t samples_count) { - // Only the writer thread can write head, so memory_order_relaxed is - // sufficient - uint32_t head = atomic_load_explicit(&buf->head, memory_order_relaxed); - - // The tail cursor is updated after the data is consumed by the reader - uint32_t tail = atomic_load_explicit(&buf->tail, memory_order_acquire); - - uint32_t can_write = (buf->alloc_size + tail - head - 1) % buf->alloc_size; - if (!can_write) { - return 0; - } - if (samples_count > can_write) { - samples_count = can_write; - } - - uint32_t right_count = buf->alloc_size - head; - if (right_count > samples_count) { - right_count = samples_count; - } - memset(buf->data + (head * buf->sample_size), 0, - right_count * buf->sample_size); - - if (samples_count > right_count) { - uint32_t left_count = samples_count - right_count; - memset(buf->data, 0, left_count * buf->sample_size); - } - - uint32_t new_head = (head + samples_count) % buf->alloc_size; - atomic_store_explicit(&buf->head, new_head, memory_order_release); - - return samples_count; -} diff --git a/app/src/util/audiobuf.h b/app/src/util/audiobuf.h index b55a5a59..5e7dd4a0 100644 --- a/app/src/util/audiobuf.h +++ b/app/src/util/audiobuf.h @@ -6,7 +6,6 @@ #include #include #include -#include #include /** @@ -50,9 +49,6 @@ 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); diff --git a/app/src/util/average.h b/app/src/util/average.h index eded9987..59fae7d1 100644 --- a/app/src/util/average.h +++ b/app/src/util/average.h @@ -3,6 +3,9 @@ #include "common.h" +#include +#include + struct sc_average { // Current average value float avg; diff --git a/app/src/util/binary.h b/app/src/util/binary.h index b6ce3201..7de9b505 100644 --- a/app/src/util/binary.h +++ b/app/src/util/binary.h @@ -4,6 +4,7 @@ #include "common.h" #include +#include #include static inline void diff --git a/app/src/util/env.c b/app/src/util/env.c deleted file mode 100644 index 127f5a1f..00000000 --- a/app/src/util/env.c +++ /dev/null @@ -1,31 +0,0 @@ -#include "env.h" - -#include -#include -#ifdef _WIN32 -# include "util/str.h" -#endif - -char * -sc_get_env(const char *varname) { -#ifdef _WIN32 - wchar_t *w_varname = sc_str_to_wchars(varname); - if (!w_varname) { - return NULL; - } - const wchar_t *value = _wgetenv(w_varname); - free(w_varname); - if (!value) { - return NULL; - } - - return sc_str_from_wchars(value); -#else - const char *value = getenv(varname); - if (!value) { - return NULL; - } - - return strdup(value); -#endif -} diff --git a/app/src/util/env.h b/app/src/util/env.h deleted file mode 100644 index 50a31165..00000000 --- a/app/src/util/env.h +++ /dev/null @@ -1,12 +0,0 @@ -#ifndef SC_ENV_H -#define SC_ENV_H - -#include "common.h" - -// Return the value of the environment variable (may be NULL). -// -// The returned value must be freed by the caller. -char * -sc_get_env(const char *varname); - -#endif diff --git a/app/src/util/intmap.h b/app/src/util/intmap.h index 7ab903ca..2898c461 100644 --- a/app/src/util/intmap.h +++ b/app/src/util/intmap.h @@ -3,7 +3,6 @@ #include "common.h" -#include #include struct sc_intmap_entry { diff --git a/app/src/util/intr.c b/app/src/util/intr.c index ddf4839f..22bd121a 100644 --- a/app/src/util/intr.c +++ b/app/src/util/intr.c @@ -1,9 +1,9 @@ #include "intr.h" -#include - #include "util/log.h" +#include + bool sc_intr_init(struct sc_intr *intr) { bool ok = sc_mutex_init(&intr->mutex); diff --git a/app/src/util/intr.h b/app/src/util/intr.h index 35bd3375..1c20f6df 100644 --- a/app/src/util/intr.h +++ b/app/src/util/intr.h @@ -6,9 +6,9 @@ #include #include -#include "util/net.h" -#include "util/process.h" -#include "util/thread.h" +#include "net.h" +#include "process.h" +#include "thread.h" /** * Interruptor to wake up a blocking call from another thread diff --git a/app/src/util/log.c b/app/src/util/log.c index 9114a258..8a347c84 100644 --- a/app/src/util/log.c +++ b/app/src/util/log.c @@ -4,10 +4,7 @@ # include #endif #include -#include -#include -#include -#include +#include static SDL_LogPriority log_level_sc_to_sdl(enum sc_log_level level) { diff --git a/app/src/util/net.c b/app/src/util/net.c index 9562ff6b..d43d1c7a 100644 --- a/app/src/util/net.c +++ b/app/src/util/net.c @@ -1,27 +1,32 @@ #include "net.h" #include +#include #include +#include "log.h" + #ifdef _WIN32 # include typedef int socklen_t; + typedef SOCKET sc_raw_socket; +# define SC_RAW_SOCKET_NONE INVALID_SOCKET #else -# include -# include +# include +# include # include # include +# include # include -# include -# include +# include # define SOCKET_ERROR -1 typedef struct sockaddr_in SOCKADDR_IN; typedef struct sockaddr SOCKADDR; typedef struct in_addr IN_ADDR; + typedef int sc_raw_socket; +# define SC_RAW_SOCKET_NONE -1 #endif -#include "util/log.h" - bool net_init(void) { #ifdef _WIN32 @@ -42,26 +47,17 @@ net_cleanup(void) { #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) { +#ifdef _WIN32 + if (sock == INVALID_SOCKET) { return SC_SOCKET_NONE; } - struct sc_socket_wrapper *socket = malloc(sizeof(*socket)); + struct sc_socket_windows *socket = malloc(sizeof(*socket)); if (!socket) { LOG_OOM(); - sc_raw_socket_close(sock); + closesocket(sock); return SC_SOCKET_NONE; } @@ -76,9 +72,9 @@ wrap(sc_raw_socket sock) { static inline sc_raw_socket unwrap(sc_socket socket) { -#ifdef SC_SOCKET_CLOSE_ON_INTERRUPT +#ifdef _WIN32 if (socket == SC_SOCKET_NONE) { - return SC_RAW_SOCKET_NONE; + return INVALID_SOCKET; } return socket->socket; @@ -87,6 +83,17 @@ unwrap(sc_socket socket) { #endif } +#ifndef HAVE_SOCK_CLOEXEC // avoid unused-function warning +static inline bool +sc_raw_socket_close(sc_raw_socket raw_sock) { +#ifndef _WIN32 + return !close(raw_sock); +#else + return !closesocket(raw_sock); +#endif +} +#endif + #ifndef HAVE_SOCK_CLOEXEC // If SOCK_CLOEXEC does not exist, the flag must be set manually once the // socket is created @@ -241,9 +248,9 @@ net_interrupt(sc_socket socket) { sc_raw_socket raw_sock = unwrap(socket); -#ifdef SC_SOCKET_CLOSE_ON_INTERRUPT +#ifdef _WIN32 if (!atomic_flag_test_and_set(&socket->closed)) { - return sc_raw_socket_close(raw_sock); + return !closesocket(raw_sock); } return true; #else @@ -255,15 +262,15 @@ bool net_close(sc_socket socket) { sc_raw_socket raw_sock = unwrap(socket); -#ifdef SC_SOCKET_CLOSE_ON_INTERRUPT +#ifdef _WIN32 bool ret = true; if (!atomic_flag_test_and_set(&socket->closed)) { - ret = sc_raw_socket_close(raw_sock); + ret = !closesocket(raw_sock); } free(socket); return ret; #else - return sc_raw_socket_close(raw_sock); + return !close(raw_sock); #endif } diff --git a/app/src/util/net.h b/app/src/util/net.h index aa99bbc4..ea54b793 100644 --- a/app/src/util/net.h +++ b/app/src/util/net.h @@ -4,40 +4,24 @@ #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; + typedef struct sc_socket_windows { + SOCKET socket; atomic_flag closed; } *sc_socket; -#else + +#else // not _WIN32 + +# include # define SC_SOCKET_NONE -1 - typedef sc_raw_socket sc_socket; + typedef int sc_socket; + #endif #define IPV4_LOCALHOST 0x7F000001 diff --git a/app/src/util/net_intr.h b/app/src/util/net_intr.h index e2bbee88..dbef528d 100644 --- a/app/src/util/net_intr.h +++ b/app/src/util/net_intr.h @@ -3,13 +3,8 @@ #include "common.h" -#include -#include -#include -#include - -#include "util/intr.h" -#include "util/net.h" +#include "intr.h" +#include "net.h" bool net_connect_intr(struct sc_intr *intr, sc_socket socket, uint32_t addr, diff --git a/app/src/util/process.c b/app/src/util/process.c index 29d89a54..9c4dcd9f 100644 --- a/app/src/util/process.c +++ b/app/src/util/process.c @@ -1,6 +1,8 @@ #include "process.h" #include +#include +#include "log.h" enum sc_process_result sc_process_execute(const char *const argv[], sc_pid *pid, unsigned flags) { diff --git a/app/src/util/process.h b/app/src/util/process.h index eec51bcc..4d9d1684 100644 --- a/app/src/util/process.h +++ b/app/src/util/process.h @@ -4,9 +4,7 @@ #include "common.h" #include -#include #include "util/thread.h" -#include "util/tick.h" #ifdef _WIN32 diff --git a/app/src/util/process_intr.c b/app/src/util/process_intr.c index 641440ab..d37bd5a5 100644 --- a/app/src/util/process_intr.c +++ b/app/src/util/process_intr.c @@ -5,7 +5,7 @@ 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; + return false; } ssize_t ret = sc_pipe_read(pipe, data, len); @@ -22,7 +22,7 @@ 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; + return false; } ssize_t ret = sc_pipe_read_all(pipe, data, len); diff --git a/app/src/util/process_intr.h b/app/src/util/process_intr.h index 020eafa1..530a9046 100644 --- a/app/src/util/process_intr.h +++ b/app/src/util/process_intr.h @@ -3,8 +3,8 @@ #include "common.h" -#include "util/intr.h" -#include "util/process.h" +#include "intr.h" +#include "process.h" ssize_t sc_pipe_read_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe, char *data, diff --git a/app/src/util/str.c b/app/src/util/str.c index 83d19c4d..755369d8 100644 --- a/app/src/util/str.c +++ b/app/src/util/str.c @@ -12,8 +12,8 @@ # include #endif -#include "util/log.h" -#include "util/strbuf.h" +#include "log.h" +#include "strbuf.h" size_t sc_strncpy(char *dest, const char *src, size_t n) { @@ -64,26 +64,6 @@ sc_str_quote(const char *src) { 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; diff --git a/app/src/util/str.h b/app/src/util/str.h index b386b48d..20da26f0 100644 --- a/app/src/util/str.h +++ b/app/src/util/str.h @@ -5,8 +5,6 @@ #include #include -#include -#include /* Stringify a numeric value */ #define SC_STR(s) SC_XSTR(s) @@ -40,15 +38,6 @@ sc_str_join(char *dst, const char *const tokens[], char sep, size_t n); 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` * diff --git a/app/src/util/strbuf.c b/app/src/util/strbuf.c index 6196d746..1892b46b 100644 --- a/app/src/util/strbuf.c +++ b/app/src/util/strbuf.c @@ -1,10 +1,11 @@ #include "strbuf.h" #include +#include #include #include -#include "util/log.h" +#include "log.h" bool sc_strbuf_init(struct sc_strbuf *buf, size_t init_cap) { diff --git a/app/src/util/thread.c b/app/src/util/thread.c index 2a5253f7..9679dfff 100644 --- a/app/src/util/thread.c +++ b/app/src/util/thread.c @@ -1,12 +1,10 @@ #include "thread.h" #include -#include -#include #include #include -#include "util/log.h" +#include "log.h" sc_thread_id SC_MAIN_THREAD_ID; diff --git a/app/src/util/tick.c b/app/src/util/tick.c index edef1070..cc0bab5e 100644 --- a/app/src/util/tick.c +++ b/app/src/util/tick.c @@ -1,7 +1,6 @@ #include "tick.h" #include -#include #include #ifdef _WIN32 # include diff --git a/app/src/util/tick.h b/app/src/util/tick.h index b037734b..2d941f23 100644 --- a/app/src/util/tick.h +++ b/app/src/util/tick.h @@ -10,14 +10,14 @@ typedef int64_t sc_tick; #define SC_TICK_FREQ 1000000 // microsecond // To be adapted if SC_TICK_FREQ changes -#define SC_TICK_TO_NS(tick) ((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) +#define SC_TICK_TO_NS(tick) ((tick) * 1000) +#define SC_TICK_TO_US(tick) (tick) +#define SC_TICK_TO_MS(tick) ((tick) / 1000) +#define SC_TICK_TO_SEC(tick) ((tick) / 1000000) +#define SC_TICK_FROM_NS(ns) ((ns) / 1000) +#define SC_TICK_FROM_US(us) (us) +#define SC_TICK_FROM_MS(ms) ((ms) * 1000) +#define SC_TICK_FROM_SEC(sec) ((sec) * 1000000) sc_tick sc_tick_now(void); diff --git a/app/src/util/timeout.c b/app/src/util/timeout.c index 21bc3a53..a1665373 100644 --- a/app/src/util/timeout.c +++ b/app/src/util/timeout.c @@ -1,9 +1,8 @@ #include "timeout.h" #include -#include -#include "util/log.h" +#include "log.h" bool sc_timeout_init(struct sc_timeout *timeout) { @@ -63,7 +62,6 @@ 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); } diff --git a/app/src/util/timeout.h b/app/src/util/timeout.h index a45ae2ae..ae171b86 100644 --- a/app/src/util/timeout.h +++ b/app/src/util/timeout.h @@ -5,8 +5,8 @@ #include -#include "util/thread.h" -#include "util/tick.h" +#include "thread.h" +#include "tick.h" struct sc_timeout { sc_thread thread; diff --git a/app/src/util/vecdeque.h b/app/src/util/vecdeque.h index e31724e2..ce559ee9 100644 --- a/app/src/util/vecdeque.h +++ b/app/src/util/vecdeque.h @@ -6,7 +6,6 @@ #include #include #include -#include #include #include diff --git a/app/src/util/vector.h b/app/src/util/vector.h index 5b399d56..97d7c389 100644 --- a/app/src/util/vector.h +++ b/app/src/util/vector.h @@ -5,8 +5,8 @@ #include #include -#include #include +#include // Adapted from vlc_vector: // diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c index da9e02ef..087e9af4 100644 --- a/app/src/v4l2_sink.c +++ b/app/src/v4l2_sink.c @@ -1,9 +1,5 @@ #include "v4l2_sink.h" -#include -#include -#include -#include #include #include "util/log.h" diff --git a/app/src/v4l2_sink.h b/app/src/v4l2_sink.h index 2b7c5b50..365a739d 100644 --- a/app/src/v4l2_sink.h +++ b/app/src/v4l2_sink.h @@ -3,13 +3,13 @@ #include "common.h" -#include #include #include -#include "frame_buffer.h" +#include "coords.h" #include "trait/frame_sink.h" -#include "util/thread.h" +#include "frame_buffer.h" +#include "util/tick.h" struct sc_v4l2_sink { struct sc_frame_sink frame_sink; // frame sink trait diff --git a/app/src/version.c b/app/src/version.c index f8610714..90ea3334 100644 --- a/app/src/version.c +++ b/app/src/version.c @@ -1,6 +1,5 @@ #include "version.h" -#include #include #include #include @@ -10,7 +9,6 @@ #ifdef HAVE_USB # include #endif -#include void scrcpy_print_version(void) { diff --git a/app/tests/test_audiobuf.c b/app/tests/test_audiobuf.c index 539ee238..94d0f07a 100644 --- a/app/tests/test_audiobuf.c +++ b/app/tests/test_audiobuf.c @@ -113,14 +113,6 @@ static void test_audiobuf_partial_read_write(void) { 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); } diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c index de605cb9..14765792 100644 --- a/app/tests/test_cli.c +++ b/app/tests/test_cli.c @@ -51,6 +51,7 @@ static void test_options(void) { "--fullscreen", "--max-fps", "30", "--max-size", "1024", + "--lock-video-orientation=2", // optional arguments require '=' // "--no-control" is not compatible with "--turn-screen-off" // "--no-playback" is not compatible with "--fulscreen" "--port", "1234:1236", @@ -79,6 +80,7 @@ static void test_options(void) { assert(opts->fullscreen); assert(!strcmp(opts->max_fps, "30")); assert(opts->max_size == 1024); + assert(opts->lock_video_orientation == 2); assert(opts->port_range.first == 1234); assert(opts->port_range.last == 1236); assert(!strcmp(opts->push_target, "/sdcard/Movies")); diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index 0d19919e..72ec61ee 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -127,8 +127,8 @@ static void test_serialize_inject_scroll_event(void) { .height = 1920, }, }, - .hscroll = 16, - .vscroll = -16, + .hscroll = 1, + .vscroll = -1, .buttons = 1, }, }; @@ -141,8 +141,8 @@ static void test_serialize_inject_scroll_event(void) { 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]) + 0x7F, 0xFF, // 1 (float encoded as i16) + 0x80, 0x00, // -1 (float encoded as i16) 0x00, 0x00, 0x00, 0x01, // 1 }; assert(!memcmp(buf, expected, sizeof(expected))); @@ -289,11 +289,11 @@ static void test_serialize_set_clipboard_long(void) { assert(!memcmp(buf, expected, sizeof(expected))); } -static void test_serialize_set_display_power(void) { +static void test_serialize_set_screen_power_mode(void) { struct sc_control_msg msg = { - .type = SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER, - .set_display_power = { - .on = true, + .type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, + .set_screen_power_mode = { + .mode = SC_SCREEN_POWER_MODE_NORMAL, }, }; @@ -302,8 +302,8 @@ static void test_serialize_set_display_power(void) { assert(size == 2); const uint8_t expected[] = { - SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER, - 0x01, // true + SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, + 0x02, // SC_SCREEN_POWER_MODE_NORMAL }; assert(!memcmp(buf, expected, sizeof(expected))); } @@ -329,8 +329,6 @@ static void test_serialize_uhid_create(void) { .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, @@ -339,13 +337,11 @@ static void test_serialize_uhid_create(void) { uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); - assert(size == 24); + assert(size == 20); 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 @@ -411,21 +407,6 @@ static void test_serialize_open_hard_keyboard(void) { 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; @@ -442,12 +423,11 @@ int main(int argc, char *argv[]) { test_serialize_get_clipboard(); test_serialize_set_clipboard(); test_serialize_set_clipboard_long(); - test_serialize_set_display_power(); + test_serialize_set_screen_power_mode(); 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; } diff --git a/app/tests/test_str.c b/app/tests/test_str.c index 4a906d92..5d365ef5 100644 --- a/app/tests/test_str.c +++ b/app/tests/test_str.c @@ -141,16 +141,6 @@ static void test_quote(void) { 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 @@ -399,7 +389,6 @@ int main(int argc, char *argv[]) { test_join_truncated_before_sep(); test_join_truncated_after_sep(); test_quote(); - test_concat(); test_utf8_truncate(); test_parse_integer(); test_parse_integers(); diff --git a/build.gradle b/build.gradle index 81c91d37..f81f7d27 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.7.1' + classpath 'com.android.tools.build:gradle:8.3.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/cross_win32.txt b/cross_win32.txt index ddbc65f3..05f9a86b 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -7,8 +7,6 @@ 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] diff --git a/cross_win64.txt b/cross_win64.txt index a6f16e16..86364ad6 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -7,8 +7,6 @@ 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] diff --git a/doc/audio.md b/doc/audio.md index 142626f5..750163e0 100644 --- a/doc/audio.md +++ b/doc/audio.md @@ -66,20 +66,6 @@ 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 @@ -184,7 +170,7 @@ 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 +scrcpy --display-buffer=200 --audio-buffer=200 ``` It is also possible to configure another audio buffer (the audio output buffer), diff --git a/doc/build.md b/doc/build.md index 7f76b4fd..63bd7ca7 100644 --- a/doc/build.md +++ b/doc/build.md @@ -77,7 +77,7 @@ pip3 install meson sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm # client build dependencies -sudo dnf install SDL2-devel ffms2-devel libusb1-devel libavdevice-free-devel meson gcc make +sudo dnf install SDL2-devel ffms2-devel libusb1-devel meson gcc make # server build dependencies sudo dnf install java-devel @@ -233,10 +233,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v3.3.1`][direct-scrcpy-server] - SHA-256: `a0f70b20aa4998fbf658c94118cd6c8dab6abbb0647a3bdab344d70bc1ebcbb8` + - [`scrcpy-server-v2.7`][direct-scrcpy-server] + SHA-256: `a23c5659f36c260f105c022d27bcb3eafffa26070e7baa9eda66d01377a1adba` -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-server-v3.3.1 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-server-v2.7 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/doc/connection.md b/doc/connection.md index dcf00147..17efbbdc 100644 --- a/doc/connection.md +++ b/doc/connection.md @@ -85,12 +85,6 @@ 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 @@ -113,17 +107,16 @@ 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. +Since Android 11, a [wireless debugging option][adb-wireless] allows to bypass +having to physically connect your device directly to your computer. [adb-wireless]: https://developer.android.com/studio/command-line/adb#wireless-android11-command-line ## Autostart -A small tool (by the scrcpy author) allows you to run arbitrary commands -whenever a new Android device is connected: [AutoAdb]. It can be used to start -scrcpy: +A small tool (by the scrcpy author) allows to run arbitrary commands whenever a +new Android device is connected: [AutoAdb]. It can be used to start scrcpy: ```bash autoadb scrcpy -s '{}' diff --git a/doc/control.md b/doc/control.md index 86c0efe6..34eb7a6a 100644 --- a/doc/control.md +++ b/doc/control.md @@ -23,20 +23,14 @@ To control the device without mirroring: 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 -``` +By default, mouse mode is switched to UHID if video mirroring is disabled (a +relative mouse mode is required). 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 +scrcpy --no-video --no-audio --keyboard=uhid +scrcpy --no-video --no-audio -K # short version ``` To use AOA instead (over USB only): @@ -100,18 +94,14 @@ 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_. +To simulate a 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_. +only inverts _x_. This only works for the default mouse mode (`--mouse=sdk`). diff --git a/doc/develop.md b/doc/develop.md index 21949ea6..e5274783 100644 --- a/doc/develop.md +++ b/doc/develop.md @@ -21,9 +21,9 @@ 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. +buffering (unless `--display-buffer=delay` is specified) to minimize latency. +The client is not aware of the device rotation (which is handled by the server), +it just knows the dimensions of the video frames it receives. Similarly, if audio is enabled, then the server sends a raw audio stream (OPUS by default) of the device audio output (or the microphone if @@ -461,30 +461,26 @@ meson setup x -Dserver_debugger=true meson configure x -Dserver_debugger=true ``` -Then recompile, and run scrcpy. +If your device runs Android 8 or below, set the `server_debugger_method` to +`old` in addition: -For Android < 11, it will start a debugger on port 5005 on the device and wait: +```bash +meson setup x -Dserver_debugger=true -Dserver_debugger_method=old +# or, if x is already configured +meson configure x -Dserver_debugger=true -Dserver_debugger_method=old +``` + +Then recompile. + +When you start scrcpy, it will start a debugger on port 5005 on the device. Redirect that port to the computer: ```bash adb forward tcp:5005 tcp:5005 ``` -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: +In Android Studio, _Run_ > _Debug_ > _Edit configurations..._ On the left, click on +`+`, _Remote_, and fill the form: - Host: `localhost` - Port: `5005` diff --git a/doc/device.md b/doc/device.md index ab1e6ba4..988ad417 100644 --- a/doc/device.md +++ b/doc/device.md @@ -18,46 +18,6 @@ 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 @@ -86,15 +46,6 @@ 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 @@ -111,16 +62,6 @@ 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 @@ -137,48 +78,3 @@ By default, on start, the device is powered on. To prevent this behavior: ```bash scrcpy --no-power-on ``` - - -## Start Android app - -To list the Android apps installed on the device: - -```bash -scrcpy --list-apps -``` - -An app, selected by its package name, can be launched on start: - -``` -scrcpy --start-app=org.mozilla.firefox -``` - -This feature can be used to run an app in a [virtual -display](virtual_display.md): - -``` -scrcpy --new-display=1920x1080 --start-app=org.videolan.vlc -``` - -The app can be optionally forced-stop before being started, by adding a `+` -prefix: - -``` -scrcpy --start-app=+org.mozilla.firefox -``` - -For convenience, it is also possible to select an app by its name, by adding a -`?` prefix: - -``` -scrcpy --start-app=?firefox -``` - -But retrieving app names may take some time (sometimes several seconds), so -passing the package name is recommended. - -The `+` and `?` prefixes can be combined (in that order): - -``` -scrcpy --start-app=+?firefox -``` diff --git a/doc/linux.md b/doc/linux.md index be433df4..6bfe3454 100644 --- a/doc/linux.md +++ b/doc/linux.md @@ -2,23 +2,6 @@ ## Install -### From the official release - -Download a static build of the [latest release]: - - - [`scrcpy-linux-x86_64-v3.3.1.tar.gz`][direct-linux-x86_64] (x86_64) - SHA-256: `bbfe54c6b178adafeaffbbfbbc1548a74486553170c63e63bdd41863ad123422` - -[latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-linux-x86_64-v3.3.1.tar.gz - -and extract it. - -_Static builds of scrcpy for Linux are still experimental._ - - -### From your package manager - Packaging status Scrcpy is packaged in several distributions and package managers: @@ -27,13 +10,13 @@ Scrcpy is packaged in several distributions and package managers: - Arch Linux: `pacman -S scrcpy` - Fedora: `dnf copr enable zeno/scrcpy && dnf install scrcpy` - Gentoo: `emerge scrcpy` - - Snap: ~~`snap install scrcpy`~~ _(obsolete version)_ + - Snap: `snap install scrcpy` - … (see [repology](https://repology.org/project/scrcpy/versions)) +### Latest version -### From an install script - -To install the latest release from `master`, follow this simplified process. +However, the packaged version is not always the latest release. To install the +latest release from `master`, follow this simplified process. First, you need to install the required packages: diff --git a/doc/macos.md b/doc/macos.md index f6b01c30..35d90e9d 100644 --- a/doc/macos.md +++ b/doc/macos.md @@ -2,27 +2,6 @@ ## Install -### From the official release - -Download a static build of the [latest release]: - - - [`scrcpy-macos-aarch64-v3.3.1.tar.gz`][direct-macos-aarch64] (aarch64) - SHA-256: `907b925900ebd8499c1e47acc9689a95bd3a6f9930eb1d7bdfbca8375ae4f139` - - - [`scrcpy-macos-x86_64-v3.3.1.tar.gz`][direct-macos-x86_64] (x86_64) - SHA-256: `69772491dad718eea82fc65c8e89febff7d41c4ce6faff02f4789a588d10fd7d` - -[latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-macos-aarch64-v3.3.1.tar.gz -[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-macos-x86_64-v3.3.1.tar.gz - -and extract it. - -_Static builds of scrcpy for macOS are still experimental._ - - -### From a package manager - Scrcpy is available in [Homebrew]: ```bash @@ -34,7 +13,7 @@ brew install scrcpy You need `adb`, accessible from your `PATH`. If you don't have it yet: ```bash -brew install --cask android-platform-tools +brew install android-platform-tools ``` Alternatively, Scrcpy is also available in [MacPorts], which sets up `adb` for you: diff --git a/doc/mouse.md b/doc/mouse.md index 0bea4aea..ae7c6834 100644 --- a/doc/mouse.md +++ b/doc/mouse.md @@ -34,9 +34,9 @@ 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. +Special capture keys, either Alt or Super, toggle +(disable or enable) the mouse capture. Use one of them to give the control of +the mouse back to the computer. ### UHID @@ -83,9 +83,9 @@ 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` + - 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 @@ -121,9 +121,9 @@ 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` + - `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: diff --git a/doc/shortcuts.md b/doc/shortcuts.md index d22eb473..841ceaa6 100644 --- a/doc/shortcuts.md +++ b/doc/shortcuts.md @@ -30,7 +30,6 @@ _[Super] is typically the Windows or Cmd key._ | 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_ @@ -54,8 +53,7 @@ _[Super] is typically the Windows or Cmd key._ | 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_ + | Tilt (slide vertically with 2 fingers) | Shift+_click-and-move_ | Drag & drop APK file | Install APK from computer | Drag & drop non-APK file | [Push file to device](control.md#push-file-to-device) diff --git a/doc/video.md b/doc/video.md index 4de6814a..ed92cb22 100644 --- a/doc/video.md +++ b/doc/video.md @@ -27,9 +27,6 @@ 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 @@ -96,7 +93,7 @@ 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 +scrcpy --video-codec=h264 --video-encoder='OMX.qcom.video.encoder.avc' ``` @@ -106,45 +103,24 @@ 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 + - `--lock-video-orientation` changes the mirroring orientation (the orientation of the video sent from the device to the computer). This affects the recording. - `--orientation` is applied on the client side, and affects display and recording. For the display, it can be changed dynamically using [shortcuts](shortcuts.md). -To capture the video with a specific orientation: +To lock the mirroring orientation (on the capture side): ```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 +scrcpy --lock-video-orientation # initial (current) orientation +scrcpy --lock-video-orientation=0 # natural orientation +scrcpy --lock-video-orientation=90 # 90° clockwise +scrcpy --lock-video-orientation=180 # 180° +scrcpy --lock-video-orientation=270 # 270° clockwise ``` -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): +To orient the video (on the rendering side): ```bash scrcpy --orientation=0 @@ -165,19 +141,6 @@ 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. @@ -191,11 +154,7 @@ 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). +If `--max-size` is also specified, resizing is applied after cropping. ## Display @@ -216,8 +175,6 @@ 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 @@ -232,15 +189,15 @@ 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 --display-buffer=50 # add 50ms buffering for display scrcpy --v4l2-buffer=300 # add 300ms buffering for v4l2 sink +scrcpy --audio-buffer=200 # set 200ms buffering for audio playback ``` They can be applied simultaneously: ```bash -scrcpy --video-buffer=50 --v4l2-buffer=300 +scrcpy --display-buffer=50 --v4l2-buffer=300 ``` diff --git a/doc/virtual_display.md b/doc/virtual_display.md deleted file mode 100644 index 9f962127..00000000 --- a/doc/virtual_display.md +++ /dev/null @@ -1,77 +0,0 @@ -# Virtual display - -## New display - -To mirror a new virtual display instead of the device screen: - -```bash -scrcpy --new-display=1920x1080 -scrcpy --new-display=1920x1080/420 # force 420 dpi -scrcpy --new-display # use the main display size and density -scrcpy --new-display=/240 # use the main display size and 240 dpi -``` - -The new virtual display is destroyed on exit. - -## Start app - -On some devices, a launcher is available in the virtual display. - -When no launcher is available (or if is explicitly disabled by -[`--no-vd-system-decorations`](#system-decorations)), the virtual display is -empty. In that case, you must [start an Android -app](device.md#start-android-app). - -For example: - -```bash -scrcpy --new-display=1920x1080 --start-app=org.videolan.vlc -``` - -The app may itself be a launcher. For example, to run the open source [Fossify -Launcher]: - -```bash -scrcpy --new-display=1920x1080 --no-vd-system-decorations --start-app=org.fossify.home -``` - -[Fossify Launcher]: https://f-droid.org/en/packages/org.fossify.home/ - - -## System decorations - -By default, virtual display system decorations are enabled. To disable them, use -`--no-vd-system-decorations`: - -``` -scrcpy --new-display --no-vd-system-decorations -``` - -This is useful for some devices which might display a broken UI, or to disable -any default launcher UI available in virtual displays. - -Note that if no app is started, no content will be rendered, so no video frame -will be produced at all. - - -## Destroy on close - -By default, when the virtual display is closed, the running apps are destroyed. - -To move them to the main display instead, use: - -``` -scrcpy --new-display --no-vd-destroy-content -``` - - -## Display IME policy - -By default, the virtual display IME appears on the default display. - -To make it appear on the local display, use `--display-ime-policy=local`: - -```bash -scrcpy --display-id=1 --display-ime-policy=local -scrcpy --new-display --display-ime-policy=local -``` diff --git a/doc/windows.md b/doc/windows.md index 8fa1921f..36e59178 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -2,45 +2,35 @@ ## Install -### From the official release - Download the [latest release]: - - [`scrcpy-win64-v3.3.1.zip`][direct-win64] (64-bit) - SHA-256: `4fcad494772a3ae5de9a133149f8856d2fc429b41795f7cf7c754e0c1bb6fbc0` - - [`scrcpy-win32-v3.3.1.zip`][direct-win32] (32-bit) - SHA-256: `ccdf1b4f5d19dfe760446a107e55b0a010a00e097d46533a161499c9333a20a6` + - [`scrcpy-win64-v2.7.zip`][direct-win64] (64-bit) + SHA-256: `5910bc18d5a16f42d84185ddc7e16a4cee6a6f5f33451559c1a1d6d0099bd5f5` + - [`scrcpy-win32-v2.7.zip`][direct-win32] (32-bit) + SHA-256: `ef4daf89d500f33d78b830625536ecb18481429dd94433e7634c824292059d06` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-win64-v3.3.1.zip -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-win32-v3.3.1.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-win64-v2.7.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-win32-v2.7.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]: +Alternatively, you could install it from packages manager, like [Chocolatey]: ```bash choco install scrcpy choco install adb # if you don't have it yet ``` -From [Scoop]: +or [Scoop]: + ```bash scoop install scrcpy scoop install adb # if you don't have it yet ``` -[WinGet]: https://github.com/microsoft/winget-cli +[Winget]: https://github.com/microsoft/winget-cli [Chocolatey]: https://chocolatey.org/ [Scoop]: https://scoop.sh diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b34b7096..e411586a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip -# https://gradle.org/release-checksums/ -distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/install_release.sh b/install_release.sh index d960932b..3cf3490c 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-server-v3.3.1 -PREBUILT_SERVER_SHA256=a0f70b20aa4998fbf658c94118cd6c8dab6abbb0647a3bdab344d70bc1ebcbb8 +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-server-v2.7 +PREBUILT_SERVER_SHA256=a23c5659f36c260f105c022d27bcb3eafffa26070e7baa9eda66d01377a1adba echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server diff --git a/meson.build b/meson.build index d991d672..f76d5ecf 100644 --- a/meson.build +++ b/meson.build @@ -1,6 +1,6 @@ project('scrcpy', 'c', - version: '3.3.1', - meson_version: '>= 0.49', + version: '2.7', + meson_version: '>= 0.48', default_options: [ 'c_std=c11', 'warning_level=2', diff --git a/meson_options.txt b/meson_options.txt index fd347734..d1030694 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -2,7 +2,7 @@ option('compile_app', type: 'boolean', value: true, description: 'Build the clie 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('server_debugger_method', type: 'combo', choices: ['old', 'new'], value: 'new', description: 'Select the debugger method (Android < 9: "old", Android >= 9: "new")') option('v4l2', type: 'boolean', value: true, description: 'Enable V4L2 feature when supported') option('usb', type: 'boolean', value: true, description: 'Enable HID/OTG features when supported') diff --git a/release.mk b/release.mk new file mode 100644 index 00000000..7f082144 --- /dev/null +++ b/release.mk @@ -0,0 +1,131 @@ +# This makefile provides recipes to build a "portable" version of scrcpy for +# Windows. +# +# Here, "portable" means that the client and server binaries are expected to be +# anywhere, but in the same directory, instead of well-defined separate +# locations (e.g. /usr/bin/scrcpy and /usr/share/scrcpy/scrcpy-server). +# +# In particular, this implies to change the location from where the client push +# the server to the device. + +.PHONY: default clean \ + test \ + build-server \ + prepare-deps \ + build-win32 build-win64 \ + dist-win32 dist-win64 \ + zip-win32 zip-win64 \ + release + +GRADLE ?= ./gradlew + +TEST_BUILD_DIR := build-test +SERVER_BUILD_DIR := build-server +WIN32_BUILD_DIR := build-win32 +WIN64_BUILD_DIR := build-win64 + +VERSION ?= $(shell git describe --tags --exclude='*install-release' --always) + +DIST := dist +WIN32_TARGET_DIR := scrcpy-win32-$(VERSION) +WIN64_TARGET_DIR := scrcpy-win64-$(VERSION) +WIN32_TARGET := $(WIN32_TARGET_DIR).zip +WIN64_TARGET := $(WIN64_TARGET_DIR).zip + +RELEASE_DIR := release-$(VERSION) + +release: clean test build-server zip-win32 zip-win64 + mkdir -p "$(RELEASE_DIR)" + cp "$(SERVER_BUILD_DIR)/server/scrcpy-server" \ + "$(RELEASE_DIR)/scrcpy-server-$(VERSION)" + cp "$(DIST)/$(WIN32_TARGET)" "$(RELEASE_DIR)" + cp "$(DIST)/$(WIN64_TARGET)" "$(RELEASE_DIR)" + cd "$(RELEASE_DIR)" && \ + sha256sum "scrcpy-server-$(VERSION)" \ + "scrcpy-win32-$(VERSION).zip" \ + "scrcpy-win64-$(VERSION).zip" > SHA256SUMS.txt + @echo "Release generated in $(RELEASE_DIR)/" + +clean: + $(GRADLE) clean + rm -rf "$(DIST)" "$(TEST_BUILD_DIR)" "$(SERVER_BUILD_DIR)" \ + "$(WIN32_BUILD_DIR)" "$(WIN64_BUILD_DIR)" + +test: + [ -d "$(TEST_BUILD_DIR)" ] || ( mkdir "$(TEST_BUILD_DIR)" && \ + meson setup "$(TEST_BUILD_DIR)" -Db_sanitize=address ) + ninja -C "$(TEST_BUILD_DIR)" + $(GRADLE) -p server check + +build-server: + [ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \ + meson setup "$(SERVER_BUILD_DIR)" --buildtype release -Dcompile_app=false ) + ninja -C "$(SERVER_BUILD_DIR)" + +prepare-deps-win32: + @app/deps/adb.sh win32 + @app/deps/sdl.sh win32 + @app/deps/ffmpeg.sh win32 + @app/deps/libusb.sh win32 + +prepare-deps-win64: + @app/deps/adb.sh win64 + @app/deps/sdl.sh win64 + @app/deps/ffmpeg.sh win64 + @app/deps/libusb.sh win64 + +build-win32: prepare-deps-win32 + rm -rf "$(WIN32_BUILD_DIR)" + mkdir -p "$(WIN32_BUILD_DIR)/local" + meson setup "$(WIN32_BUILD_DIR)" \ + --pkg-config-path="app/deps/work/install/win32/lib/pkgconfig" \ + -Dc_args="-I$(PWD)/app/deps/work/install/win32/include" \ + -Dc_link_args="-L$(PWD)/app/deps/work/install/win32/lib" \ + --cross-file=cross_win32.txt \ + --buildtype=release --strip -Db_lto=true \ + -Dcompile_server=false \ + -Dportable=true + ninja -C "$(WIN32_BUILD_DIR)" + +build-win64: prepare-deps-win64 + rm -rf "$(WIN64_BUILD_DIR)" + mkdir -p "$(WIN64_BUILD_DIR)/local" + meson setup "$(WIN64_BUILD_DIR)" \ + --pkg-config-path="app/deps/work/install/win64/lib/pkgconfig" \ + -Dc_args="-I$(PWD)/app/deps/work/install/win64/include" \ + -Dc_link_args="-L$(PWD)/app/deps/work/install/win64/lib" \ + --cross-file=cross_win64.txt \ + --buildtype=release --strip -Db_lto=true \ + -Dcompile_server=false \ + -Dportable=true + ninja -C "$(WIN64_BUILD_DIR)" + +dist-win32: build-server build-win32 + mkdir -p "$(DIST)/$(WIN32_TARGET_DIR)" + cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN32_TARGET_DIR)/" + cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/data/scrcpy-console.bat "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/deps/work/install/win32/bin/*.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/deps/work/install/win32/bin/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" + +dist-win64: build-server build-win64 + mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)" + cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN64_TARGET_DIR)/" + cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/data/scrcpy-console.bat "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/deps/work/install/win64/bin/*.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/deps/work/install/win64/bin/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" + +zip-win32: dist-win32 + cd "$(DIST)"; \ + zip -r "$(WIN32_TARGET)" "$(WIN32_TARGET_DIR)" + +zip-win64: dist-win64 + cd "$(DIST)"; \ + zip -r "$(WIN64_TARGET)" "$(WIN64_TARGET_DIR)" diff --git a/release.sh b/release.sh new file mode 100755 index 00000000..51ce2e38 --- /dev/null +++ b/release.sh @@ -0,0 +1,2 @@ +#!/bin/bash +make -f release.mk diff --git a/release/.gitignore b/release/.gitignore deleted file mode 100644 index ed363cdf..00000000 --- a/release/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/work -/output diff --git a/release/build_common b/release/build_common deleted file mode 100644 index 199a80b6..00000000 --- a/release/build_common +++ /dev/null @@ -1,5 +0,0 @@ -# This file must be sourced from the release scripts directory -WORK_DIR="$PWD/work" -OUTPUT_DIR="$PWD/output" - -VERSION="${VERSION:-$(git describe --tags --always)}" diff --git a/release/build_linux.sh b/release/build_linux.sh deleted file mode 100755 index 6bca6979..00000000 --- a/release/build_linux.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash -set -ex -cd "$(dirname ${BASH_SOURCE[0]})" -. build_common -cd .. # root project dir - -if [[ $# != 1 ]] -then - echo "Syntax: $0 " >&2 - exit 1 -fi - -ARCH="$1" -LINUX_BUILD_DIR="$WORK_DIR/build-linux-$ARCH" - -app/deps/adb_linux.sh -app/deps/sdl.sh linux native static -app/deps/dav1d.sh linux native static -app/deps/ffmpeg.sh linux native static -app/deps/libusb.sh linux native static - -DEPS_INSTALL_DIR="$PWD/app/deps/work/install/linux-native-static" -ADB_INSTALL_DIR="$PWD/app/deps/work/install/adb-linux" - -rm -rf "$LINUX_BUILD_DIR" -meson setup "$LINUX_BUILD_DIR" \ - --pkg-config-path="$DEPS_INSTALL_DIR/lib/pkgconfig" \ - -Dc_args="-I$DEPS_INSTALL_DIR/include" \ - -Dc_link_args="-L$DEPS_INSTALL_DIR/lib" \ - --buildtype=release \ - --strip \ - -Db_lto=true \ - -Dcompile_server=false \ - -Dportable=true \ - -Dstatic=true -ninja -C "$LINUX_BUILD_DIR" - -# Group intermediate outputs into a 'dist' directory -mkdir -p "$LINUX_BUILD_DIR/dist" -cp "$LINUX_BUILD_DIR"/app/scrcpy "$LINUX_BUILD_DIR/dist/" -cp app/data/icon.png "$LINUX_BUILD_DIR/dist/" -cp app/scrcpy.1 "$LINUX_BUILD_DIR/dist/" -cp -r "$ADB_INSTALL_DIR"/. "$LINUX_BUILD_DIR/dist/" diff --git a/release/build_macos.sh b/release/build_macos.sh deleted file mode 100755 index 8f4beb9b..00000000 --- a/release/build_macos.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash -set -ex -cd "$(dirname ${BASH_SOURCE[0]})" -. build_common -cd .. # root project dir - -if [[ $# != 1 ]] -then - echo "Syntax: $0 " >&2 - exit 1 -fi - -ARCH="$1" -MACOS_BUILD_DIR="$WORK_DIR/build-macos-$ARCH" - -app/deps/adb_macos.sh -app/deps/sdl.sh macos native static -app/deps/dav1d.sh macos native static -app/deps/ffmpeg.sh macos native static -app/deps/libusb.sh macos native static - -DEPS_INSTALL_DIR="$PWD/app/deps/work/install/macos-native-static" -ADB_INSTALL_DIR="$PWD/app/deps/work/install/adb-macos" - -rm -rf "$MACOS_BUILD_DIR" -meson setup "$MACOS_BUILD_DIR" \ - --pkg-config-path="$DEPS_INSTALL_DIR/lib/pkgconfig" \ - -Dc_args="-I$DEPS_INSTALL_DIR/include" \ - -Dc_link_args="-L$DEPS_INSTALL_DIR/lib" \ - --buildtype=release \ - --strip \ - -Db_lto=true \ - -Dcompile_server=false \ - -Dportable=true \ - -Dstatic=true -ninja -C "$MACOS_BUILD_DIR" - -# Group intermediate outputs into a 'dist' directory -mkdir -p "$MACOS_BUILD_DIR/dist" -cp "$MACOS_BUILD_DIR"/app/scrcpy "$MACOS_BUILD_DIR/dist/" -cp app/data/icon.png "$MACOS_BUILD_DIR/dist/" -cp app/scrcpy.1 "$MACOS_BUILD_DIR/dist/" -cp -r "$ADB_INSTALL_DIR"/. "$MACOS_BUILD_DIR/dist/" diff --git a/release/build_server.sh b/release/build_server.sh deleted file mode 100755 index f52672de..00000000 --- a/release/build_server.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -set -ex -cd "$(dirname ${BASH_SOURCE[0]})" -. build_common -cd .. # root project dir - -GRADLE="${GRADLE:-./gradlew}" -SERVER_BUILD_DIR="$WORK_DIR/build-server" - -rm -rf "$SERVER_BUILD_DIR" -"$GRADLE" -p server assembleRelease -mkdir -p "$SERVER_BUILD_DIR/server" -cp server/build/outputs/apk/release/server-release-unsigned.apk \ - "$SERVER_BUILD_DIR/server/scrcpy-server" diff --git a/release/build_windows.sh b/release/build_windows.sh deleted file mode 100755 index c83d2e31..00000000 --- a/release/build_windows.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/bin/bash -set -ex - -case "$1" in - 32) - WINXX=win32 - ;; - 64) - WINXX=win64 - ;; - *) - echo "ERROR: $0 must be called with one argument: 32 or 64" >&2 - exit 1 - ;; -esac - -cd "$(dirname ${BASH_SOURCE[0]})" -. build_common -cd .. # root project dir - -WINXX_BUILD_DIR="$WORK_DIR/build-$WINXX" - -app/deps/adb_windows.sh -app/deps/sdl.sh $WINXX cross shared -app/deps/dav1d.sh $WINXX cross shared -app/deps/ffmpeg.sh $WINXX cross shared -app/deps/libusb.sh $WINXX cross shared - -DEPS_INSTALL_DIR="$PWD/app/deps/work/install/$WINXX-cross-shared" -ADB_INSTALL_DIR="$PWD/app/deps/work/install/adb-windows" - -rm -rf "$WINXX_BUILD_DIR" -meson setup "$WINXX_BUILD_DIR" \ - --pkg-config-path="$DEPS_INSTALL_DIR/lib/pkgconfig" \ - -Dc_args="-I$DEPS_INSTALL_DIR/include" \ - -Dc_link_args="-L$DEPS_INSTALL_DIR/lib" \ - --cross-file=cross_$WINXX.txt \ - --buildtype=release \ - --strip \ - -Db_lto=true \ - -Dcompile_server=false \ - -Dportable=true -ninja -C "$WINXX_BUILD_DIR" - -# Group intermediate outputs into a 'dist' directory -mkdir -p "$WINXX_BUILD_DIR/dist" -cp "$WINXX_BUILD_DIR"/app/scrcpy.exe "$WINXX_BUILD_DIR/dist/" -cp app/data/scrcpy-console.bat "$WINXX_BUILD_DIR/dist/" -cp app/data/scrcpy-noconsole.vbs "$WINXX_BUILD_DIR/dist/" -cp app/data/icon.png "$WINXX_BUILD_DIR/dist/" -cp app/data/open_a_terminal_here.bat "$WINXX_BUILD_DIR/dist/" -cp "$DEPS_INSTALL_DIR"/bin/*.dll "$WINXX_BUILD_DIR/dist/" -cp -r "$ADB_INSTALL_DIR"/. "$WINXX_BUILD_DIR/dist/" diff --git a/release/generate_checksums.sh b/release/generate_checksums.sh deleted file mode 100755 index 2785c6c3..00000000 --- a/release/generate_checksums.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -set -ex -cd "$(dirname ${BASH_SOURCE[0]})" -. build_common - -cd "$OUTPUT_DIR" -sha256sum "scrcpy-server-$VERSION" \ - "scrcpy-linux-x86_64-$VERSION.tar.gz" \ - "scrcpy-win32-$VERSION.zip" \ - "scrcpy-win64-$VERSION.zip" \ - "scrcpy-macos-aarch64-$VERSION.tar.gz" \ - "scrcpy-macos-x86_64-$VERSION.tar.gz" \ - | tee SHA256SUMS.txt -echo "Release checksums generated in $PWD/SHA256SUMS.txt" diff --git a/release/package_client.sh b/release/package_client.sh deleted file mode 100755 index 51997e75..00000000 --- a/release/package_client.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/bin/bash -set -ex -cd "$(dirname ${BASH_SOURCE[0]})" -. build_common -cd .. # root project dir - -if [[ $# != 2 ]] -then - # : for example win64 - # : zip or tar.gz - echo "Syntax: $0 " >&2 - exit 1 -fi - -FORMAT=$2 - -if [[ "$FORMAT" != zip && "$FORMAT" != tar.gz ]] -then - echo "Invalid format (expected zip or tar.gz): $FORMAT" >&2 - exit 1 -fi - -BUILD_DIR="$WORK_DIR/build-$1" -ARCHIVE_DIR="$BUILD_DIR/release-archive" -TARGET_DIRNAME="scrcpy-$1-$VERSION" - -rm -rf "$ARCHIVE_DIR/$TARGET_DIRNAME" -mkdir -p "$ARCHIVE_DIR/$TARGET_DIRNAME" - -cp -r "$BUILD_DIR/dist/." "$ARCHIVE_DIR/$TARGET_DIRNAME/" -cp "$WORK_DIR/build-server/server/scrcpy-server" "$ARCHIVE_DIR/$TARGET_DIRNAME/" - -mkdir -p "$OUTPUT_DIR" - -cd "$ARCHIVE_DIR" -rm -f "$OUTPUT_DIR/$TARGET_DIRNAME.$FORMAT" - -case "$FORMAT" in - zip) - zip -r "$OUTPUT_DIR/$TARGET_DIRNAME.zip" "$TARGET_DIRNAME" - ;; - tar.gz) - tar cvzf "$OUTPUT_DIR/$TARGET_DIRNAME.tar.gz" "$TARGET_DIRNAME" - ;; - *) - echo "Invalid format (expected zip or tar.gz): $FORMAT" >&2 - exit 1 -esac - -rm -rf "$TARGET_DIRNAME" -cd - -echo "Generated '$OUTPUT_DIR/$TARGET_DIRNAME.$FORMAT'" diff --git a/release/package_server.sh b/release/package_server.sh deleted file mode 100755 index a856cebb..00000000 --- a/release/package_server.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -set -ex -cd "$(dirname ${BASH_SOURCE[0]})" -OUTPUT_DIR="$PWD/output" -. build_common -cd .. # root project dir - -mkdir -p "$OUTPUT_DIR" -cp "$WORK_DIR/build-server/server/scrcpy-server" "$OUTPUT_DIR/scrcpy-server-$VERSION" -echo "Generated '$OUTPUT_DIR/scrcpy-server-$VERSION'" diff --git a/release/release.sh b/release/release.sh deleted file mode 100755 index ddba585b..00000000 --- a/release/release.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash -# To customize the version name: -# VERSION=myversion ./release.sh -set -e - -cd "$(dirname ${BASH_SOURCE[0]})" -rm -rf output - -./test_server.sh -./test_client.sh - -./build_server.sh -./build_windows.sh 32 -./build_windows.sh 64 -./build_linux.sh x86_64 - -./package_server.sh -./package_client.sh win32 zip -./package_client.sh win64 zip -./package_client.sh linux-x86_64 tar.gz - -./generate_checksums.sh - -echo "Release generated in $PWD/output" diff --git a/release/test_client.sh b/release/test_client.sh deleted file mode 100755 index 6059541d..00000000 --- a/release/test_client.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -set -ex -cd "$(dirname ${BASH_SOURCE[0]})" -. build_common -cd .. # root project dir - -TEST_BUILD_DIR="$WORK_DIR/build-test" - -rm -rf "$TEST_BUILD_DIR" -meson setup "$TEST_BUILD_DIR" -Dcompile_server=false \ - -Db_sanitize=address,undefined -ninja -C "$TEST_BUILD_DIR" test diff --git a/release/test_server.sh b/release/test_server.sh deleted file mode 100755 index 940e8c1a..00000000 --- a/release/test_server.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -set -ex -cd "$(dirname ${BASH_SOURCE[0]})" -. build_common -cd .. # root project dir - -GRADLE="${GRADLE:-./gradlew}" - -"$GRADLE" -p server check diff --git a/server/build.gradle b/server/build.gradle index 31092b12..655298a9 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -2,13 +2,13 @@ apply plugin: 'com.android.application' android { namespace 'com.genymobile.scrcpy' - compileSdk 35 + compileSdk 34 defaultConfig { applicationId "com.genymobile.scrcpy" minSdkVersion 21 - targetSdkVersion 35 - versionCode 30301 - versionName "3.3.1" + targetSdkVersion 34 + versionCode 20700 + versionName "2.7" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 193a9902..ab6c821d 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -12,11 +12,10 @@ set -e SCRCPY_DEBUG=false -SCRCPY_VERSION_NAME=3.3.1 +SCRCPY_VERSION_NAME=2.7 -PLATFORM=${ANDROID_PLATFORM:-35} -BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.0.0} -PLATFORM_TOOLS="$ANDROID_HOME/platforms/android-$PLATFORM" +PLATFORM=${ANDROID_PLATFORM:-34} +BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-34.0.0} BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS" BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})" @@ -24,8 +23,7 @@ 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" +ANDROID_JAR="$ANDROID_HOME/platforms/android-$PLATFORM/android.jar" LAMBDA_JAR="$BUILD_TOOLS_DIR/core-lambda-stubs.jar" echo "Platform: android-$PLATFORM" @@ -47,22 +45,16 @@ EOF echo "Generating java from aidl..." cd "$SERVER_DIR/src/main/aidl" -"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. \ +"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" android/view/IRotationWatcher.aidl +"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" \ android/content/IOnPrimaryClipChangedListener.aidl -"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. -p "$ANDROID_AIDL" \ - android/view/IDisplayWindowListener.aidl - -# Fake sources to expose hidden Android types to the project -FAKE_SRC=( \ - android/content/*java \ -) +"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" android/view/IDisplayFoldListener.aidl 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 \ @@ -76,11 +68,10 @@ done echo "Compiling java sources..." cd ../java -javac -encoding UTF-8 -bootclasspath "$ANDROID_JAR" \ +javac -bootclasspath "$ANDROID_JAR" \ -cp "$LAMBDA_JAR:$GEN_DIR" \ -d "$CLASSES_DIR" \ -source 1.8 -target 1.8 \ - ${FAKE_SRC[@]} \ ${SRC[@]} echo "Dexing..." diff --git a/server/meson.build b/server/meson.build index 55828e2d..42b97981 100644 --- a/server/meson.build +++ b/server/meson.build @@ -23,9 +23,3 @@ else install: true, install_dir: 'share/scrcpy') endif - -if meson.version().version_compare('>= 0.58.0') - devenv = environment() - devenv.set('SCRCPY_SERVER_PATH', meson.current_build_dir() / 'scrcpy-server') - meson.add_devenv(devenv) -endif diff --git a/server/src/main/aidl/android/view/IDisplayFoldListener.aidl b/server/src/main/aidl/android/view/IDisplayFoldListener.aidl new file mode 100644 index 00000000..2c91149d --- /dev/null +++ b/server/src/main/aidl/android/view/IDisplayFoldListener.aidl @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +/** + * {@hide} + */ +oneway interface IDisplayFoldListener +{ + /** Called when the foldedness of a display changes */ + void onDisplayFoldChanged(int displayId, boolean folded); +} diff --git a/server/src/main/aidl/android/view/IDisplayWindowListener.aidl b/server/src/main/aidl/android/view/IDisplayWindowListener.aidl deleted file mode 100644 index 2b331175..00000000 --- a/server/src/main/aidl/android/view/IDisplayWindowListener.aidl +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.view; - -import android.graphics.Rect; -import android.content.res.Configuration; - -import java.util.List; - -/** - * Interface to listen for changes to display window-containers. - * - * This differs from DisplayManager's DisplayListener in a couple ways: - * - onDisplayAdded is always called after the display is actually added to the WM hierarchy. - * This corresponds to the DisplayContent and not the raw Dislay from DisplayManager. - * - onDisplayConfigurationChanged is called for all configuration changes, not just changes - * to displayinfo (eg. windowing-mode). - * - */ -oneway interface IDisplayWindowListener { - - /** - * Called when a new display is added to the WM hierarchy. The existing display ids are returned - * when this listener is registered with WM via {@link #registerDisplayWindowListener}. - */ - void onDisplayAdded(int displayId); - - /** - * Called when a display's window-container configuration has changed. - */ - void onDisplayConfigurationChanged(int displayId, in Configuration newConfig); - - /** - * Called when a display is removed from the hierarchy. - */ - void onDisplayRemoved(int displayId); - - /** - * Called when fixed rotation is started on a display. - */ - void onFixedRotationStarted(int displayId, int newRotation); - - /** - * Called when the previous fixed rotation on a display is finished. - */ - void onFixedRotationFinished(int displayId); - - /** - * Called when the keep clear ares on a display have changed. - */ - void onKeepClearAreasChanged(int displayId, in List restricted, in List unrestricted); -} diff --git a/server/src/main/aidl/android/view/IRotationWatcher.aidl b/server/src/main/aidl/android/view/IRotationWatcher.aidl new file mode 100644 index 00000000..2cc5e44a --- /dev/null +++ b/server/src/main/aidl/android/view/IRotationWatcher.aidl @@ -0,0 +1,25 @@ +/* //device/java/android/android/hardware/ISensorListener.aidl +** +** Copyright 2008, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package android.view; + +/** + * {@hide} + */ +interface IRotationWatcher { + oneway void onRotationChanged(int rotation); +} diff --git a/server/src/main/java/android/content/IContentProvider.java b/server/src/main/java/android/content/IContentProvider.java deleted file mode 100644 index bb907dd3..00000000 --- a/server/src/main/java/android/content/IContentProvider.java +++ /dev/null @@ -1,5 +0,0 @@ -package android.content; - -public interface IContentProvider { - // android.content.IContentProvider is hidden, this is a fake one to expose the type to the project -} diff --git a/server/src/main/java/com/genymobile/scrcpy/AndroidVersions.java b/server/src/main/java/com/genymobile/scrcpy/AndroidVersions.java deleted file mode 100644 index 5303924a..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/AndroidVersions.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.genymobile.scrcpy; - -import android.os.Build; - -/** - * Android version code constants, done right. - *

- * API levels - */ -public final class AndroidVersions { - - private AndroidVersions() { - // not instantiable - } - - public static final int API_21_ANDROID_5_0 = Build.VERSION_CODES.LOLLIPOP; - public static final int API_22_ANDROID_5_1 = Build.VERSION_CODES.LOLLIPOP_MR1; - public static final int API_23_ANDROID_6_0 = Build.VERSION_CODES.M; - public static final int API_24_ANDROID_7_0 = Build.VERSION_CODES.N; - public static final int API_25_ANDROID_7_1 = Build.VERSION_CODES.N_MR1; - public static final int API_26_ANDROID_8_0 = Build.VERSION_CODES.O; - public static final int API_27_ANDROID_8_1 = Build.VERSION_CODES.O_MR1; - public static final int API_28_ANDROID_9 = Build.VERSION_CODES.P; - public static final int API_29_ANDROID_10 = Build.VERSION_CODES.Q; - public static final int API_30_ANDROID_11 = Build.VERSION_CODES.R; - public static final int API_31_ANDROID_12 = Build.VERSION_CODES.S; - public static final int API_32_ANDROID_12L = Build.VERSION_CODES.S_V2; - public static final int API_33_ANDROID_13 = Build.VERSION_CODES.TIRAMISU; - public static final int API_34_ANDROID_14 = Build.VERSION_CODES.UPSIDE_DOWN_CAKE; - public static final int API_35_ANDROID_15 = Build.VERSION_CODES.VANILLA_ICE_CREAM; - -} diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index 77018afa..1b8d4248 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -4,12 +4,6 @@ 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; @@ -22,154 +16,59 @@ import java.io.OutputStream; */ public final class CleanUp { - // Dynamic options - private static final int PENDING_CHANGE_DISPLAY_POWER = 1 << 0; - private int pendingChanges; - private boolean pendingRestoreDisplayPower; + private static final int MSG_TYPE_MASK = 0b11; + private static final int MSG_TYPE_RESTORE_STAY_ON = 0; + private static final int MSG_TYPE_DISABLE_SHOW_TOUCHES = 1; + private static final int MSG_TYPE_RESTORE_NORMAL_POWER_MODE = 2; + private static final int MSG_TYPE_POWER_OFF_SCREEN = 3; - private Thread thread; - private boolean interrupted; + private static final int MSG_PARAM_SHIFT = 2; - private CleanUp(Options options) { - thread = new Thread(() -> runCleanUp(options), "cleanup"); - thread.start(); + private final OutputStream out; + + public CleanUp(OutputStream out) { + this.out = out; } - 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), - }; + public static CleanUp configure(int displayId) throws IOException { + String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(displayId)}; ProcessBuilder builder = new ProcessBuilder(cmd); builder.environment().put("CLASSPATH", Server.SERVER_PATH); Process process = builder.start(); - OutputStream out = process.getOutputStream(); + return new CleanUp(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(); - } + private boolean sendMessage(int type, int param) { + assert (type & ~MSG_TYPE_MASK) == 0; + int msg = type | param << MSG_PARAM_SHIFT; + try { + out.write(msg); + out.flush(); + return true; + } catch (IOException e) { + Ln.w("Could not configure cleanup (type=" + type + ", param=" + param + ")", e); + return false; } } - public synchronized void setRestoreDisplayPower(boolean restoreDisplayPower) { - pendingRestoreDisplayPower = restoreDisplayPower; - pendingChanges |= PENDING_CHANGE_DISPLAY_POWER; - notify(); + public boolean setRestoreStayOn(int restoreValue) { + // Restore the value (between 0 and 7), -1 to not restore + // + assert restoreValue >= -1 && restoreValue <= 7; + return sendMessage(MSG_TYPE_RESTORE_STAY_ON, restoreValue & 0b1111); + } + + public boolean setDisableShowTouches(boolean disableOnExit) { + return sendMessage(MSG_TYPE_DISABLE_SHOW_TOUCHES, disableOnExit ? 1 : 0); + } + + public boolean setRestoreNormalPowerMode(boolean restoreOnExit) { + return sendMessage(MSG_TYPE_RESTORE_NORMAL_POWER_MODE, restoreOnExit ? 1 : 0); + } + + public boolean setPowerOffScreen(boolean powerOffScreenOnExit) { + return sendMessage(MSG_TYPE_POWER_OFF_SCREEN, powerOffScreenOnExit ? 1 : 0); } public static void unlinkSelf() { @@ -180,40 +79,39 @@ public final class CleanUp { } } - @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; + int restoreStayOn = -1; + boolean disableShowTouches = false; + boolean restoreNormalPowerMode = false; + boolean powerOffScreen = 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; + int type = msg & MSG_TYPE_MASK; + int param = msg >> MSG_PARAM_SHIFT; + switch (type) { + case MSG_TYPE_RESTORE_STAY_ON: + restoreStayOn = param > 7 ? -1 : param; + break; + case MSG_TYPE_DISABLE_SHOW_TOUCHES: + disableShowTouches = param != 0; + break; + case MSG_TYPE_RESTORE_NORMAL_POWER_MODE: + restoreNormalPowerMode = param != 0; + break; + case MSG_TYPE_POWER_OFF_SCREEN: + powerOffScreen = param != 0; + break; + default: + Ln.w("Unexpected msg type: " + type); + break; + } } } catch (IOException e) { // Expected when the server is dead @@ -239,29 +137,13 @@ public final class CleanUp { } } - 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 (Device.isScreenOn()) { if (powerOffScreen) { Ln.i("Power off screen"); - Device.powerOffScreen(targetDisplayId); - } else if (restoreDisplayPower) { - Ln.i("Restoring display power"); - Device.setDisplayPower(targetDisplayId, true); + Device.powerOffScreen(displayId); + } else if (restoreNormalPowerMode) { + Ln.i("Restoring normal power mode"); + Device.setScreenPowerMode(Device.POWER_MODE_NORMAL); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java index b43e9e1b..2ea7bf4a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java +++ b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java @@ -1,20 +1,12 @@ package com.genymobile.scrcpy; -import com.genymobile.scrcpy.wrappers.ServiceManager; - -import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.AttributionSource; -import android.content.ClipboardManager; -import android.content.ContentResolver; import android.content.Context; import android.content.ContextWrapper; -import android.content.IContentProvider; -import android.os.Binder; +import android.os.Build; import android.os.Process; -import java.lang.reflect.Field; - public final class FakeContext extends ContextWrapper { public static final String PACKAGE_NAME = "com.android.shell"; @@ -26,38 +18,6 @@ public final class FakeContext extends ContextWrapper { 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()); } @@ -72,7 +32,7 @@ public final class FakeContext extends ContextWrapper { return PACKAGE_NAME; } - @TargetApi(AndroidVersions.API_31_ANDROID_12) + @TargetApi(Build.VERSION_CODES.S) @Override public AttributionSource getAttributionSource() { AttributionSource.Builder builder = new AttributionSource.Builder(Process.SHELL_UID); @@ -90,30 +50,4 @@ public final class FakeContext extends ContextWrapper { public Context getApplicationContext() { return this; } - - @Override - public ContentResolver getContentResolver() { - return contentResolver; - } - - @SuppressLint("SoonBlockedPrivateApi") - @Override - public Object getSystemService(String name) { - Object service = super.getSystemService(name); - if (service == null) { - return null; - } - - if (Context.CLIPBOARD_SERVICE.equals(name)) { - try { - Field field = ClipboardManager.class.getDeclaredField("mContext"); - field.setAccessible(true); - field.set(service, this); - } catch (ReflectiveOperationException e) { - throw new RuntimeException(e); - } - } - - return service; - } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 66bb68e8..51daeced 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -2,9 +2,6 @@ package com.genymobile.scrcpy; import com.genymobile.scrcpy.audio.AudioCodec; import com.genymobile.scrcpy.audio.AudioSource; -import com.genymobile.scrcpy.device.Device; -import com.genymobile.scrcpy.device.NewDisplay; -import com.genymobile.scrcpy.device.Orientation; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.CodecOption; import com.genymobile.scrcpy.util.Ln; @@ -12,10 +9,8 @@ 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; @@ -35,7 +30,7 @@ public class Options { private int videoBitRate = 8000000; private int audioBitRate = 128000; private float maxFps; - private float angle; + private int lockVideoOrientation = -1; private boolean tunnelForward; private Rect crop; private boolean control = true; @@ -48,8 +43,6 @@ public class Options { private boolean cameraHighSpeed; private boolean showTouches; private boolean stayAwake; - private int screenOffTimeout = -1; - private int displayImePolicy = -1; private List videoCodecOptions; private List audioCodecOptions; @@ -61,18 +54,10 @@ public class Options { 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 @@ -132,8 +117,8 @@ public class Options { return maxFps; } - public float getAngle() { - return angle; + public int getLockVideoOrientation() { + return lockVideoOrientation; } public boolean isTunnelForward() { @@ -184,14 +169,6 @@ public class Options { return stayAwake; } - public int getScreenOffTimeout() { - return screenOffTimeout; - } - - public int getDisplayImePolicy() { - return displayImePolicy; - } - public List getVideoCodecOptions() { return videoCodecOptions; } @@ -228,28 +205,8 @@ public class Options { 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; + return listEncoders || listDisplays || listCameras || listCameraSizes; } public boolean getListEncoders() { @@ -268,10 +225,6 @@ public class Options { return listCameraSizes; } - public boolean getListApps() { - return listApps; - } - public boolean getSendDeviceMeta() { return sendDeviceMeta; } @@ -370,8 +323,8 @@ public class Options { case "max_fps": options.maxFps = parseFloat("max_fps", value); break; - case "angle": - options.angle = parseFloat("angle", value); + case "lock_video_orientation": + options.lockVideoOrientation = Integer.parseInt(value); break; case "tunnel_forward": options.tunnelForward = Boolean.parseBoolean(value); @@ -393,12 +346,6 @@ public class Options { 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; @@ -441,9 +388,6 @@ public class Options { 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; @@ -474,23 +418,6 @@ public class Options { 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; @@ -518,11 +445,6 @@ public class Options { } } - if (options.newDisplay != null) { - assert options.displayId == 0 : "Must not set both displayId and newDisplay"; - options.displayId = Device.DISPLAY_ID_NONE; - } - return options; } @@ -553,9 +475,6 @@ public class Options { } 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); } @@ -582,70 +501,4 @@ public class Options { 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); - } - } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index a08c948c..7817fdf5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -9,28 +9,26 @@ 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.control.DeviceMessage; 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.util.Settings; +import com.genymobile.scrcpy.util.SettingsException; 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.BatteryManager; 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; @@ -58,7 +56,17 @@ public final class Server { this.fatalError = true; } if (running == 0 || this.fatalError) { - Looper.getMainLooper().quitSafely(); + notify(); + } + } + + synchronized void await() { + try { + while (running > 0 && !fatalError) { + wait(); + } + } catch (InterruptedException e) { + // ignore } } } @@ -67,27 +75,63 @@ public final class Server { // not instantiable } + private static void initAndCleanUp(Options options, CleanUp cleanUp) { + // This method is called from its own thread, so it may only configure cleanup actions which are NOT dynamic (i.e. they are configured once + // and for all, they cannot be changed from another thread) + + 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 + if (!"1".equals(oldValue)) { + if (!cleanUp.setDisableShowTouches(true)) { + Ln.e("Could not disable show touch on exit"); + } + } + } catch (SettingsException e) { + Ln.e("Could not change \"show_touches\"", e); + } + } + + if (options.getStayAwake()) { + int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS; + try { + String oldValue = Settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn)); + try { + int restoreStayOn = Integer.parseInt(oldValue); + if (restoreStayOn != stayOn) { + // Restore only if the current value is different + if (!cleanUp.setRestoreStayOn(restoreStayOn)) { + Ln.e("Could not restore stay on on exit"); + } + } + } catch (NumberFormatException e) { + // ignore + } + } catch (SettingsException e) { + Ln.e("Could not change \"stay_on_while_plugged_in\"", e); + } + } + + if (options.getPowerOffScreenOnClose()) { + if (!cleanUp.setPowerOffScreen(true)) { + Ln.e("Could not power off screen on exit"); + } + } + } + private static void scrcpy(Options options) throws IOException, ConfigurationException { - if (Build.VERSION.SDK_INT < AndroidVersions.API_31_ANDROID_12 && options.getVideoSource() == VideoSource.CAMERA) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S && 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; + Thread initThread = null; if (options.getCleanup()) { - cleanUp = CleanUp.start(options); + cleanUp = CleanUp.configure(options.getDisplayId()); + initThread = startInitThread(options, cleanUp); } int scid = options.getScid(); @@ -96,6 +140,9 @@ public final class Server { boolean video = options.getVideo(); boolean audio = options.getAudio(); boolean sendDummyByte = options.getSendDummyByte(); + boolean camera = video && options.getVideoSource() == VideoSource.CAMERA; + + final Device device = camera ? null : new Device(options); Workarounds.apply(); @@ -107,11 +154,13 @@ public final class Server { connection.sendDeviceMeta(Device.getDeviceName()); } - Controller controller = null; - if (control) { ControlChannel controlChannel = connection.getControlChannel(); - controller = new Controller(controlChannel, cleanUp, options); + Controller controller = new Controller(device, controlChannel, cleanUp, options.getClipboardAutosync(), options.getPowerOn()); + device.setClipboardListener(text -> { + DeviceMessage msg = DeviceMessage.createClipboard(text); + controller.getSender().send(msg); + }); asyncProcessors.add(controller); } @@ -130,7 +179,8 @@ public final class Server { if (audioCodec == AudioCodec.RAW) { audioRecorder = new AudioRawRecorder(audioCapture, audioStreamer); } else { - audioRecorder = new AudioEncoder(audioCapture, audioStreamer, options); + audioRecorder = new AudioEncoder(audioCapture, audioStreamer, options.getAudioBitRate(), options.getAudioCodecOptions(), + options.getAudioEncoder()); } asyncProcessors.add(audioRecorder); } @@ -140,22 +190,14 @@ public final class Server { 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); - } + surfaceCapture = new ScreenCapture(device); } else { - surfaceCapture = new CameraCapture(options); + surfaceCapture = new CameraCapture(options.getCameraId(), options.getCameraFacing(), options.getCameraSize(), + options.getMaxSize(), options.getCameraAspectRatio(), options.getCameraFps(), options.getCameraHighSpeed()); } - SurfaceEncoder surfaceEncoder = new SurfaceEncoder(surfaceCapture, videoStreamer, options); + SurfaceEncoder surfaceEncoder = new SurfaceEncoder(surfaceCapture, videoStreamer, options.getVideoBitRate(), options.getMaxFps(), + options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError()); asyncProcessors.add(surfaceEncoder); - - if (controller != null) { - controller.setSurfaceCapture(surfaceCapture); - } } Completion completion = new Completion(asyncProcessors.size()); @@ -165,27 +207,24 @@ public final class Server { }); } - Looper.loop(); // interrupted by the Completion implementation + completion.await(); } finally { - if (cleanUp != null) { - cleanUp.interrupt(); + if (initThread != null) { + initThread.interrupt(); } for (AsyncProcessor asyncProcessor : asyncProcessors) { asyncProcessor.stop(); } - OpenGLRunner.quit(); // quit the OpenGL thread, if any - connection.shutdown(); try { - if (cleanUp != null) { - cleanUp.join(); + if (initThread != null) { + initThread.join(); } for (AsyncProcessor asyncProcessor : asyncProcessors) { asyncProcessor.join(); } - OpenGLRunner.join(); } catch (InterruptedException e) { // ignore } @@ -194,19 +233,10 @@ public final class Server { } } - 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); - } - } + private static Thread startInitThread(final Options options, final CleanUp cleanUp) { + Thread thread = new Thread(() -> initAndCleanUp(options, cleanUp), "init-cleanup"); + thread.start(); + return thread; } public static void main(String... args) { @@ -229,8 +259,6 @@ public final class Server { Ln.e("Exception on thread " + t, e); }); - prepareMainLooper(); - Options options = Options.parse(args); Ln.disableSystemStreams(); @@ -254,11 +282,6 @@ public final class Server { 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; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index b89f19ae..7de98b72 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -29,6 +29,8 @@ public final class Workarounds { private static final Object ACTIVITY_THREAD; static { + prepareMainLooper(); + try { // ActivityThread activityThread = new ActivityThread(); ACTIVITY_THREAD_CLASS = Class.forName("android.app.ActivityThread"); @@ -40,11 +42,6 @@ public final class Workarounds { 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); } @@ -55,7 +52,7 @@ public final class Workarounds { } public static void apply() { - if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // On some Samsung devices, DisplayManagerGlobal.getDisplayInfoLocked() calls ActivityThread.currentActivityThread().getConfiguration(), // which requires a non-null ConfigurationController. // ConfigurationController was introduced in Android 12, so do not attempt to set it on lower versions. @@ -75,6 +72,19 @@ public final class Workarounds { fillAppContext(); } + @SuppressWarnings("deprecation") + private static void prepareMainLooper() { + // Some devices internally create a Handler when creating an input Surface, causing an exception: + // "Can't create handler inside thread that has not called Looper.prepare()" + // + // + // Use Looper.prepareMainLooper() instead of Looper.prepare() to avoid a NullPointerException: + // "Attempt to read from field 'android.os.MessageQueue android.os.Looper.mQueue' + // on a null object reference" + // + Looper.prepareMainLooper(); + } + private static void fillAppInfo() { try { // ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData(); @@ -122,13 +132,10 @@ public final class Workarounds { 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); @@ -148,7 +155,7 @@ public final class Workarounds { } } - @TargetApi(AndroidVersions.API_30_ANDROID_11) + @TargetApi(Build.VERSION_CODES.R) @SuppressLint("WrongConstant,MissingPermission") public static AudioRecord createAudioRecord(int source, int sampleRate, int channelConfig, int channels, int channelMask, int encoding) throws AudioCaptureException { @@ -219,7 +226,7 @@ public final class Workarounds { int[] session = new int[]{AudioManager.AUDIO_SESSION_ID_GENERATE}; int initResult; - if (Build.VERSION.SDK_INT < AndroidVersions.API_31_ANDROID_12) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { // private native final int native_setup(Object audiorecord_this, // Object /*AudioAttributes*/ attributes, // int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat, @@ -245,7 +252,7 @@ public final class Workarounds { Method getParcelMethod = attributionSourceState.getClass().getDeclaredMethod("getParcel"); Parcel attributionSourceParcel = (Parcel) getParcelMethod.invoke(attributionSourceState); - if (Build.VERSION.SDK_INT < AndroidVersions.API_34_ANDROID_14) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { // private native int native_setup(Object audiorecordThis, // Object /*AudioAttributes*/ attributes, // int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat, diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java index bf870bee..8d4a4c2d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java @@ -1,6 +1,5 @@ package com.genymobile.scrcpy.audio; -import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.Workarounds; import com.genymobile.scrcpy.util.Ln; @@ -12,6 +11,7 @@ import android.content.ComponentName; import android.content.Intent; import android.media.AudioRecord; import android.media.MediaCodec; +import android.media.MediaRecorder; import android.os.Build; import android.os.SystemClock; @@ -31,14 +31,25 @@ public class AudioDirectCapture implements AudioCapture { private AudioRecordReader reader; public AudioDirectCapture(AudioSource audioSource) { - this.audioSource = audioSource.getDirectAudioSource(); + this.audioSource = getAudioSourceValue(audioSource); } - @TargetApi(AndroidVersions.API_23_ANDROID_6_0) + private static int getAudioSourceValue(AudioSource audioSource) { + switch (audioSource) { + case OUTPUT: + return MediaRecorder.AudioSource.REMOTE_SUBMIX; + case MIC: + return MediaRecorder.AudioSource.MIC; + default: + throw new IllegalArgumentException("Unsupported audio source: " + audioSource); + } + } + + @TargetApi(Build.VERSION_CODES.M) @SuppressLint({"WrongConstant", "MissingPermission"}) private static AudioRecord createAudioRecord(int audioSource) { AudioRecord.Builder builder = new AudioRecord.Builder(); - if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // On older APIs, Workarounds.fillAppInfo() must be called beforehand builder.setContext(FakeContext.get()); } @@ -106,7 +117,7 @@ public class AudioDirectCapture implements AudioCapture { @Override public void checkCompatibility() throws AudioCaptureException { - if (Build.VERSION.SDK_INT < AndroidVersions.API_30_ANDROID_11) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { Ln.w("Audio disabled: it is not supported before Android 11"); throw new AudioCaptureException(); } @@ -114,7 +125,7 @@ public class AudioDirectCapture implements AudioCapture { @Override public void start() throws AudioCaptureException { - if (Build.VERSION.SDK_INT == AndroidVersions.API_30_ANDROID_11) { + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { startWorkaroundAndroid11(); try { tryStartRecording(5, 100); @@ -135,7 +146,7 @@ public class AudioDirectCapture implements AudioCapture { } @Override - @TargetApi(AndroidVersions.API_24_ANDROID_7_0) + @TargetApi(Build.VERSION_CODES.N) public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) { return reader.read(outDirectBuffer, outBufferInfo); } diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java index 33177228..8230e054 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java @@ -1,16 +1,14 @@ package com.genymobile.scrcpy.audio; -import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.AsyncProcessor; -import com.genymobile.scrcpy.Options; -import com.genymobile.scrcpy.device.ConfigurationException; -import com.genymobile.scrcpy.device.Streamer; import com.genymobile.scrcpy.util.Codec; import com.genymobile.scrcpy.util.CodecOption; import com.genymobile.scrcpy.util.CodecUtils; +import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.util.IO; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.LogUtils; +import com.genymobile.scrcpy.device.Streamer; import android.annotation.TargetApi; import android.media.MediaCodec; @@ -55,9 +53,6 @@ public final class AudioEncoder implements AsyncProcessor { 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); @@ -71,12 +66,12 @@ public final class AudioEncoder implements AsyncProcessor { private boolean ended; - public AudioEncoder(AudioCapture capture, Streamer streamer, Options options) { + public AudioEncoder(AudioCapture capture, Streamer streamer, int bitRate, List codecOptions, String encoderName) { this.capture = capture; this.streamer = streamer; - this.bitRate = options.getAudioBitRate(); - this.codecOptions = options.getAudioCodecOptions(); - this.encoderName = options.getAudioEncoder(); + this.bitRate = bitRate; + this.codecOptions = codecOptions; + this.encoderName = encoderName; } private static MediaFormat createFormat(String mimeType, int bitRate, List codecOptions) { @@ -98,7 +93,7 @@ public final class AudioEncoder implements AsyncProcessor { return format; } - @TargetApi(AndroidVersions.API_24_ANDROID_7_0) + @TargetApi(Build.VERSION_CODES.N) private void inputThread(MediaCodec mediaCodec, AudioCapture capture) throws IOException, InterruptedException { final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); @@ -121,9 +116,6 @@ public final class AudioEncoder implements AsyncProcessor { 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); @@ -131,25 +123,6 @@ public final class AudioEncoder implements AsyncProcessor { } } - 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(() -> { @@ -202,9 +175,9 @@ public final class AudioEncoder implements AsyncProcessor { } } - @TargetApi(AndroidVersions.API_23_ANDROID_6_0) + @TargetApi(Build.VERSION_CODES.M) private void encode() throws IOException, ConfigurationException, AudioCaptureException { - if (Build.VERSION.SDK_INT < AndroidVersions.API_30_ANDROID_11) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { Ln.w("Audio disabled: it is not supported before Android 11"); streamer.writeDisableStream(false); return; @@ -219,12 +192,6 @@ public final class AudioEncoder implements AsyncProcessor { 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(); @@ -320,13 +287,7 @@ public final class AudioEncoder implements AsyncProcessor { 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; + return MediaCodec.createByCodecName(encoderName); } catch (IllegalArgumentException e) { Ln.e("Audio encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildAudioEncoderListMessage()); throw new ConfigurationException("Unknown encoder: " + encoderName); @@ -347,7 +308,7 @@ public final class AudioEncoder implements AsyncProcessor { } private final class EncoderCallback extends MediaCodec.Callback { - @TargetApi(AndroidVersions.API_24_ANDROID_7_0) + @TargetApi(Build.VERSION_CODES.N) @Override public void onInputBufferAvailable(MediaCodec codec, int index) { try { diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java index 009a239a..e38493f2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java @@ -1,6 +1,5 @@ package com.genymobile.scrcpy.audio; -import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.util.Ln; @@ -109,7 +108,7 @@ public final class AudioPlaybackCapture implements AudioCapture { @Override public void checkCompatibility() throws AudioCaptureException { - if (Build.VERSION.SDK_INT < AndroidVersions.API_33_ANDROID_13) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { Ln.w("Audio disabled: audio playback capture source not supported before Android 13"); throw new AudioCaptureException(); } @@ -131,7 +130,7 @@ public final class AudioPlaybackCapture implements AudioCapture { } @Override - @TargetApi(AndroidVersions.API_24_ANDROID_7_0) + @TargetApi(Build.VERSION_CODES.N) public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) { return reader.read(outDirectBuffer, outBufferInfo); } diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java index 9645bbbd..323caae4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java @@ -1,10 +1,9 @@ package com.genymobile.scrcpy.audio; -import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.AsyncProcessor; -import com.genymobile.scrcpy.device.Streamer; import com.genymobile.scrcpy.util.IO; import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.device.Streamer; import android.media.MediaCodec; import android.os.Build; @@ -25,7 +24,7 @@ public final class AudioRawRecorder implements AsyncProcessor { } private void record() throws IOException, AudioCaptureException { - if (Build.VERSION.SDK_INT < AndroidVersions.API_30_ANDROID_11) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { Ln.w("Audio disabled: it is not supported before Android 11"); streamer.writeDisableStream(false); return; diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioRecordReader.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRecordReader.java index 32b42257..80286831 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioRecordReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRecordReader.java @@ -1,12 +1,12 @@ package com.genymobile.scrcpy.audio; -import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.util.Ln; import android.annotation.TargetApi; import android.media.AudioRecord; import android.media.AudioTimestamp; import android.media.MediaCodec; +import android.os.Build; import java.nio.ByteBuffer; @@ -26,7 +26,7 @@ public class AudioRecordReader { this.recorder = recorder; } - @TargetApi(AndroidVersions.API_24_ANDROID_7_0) + @TargetApi(Build.VERSION_CODES.N) public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) { int r = recorder.read(outDirectBuffer, AudioConfig.MAX_READ_SIZE); if (r <= 0) { diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java index d16b5e38..6082f20e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java @@ -1,38 +1,20 @@ package com.genymobile.scrcpy.audio; -import android.annotation.SuppressLint; -import android.media.MediaRecorder; - -@SuppressLint("InlinedApi") public enum AudioSource { - OUTPUT("output", MediaRecorder.AudioSource.REMOTE_SUBMIX), - MIC("mic", MediaRecorder.AudioSource.MIC), - PLAYBACK("playback", -1), - MIC_UNPROCESSED("mic-unprocessed", MediaRecorder.AudioSource.UNPROCESSED), - MIC_CAMCORDER("mic-camcorder", MediaRecorder.AudioSource.CAMCORDER), - MIC_VOICE_RECOGNITION("mic-voice-recognition", MediaRecorder.AudioSource.VOICE_RECOGNITION), - MIC_VOICE_COMMUNICATION("mic-voice-communication", MediaRecorder.AudioSource.VOICE_COMMUNICATION), - VOICE_CALL("voice-call", MediaRecorder.AudioSource.VOICE_CALL), - VOICE_CALL_UPLINK("voice-call-uplink", MediaRecorder.AudioSource.VOICE_UPLINK), - VOICE_CALL_DOWNLINK("voice-call-downlink", MediaRecorder.AudioSource.VOICE_DOWNLINK), - VOICE_PERFORMANCE("voice-performance", MediaRecorder.AudioSource.VOICE_PERFORMANCE); + OUTPUT("output"), + MIC("mic"), + PLAYBACK("playback"); private final String name; - private final int directAudioSource; - AudioSource(String name, int directAudioSource) { + AudioSource(String name) { 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)) { diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java index 0eb96adc..d1406ed0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java @@ -17,14 +17,12 @@ public final class ControlMessage { 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_SET_SCREEN_POWER_MODE = 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; @@ -35,7 +33,7 @@ public final class ControlMessage { private int type; private String text; private int metaState; // KeyEvent.META_* - private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* + private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or POWER_MODE_* private int keycode; // KeyEvent.KEYCODE_* private int actionButton; // MotionEvent.BUTTON_* private int buttons; // MotionEvent.BUTTON_* @@ -50,9 +48,6 @@ public final class ControlMessage { private long sequence; private int id; private byte[] data; - private boolean on; - private int vendorId; - private int productId; private ControlMessage() { } @@ -120,10 +115,13 @@ public final class ControlMessage { return msg; } - public static ControlMessage createSetDisplayPower(boolean on) { + /** + * @param mode one of the {@code Device.SCREEN_POWER_MODE_*} constants + */ + public static ControlMessage createSetScreenPowerMode(int mode) { ControlMessage msg = new ControlMessage(); - msg.type = TYPE_SET_DISPLAY_POWER; - msg.on = on; + msg.type = TYPE_SET_SCREEN_POWER_MODE; + msg.action = mode; return msg; } @@ -133,12 +131,10 @@ public final class ControlMessage { return msg; } - public static ControlMessage createUhidCreate(int id, int vendorId, int productId, String name, byte[] reportDesc) { + public static ControlMessage createUhidCreate(int id, 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; @@ -159,13 +155,6 @@ public final class ControlMessage { 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; } @@ -237,16 +226,4 @@ public final class ControlMessage { public byte[] getData() { return data; } - - public boolean getOn() { - return on; - } - - public int getVendorId() { - return vendorId; - } - - public int getProductId() { - return productId; - } } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java index 830a7ec7..45116935 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java @@ -1,7 +1,7 @@ package com.genymobile.scrcpy.control; -import com.genymobile.scrcpy.device.Position; import com.genymobile.scrcpy.util.Binary; +import com.genymobile.scrcpy.device.Position; import java.io.BufferedInputStream; import java.io.DataInputStream; @@ -39,14 +39,13 @@ public class ControlMessageReader { return parseGetClipboard(); case ControlMessage.TYPE_SET_CLIPBOARD: return parseSetClipboard(); - case ControlMessage.TYPE_SET_DISPLAY_POWER: - return parseSetDisplayPower(); + case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: + return parseSetScreenPowerMode(); 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(); @@ -54,8 +53,6 @@ public class ControlMessageReader { return parseUhidInput(); case ControlMessage.TYPE_UHID_DESTROY: return parseUhidDestroy(); - case ControlMessage.TYPE_START_APP: - return parseStartApp(); default: throw new ControlProtocolException("Unknown event type: " + type); } @@ -112,9 +109,8 @@ public class ControlMessageReader { 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; + float hScroll = Binary.i16FixedPointToFloat(dis.readShort()); + float vScroll = Binary.i16FixedPointToFloat(dis.readShort()); int buttons = dis.readInt(); return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll, buttons); } @@ -136,18 +132,16 @@ public class ControlMessageReader { return ControlMessage.createSetClipboard(sequence, text, paste); } - private ControlMessage parseSetDisplayPower() throws IOException { - boolean on = dis.readBoolean(); - return ControlMessage.createSetDisplayPower(on); + private ControlMessage parseSetScreenPowerMode() throws IOException { + int mode = dis.readUnsignedByte(); + return ControlMessage.createSetScreenPowerMode(mode); } 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); + return ControlMessage.createUhidCreate(id, name, data); } private ControlMessage parseUhidInput() throws IOException { @@ -161,11 +155,6 @@ public class ControlMessageReader { 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(); diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index b4a8e3ca..38251655 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -1,67 +1,28 @@ package com.genymobile.scrcpy.control; -import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.AsyncProcessor; import com.genymobile.scrcpy.CleanUp; -import com.genymobile.scrcpy.Options; import com.genymobile.scrcpy.device.Device; -import com.genymobile.scrcpy.device.DeviceApp; -import com.genymobile.scrcpy.device.DisplayInfo; +import com.genymobile.scrcpy.util.Ln; 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; - } - } +public class Controller implements AsyncProcessor { private static final int DEFAULT_DEVICE_ID = 0; @@ -69,14 +30,12 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { 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 Device device; private final ControlChannel controlChannel; private final CleanUp cleanUp; private final DeviceMessageSender sender; @@ -85,103 +44,27 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { 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; + private boolean keepPowerModeOff; - // Used for resetting video encoding on RESET_VIDEO message - private SurfaceCapture surfaceCapture; - - public Controller(ControlChannel controlChannel, CleanUp cleanUp, Options options) { - this.displayId = options.getDisplayId(); + public Controller(Device device, ControlChannel controlChannel, CleanUp cleanUp, boolean clipboardAutosync, boolean powerOn) { + this.device = device; this.controlChannel = controlChannel; this.cleanUp = cleanUp; - this.clipboardAutosync = options.getClipboardAutosync(); - this.powerOn = options.getPowerOn(); + this.clipboardAutosync = clipboardAutosync; + this.powerOn = powerOn; 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); + uhidManager = new UhidManager(sender); } - return uhidManager; } @@ -201,8 +84,8 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { 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); + if (powerOn && !Device.isScreenOn()) { + device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC); // dirty hack // After POWER is injected, the device is powered on asynchronously. @@ -255,6 +138,10 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { sender.join(); } + public DeviceMessageSender getSender() { + return sender; + } + private boolean handleEvent() throws IOException { ControlMessage msg; try { @@ -266,27 +153,27 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { switch (msg.getType()) { case ControlMessage.TYPE_INJECT_KEYCODE: - if (supportsInputEvents) { + if (device.supportsInputEvents()) { injectKeycode(msg.getAction(), msg.getKeycode(), msg.getRepeat(), msg.getMetaState()); } break; case ControlMessage.TYPE_INJECT_TEXT: - if (supportsInputEvents) { + if (device.supportsInputEvents()) { injectText(msg.getText()); } break; case ControlMessage.TYPE_INJECT_TOUCH_EVENT: - if (supportsInputEvents) { + if (device.supportsInputEvents()) { injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getActionButton(), msg.getButtons()); } break; case ControlMessage.TYPE_INJECT_SCROLL_EVENT: - if (supportsInputEvents) { + if (device.supportsInputEvents()) { injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll(), msg.getButtons()); } break; case ControlMessage.TYPE_BACK_OR_SCREEN_ON: - if (supportsInputEvents) { + if (device.supportsInputEvents()) { pressBackOrTurnScreenOn(msg.getAction()); } break; @@ -305,16 +192,25 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { case ControlMessage.TYPE_SET_CLIPBOARD: setClipboard(msg.getText(), msg.getPaste(), msg.getSequence()); break; - case ControlMessage.TYPE_SET_DISPLAY_POWER: - if (supportsInputEvents) { - setDisplayPower(msg.getOn()); + case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: + if (device.supportsInputEvents()) { + int mode = msg.getAction(); + boolean setPowerModeOk = Device.setScreenPowerMode(mode); + if (setPowerModeOk) { + keepPowerModeOff = mode == Device.POWER_MODE_OFF; + Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on")); + if (cleanUp != null) { + boolean mustRestoreOnExit = mode != Device.POWER_MODE_NORMAL; + cleanUp.setRestoreNormalPowerMode(mustRestoreOnExit); + } + } } break; case ControlMessage.TYPE_ROTATE_DEVICE: - Device.rotateDevice(getActionDisplayId()); + device.rotateDevice(); break; case ControlMessage.TYPE_UHID_CREATE: - getUhidManager().open(msg.getId(), msg.getVendorId(), msg.getProductId(), msg.getText(), msg.getData()); + getUhidManager().open(msg.getId(), msg.getText(), msg.getData()); break; case ControlMessage.TYPE_UHID_INPUT: getUhidManager().writeInput(msg.getId(), msg.getData()); @@ -325,12 +221,6 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { 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 } @@ -339,11 +229,10 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { } 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); + if (keepPowerModeOff && action == KeyEvent.ACTION_UP && (keycode == KeyEvent.KEYCODE_POWER || keycode == KeyEvent.KEYCODE_WAKEUP)) { + schedulePowerModeOff(); } - return injectKeyEvent(action, keycode, repeat, metaState, Device.INJECT_MODE_ASYNC); + return device.injectKeyEvent(action, keycode, repeat, metaState, Device.INJECT_MODE_ASYNC); } private boolean injectChar(char c) { @@ -353,10 +242,8 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { if (events == null) { return false; } - - int actionDisplayId = getActionDisplayId(); for (KeyEvent event : events) { - if (!Device.injectEvent(event, actionDisplayId, Device.INJECT_MODE_ASYNC)) { + if (!device.injectEvent(event, Device.INJECT_MODE_ASYNC)) { return false; } } @@ -375,47 +262,15 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { 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) { + Point point = device.getPhysicalPoint(position); + if (point == null) { + Ln.w("Ignore touch event, it was generated for a different device size"); 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"); @@ -463,13 +318,13 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { * * Otherwise, Chrome does not work properly: */ - if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0 && source == InputDevice.SOURCE_MOUSE) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && source == InputDevice.SOURCE_MOUSE) { if (action == MotionEvent.ACTION_DOWN) { if (actionButton == buttons) { // First button pressed: ACTION_DOWN MotionEvent downEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_DOWN, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); - if (!Device.injectEvent(downEvent, targetDisplayId, Device.INJECT_MODE_ASYNC)) { + if (!device.injectEvent(downEvent, Device.INJECT_MODE_ASYNC)) { return false; } } @@ -480,7 +335,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { if (!InputManager.setActionButton(pressEvent, actionButton)) { return false; } - if (!Device.injectEvent(pressEvent, targetDisplayId, Device.INJECT_MODE_ASYNC)) { + if (!device.injectEvent(pressEvent, Device.INJECT_MODE_ASYNC)) { return false; } @@ -494,7 +349,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { if (!InputManager.setActionButton(releaseEvent, actionButton)) { return false; } - if (!Device.injectEvent(releaseEvent, targetDisplayId, Device.INJECT_MODE_ASYNC)) { + if (!device.injectEvent(releaseEvent, Device.INJECT_MODE_ASYNC)) { return false; } @@ -502,7 +357,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { // 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)) { + if (!device.injectEvent(upEvent, Device.INJECT_MODE_ASYNC)) { return false; } } @@ -513,20 +368,17 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { 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); + return device.injectEvent(event, 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) { + Point point = device.getPhysicalPoint(position); + if (point == null) { + // ignore event return false; } - Point point = pair.first; - int targetDisplayId = pair.second; - MotionEvent.PointerProperties props = pointerProperties[0]; props.id = 0; @@ -538,22 +390,22 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { 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); + return device.injectEvent(event, Device.INJECT_MODE_ASYNC); } /** - * Schedule a call to set display power to off after a small delay. + * Schedule a call to set power mode to off after a small delay. */ - private static void scheduleDisplayPowerOff(int displayId) { + private static void schedulePowerModeOff() { EXECUTOR.schedule(() -> { - Ln.i("Forcing display off"); - Device.setDisplayPower(displayId, false); + Ln.i("Forcing screen off"); + Device.setScreenPowerMode(Device.POWER_MODE_OFF); }, 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); + if (Device.isScreenOn()) { + return device.injectKeyEvent(action, KeyEvent.KEYCODE_BACK, 0, 0, Device.INJECT_MODE_ASYNC); } // Screen is off @@ -563,19 +415,18 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { return true; } - if (keepDisplayPowerOff) { - assert displayId != Device.DISPLAY_ID_NONE; - scheduleDisplayPowerOff(displayId); + if (keepPowerModeOff) { + schedulePowerModeOff(); } - return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC); + return device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC); } private void getClipboard(int copyKey) { // On Android >= 7, press the COPY or CUT key if requested - if (copyKey != ControlMessage.COPY_KEY_NONE && Build.VERSION.SDK_INT >= AndroidVersions.API_24_ANDROID_7_0 && supportsInputEvents) { + if (copyKey != ControlMessage.COPY_KEY_NONE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) { int key = copyKey == ControlMessage.COPY_KEY_COPY ? KeyEvent.KEYCODE_COPY : KeyEvent.KEYCODE_CUT; // Wait until the event is finished, to ensure that the clipboard text we read just after is the correct one - pressReleaseKeycode(key, Device.INJECT_MODE_WAIT_FOR_FINISH); + device.pressReleaseKeycode(key, Device.INJECT_MODE_WAIT_FOR_FINISH); } // If clipboard autosync is enabled, then the device clipboard is synchronized to the computer clipboard whenever it changes, in @@ -591,16 +442,14 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { } private boolean setClipboard(String text, boolean paste, long sequence) { - isSettingClipboard.set(true); - boolean ok = Device.setClipboardText(text); - isSettingClipboard.set(false); + boolean ok = device.setClipboardText(text); if (ok) { Ln.i("Device clipboard set"); } // On Android >= 7, also press the PASTE key if requested - if (paste && Build.VERSION.SDK_INT >= AndroidVersions.API_24_ANDROID_7_0 && supportsInputEvents) { - pressReleaseKeycode(KeyEvent.KEYCODE_PASTE, Device.INJECT_MODE_ASYNC); + if (paste && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) { + device.pressReleaseKeycode(KeyEvent.KEYCODE_PASTE, Device.INJECT_MODE_ASYNC); } if (sequence != ControlMessage.SEQUENCE_INVALID) { @@ -616,142 +465,4 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { Intent intent = new Intent("android.settings.HARD_KEYBOARD_SETTINGS"); ServiceManager.getActivityManager().startActivity(intent); } - - private boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int injectMode) { - return Device.injectKeyEvent(action, keyCode, repeat, metaState, getActionDisplayId(), injectMode); - } - - private boolean pressReleaseKeycode(int keyCode, int injectMode) { - return Device.pressReleaseKeycode(keyCode, getActionDisplayId(), injectMode); - } - - private int getActionDisplayId() { - if (displayId != Device.DISPLAY_ID_NONE) { - // Real screen mirrored, use the source display id - return displayId; - } - - // Virtual display created by --new-display, use the virtualDisplayId - DisplayData data = displayData.get(); - if (data == null) { - // If no virtual display id is initialized yet, use the main display id - return 0; - } - - return data.virtualDisplayId; - } - - private void startAppAsync(String name) { - if (startAppExecutor == null) { - startAppExecutor = Executors.newSingleThreadExecutor(); - } - - // Listing and selecting the app may take a lot of time - startAppExecutor.submit(() -> startApp(name)); - } - - private void startApp(String name) { - boolean forceStopBeforeStart = name.startsWith("+"); - if (forceStopBeforeStart) { - name = name.substring(1); - } - - DeviceApp app; - boolean searchByName = name.startsWith("?"); - if (searchByName) { - name = name.substring(1); - - Ln.i("Processing Android apps... (this may take some time)"); - List apps = Device.findByName(name); - if (apps.isEmpty()) { - Ln.w("No app found for name \"" + name + "\""); - return; - } - - if (apps.size() > 1) { - String title = "No unique app found for name \"" + name + "\":"; - Ln.w(LogUtils.buildAppListMessage(title, apps)); - return; - } - - app = apps.get(0); - } else { - app = Device.findByPackageName(name); - if (app == null) { - Ln.w("No app found for package \"" + name + "\""); - return; - } - } - - int startAppDisplayId = getStartAppDisplayId(); - if (startAppDisplayId == Device.DISPLAY_ID_NONE) { - Ln.e("No known display id to start app \"" + name + "\""); - return; - } - - Ln.i("Starting app \"" + app.getName() + "\" [" + app.getPackageName() + "] on display " + startAppDisplayId + "..."); - Device.startApp(app.getPackageName(), startAppDisplayId, forceStopBeforeStart); - } - - private int getStartAppDisplayId() { - if (displayId != Device.DISPLAY_ID_NONE) { - return displayId; - } - - // Mirroring a new virtual display id (using --new-display-id feature) - try { - // Wait for at most 1 second until a virtual display id is known - DisplayData data = waitDisplayData(1000); - if (data != null) { - return data.virtualDisplayId; - } - } catch (InterruptedException e) { - // do nothing - } - - // No display id available - return Device.DISPLAY_ID_NONE; - } - - private DisplayData waitDisplayData(long timeoutMillis) throws InterruptedException { - long deadline = System.currentTimeMillis() + timeoutMillis; - - synchronized (displayDataAvailable) { - DisplayData data = displayData.get(); - while (data == null) { - long timeout = deadline - System.currentTimeMillis(); - if (timeout < 0) { - return null; - } - if (timeout > 0) { - displayDataAvailable.wait(timeout); - } - data = displayData.get(); - } - - return data; - } - } - - private void setDisplayPower(boolean on) { - // Change the power of the main display when mirroring a virtual display - int targetDisplayId = displayId != Device.DISPLAY_ID_NONE ? displayId : 0; - boolean setDisplayPowerOk = Device.setDisplayPower(targetDisplayId, on); - if (setDisplayPowerOk) { - // Do not keep display power off for virtual displays: MOD+p must wake up the physical device - keepDisplayPowerOff = displayId != Device.DISPLAY_ID_NONE && !on; - Ln.i("Device display turned " + (on ? "on" : "off")); - if (cleanUp != null) { - boolean mustRestoreOnExit = !on; - cleanUp.setRestoreDisplayPower(mustRestoreOnExit); - } - } - } - - private void resetVideo() { - if (surfaceCapture != null) { - Ln.i("Video capture reset"); - surfaceCapture.requestInvalidate(); - } - } } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java b/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java deleted file mode 100644 index 60109b51..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.genymobile.scrcpy.control; - -import com.genymobile.scrcpy.device.Point; -import com.genymobile.scrcpy.device.Position; -import com.genymobile.scrcpy.device.Size; -import com.genymobile.scrcpy.util.AffineMatrix; - -public final class PositionMapper { - - private final Size videoSize; - private final AffineMatrix videoToDeviceMatrix; - - public PositionMapper(Size videoSize, AffineMatrix videoToDeviceMatrix) { - this.videoSize = videoSize; - this.videoToDeviceMatrix = videoToDeviceMatrix; - } - - public static PositionMapper create(Size videoSize, AffineMatrix filterTransform, Size targetSize) { - boolean convertToPixels = !videoSize.equals(targetSize) || filterTransform != null; - AffineMatrix transform = filterTransform; - if (convertToPixels) { - AffineMatrix inputTransform = AffineMatrix.ndcFromPixels(videoSize); - AffineMatrix outputTransform = AffineMatrix.ndcToPixels(targetSize); - transform = outputTransform.multiply(transform).multiply(inputTransform); - } - - return new PositionMapper(videoSize, transform); - } - - public Size getVideoSize() { - return videoSize; - } - - public Point map(Position position) { - Size clientVideoSize = position.getScreenSize(); - if (!videoSize.equals(clientVideoSize)) { - // The client sends a click relative to a video with wrong dimensions, - // the device may have been rotated since the event was generated, so ignore the event - return null; - } - - Point point = position.getPoint(); - if (videoToDeviceMatrix != null) { - point = videoToDeviceMatrix.apply(point); - } - return point; - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java index 20532c0b..d8cfd81f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java @@ -1,9 +1,7 @@ package com.genymobile.scrcpy.control; -import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.StringUtils; -import com.genymobile.scrcpy.wrappers.ServiceManager; import android.os.Build; import android.os.HandlerThread; @@ -32,21 +30,15 @@ public final class UhidManager { 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) { + public UhidManager(DeviceMessageSender sender) { this.sender = sender; - this.displayUniqueId = displayUniqueId; - if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { HandlerThread thread = new HandlerThread("UHidManager"); thread.start(); queue = thread.getLooper().getQueue(); @@ -55,26 +47,19 @@ public final class UhidManager { } } - public void open(int id, int vendorId, int productId, String name, byte[] reportDesc) throws IOException { + public void open(int id, 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); + byte[] req = buildUhidCreate2Req(name, reportDesc); Os.write(fd, req, 0, req.length); - if (firstDevice) { - addUniqueIdAssociation(); - } registerUhidListener(id, fd); } catch (Exception e) { close(fd); @@ -86,7 +71,7 @@ public final class UhidManager { } private void registerUhidListener(int id, FileDescriptor fd) { - if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { queue.addOnFileDescriptorEventListener(fd, MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT, (fd2, events) -> { try { buffer.clear(); @@ -112,7 +97,7 @@ public final class UhidManager { } private void unregisterUhidListener(FileDescriptor fd) { - if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { queue.removeOnFileDescriptorEventListener(fd); } } @@ -162,7 +147,7 @@ public final class UhidManager { } } - private static byte[] buildUhidCreate2Req(int vendorId, int productId, String name, byte[] reportDesc, String phys) { + private static byte[] buildUhidCreate2Req(String name, byte[] reportDesc) { /* * struct uhid_event { * uint32_t type; @@ -184,27 +169,21 @@ public final class UhidManager { * } __attribute__((__packed__)); */ + byte[] empty = new byte[256]; 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); + String actualName = name.isEmpty() ? "scrcpy" : "scrcpy: " + name; + byte[] utf8Name = actualName.getBytes(StandardCharsets.UTF_8); + int len = StringUtils.getUtf8TruncationIndex(utf8Name, 127); + assert len <= 127; + buf.put(utf8Name, 0, len); + buf.put(empty, 0, 256 - len); - 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); // vendor id + buf.putInt(0); // product id buf.putInt(0); // version buf.putInt(0); // country; buf.put(reportDesc); @@ -239,26 +218,15 @@ public final class UhidManager { 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) { @@ -268,20 +236,4 @@ public final class UhidManager { Ln.e("Failed to close uhid: " + e.getMessage()); } } - - private boolean mustUseInputPort() { - return Build.VERSION.SDK_INT >= AndroidVersions.API_35_ANDROID_15 && displayUniqueId != null; - } - - private void addUniqueIdAssociation() { - if (mustUseInputPort()) { - ServiceManager.getInputManager().addUniqueIdAssociationByPort(INPUT_PORT, displayUniqueId); - } - } - - private void removeUniqueIdAssociation() { - if (mustUseInputPort()) { - ServiceManager.getInputManager().removeUniqueIdAssociationByPort(INPUT_PORT); - } - } } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index 3553dc27..5a1083fd 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -1,9 +1,9 @@ package com.genymobile.scrcpy.device; -import com.genymobile.scrcpy.AndroidVersions; -import com.genymobile.scrcpy.FakeContext; +import com.genymobile.scrcpy.Options; import com.genymobile.scrcpy.util.Ln; -import com.genymobile.scrcpy.wrappers.ActivityManager; +import com.genymobile.scrcpy.util.LogUtils; +import com.genymobile.scrcpy.video.ScreenInfo; import com.genymobile.scrcpy.wrappers.ClipboardManager; import com.genymobile.scrcpy.wrappers.DisplayControl; import com.genymobile.scrcpy.wrappers.InputManager; @@ -11,28 +11,22 @@ 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.content.IOnPrimaryClipChangedListener; +import android.graphics.Rect; import android.os.Build; -import android.os.Bundle; import android.os.IBinder; import android.os.SystemClock; +import android.view.IDisplayFoldListener; +import android.view.IRotationWatcher; 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; +import java.util.concurrent.atomic.AtomicBoolean; 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; @@ -40,12 +34,180 @@ public final class Device { 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; + public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1; + public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2; - private Device() { - // not instantiable + public interface RotationListener { + void onRotationChanged(int rotation); + } + + public interface FoldListener { + void onFoldChanged(int displayId, boolean folded); + } + + public interface ClipboardListener { + void onClipboardTextChanged(String text); + } + + private final Rect crop; + private int maxSize; + private final int lockVideoOrientation; + + private Size deviceSize; + private ScreenInfo screenInfo; + private RotationListener rotationListener; + private FoldListener foldListener; + private ClipboardListener clipboardListener; + private final AtomicBoolean isSettingClipboard = new AtomicBoolean(); + + /** + * Logical display identifier + */ + private final int displayId; + + /** + * The surface flinger layer stack associated with this logical display + */ + private final int layerStack; + + private final boolean supportsInputEvents; + + public Device(Options options) throws ConfigurationException { + displayId = options.getDisplayId(); + DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId); + if (displayInfo == null) { + Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage()); + throw new ConfigurationException("Unknown display id: " + displayId); + } + + int displayInfoFlags = displayInfo.getFlags(); + + deviceSize = displayInfo.getSize(); + crop = options.getCrop(); + maxSize = options.getMaxSize(); + lockVideoOrientation = options.getLockVideoOrientation(); + + screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), deviceSize, crop, maxSize, lockVideoOrientation); + layerStack = displayInfo.getLayerStack(); + + ServiceManager.getWindowManager().registerRotationWatcher(new IRotationWatcher.Stub() { + @Override + public void onRotationChanged(int rotation) { + synchronized (Device.this) { + screenInfo = screenInfo.withDeviceRotation(rotation); + + // notify + if (rotationListener != null) { + rotationListener.onRotationChanged(rotation); + } + } + } + }, displayId); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ServiceManager.getWindowManager().registerDisplayFoldListener(new IDisplayFoldListener.Stub() { + @Override + public void onDisplayFoldChanged(int displayId, boolean folded) { + if (Device.this.displayId != displayId) { + // Ignore events related to other display ids + return; + } + + synchronized (Device.this) { + DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId); + if (displayInfo == null) { + Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage()); + return; + } + + deviceSize = displayInfo.getSize(); + screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), deviceSize, crop, maxSize, lockVideoOrientation); + // notify + if (foldListener != null) { + foldListener.onFoldChanged(displayId, folded); + } + } + } + }); + } + + if (options.getControl() && options.getClipboardAutosync()) { + // If control and autosync are enabled, synchronize Android clipboard to the computer automatically + ClipboardManager clipboardManager = ServiceManager.getClipboardManager(); + if (clipboardManager != null) { + clipboardManager.addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() { + @Override + public void dispatchPrimaryClipChanged() { + if (isSettingClipboard.get()) { + // This is a notification for the change we are currently applying, ignore it + return; + } + synchronized (Device.this) { + if (clipboardListener != null) { + String text = getClipboardText(); + if (text != null) { + clipboardListener.onClipboardTextChanged(text); + } + } + } + } + }); + } else { + Ln.w("No clipboard manager, copy-paste between device and computer will not work"); + } + } + + if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) { + Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted"); + } + + // main display or any display on Android >= Q + supportsInputEvents = displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; + if (!supportsInputEvents) { + Ln.w("Input events are not supported for secondary displays before Android 10"); + } + } + + public int getDisplayId() { + return displayId; + } + + public synchronized void setMaxSize(int newMaxSize) { + maxSize = newMaxSize; + screenInfo = ScreenInfo.computeScreenInfo(screenInfo.getReverseVideoRotation(), deviceSize, crop, newMaxSize, lockVideoOrientation); + } + + public synchronized ScreenInfo getScreenInfo() { + return screenInfo; + } + + public int getLayerStack() { + return layerStack; + } + + public Point getPhysicalPoint(Position position) { + // it hides the field on purpose, to read it with a lock + @SuppressWarnings("checkstyle:HiddenField") + ScreenInfo screenInfo = getScreenInfo(); // read with synchronization + + // ignore the locked video orientation, the events will apply in coordinates considered in the physical device orientation + Size unlockedVideoSize = screenInfo.getUnlockedVideoSize(); + + int reverseVideoRotation = screenInfo.getReverseVideoRotation(); + // reverse the video rotation to apply the events + Position devicePosition = position.rotate(reverseVideoRotation); + + Size clientVideoSize = devicePosition.getScreenSize(); + if (!unlockedVideoSize.equals(clientVideoSize)) { + // The client sends a click relative to a video with wrong dimensions, + // the device may have been rotated since the event was generated, so ignore the event + return null; + } + Rect contentRect = screenInfo.getContentRect(); + Point point = devicePosition.getPoint(); + int convertedX = contentRect.left + point.getX() * contentRect.width() / unlockedVideoSize.getWidth(); + int convertedY = contentRect.top + point.getY() * contentRect.height() / unlockedVideoSize.getHeight(); + return new Point(convertedX, convertedY); } public static String getDeviceName() { @@ -53,8 +215,11 @@ public final class Device { } 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; + return displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; + } + + public boolean supportsInputEvents() { + return supportsInputEvents; } public static boolean injectEvent(InputEvent inputEvent, int displayId, int injectMode) { @@ -69,6 +234,10 @@ public final class Device { return ServiceManager.getInputManager().injectInputEvent(inputEvent, injectMode); } + public boolean injectEvent(InputEvent event, int injectMode) { + return injectEvent(event, displayId, injectMode); + } + public static boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int displayId, int injectMode) { long now = SystemClock.uptimeMillis(); KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, @@ -76,14 +245,33 @@ public final class Device { return injectEvent(event, displayId, injectMode); } + public boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int injectMode) { + return injectKeyEvent(action, keyCode, repeat, metaState, displayId, injectMode); + } + public static boolean pressReleaseKeycode(int keyCode, int displayId, int injectMode) { return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0, displayId, injectMode) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0, displayId, injectMode); } - public static boolean isScreenOn(int displayId) { - assert displayId != DISPLAY_ID_NONE; - return ServiceManager.getPowerManager().isScreenOn(displayId); + public boolean pressReleaseKeycode(int keyCode, int injectMode) { + return pressReleaseKeycode(keyCode, displayId, injectMode); + } + + public static boolean isScreenOn() { + return ServiceManager.getPowerManager().isScreenOn(); + } + + public synchronized void setRotationListener(RotationListener rotationListener) { + this.rotationListener = rotationListener; + } + + public synchronized void setFoldListener(FoldListener foldlistener) { + this.foldListener = foldlistener; + } + + public synchronized void setClipboardListener(ClipboardListener clipboardListener) { + this.clipboardListener = clipboardListener; } public static void expandNotificationPanel() { @@ -110,7 +298,7 @@ public final class Device { return s.toString(); } - public static boolean setClipboardText(String text) { + public boolean setClipboardText(String text) { ClipboardManager clipboardManager = ServiceManager.getClipboardManager(); if (clipboardManager == null) { return false; @@ -125,20 +313,20 @@ public final class Device { return false; } - return clipboardManager.setText(text); + isSettingClipboard.set(true); + boolean ok = clipboardManager.setText(text); + isSettingClipboard.set(false); + return ok; } - 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; + /** + * @param mode one of the {@code POWER_MODE_*} constants + */ + public static boolean setScreenPowerMode(int mode) { + boolean applyToMultiPhysicalDisplays = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; if (applyToMultiPhysicalDisplays - && Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14 + && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && Build.BRAND.equalsIgnoreCase("honor") && SurfaceControl.hasGetBuildInDisplayMethod()) { // Workaround for Honor devices with Android 14: @@ -147,11 +335,10 @@ public final class Device { 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(); + Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && !SurfaceControl.hasGetPhysicalDisplayIdsMethod(); // Change the power mode for all physical displays long[] physicalDisplayIds = useDisplayControl ? DisplayControl.getPhysicalDisplayIds() : SurfaceControl.getPhysicalDisplayIds(); @@ -179,9 +366,7 @@ public final class Device { } public static boolean powerOffScreen(int displayId) { - assert displayId != DISPLAY_ID_NONE; - - if (!isScreenOn(displayId)) { + if (!isScreenOn()) { return true; } return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC); @@ -190,9 +375,7 @@ public final class Device { /** * 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; - + public void rotateDevice() { WindowManager wm = ServiceManager.getWindowManager(); boolean accelerometerRotation = !wm.isRotationFrozen(displayId); @@ -211,8 +394,6 @@ public final class Device { } private static int getCurrentRotation(int displayId) { - assert displayId != DISPLAY_ID_NONE; - if (displayId == 0) { return ServiceManager.getWindowManager().getRotation(); } @@ -220,96 +401,4 @@ public final class Device { DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId); return displayInfo.getRotation(); } - - public static List listApps() { - List apps = new ArrayList<>(); - PackageManager pm = FakeContext.get().getPackageManager(); - for (ApplicationInfo appInfo : getLaunchableApps(pm)) { - apps.add(toApp(pm, appInfo)); - } - - return apps; - } - - @SuppressLint("QueryPermissionsNeeded") - private static List getLaunchableApps(PackageManager pm) { - List result = new ArrayList<>(); - for (ApplicationInfo appInfo : pm.getInstalledApplications(PackageManager.GET_META_DATA)) { - if (appInfo.enabled && getLaunchIntent(pm, appInfo.packageName) != null) { - result.add(appInfo); - } - } - - return result; - } - - public static Intent getLaunchIntent(PackageManager pm, String packageName) { - Intent launchIntent = pm.getLaunchIntentForPackage(packageName); - if (launchIntent != null) { - return launchIntent; - } - - return pm.getLeanbackLaunchIntentForPackage(packageName); - } - - private static DeviceApp toApp(PackageManager pm, ApplicationInfo appInfo) { - String name = pm.getApplicationLabel(appInfo).toString(); - boolean system = (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; - return new DeviceApp(appInfo.packageName, name, system); - } - - @SuppressLint("QueryPermissionsNeeded") - public static DeviceApp findByPackageName(String packageName) { - PackageManager pm = FakeContext.get().getPackageManager(); - // No need to filter by "launchable" apps, an error will be reported on start if the app is not launchable - for (ApplicationInfo appInfo : pm.getInstalledApplications(PackageManager.GET_META_DATA)) { - if (packageName.equals(appInfo.packageName)) { - return toApp(pm, appInfo); - } - } - - return null; - } - - @SuppressLint("QueryPermissionsNeeded") - public static List findByName(String searchName) { - List result = new ArrayList<>(); - searchName = searchName.toLowerCase(Locale.getDefault()); - - PackageManager pm = FakeContext.get().getPackageManager(); - for (ApplicationInfo appInfo : getLaunchableApps(pm)) { - String name = pm.getApplicationLabel(appInfo).toString(); - if (name.toLowerCase(Locale.getDefault()).startsWith(searchName)) { - boolean system = (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; - result.add(new DeviceApp(appInfo.packageName, name, system)); - } - } - - return result; - } - - public static void startApp(String packageName, int displayId, boolean forceStop) { - PackageManager pm = FakeContext.get().getPackageManager(); - - Intent launchIntent = getLaunchIntent(pm, packageName); - if (launchIntent == null) { - Ln.w("Cannot create launch intent for app " + packageName); - return; - } - - launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - - Bundle options = null; - if (Build.VERSION.SDK_INT >= AndroidVersions.API_26_ANDROID_8_0) { - ActivityOptions launchOptions = ActivityOptions.makeBasic(); - launchOptions.setLaunchDisplayId(displayId); - options = launchOptions.toBundle(); - } - - ActivityManager am = ServiceManager.getActivityManager(); - if (forceStop) { - am.forceStopPackage(packageName); - } - am.startActivity(launchIntent, options); - } } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/DeviceApp.java b/server/src/main/java/com/genymobile/scrcpy/device/DeviceApp.java deleted file mode 100644 index ed292efa..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/device/DeviceApp.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.genymobile.scrcpy.device; - -public final class DeviceApp { - - private final String packageName; - private final String name; - private final boolean system; - - public DeviceApp(String packageName, String name, boolean system) { - this.packageName = packageName; - this.name = name; - this.system = system; - } - - public String getPackageName() { - return packageName; - } - - public String getName() { - return name; - } - - public boolean isSystem() { - return system; - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java b/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java index 8d26b7ce..2973710d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java @@ -6,19 +6,15 @@ public final class DisplayInfo { 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) { + public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags) { this.displayId = displayId; this.size = size; this.rotation = rotation; this.layerStack = layerStack; this.flags = flags; - this.dpi = dpi; - this.uniqueId = uniqueId; } public int getDisplayId() { @@ -40,12 +36,5 @@ public final class DisplayInfo { public int getFlags() { return flags; } - - public int getDpi() { - return dpi; - } - - public String getUniqueId() { - return uniqueId; - } } + diff --git a/server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java b/server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java deleted file mode 100644 index 3aa2996a..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.genymobile.scrcpy.device; - -public final class NewDisplay { - private Size size; - private int dpi; - - public NewDisplay() { - // Auto size and dpi - } - - public NewDisplay(Size size, int dpi) { - this.size = size; - this.dpi = dpi; - } - - public Size getSize() { - return size; - } - - public int getDpi() { - return dpi; - } - - public boolean hasExplicitSize() { - return size != null; - } - - public boolean hasExplicitDpi() { - return dpi != 0; - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Orientation.java b/server/src/main/java/com/genymobile/scrcpy/device/Orientation.java deleted file mode 100644 index 81168aae..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/device/Orientation.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.genymobile.scrcpy.device; - -public enum Orientation { - - // @formatter:off - Orient0("0"), - Orient90("90"), - Orient180("180"), - Orient270("270"), - Flip0("flip0"), - Flip90("flip90"), - Flip180("flip180"), - Flip270("flip270"); - - public enum Lock { - Unlocked, LockedInitial, LockedValue, - } - - private final String name; - - Orientation(String name) { - this.name = name; - } - - public static Orientation getByName(String name) { - for (Orientation orientation : values()) { - if (orientation.name.equals(name)) { - return orientation; - } - } - - throw new IllegalArgumentException("Unknown orientation: " + name); - } - - public static Orientation fromRotation(int ccwRotation) { - assert ccwRotation >= 0 && ccwRotation < 4; - // Display rotation is expressed counter-clockwise, orientation is expressed clockwise - int cwRotation = (4 - ccwRotation) % 4; - return values()[cwRotation]; - } - - public boolean isFlipped() { - return (ordinal() & 4) != 0; - } - - public int getRotation() { - return ordinal() & 3; - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Size.java b/server/src/main/java/com/genymobile/scrcpy/device/Size.java index b448273d..bc9dce1c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Size.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Size.java @@ -21,69 +21,10 @@ public final class Size { 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); } @@ -107,6 +48,6 @@ public final class Size { @Override public String toString() { - return width + "x" + height; + return "Size{" + "width=" + width + ", height=" + height + '}'; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/opengl/AffineOpenGLFilter.java b/server/src/main/java/com/genymobile/scrcpy/opengl/AffineOpenGLFilter.java deleted file mode 100644 index 7608a574..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/opengl/AffineOpenGLFilter.java +++ /dev/null @@ -1,135 +0,0 @@ -package com.genymobile.scrcpy.opengl; - -import com.genymobile.scrcpy.util.AffineMatrix; - -import android.opengl.GLES11Ext; -import android.opengl.GLES20; - -import java.nio.FloatBuffer; - -public class AffineOpenGLFilter implements OpenGLFilter { - - private int program; - private FloatBuffer vertexBuffer; - private FloatBuffer texCoordsBuffer; - private final float[] userMatrix; - - private int vertexPosLoc; - private int texCoordsInLoc; - - private int texLoc; - private int texMatrixLoc; - private int userMatrixLoc; - - public AffineOpenGLFilter(AffineMatrix transform) { - userMatrix = transform.to4x4(); - } - - @Override - public void init() throws OpenGLException { - // @formatter:off - String vertexShaderCode = "#version 100\n" - + "attribute vec4 vertex_pos;\n" - + "attribute vec4 tex_coords_in;\n" - + "varying vec2 tex_coords;\n" - + "uniform mat4 tex_matrix;\n" - + "uniform mat4 user_matrix;\n" - + "void main() {\n" - + " gl_Position = vertex_pos;\n" - + " tex_coords = (tex_matrix * user_matrix * tex_coords_in).xy;\n" - + "}"; - - // @formatter:off - String fragmentShaderCode = "#version 100\n" - + "#extension GL_OES_EGL_image_external : require\n" - + "precision highp float;\n" - + "uniform samplerExternalOES tex;\n" - + "varying vec2 tex_coords;\n" - + "void main() {\n" - + " if (tex_coords.x >= 0.0 && tex_coords.x <= 1.0\n" - + " && tex_coords.y >= 0.0 && tex_coords.y <= 1.0) {\n" - + " gl_FragColor = texture2D(tex, tex_coords);\n" - + " } else {\n" - + " gl_FragColor = vec4(0.0);\n" - + " }\n" - + "}"; - - program = GLUtils.createProgram(vertexShaderCode, fragmentShaderCode); - if (program == 0) { - throw new OpenGLException("Cannot create OpenGL program"); - } - - float[] vertices = { - -1, -1, // Bottom-left - 1, -1, // Bottom-right - -1, 1, // Top-left - 1, 1, // Top-right - }; - - float[] texCoords = { - 0, 0, // Bottom-left - 1, 0, // Bottom-right - 0, 1, // Top-left - 1, 1, // Top-right - }; - - // OpenGL will fill the 3rd and 4th coordinates of the vec4 automatically with 0.0 and 1.0 respectively - vertexBuffer = GLUtils.createFloatBuffer(vertices); - texCoordsBuffer = GLUtils.createFloatBuffer(texCoords); - - vertexPosLoc = GLES20.glGetAttribLocation(program, "vertex_pos"); - assert vertexPosLoc != -1; - - texCoordsInLoc = GLES20.glGetAttribLocation(program, "tex_coords_in"); - assert texCoordsInLoc != -1; - - texLoc = GLES20.glGetUniformLocation(program, "tex"); - assert texLoc != -1; - - texMatrixLoc = GLES20.glGetUniformLocation(program, "tex_matrix"); - assert texMatrixLoc != -1; - - userMatrixLoc = GLES20.glGetUniformLocation(program, "user_matrix"); - assert userMatrixLoc != -1; - } - - @Override - public void draw(int textureId, float[] texMatrix) { - GLES20.glUseProgram(program); - GLUtils.checkGlError(); - - GLES20.glEnableVertexAttribArray(vertexPosLoc); - GLUtils.checkGlError(); - GLES20.glEnableVertexAttribArray(texCoordsInLoc); - GLUtils.checkGlError(); - - GLES20.glVertexAttribPointer(vertexPosLoc, 2, GLES20.GL_FLOAT, false, 0, vertexBuffer); - GLUtils.checkGlError(); - GLES20.glVertexAttribPointer(texCoordsInLoc, 2, GLES20.GL_FLOAT, false, 0, texCoordsBuffer); - GLUtils.checkGlError(); - - GLES20.glActiveTexture(GLES20.GL_TEXTURE0); - GLUtils.checkGlError(); - GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId); - GLUtils.checkGlError(); - GLES20.glUniform1i(texLoc, 0); - GLUtils.checkGlError(); - - GLES20.glUniformMatrix4fv(texMatrixLoc, 1, false, texMatrix, 0); - GLUtils.checkGlError(); - - GLES20.glUniformMatrix4fv(userMatrixLoc, 1, false, userMatrix, 0); - GLUtils.checkGlError(); - - GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); - GLUtils.checkGlError(); - GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); - GLUtils.checkGlError(); - } - - @Override - public void release() { - GLES20.glDeleteProgram(program); - GLUtils.checkGlError(); - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/opengl/GLUtils.java b/server/src/main/java/com/genymobile/scrcpy/opengl/GLUtils.java deleted file mode 100644 index 72a3f400..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/opengl/GLUtils.java +++ /dev/null @@ -1,124 +0,0 @@ -package com.genymobile.scrcpy.opengl; - -import com.genymobile.scrcpy.BuildConfig; -import com.genymobile.scrcpy.util.Ln; - -import android.opengl.GLES20; -import android.opengl.GLU; - -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.nio.FloatBuffer; - -public final class GLUtils { - - private static final boolean DEBUG = BuildConfig.DEBUG; - - private GLUtils() { - // not instantiable - } - - public static int createProgram(String vertexSource, String fragmentSource) { - int vertexShader = createShader(GLES20.GL_VERTEX_SHADER, vertexSource); - if (vertexShader == 0) { - return 0; - } - - int fragmentShader = createShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource); - if (fragmentShader == 0) { - GLES20.glDeleteShader(vertexShader); - return 0; - } - - int program = GLES20.glCreateProgram(); - if (program == 0) { - GLES20.glDeleteShader(fragmentShader); - GLES20.glDeleteShader(vertexShader); - return 0; - } - - GLES20.glAttachShader(program, vertexShader); - checkGlError(); - GLES20.glAttachShader(program, fragmentShader); - checkGlError(); - GLES20.glLinkProgram(program); - checkGlError(); - - int[] linkStatus = new int[1]; - GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0); - if (linkStatus[0] == 0) { - Ln.e("Could not link program: " + GLES20.glGetProgramInfoLog(program)); - GLES20.glDeleteProgram(program); - GLES20.glDeleteShader(fragmentShader); - GLES20.glDeleteShader(vertexShader); - return 0; - } - - return program; - } - - public static int createShader(int type, String source) { - int shader = GLES20.glCreateShader(type); - if (shader == 0) { - Ln.e(getGlErrorMessage("Could not create shader")); - return 0; - } - - GLES20.glShaderSource(shader, source); - GLES20.glCompileShader(shader); - - int[] compileStatus = new int[1]; - GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compileStatus, 0); - if (compileStatus[0] == 0) { - Ln.e("Could not compile " + getShaderTypeString(type) + ": " + GLES20.glGetShaderInfoLog(shader)); - GLES20.glDeleteShader(shader); - return 0; - } - - return shader; - } - - private static String getShaderTypeString(int type) { - switch (type) { - case GLES20.GL_VERTEX_SHADER: - return "vertex shader"; - case GLES20.GL_FRAGMENT_SHADER: - return "fragment shader"; - default: - return "shader"; - } - } - - /** - * Throws a runtime exception if {@link GLES20#glGetError()} returns an error (useful for debugging). - */ - public static void checkGlError() { - if (DEBUG) { - int error = GLES20.glGetError(); - if (error != GLES20.GL_NO_ERROR) { - throw new RuntimeException(toErrorString(error)); - } - } - } - - public static String getGlErrorMessage(String userError) { - int glError = GLES20.glGetError(); - if (glError == GLES20.GL_NO_ERROR) { - return userError; - } - - return userError + " (" + toErrorString(glError) + ")"; - } - - private static String toErrorString(int glError) { - String errorString = GLU.gluErrorString(glError); - return "glError 0x" + Integer.toHexString(glError) + " " + errorString; - } - - public static FloatBuffer createFloatBuffer(float[] values) { - FloatBuffer fb = ByteBuffer.allocateDirect(values.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); - fb.put(values); - fb.position(0); - return fb; - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLException.java b/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLException.java deleted file mode 100644 index cbc9539b..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLException.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.genymobile.scrcpy.opengl; - -import java.io.IOException; - -public class OpenGLException extends IOException { - public OpenGLException(String message) { - super(message); - } - - public OpenGLException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLFilter.java b/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLFilter.java deleted file mode 100644 index 6f27777e..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLFilter.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.genymobile.scrcpy.opengl; - -public interface OpenGLFilter { - - /** - * Initialize the OpenGL filter (typically compile the shaders and create the program). - * - * @throws OpenGLException if an initialization error occurs - */ - void init() throws OpenGLException; - - /** - * Render a frame (call for each frame). - */ - void draw(int textureId, float[] texMatrix); - - /** - * Release resources. - */ - void release(); -} diff --git a/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLRunner.java b/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLRunner.java deleted file mode 100644 index 86bd1859..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLRunner.java +++ /dev/null @@ -1,258 +0,0 @@ -package com.genymobile.scrcpy.opengl; - -import com.genymobile.scrcpy.device.Size; - -import android.graphics.SurfaceTexture; -import android.opengl.EGL14; -import android.opengl.EGLConfig; -import android.opengl.EGLContext; -import android.opengl.EGLDisplay; -import android.opengl.EGLExt; -import android.opengl.EGLSurface; -import android.opengl.GLES11Ext; -import android.opengl.GLES20; -import android.os.Handler; -import android.os.HandlerThread; -import android.view.Surface; - -import java.util.concurrent.Semaphore; - -public final class OpenGLRunner { - - private static HandlerThread handlerThread; - private static Handler handler; - private static boolean quit; - - private EGLDisplay eglDisplay; - private EGLContext eglContext; - private EGLSurface eglSurface; - - private final OpenGLFilter filter; - private final float[] overrideTransformMatrix; - - private SurfaceTexture surfaceTexture; - private Surface inputSurface; - private int textureId; - - private boolean stopped; - - public OpenGLRunner(OpenGLFilter filter, float[] overrideTransformMatrix) { - this.filter = filter; - this.overrideTransformMatrix = overrideTransformMatrix; - } - - public OpenGLRunner(OpenGLFilter filter) { - this(filter, null); - } - - public static synchronized void initOnce() { - if (handlerThread == null) { - if (quit) { - throw new IllegalStateException("Could not init OpenGLRunner after it is quit"); - } - handlerThread = new HandlerThread("OpenGLRunner"); - handlerThread.start(); - handler = new Handler(handlerThread.getLooper()); - } - } - - public static void quit() { - HandlerThread thread; - synchronized (OpenGLRunner.class) { - thread = handlerThread; - quit = true; - } - if (thread != null) { - thread.quitSafely(); - } - } - - public static void join() throws InterruptedException { - HandlerThread thread; - synchronized (OpenGLRunner.class) { - thread = handlerThread; - } - if (thread != null) { - thread.join(); - } - } - - public Surface start(Size inputSize, Size outputSize, Surface outputSurface) throws OpenGLException { - initOnce(); - - // Simulate CompletableFuture, but working for all Android versions - final Semaphore sem = new Semaphore(0); - Throwable[] throwableRef = new Throwable[1]; - - // The whole OpenGL execution must be performed on a Handler, so that SurfaceTexture.setOnFrameAvailableListener() works correctly. - // See - handler.post(() -> { - try { - run(inputSize, outputSize, outputSurface); - } catch (Throwable throwable) { - throwableRef[0] = throwable; - } finally { - sem.release(); - } - }); - - try { - sem.acquire(); - } catch (InterruptedException e) { - // Behave as if this method call was synchronous - Thread.currentThread().interrupt(); - } - - Throwable throwable = throwableRef[0]; - if (throwable != null) { - if (throwable instanceof OpenGLException) { - throw (OpenGLException) throwable; - } - throw new OpenGLException("Asynchronous OpenGL runner init failed", throwable); - } - - // Synchronization is ok: inputSurface is written before sem.release() and read after sem.acquire() - return inputSurface; - } - - private void run(Size inputSize, Size outputSize, Surface outputSurface) throws OpenGLException { - eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); - if (eglDisplay == EGL14.EGL_NO_DISPLAY) { - throw new OpenGLException("Unable to get EGL14 display"); - } - - int[] version = new int[2]; - if (!EGL14.eglInitialize(eglDisplay, version, 0, version, 1)) { - throw new OpenGLException("Unable to initialize EGL14"); - } - - // @formatter:off - int[] attribList = { - EGL14.EGL_RED_SIZE, 8, - EGL14.EGL_GREEN_SIZE, 8, - EGL14.EGL_BLUE_SIZE, 8, - EGL14.EGL_ALPHA_SIZE, 8, - EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, - EGL14.EGL_NONE - }; - - EGLConfig[] configs = new EGLConfig[1]; - int[] numConfigs = new int[1]; - EGL14.eglChooseConfig(eglDisplay, attribList, 0, configs, 0, configs.length, numConfigs, 0); - if (numConfigs[0] <= 0) { - EGL14.eglTerminate(eglDisplay); - throw new OpenGLException("Unable to find ES2 EGL config"); - } - EGLConfig eglConfig = configs[0]; - - // @formatter:off - int[] contextAttribList = { - EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, - EGL14.EGL_NONE - }; - eglContext = EGL14.eglCreateContext(eglDisplay, eglConfig, EGL14.EGL_NO_CONTEXT, contextAttribList, 0); - if (eglContext == null) { - EGL14.eglTerminate(eglDisplay); - throw new OpenGLException("Failed to create EGL context"); - } - - int[] surfaceAttribList = { - EGL14.EGL_NONE - }; - eglSurface = EGL14.eglCreateWindowSurface(eglDisplay, eglConfig, outputSurface, surfaceAttribList, 0); - if (eglSurface == null) { - EGL14.eglDestroyContext(eglDisplay, eglContext); - EGL14.eglTerminate(eglDisplay); - throw new OpenGLException("Failed to create EGL window surface"); - } - - if (!EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)) { - EGL14.eglDestroySurface(eglDisplay, eglSurface); - EGL14.eglDestroyContext(eglDisplay, eglContext); - EGL14.eglTerminate(eglDisplay); - throw new OpenGLException("Failed to make EGL context current"); - } - - int[] textures = new int[1]; - GLES20.glGenTextures(1, textures, 0); - GLUtils.checkGlError(); - textureId = textures[0]; - - GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); - GLUtils.checkGlError(); - GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); - GLUtils.checkGlError(); - GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); - GLUtils.checkGlError(); - GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); - GLUtils.checkGlError(); - - surfaceTexture = new SurfaceTexture(textureId); - surfaceTexture.setDefaultBufferSize(inputSize.getWidth(), inputSize.getHeight()); - inputSurface = new Surface(surfaceTexture); - - filter.init(); - - surfaceTexture.setOnFrameAvailableListener(surfaceTexture -> { - if (stopped) { - // Make sure to never render after resources have been released - return; - } - - render(outputSize); - }, handler); - } - - private void render(Size outputSize) { - GLES20.glViewport(0, 0, outputSize.getWidth(), outputSize.getHeight()); - GLUtils.checkGlError(); - - surfaceTexture.updateTexImage(); - - float[] matrix; - if (overrideTransformMatrix != null) { - matrix = overrideTransformMatrix; - } else { - matrix = new float[16]; - surfaceTexture.getTransformMatrix(matrix); - } - - filter.draw(textureId, matrix); - - EGLExt.eglPresentationTimeANDROID(eglDisplay, eglSurface, surfaceTexture.getTimestamp()); - EGL14.eglSwapBuffers(eglDisplay, eglSurface); - } - - public void stopAndRelease() { - final Semaphore sem = new Semaphore(0); - - handler.post(() -> { - stopped = true; - surfaceTexture.setOnFrameAvailableListener(null, handler); - - filter.release(); - - int[] textures = {textureId}; - GLES20.glDeleteTextures(1, textures, 0); - GLUtils.checkGlError(); - - EGL14.eglDestroySurface(eglDisplay, eglSurface); - EGL14.eglDestroyContext(eglDisplay, eglContext); - EGL14.eglTerminate(eglDisplay); - eglDisplay = EGL14.EGL_NO_DISPLAY; - eglContext = EGL14.EGL_NO_CONTEXT; - eglSurface = EGL14.EGL_NO_SURFACE; - surfaceTexture.release(); - inputSurface.release(); - - sem.release(); - }); - - try { - sem.acquire(); - } catch (InterruptedException e) { - // Behave as if this method call was synchronous - Thread.currentThread().interrupt(); - } - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/util/AffineMatrix.java b/server/src/main/java/com/genymobile/scrcpy/util/AffineMatrix.java deleted file mode 100644 index 0db74af6..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/util/AffineMatrix.java +++ /dev/null @@ -1,368 +0,0 @@ -package com.genymobile.scrcpy.util; - -import com.genymobile.scrcpy.device.Point; -import com.genymobile.scrcpy.device.Size; - -/** - * Represents a 2D affine transform (a 3x3 matrix): - * - *

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

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

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

- * (x, y) is the bottom-left corner, (w, h) is the size of the rectangle. - * - * @param x horizontal coordinate (increasing to the right) - * @param y vertical coordinate (increasing upwards) - * @param w width - * @param h height - * @return the matrix - */ - public static AffineMatrix reframe(double x, double y, double w, double h) { - if (w == 0 || h == 0) { - throw new IllegalArgumentException("Cannot reframe to an empty area: " + w + "x" + h); - } - return scale(1 / w, 1 / h).multiply(translate(-x, -y)); - } - - /** - * Return an orthogonal rotation matrix. - * - * @param ccwRotation the counter-clockwise rotation - * @return the matrix - */ - public static AffineMatrix rotateOrtho(int ccwRotation) { - switch (ccwRotation) { - case 0: - return IDENTITY; - case 1: - // 90° counter-clockwise - return new AffineMatrix(0, 1, -1, 0, 1, 0); - case 2: - // 180° - return new AffineMatrix(-1, 0, 0, -1, 1, 1); - case 3: - // 90° clockwise - return new AffineMatrix(0, -1, 1, 0, 0, 1); - default: - throw new IllegalArgumentException("Invalid rotation: " + ccwRotation); - } - } - - /** - * Return an horizontal flip matrix. - * - * @return the matrix - */ - public static AffineMatrix hflip() { - return new AffineMatrix(-1, 0, 0, 1, 1, 0); - } - - /** - * Return a vertical flip matrix. - * - * @return the matrix - */ - public static AffineMatrix vflip() { - return new AffineMatrix(1, 0, 0, -1, 0, 1); - } - - /** - * Return a rotation matrix. - * - * @param ccwDegrees the angle, in degrees (counter-clockwise) - * @return the matrix - */ - public static AffineMatrix rotate(double ccwDegrees) { - double radians = Math.toRadians(ccwDegrees); - double cos = Math.cos(radians); - double sin = Math.sin(radians); - return new AffineMatrix(cos, sin, -sin, cos, 0, 0); - } - - /** - * Export this affine transform to a 4x4 column-major order matrix. - * - * @param matrix output 4x4 matrix - */ - public void to4x4(float[] matrix) { - // matrix is a 4x4 matrix in column-major order - - // Column 0 - matrix[0] = (float) a; - matrix[1] = (float) b; - matrix[2] = 0; - matrix[3] = 0; - - // Column 1 - matrix[4] = (float) c; - matrix[5] = (float) d; - matrix[6] = 0; - matrix[7] = 0; - - // Column 2 - matrix[8] = 0; - matrix[9] = 0; - matrix[10] = 1; - matrix[11] = 0; - - // Column 3 - matrix[12] = (float) e; - matrix[13] = (float) f; - matrix[14] = 0; - matrix[15] = 1; - } - - /** - * Export this affine transform to a 4x4 column-major order matrix. - * - * @return 4x4 matrix - */ - public float[] to4x4() { - float[] matrix = new float[16]; - to4x4(matrix); - return matrix; - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/util/Codec.java b/server/src/main/java/com/genymobile/scrcpy/util/Codec.java index b350409b..a363bd8b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/Codec.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/Codec.java @@ -1,7 +1,5 @@ package com.genymobile.scrcpy.util; -import android.media.MediaCodec; - public interface Codec { enum Type { @@ -16,9 +14,4 @@ public interface Codec { String getName(); String getMimeType(); - - static String getMimeType(MediaCodec codec) { - String[] types = codec.getCodecInfo().getSupportedTypes(); - return types.length > 0 ? types[0] : null; - } } diff --git a/server/src/main/java/com/genymobile/scrcpy/util/CodecUtils.java b/server/src/main/java/com/genymobile/scrcpy/util/CodecUtils.java index 3a01256a..5b0c95e8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/CodecUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/CodecUtils.java @@ -1,5 +1,8 @@ package com.genymobile.scrcpy.util; +import com.genymobile.scrcpy.audio.AudioCodec; +import com.genymobile.scrcpy.video.VideoCodec; + import android.media.MediaCodecInfo; import android.media.MediaCodecList; import android.media.MediaFormat; @@ -10,6 +13,24 @@ import java.util.List; public final class CodecUtils { + public static final class DeviceEncoder { + private final Codec codec; + private final MediaCodecInfo info; + + DeviceEncoder(Codec codec, MediaCodecInfo info) { + this.codec = codec; + this.info = info; + } + + public Codec getCodec() { + return codec; + } + + public MediaCodecInfo getInfo() { + return info; + } + } + private CodecUtils() { // not instantiable } @@ -26,7 +47,7 @@ public final class CodecUtils { } } - public static MediaCodecInfo[] getEncoders(MediaCodecList codecs, String mimeType) { + private static MediaCodecInfo[] getEncoders(MediaCodecList codecs, String mimeType) { List result = new ArrayList<>(); for (MediaCodecInfo codecInfo : codecs.getCodecInfos()) { if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(mimeType)) { @@ -35,4 +56,26 @@ public final class CodecUtils { } return result.toArray(new MediaCodecInfo[result.size()]); } + + public static List listVideoEncoders() { + List encoders = new ArrayList<>(); + MediaCodecList codecs = new MediaCodecList(MediaCodecList.REGULAR_CODECS); + for (VideoCodec codec : VideoCodec.values()) { + for (MediaCodecInfo info : getEncoders(codecs, codec.getMimeType())) { + encoders.add(new DeviceEncoder(codec, info)); + } + } + return encoders; + } + + public static List listAudioEncoders() { + List encoders = new ArrayList<>(); + MediaCodecList codecs = new MediaCodecList(MediaCodecList.REGULAR_CODECS); + for (AudioCodec codec : AudioCodec.values()) { + for (MediaCodecInfo info : getEncoders(codecs, codec.getMimeType())) { + encoders.add(new DeviceEncoder(codec, info)); + } + } + return encoders; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/util/IO.java b/server/src/main/java/com/genymobile/scrcpy/util/IO.java index 16ddaedd..ab3fa59f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/IO.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/IO.java @@ -1,9 +1,7 @@ package com.genymobile.scrcpy.util; -import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.BuildConfig; -import android.os.Build; import android.system.ErrnoException; import android.system.Os; import android.system.OsConstants; @@ -19,38 +17,23 @@ public final class 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); + // ByteBuffer position is not updated as expected by Os.write() on old Android versions, so + // count the remaining bytes manually. + // See . + int remaining = from.remaining(); + while (remaining > 0) { + try { + int w = Os.write(fd, from); if (BuildConfig.DEBUG && w < 0) { // w should not be negative, since an exception is thrown on error throw new AssertionError("Os.write() returned a negative value (" + w + ")"); } remaining -= w; - position += w; - from.position(position); + } catch (ErrnoException e) { + if (e.errno != OsConstants.EINTR) { + throw new IOException(e); + } } } } @@ -72,8 +55,4 @@ public final class IO { Throwable cause = e.getCause(); return cause instanceof ErrnoException && ((ErrnoException) cause).errno == OsConstants.EPIPE; } - - public static boolean isBrokenPipe(Exception e) { - return e instanceof IOException && isBrokenPipe((IOException) e); - } } diff --git a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java index 4f8927ec..aee1594a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java @@ -1,31 +1,19 @@ package com.genymobile.scrcpy.util; -import com.genymobile.scrcpy.AndroidVersions; -import com.genymobile.scrcpy.audio.AudioCodec; -import com.genymobile.scrcpy.device.Device; -import com.genymobile.scrcpy.device.DeviceApp; import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.Size; -import com.genymobile.scrcpy.video.VideoCodec; import com.genymobile.scrcpy.wrappers.DisplayManager; import com.genymobile.scrcpy.wrappers.ServiceManager; -import android.annotation.SuppressLint; -import android.annotation.TargetApi; import android.graphics.Rect; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraManager; import android.hardware.camera2.params.StreamConfigurationMap; import android.media.MediaCodec; -import android.media.MediaCodecInfo; -import android.media.MediaCodecList; -import android.os.Build; import android.util.Range; -import java.util.Collections; import java.util.List; -import java.util.Objects; import java.util.SortedSet; import java.util.TreeSet; @@ -35,54 +23,32 @@ public final class 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(')'); - } - } - + public static String buildVideoEncoderListMessage() { + StringBuilder builder = new StringBuilder("List of video encoders:"); + List videoEncoders = CodecUtils.listVideoEncoders(); + if (videoEncoders.isEmpty()) { + builder.append("\n (none)"); + } else { + for (CodecUtils.DeviceEncoder encoder : videoEncoders) { + builder.append("\n --video-codec=").append(encoder.getCodec().getName()); + builder.append(" --video-encoder='").append(encoder.getInfo().getName()).append("'"); } } - return builder.toString(); } - public static String 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"; + StringBuilder builder = new StringBuilder("List of audio encoders:"); + List audioEncoders = CodecUtils.listAudioEncoders(); + if (audioEncoders.isEmpty()) { + builder.append("\n (none)"); + } else { + for (CodecUtils.DeviceEncoder encoder : audioEncoders) { + builder.append("\n --audio-codec=").append(encoder.getCodec().getName()); + builder.append(" --audio-encoder='").append(encoder.getInfo().getName()).append("'"); + } } - if (info.isHardwareAccelerated()) { - return "hw"; - } - return "hybrid"; + return builder.toString(); } public static String buildDisplayListMessage() { @@ -120,39 +86,17 @@ public final class LogUtils { } } - 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) { + if (cameraIds == null || 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); + CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id); int facing = characteristics.get(CameraCharacteristics.LENS_FACING); builder.append(" (").append(getCameraFacingName(facing)).append(", "); @@ -163,10 +107,8 @@ public final class LogUtils { 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); - } + 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); @@ -212,57 +154,4 @@ public final class LogUtils { } return set; } - - - public static String buildAppListMessage() { - List apps = Device.listApps(); - return buildAppListMessage("List of apps:", apps); - } - - @SuppressLint("QueryPermissionsNeeded") - public static String buildAppListMessage(String title, List apps) { - StringBuilder builder = new StringBuilder(title); - - // Sort by: - // 1. system flag (system apps are before non-system apps) - // 2. name - // 3. package name - // Comparator.comparing() was introduced in API 24, so it cannot be used here to simplify the code - Collections.sort(apps, (thisApp, otherApp) -> { - // System apps first - int cmp = -Boolean.compare(thisApp.isSystem(), otherApp.isSystem()); - if (cmp != 0) { - return cmp; - } - - cmp = Objects.compare(thisApp.getName(), otherApp.getName(), String::compareTo); - if (cmp != 0) { - return cmp; - } - - return Objects.compare(thisApp.getPackageName(), otherApp.getPackageName(), String::compareTo); - }); - - final int column = 30; - for (DeviceApp app : apps) { - String name = app.getName(); - int padding = column - name.length(); - builder.append("\n "); - if (app.isSystem()) { - builder.append("* "); - } else { - builder.append("- "); - - } - builder.append(name); - if (padding > 0) { - builder.append(String.format("%" + padding + "s", " ")); - } else { - builder.append("\n ").append(String.format("%" + column + "s", " ")); - } - builder.append(" ").append(app.getPackageName()); - } - - return builder.toString(); - } } diff --git a/server/src/main/java/com/genymobile/scrcpy/util/Settings.java b/server/src/main/java/com/genymobile/scrcpy/util/Settings.java index e6465525..d9e82d62 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/Settings.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/Settings.java @@ -1,6 +1,5 @@ package com.genymobile.scrcpy.util; -import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.wrappers.ContentProvider; import com.genymobile.scrcpy.wrappers.ServiceManager; @@ -35,7 +34,7 @@ public final class Settings { } public static String getValue(String table, String key) throws SettingsException { - if (Build.VERSION.SDK_INT <= AndroidVersions.API_30_ANDROID_11) { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { // on Android >= 12, it always fails: try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { return provider.getValue(table, key); @@ -48,7 +47,7 @@ public final class Settings { } public static void putValue(String table, String key, String value) throws SettingsException { - if (Build.VERSION.SDK_INT <= AndroidVersions.API_30_ANDROID_11) { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { // on Android >= 12, it always fails: try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { provider.putValue(table, key, value); @@ -61,7 +60,7 @@ public final class Settings { } public static String getAndPutValue(String table, String key, String value) throws SettingsException { - if (Build.VERSION.SDK_INT <= AndroidVersions.API_30_ANDROID_11) { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { // on Android >= 12, it always fails: try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { String oldValue = provider.getValue(table, key); diff --git a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java index 0e147cb7..7d2e2055 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java @@ -1,17 +1,8 @@ package com.genymobile.scrcpy.video; -import com.genymobile.scrcpy.AndroidVersions; -import com.genymobile.scrcpy.Options; -import com.genymobile.scrcpy.device.ConfigurationException; -import com.genymobile.scrcpy.device.Orientation; -import com.genymobile.scrcpy.device.Size; -import com.genymobile.scrcpy.opengl.AffineOpenGLFilter; -import com.genymobile.scrcpy.opengl.OpenGLFilter; -import com.genymobile.scrcpy.opengl.OpenGLRunner; -import com.genymobile.scrcpy.util.AffineMatrix; import com.genymobile.scrcpy.util.HandlerExecutor; import com.genymobile.scrcpy.util.Ln; -import com.genymobile.scrcpy.util.LogUtils; +import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.wrappers.ServiceManager; import android.annotation.SuppressLint; @@ -29,6 +20,7 @@ import android.hardware.camera2.params.OutputConfiguration; import android.hardware.camera2.params.SessionConfiguration; import android.hardware.camera2.params.StreamConfigurationMap; import android.media.MediaCodec; +import android.os.Build; import android.os.Handler; import android.os.HandlerThread; import android.util.Range; @@ -46,13 +38,6 @@ 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; @@ -60,16 +45,9 @@ public class CameraCapture extends SurfaceCapture { 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 Size size; private HandlerThread cameraThread; private Handler cameraHandler; @@ -78,22 +56,19 @@ public class CameraCapture extends SurfaceCapture { 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(); + public CameraCapture(String explicitCameraId, CameraFacing cameraFacing, Size explicitSize, int maxSize, CameraAspectRatio aspectRatio, int fps, + boolean highSpeed) { + this.explicitCameraId = explicitCameraId; + this.cameraFacing = cameraFacing; + this.explicitSize = explicitSize; + this.maxSize = maxSize; + this.aspectRatio = aspectRatio; + this.fps = fps; + this.highSpeed = highSpeed; } @Override - protected void init() throws ConfigurationException, IOException { + public void init() throws IOException { cameraThread = new HandlerThread("camera"); cameraThread.start(); cameraHandler = new Handler(cameraThread.getLooper()); @@ -102,7 +77,12 @@ public class CameraCapture extends SurfaceCapture { try { cameraId = selectCamera(explicitCameraId, cameraFacing); if (cameraId == null) { - throw new ConfigurationException("No matching camera found"); + throw new IOException("No matching camera found"); + } + + size = selectSize(cameraId, explicitSize, maxSize, aspectRatio, highSpeed); + if (size == null) { + throw new IOException("Could not select camera size"); } Ln.i("Using camera '" + cameraId + "'"); @@ -112,45 +92,14 @@ public class CameraCapture extends SurfaceCapture { } } - @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(); + private static String selectCamera(String explicitCameraId, CameraFacing cameraFacing) throws CameraAccessException { 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; } + CameraManager cameraManager = ServiceManager.getCameraManager(); + + String[] cameraIds = cameraManager.getCameraIdList(); if (cameraFacing == null) { // Use the first one return cameraIds.length > 0 ? cameraIds[0] : null; @@ -169,7 +118,7 @@ public class CameraCapture extends SurfaceCapture { return null; } - @TargetApi(AndroidVersions.API_24_ANDROID_7_0) + @TargetApi(Build.VERSION_CODES.N) private static Size selectSize(String cameraId, Size explicitSize, int maxSize, CameraAspectRatio aspectRatio, boolean highSpeed) throws CameraAccessException { if (explicitSize != null) { @@ -252,33 +201,15 @@ public class CameraCapture extends SurfaceCapture { @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) { @@ -291,7 +222,7 @@ public class CameraCapture extends SurfaceCapture { @Override public Size getSize() { - return videoSize; + return size; } @Override @@ -301,11 +232,17 @@ public class CameraCapture extends SurfaceCapture { } this.maxSize = maxSize; - return true; + try { + size = selectSize(cameraId, null, maxSize, aspectRatio, highSpeed); + return size != null; + } catch (CameraAccessException e) { + Ln.w("Could not select camera size", e); + return false; + } } @SuppressLint("MissingPermission") - @TargetApi(AndroidVersions.API_31_ANDROID_12) + @TargetApi(Build.VERSION_CODES.S) private CameraDevice openCamera(String id) throws CameraAccessException, InterruptedException { CompletableFuture future = new CompletableFuture<>(); ServiceManager.getCameraManager().openCamera(id, new CameraDevice.StateCallback() { @@ -319,7 +256,7 @@ public class CameraCapture extends SurfaceCapture { public void onDisconnected(CameraDevice camera) { Ln.w("Camera disconnected"); disconnected.set(true); - invalidate(); + requestReset(); } @Override @@ -352,7 +289,7 @@ public class CameraCapture extends SurfaceCapture { } } - @TargetApi(AndroidVersions.API_31_ANDROID_12) + @TargetApi(Build.VERSION_CODES.S) private CameraCaptureSession createCaptureSession(CameraDevice camera, Surface surface) throws CameraAccessException, InterruptedException { CompletableFuture future = new CompletableFuture<>(); OutputConfiguration outputConfig = new OutputConfiguration(surface); @@ -391,7 +328,7 @@ public class CameraCapture extends SurfaceCapture { return requestBuilder.build(); } - @TargetApi(AndroidVersions.API_31_ANDROID_12) + @TargetApi(Build.VERSION_CODES.S) private void setRepeatingRequest(CameraCaptureSession session, CaptureRequest request) throws CameraAccessException, InterruptedException { CameraCaptureSession.CaptureCallback callback = new CameraCaptureSession.CaptureCallback() { @Override @@ -418,9 +355,4 @@ public class CameraCapture extends SurfaceCapture { public boolean isClosed() { return disconnected.get(); } - - @Override - public void requestInvalidate() { - // do nothing (the user could not request a reset anyway for now, since there is no controller for camera mirroring) - } } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/CaptureReset.java b/server/src/main/java/com/genymobile/scrcpy/video/CaptureReset.java deleted file mode 100644 index 79d32d7c..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/video/CaptureReset.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.genymobile.scrcpy.video; - -import android.media.MediaCodec; - -import java.util.concurrent.atomic.AtomicBoolean; - -public class CaptureReset implements SurfaceCapture.CaptureListener { - - private final AtomicBoolean reset = new AtomicBoolean(); - - // Current instance of MediaCodec to "interrupt" on reset - private MediaCodec runningMediaCodec; - - public boolean consumeReset() { - return reset.getAndSet(false); - } - - public synchronized void reset() { - reset.set(true); - if (runningMediaCodec != null) { - try { - runningMediaCodec.signalEndOfInputStream(); - } catch (IllegalStateException e) { - // ignore - } - } - } - - public synchronized void setRunningMediaCodec(MediaCodec runningMediaCodec) { - this.runningMediaCodec = runningMediaCodec; - } - - @Override - public void onInvalidated() { - reset(); - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/video/DisplaySizeMonitor.java b/server/src/main/java/com/genymobile/scrcpy/video/DisplaySizeMonitor.java deleted file mode 100644 index 3d7cccfe..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/video/DisplaySizeMonitor.java +++ /dev/null @@ -1,141 +0,0 @@ -package com.genymobile.scrcpy.video; - -import com.genymobile.scrcpy.AndroidVersions; -import com.genymobile.scrcpy.device.Device; -import com.genymobile.scrcpy.device.DisplayInfo; -import com.genymobile.scrcpy.device.Size; -import com.genymobile.scrcpy.util.Ln; -import com.genymobile.scrcpy.wrappers.DisplayManager; -import com.genymobile.scrcpy.wrappers.DisplayWindowListener; -import com.genymobile.scrcpy.wrappers.ServiceManager; - -import android.content.res.Configuration; -import android.os.Build; -import android.os.Handler; -import android.os.HandlerThread; -import android.view.IDisplayWindowListener; - -public class DisplaySizeMonitor { - - public interface Listener { - void onDisplaySizeChanged(); - } - - // On Android 14, DisplayListener may be broken (it never sends events). This is fixed in recent Android 14 upgrades, but we can't really - // detect it directly, so register a DisplayWindowListener (introduced in Android 11) to listen to configuration changes instead. - // It has been broken again after an Android 15 upgrade: - // So use the default method only before Android 14. - private static final boolean USE_DEFAULT_METHOD = Build.VERSION.SDK_INT < AndroidVersions.API_34_ANDROID_14; - - private DisplayManager.DisplayListenerHandle displayListenerHandle; - private HandlerThread handlerThread; - - private IDisplayWindowListener displayWindowListener; - - private int displayId = Device.DISPLAY_ID_NONE; - - private Size sessionDisplaySize; - - private Listener listener; - - public void start(int displayId, Listener listener) { - // Once started, the listener and the displayId must never change - assert listener != null; - this.listener = listener; - - assert this.displayId == Device.DISPLAY_ID_NONE; - this.displayId = displayId; - - if (USE_DEFAULT_METHOD) { - handlerThread = new HandlerThread("DisplayListener"); - handlerThread.start(); - Handler handler = new Handler(handlerThread.getLooper()); - displayListenerHandle = ServiceManager.getDisplayManager().registerDisplayListener(eventDisplayId -> { - if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Ln.v("DisplaySizeMonitor: onDisplayChanged(" + eventDisplayId + ")"); - } - - if (eventDisplayId == displayId) { - checkDisplaySizeChanged(); - } - }, handler); - } else { - displayWindowListener = new DisplayWindowListener() { - @Override - public void onDisplayConfigurationChanged(int eventDisplayId, Configuration newConfig) { - if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Ln.v("DisplaySizeMonitor: onDisplayConfigurationChanged(" + eventDisplayId + ")"); - } - - if (eventDisplayId == displayId) { - checkDisplaySizeChanged(); - } - } - }; - ServiceManager.getWindowManager().registerDisplayWindowListener(displayWindowListener); - } - } - - /** - * Stop and release the monitor. - *

- * It must not be used anymore. - * It is ok to call this method even if {@link #start(int, Listener)} was not called. - */ - public void stopAndRelease() { - if (USE_DEFAULT_METHOD) { - // displayListenerHandle may be null if registration failed - if (displayListenerHandle != null) { - ServiceManager.getDisplayManager().unregisterDisplayListener(displayListenerHandle); - displayListenerHandle = null; - } - - if (handlerThread != null) { - handlerThread.quitSafely(); - } - } else if (displayWindowListener != null) { - ServiceManager.getWindowManager().unregisterDisplayWindowListener(displayWindowListener); - } - } - - private synchronized Size getSessionDisplaySize() { - return sessionDisplaySize; - } - - public synchronized void setSessionDisplaySize(Size sessionDisplaySize) { - this.sessionDisplaySize = sessionDisplaySize; - } - - private void checkDisplaySizeChanged() { - DisplayInfo di = ServiceManager.getDisplayManager().getDisplayInfo(displayId); - if (di == null) { - Ln.w("DisplayInfo for " + displayId + " cannot be retrieved"); - // We can't compare with the current size, so reset unconditionally - if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Ln.v("DisplaySizeMonitor: requestReset(): " + getSessionDisplaySize() + " -> (unknown)"); - } - setSessionDisplaySize(null); - listener.onDisplaySizeChanged(); - } else { - Size size = di.getSize(); - - // The field is hidden on purpose, to read it with synchronization - @SuppressWarnings("checkstyle:HiddenField") - Size sessionDisplaySize = getSessionDisplaySize(); // synchronized - - // .equals() also works if sessionDisplaySize == null - if (!size.equals(sessionDisplaySize)) { - // Reset only if the size is different - if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Ln.v("DisplaySizeMonitor: requestReset(): " + sessionDisplaySize + " -> " + size); - } - // Set the new size immediately, so that a future onDisplayChanged() event called before the asynchronous prepare() - // considers that the current size is the requested size (to avoid a duplicate requestReset()) - setSessionDisplaySize(size); - listener.onDisplaySizeChanged(); - } else if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Ln.v("DisplaySizeMonitor: Size not changed (" + size + "): do not requestReset()"); - } - } - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java deleted file mode 100644 index 792b3a8a..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ /dev/null @@ -1,267 +0,0 @@ -package com.genymobile.scrcpy.video; - -import com.genymobile.scrcpy.AndroidVersions; -import com.genymobile.scrcpy.Options; -import com.genymobile.scrcpy.control.PositionMapper; -import com.genymobile.scrcpy.device.DisplayInfo; -import com.genymobile.scrcpy.device.NewDisplay; -import com.genymobile.scrcpy.device.Orientation; -import com.genymobile.scrcpy.device.Size; -import com.genymobile.scrcpy.opengl.AffineOpenGLFilter; -import com.genymobile.scrcpy.opengl.OpenGLFilter; -import com.genymobile.scrcpy.opengl.OpenGLRunner; -import com.genymobile.scrcpy.util.AffineMatrix; -import com.genymobile.scrcpy.util.Ln; -import com.genymobile.scrcpy.wrappers.ServiceManager; - -import android.graphics.Rect; -import android.hardware.display.VirtualDisplay; -import android.os.Build; -import android.view.Surface; - -import java.io.IOException; - -public class NewDisplayCapture extends SurfaceCapture { - - // Internal fields copied from android.hardware.display.DisplayManager - private static final int VIRTUAL_DISPLAY_FLAG_PUBLIC = android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC; - private static final int VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY = android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY; - private static final int VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH = 1 << 6; - private static final int VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT = 1 << 7; - private static final int VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL = 1 << 8; - private static final int VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS = 1 << 9; - private static final int VIRTUAL_DISPLAY_FLAG_TRUSTED = 1 << 10; - private static final int VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP = 1 << 11; - private static final int VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED = 1 << 12; - private static final int VIRTUAL_DISPLAY_FLAG_TOUCH_FEEDBACK_DISABLED = 1 << 13; - private static final int VIRTUAL_DISPLAY_FLAG_OWN_FOCUS = 1 << 14; - private static final int VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP = 1 << 15; - - private final VirtualDisplayListener vdListener; - private final NewDisplay newDisplay; - - private final DisplaySizeMonitor displaySizeMonitor = new DisplaySizeMonitor(); - - private AffineMatrix displayTransform; - private AffineMatrix eventTransform; - private OpenGLRunner glRunner; - - private Size mainDisplaySize; - private int mainDisplayDpi; - private int maxSize; - private int displayImePolicy; - private final Rect crop; - private final boolean captureOrientationLocked; - private final Orientation captureOrientation; - private final float angle; - private final boolean vdDestroyContent; - private final boolean vdSystemDecorations; - - private VirtualDisplay virtualDisplay; - private Size videoSize; - private Size displaySize; // the logical size of the display (including rotation) - private Size physicalSize; // the physical size of the display (without rotation) - - private int dpi; - - public NewDisplayCapture(VirtualDisplayListener vdListener, Options options) { - this.vdListener = vdListener; - this.newDisplay = options.getNewDisplay(); - assert newDisplay != null; - this.maxSize = options.getMaxSize(); - this.displayImePolicy = options.getDisplayImePolicy(); - this.crop = options.getCrop(); - assert options.getCaptureOrientationLock() != null; - this.captureOrientationLocked = options.getCaptureOrientationLock() != Orientation.Lock.Unlocked; - this.captureOrientation = options.getCaptureOrientation(); - assert captureOrientation != null; - this.angle = options.getAngle(); - this.vdDestroyContent = options.getVDDestroyContent(); - this.vdSystemDecorations = options.getVDSystemDecorations(); - } - - @Override - protected void init() { - displaySize = newDisplay.getSize(); - dpi = newDisplay.getDpi(); - if (displaySize == null || dpi == 0) { - DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(0); - if (displayInfo != null) { - mainDisplaySize = displayInfo.getSize(); - if ((displayInfo.getRotation() % 2) != 0) { - mainDisplaySize = mainDisplaySize.rotate(); // Use the natural device orientation (at rotation 0), not the current one - } - mainDisplayDpi = displayInfo.getDpi(); - } else { - Ln.w("Main display not found, fallback to 1920x1080 240dpi"); - mainDisplaySize = new Size(1920, 1080); - mainDisplayDpi = 240; - } - } - } - - @Override - public void prepare() { - int displayRotation; - if (virtualDisplay == null) { - if (!newDisplay.hasExplicitSize()) { - displaySize = mainDisplaySize; - } - if (!newDisplay.hasExplicitDpi()) { - dpi = scaleDpi(mainDisplaySize, mainDisplayDpi, displaySize); - } - - videoSize = displaySize; - displayRotation = 0; - // Set the current display size to avoid an unnecessary call to invalidate() - displaySizeMonitor.setSessionDisplaySize(displaySize); - } else { - DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(virtualDisplay.getDisplay().getDisplayId()); - displaySize = displayInfo.getSize(); - dpi = displayInfo.getDpi(); - displayRotation = displayInfo.getRotation(); - } - - VideoFilter filter = new VideoFilter(displaySize); - - if (crop != null) { - boolean transposed = (displayRotation % 2) != 0; - filter.addCrop(crop, transposed); - } - - filter.addOrientation(displayRotation, captureOrientationLocked, captureOrientation); - filter.addAngle(angle); - - Size filteredSize = filter.getOutputSize(); - if (!filteredSize.isMultipleOf8() || (maxSize != 0 && filteredSize.getMax() > maxSize)) { - if (maxSize != 0) { - filteredSize = filteredSize.limit(maxSize); - } - filteredSize = filteredSize.round8(); - filter.addResize(filteredSize); - } - - eventTransform = filter.getInverseTransform(); - - // DisplayInfo gives the oriented size (so videoSize includes the display rotation) - videoSize = filter.getOutputSize(); - - // But the virtual display video always remains in the origin orientation (the video itself is not rotated, so it must rotated manually). - // This additional display rotation must not be included in the input events transform (the expected coordinates are already in the - // physical display size) - if ((displayRotation % 2) == 0) { - physicalSize = displaySize; - } else { - physicalSize = displaySize.rotate(); - } - VideoFilter displayFilter = new VideoFilter(physicalSize); - displayFilter.addRotation(displayRotation); - AffineMatrix displayRotationMatrix = displayFilter.getInverseTransform(); - - // Take care of multiplication order: - // displayTransform = (FILTER_MATRIX * DISPLAY_FILTER_MATRIX)⁻¹ - // = DISPLAY_FILTER_MATRIX⁻¹ * FILTER_MATRIX⁻¹ - // = displayRotationMatrix * eventTransform - displayTransform = AffineMatrix.multiplyAll(displayRotationMatrix, eventTransform); - } - - public void startNew(Surface surface) { - int virtualDisplayId; - try { - int flags = VIRTUAL_DISPLAY_FLAG_PUBLIC - | VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY - | VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH - | VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT; - if (vdDestroyContent) { - flags |= VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL; - } - if (vdSystemDecorations) { - flags |= VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS; - } - if (Build.VERSION.SDK_INT >= AndroidVersions.API_33_ANDROID_13) { - flags |= VIRTUAL_DISPLAY_FLAG_TRUSTED - | VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP - | VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED - | VIRTUAL_DISPLAY_FLAG_TOUCH_FEEDBACK_DISABLED; - if (Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14) { - flags |= VIRTUAL_DISPLAY_FLAG_OWN_FOCUS - | VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP; - } - } - virtualDisplay = ServiceManager.getDisplayManager() - .createNewVirtualDisplay("scrcpy", displaySize.getWidth(), displaySize.getHeight(), dpi, surface, flags); - virtualDisplayId = virtualDisplay.getDisplay().getDisplayId(); - Ln.i("New display: " + displaySize.getWidth() + "x" + displaySize.getHeight() + "/" + dpi + " (id=" + virtualDisplayId + ")"); - - if (displayImePolicy != -1) { - ServiceManager.getWindowManager().setDisplayImePolicy(virtualDisplayId, displayImePolicy); - } - - displaySizeMonitor.start(virtualDisplayId, this::invalidate); - } catch (Exception e) { - Ln.e("Could not create display", e); - throw new AssertionError("Could not create display"); - } - } - - @Override - public void start(Surface surface) throws IOException { - if (displayTransform != null) { - assert glRunner == null; - OpenGLFilter glFilter = new AffineOpenGLFilter(displayTransform); - glRunner = new OpenGLRunner(glFilter); - surface = glRunner.start(physicalSize, videoSize, surface); - } - - if (virtualDisplay == null) { - startNew(surface); - } else { - virtualDisplay.setSurface(surface); - } - - if (vdListener != null) { - PositionMapper positionMapper = PositionMapper.create(videoSize, eventTransform, displaySize); - vdListener.onNewVirtualDisplay(virtualDisplay.getDisplay().getDisplayId(), positionMapper); - } - } - - @Override - public void stop() { - if (glRunner != null) { - glRunner.stopAndRelease(); - glRunner = null; - } - } - - @Override - public void release() { - displaySizeMonitor.stopAndRelease(); - - if (virtualDisplay != null) { - virtualDisplay.release(); - virtualDisplay = null; - } - } - - @Override - public synchronized Size getSize() { - return videoSize; - } - - @Override - public synchronized boolean setMaxSize(int newMaxSize) { - maxSize = newMaxSize; - return true; - } - - private static int scaleDpi(Size initialSize, int initialDpi, Size size) { - int den = initialSize.getMax(); - int num = size.getMax(); - return initialDpi * num / den; - } - - @Override - public void requestInvalidate() { - invalidate(); - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index 5f4e1803..fbeca2af 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -1,19 +1,8 @@ package com.genymobile.scrcpy.video; -import com.genymobile.scrcpy.AndroidVersions; -import com.genymobile.scrcpy.Options; -import com.genymobile.scrcpy.control.PositionMapper; -import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.device.Device; -import com.genymobile.scrcpy.device.DisplayInfo; -import com.genymobile.scrcpy.device.Orientation; -import com.genymobile.scrcpy.device.Size; -import com.genymobile.scrcpy.opengl.AffineOpenGLFilter; -import com.genymobile.scrcpy.opengl.OpenGLFilter; -import com.genymobile.scrcpy.opengl.OpenGLRunner; -import com.genymobile.scrcpy.util.AffineMatrix; import com.genymobile.scrcpy.util.Ln; -import com.genymobile.scrcpy.util.LogUtils; +import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.SurfaceControl; @@ -23,85 +12,32 @@ 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(); +public class ScreenCapture extends SurfaceCapture implements Device.RotationListener, Device.FoldListener { + private final Device device; 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(); + public ScreenCapture(Device device) { + this.device = device; } @Override public void init() { - displaySizeMonitor.start(displayId, this::invalidate); + device.setRotationListener(this); + device.setFoldListener(this); } @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); - } + public void start(Surface surface) { + ScreenInfo screenInfo = device.getScreenInfo(); + Rect contentRect = screenInfo.getContentRect(); - if ((displayInfo.getFlags() & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) { - Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted"); - } + // does not include the locked video orientation + Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect(); + int videoRotation = screenInfo.getVideoRotation(); + int layerStack = device.getLayerStack(); - 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; @@ -111,30 +47,15 @@ public class ScreenCapture extends SurfaceCapture { 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 { + Rect videoRect = screenInfo.getVideoSize().toRect(); virtualDisplay = ServiceManager.getDisplayManager() - .createVirtualDisplay("scrcpy", inputSize.getWidth(), inputSize.getHeight(), displayId, surface); + .createVirtualDisplay("scrcpy", videoRect.width(), videoRect.height(), device.getDisplayId(), 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); + setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack); Ln.d("Display: using SurfaceControl API"); } catch (Exception surfaceControlException) { Ln.e("Could not create display using DisplayManager", displayManagerException); @@ -142,36 +63,12 @@ public class ScreenCapture extends SurfaceCapture { 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(); - + device.setRotationListener(null); + device.setFoldListener(null); if (display != null) { SurfaceControl.destroyDisplay(display); display = null; @@ -184,36 +81,41 @@ public class ScreenCapture extends SurfaceCapture { @Override public Size getSize() { - return videoSize; + return device.getScreenInfo().getVideoSize(); } @Override - public boolean setMaxSize(int newMaxSize) { - maxSize = newMaxSize; + public boolean setMaxSize(int maxSize) { + device.setMaxSize(maxSize); return true; } + @Override + public void onFoldChanged(int displayId, boolean folded) { + requestReset(); + } + + @Override + public void onRotationChanged(int rotation) { + requestReset(); + } + private static IBinder createDisplay() throws Exception { // Since Android 12 (preview), secure displays could not be created with shell permissions anymore. // On Android 12 preview, SDK_INT is still R (not S), but CODENAME is "S". - boolean secure = Build.VERSION.SDK_INT < AndroidVersions.API_30_ANDROID_11 || (Build.VERSION.SDK_INT == AndroidVersions.API_30_ANDROID_11 - && !"S".equals(Build.VERSION.CODENAME)); + boolean secure = Build.VERSION.SDK_INT < Build.VERSION_CODES.R || (Build.VERSION.SDK_INT == Build.VERSION_CODES.R && !"S".equals( + Build.VERSION.CODENAME)); return SurfaceControl.createDisplay("scrcpy", secure); } - private static void setDisplaySurface(IBinder display, Surface surface, Rect deviceRect, Rect displayRect, int layerStack) { + private static void setDisplaySurface(IBinder display, Surface surface, int orientation, Rect deviceRect, Rect displayRect, int layerStack) { SurfaceControl.openTransaction(); try { SurfaceControl.setDisplaySurface(display, surface); - SurfaceControl.setDisplayProjection(display, 0, deviceRect, displayRect); + SurfaceControl.setDisplayProjection(display, orientation, deviceRect, displayRect); SurfaceControl.setDisplayLayerStack(display, layerStack); } finally { SurfaceControl.closeTransaction(); } } - - @Override - public void requestInvalidate() { - invalidate(); - } } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java new file mode 100644 index 00000000..ba537b17 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java @@ -0,0 +1,171 @@ +package com.genymobile.scrcpy.video; + +import com.genymobile.scrcpy.BuildConfig; +import com.genymobile.scrcpy.device.Device; +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.device.Size; + +import android.graphics.Rect; + +public final class ScreenInfo { + /** + * Device (physical) size, possibly cropped + */ + private final Rect contentRect; // device size, possibly cropped + + /** + * Video size, possibly smaller than the device size, already taking the device rotation and crop into account. + *

+ * However, it does not include the locked video orientation. + */ + private final Size unlockedVideoSize; + + /** + * Device rotation, related to the natural device orientation (0, 1, 2 or 3) + */ + private final int deviceRotation; + + /** + * The locked video orientation (-1: disabled, 0: normal, 1: 90° CCW, 2: 180°, 3: 90° CW) + */ + private final int lockedVideoOrientation; + + public ScreenInfo(Rect contentRect, Size unlockedVideoSize, int deviceRotation, int lockedVideoOrientation) { + this.contentRect = contentRect; + this.unlockedVideoSize = unlockedVideoSize; + this.deviceRotation = deviceRotation; + this.lockedVideoOrientation = lockedVideoOrientation; + } + + public Rect getContentRect() { + return contentRect; + } + + /** + * Return the video size as if locked video orientation was not set. + * + * @return the unlocked video size + */ + public Size getUnlockedVideoSize() { + return unlockedVideoSize; + } + + /** + * Return the actual video size if locked video orientation is set. + * + * @return the actual video size + */ + public Size getVideoSize() { + if (getVideoRotation() % 2 == 0) { + return unlockedVideoSize; + } + + return unlockedVideoSize.rotate(); + } + + public int getDeviceRotation() { + return deviceRotation; + } + + public ScreenInfo withDeviceRotation(int newDeviceRotation) { + if (newDeviceRotation == deviceRotation) { + return this; + } + // true if changed between portrait and landscape + boolean orientationChanged = (deviceRotation + newDeviceRotation) % 2 != 0; + Rect newContentRect; + Size newUnlockedVideoSize; + if (orientationChanged) { + newContentRect = flipRect(contentRect); + newUnlockedVideoSize = unlockedVideoSize.rotate(); + } else { + newContentRect = contentRect; + newUnlockedVideoSize = unlockedVideoSize; + } + return new ScreenInfo(newContentRect, newUnlockedVideoSize, newDeviceRotation, lockedVideoOrientation); + } + + public static ScreenInfo computeScreenInfo(int rotation, Size deviceSize, Rect crop, int maxSize, int lockedVideoOrientation) { + if (lockedVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL) { + // The user requested to lock the video orientation to the current orientation + lockedVideoOrientation = rotation; + } + + Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight()); + if (crop != null) { + if (rotation % 2 != 0) { // 180s preserve dimensions + // the crop (provided by the user) is expressed in the natural orientation + crop = flipRect(crop); + } + if (!contentRect.intersect(crop)) { + // intersect() changes contentRect so that it is intersected with crop + Ln.w("Crop rectangle (" + formatCrop(crop) + ") does not intersect device screen (" + formatCrop(deviceSize.toRect()) + ")"); + contentRect = new Rect(); // empty + } + } + + Size videoSize = computeVideoSize(contentRect.width(), contentRect.height(), maxSize); + return new ScreenInfo(contentRect, videoSize, rotation, lockedVideoOrientation); + } + + private static String formatCrop(Rect rect) { + return rect.width() + ":" + rect.height() + ":" + rect.left + ":" + rect.top; + } + + private static Size computeVideoSize(int w, int h, int maxSize) { + // Compute the video size and the padding of the content inside this video. + // Principle: + // - scale down the great side of the screen to maxSize (if necessary); + // - scale down the other side so that the aspect ratio is preserved; + // - round this value to the nearest multiple of 8 (H.264 only accepts multiples of 8) + w &= ~7; // in case it's not a multiple of 8 + h &= ~7; + if (maxSize > 0) { + if (BuildConfig.DEBUG && maxSize % 8 != 0) { + throw new AssertionError("Max size must be a multiple of 8"); + } + boolean portrait = h > w; + int major = portrait ? h : w; + int minor = portrait ? w : h; + if (major > maxSize) { + int minorExact = minor * maxSize / major; + // +4 to round the value to the nearest multiple of 8 + minor = (minorExact + 4) & ~7; + major = maxSize; + } + w = portrait ? minor : major; + h = portrait ? major : minor; + } + return new Size(w, h); + } + + private static Rect flipRect(Rect crop) { + return new Rect(crop.top, crop.left, crop.bottom, crop.right); + } + + /** + * Return the rotation to apply to the device rotation to get the requested locked video orientation + * + * @return the rotation offset + */ + public int getVideoRotation() { + if (lockedVideoOrientation == -1) { + // no offset + return 0; + } + return (deviceRotation + 4 - lockedVideoOrientation) % 4; + } + + /** + * Return the rotation to apply to the requested locked video orientation to get the device rotation + * + * @return the (reverse) rotation offset + */ + public int getReverseVideoRotation() { + if (lockedVideoOrientation == -1) { + // no offset + return 0; + } + return (lockedVideoOrientation + 4 - deviceRotation) % 4; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java index 39d3bdb8..3118ddc8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java @@ -1,55 +1,46 @@ package com.genymobile.scrcpy.video; -import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.device.Size; import android.view.Surface; import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; /** * A video source which can be rendered on a Surface for encoding. */ public abstract class SurfaceCapture { - public interface CaptureListener { - void onInvalidated(); - } - - private CaptureListener listener; + private final AtomicBoolean resetCapture = new AtomicBoolean(); /** - * Notify the listener that the capture has been invalidated (for example, because its size changed). + * Request the encoding session to be restarted, for example if the capture implementation detects that the video source size has changed (on + * device rotation for example). */ - protected void invalidate() { - listener.onInvalidated(); + protected void requestReset() { + resetCapture.set(true); } /** - * Called once before the first capture starts. + * Consume the reset request (intended to be called by the encoder). + * + * @return {@code true} if a reset request was pending, {@code false} otherwise. */ - public final void init(CaptureListener listener) throws ConfigurationException, IOException { - this.listener = listener; - init(); + public boolean consumeReset() { + return resetCapture.getAndSet(false); } /** - * Called once before the first capture starts. + * Called once before the capture starts. */ - protected abstract void init() throws ConfigurationException, IOException; + public abstract void init() throws IOException; /** - * Called after the last capture ends (if and only if {@link #init()} has been called). + * Called after the 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. * @@ -57,13 +48,6 @@ public abstract class SurfaceCapture { */ public abstract void start(Surface surface) throws IOException; - /** - * Stop the capture. - */ - public void stop() { - // Do nothing by default - } - /** * Return the video size * @@ -86,11 +70,4 @@ public abstract class SurfaceCapture { public boolean isClosed() { return false; } - - /** - * Manually request to invalidate (typically a user request). - *

- * The capture implementation is free to ignore the request and do nothing. - */ - public abstract void requestInvalidate(); } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java index 236a5f48..a5f2d1e9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java @@ -1,17 +1,15 @@ package com.genymobile.scrcpy.video; -import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.AsyncProcessor; -import com.genymobile.scrcpy.Options; -import com.genymobile.scrcpy.device.ConfigurationException; -import com.genymobile.scrcpy.device.Size; -import com.genymobile.scrcpy.device.Streamer; import com.genymobile.scrcpy.util.Codec; import com.genymobile.scrcpy.util.CodecOption; import com.genymobile.scrcpy.util.CodecUtils; +import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.util.IO; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.LogUtils; +import com.genymobile.scrcpy.device.Size; +import com.genymobile.scrcpy.device.Streamer; import android.media.MediaCodec; import android.media.MediaCodecInfo; @@ -50,16 +48,15 @@ public class SurfaceEncoder implements AsyncProcessor { private Thread thread; private final AtomicBoolean stopped = new AtomicBoolean(); - private final CaptureReset reset = new CaptureReset(); - - public SurfaceEncoder(SurfaceCapture capture, Streamer streamer, Options options) { + public SurfaceEncoder(SurfaceCapture capture, Streamer streamer, int videoBitRate, float maxFps, List codecOptions, + String encoderName, boolean downsizeOnError) { 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(); + this.videoBitRate = videoBitRate; + this.maxFps = maxFps; + this.codecOptions = codecOptions; + this.encoderName = encoderName; + this.downsizeOnError = downsizeOnError; } private void streamCapture() throws IOException, ConfigurationException { @@ -67,73 +64,38 @@ public class SurfaceEncoder implements AsyncProcessor { MediaCodec mediaCodec = createMediaCodec(codec, encoderName); MediaFormat format = createFormat(codec.getMimeType(), videoBitRate, maxFps, codecOptions); - capture.init(reset); + capture.init(); try { + streamer.writeVideoHeader(capture.getSize()); + 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()); + alive = encode(mediaCodec, streamer); + // do not call stop() on exception, it would trigger an IllegalStateException + mediaCodec.stop(); + } catch (IllegalStateException | IllegalArgumentException e) { + Ln.e("Encoding error: " + e.getClass().getName() + ": " + e.getMessage()); if (!prepareRetry(size)) { throw e; } + Ln.i("Retrying..."); alive = true; } finally { - 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(); @@ -194,16 +156,25 @@ public class SurfaceEncoder implements AsyncProcessor { return 0; } - private void encode(MediaCodec codec, Streamer streamer) throws IOException { + private boolean encode(MediaCodec codec, Streamer streamer) throws IOException { + boolean eof = false; + boolean alive = true; MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); - boolean eos; - do { + while (!capture.consumeReset() && !eof) { + if (stopped.get()) { + alive = false; + break; + } 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) { + if (capture.consumeReset()) { + // must restart encoding with new size + break; + } + + eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0; + if (outputBufferId >= 0) { ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId); boolean isConfig = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0; @@ -220,20 +191,21 @@ public class SurfaceEncoder implements AsyncProcessor { codec.releaseOutputBuffer(outputBufferId, false); } } - } while (!eos); + } + + if (capture.isClosed()) { + // The capture might have been closed internally (for example if the camera is disconnected) + alive = false; + } + + return !eof && alive; } private static MediaCodec createMediaCodec(Codec codec, String encoderName) throws IOException, ConfigurationException { if (encoderName != null) { Ln.d("Creating encoder by name: '" + encoderName + "'"); try { - 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; + return MediaCodec.createByCodecName(encoderName); } catch (IllegalArgumentException e) { Ln.e("Video encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildVideoEncoderListMessage()); throw new ConfigurationException("Unknown encoder: " + encoderName); @@ -260,7 +232,7 @@ public class SurfaceEncoder implements AsyncProcessor { // 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) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { format.setInteger(MediaFormat.KEY_COLOR_RANGE, MediaFormat.COLOR_RANGE_LIMITED); } format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, DEFAULT_I_FRAME_INTERVAL); @@ -313,7 +285,6 @@ public class SurfaceEncoder implements AsyncProcessor { public void stop() { if (thread != null) { stopped.set(true); - reset.reset(); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java b/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java deleted file mode 100644 index a27915ee..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java +++ /dev/null @@ -1,119 +0,0 @@ -package com.genymobile.scrcpy.video; - -import com.genymobile.scrcpy.device.Orientation; -import com.genymobile.scrcpy.device.Size; -import com.genymobile.scrcpy.util.AffineMatrix; - -import android.graphics.Rect; - -public class VideoFilter { - - private Size size; - private AffineMatrix transform; - - public VideoFilter(Size inputSize) { - this.size = inputSize; - } - - public Size getOutputSize() { - return size; - } - - public AffineMatrix getTransform() { - return transform; - } - - /** - * Return the inverse transform. - *

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

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

    - *
  • The OpenGL filter expects the matrix to transform the image coordinates, which is the inverse transform;
  • - *
  • The click positions must be transformed back to the device positions, using the inverse transform too.
  • - *
- * - * @return the inverse transform - */ - public AffineMatrix getInverseTransform() { - if (transform == null) { - return null; - } - return transform.invert(); - } - - private static Rect transposeRect(Rect rect) { - return new Rect(rect.top, rect.left, rect.bottom, rect.right); - } - - public void addCrop(Rect crop, boolean transposed) { - if (transposed) { - crop = transposeRect(crop); - } - - double inputWidth = size.getWidth(); - double inputHeight = size.getHeight(); - - if (crop.left < 0 || crop.top < 0 || crop.right > inputWidth || crop.bottom > inputHeight) { - throw new IllegalArgumentException("Crop " + crop + " exceeds the input area (" + size + ")"); - } - - double x = crop.left / inputWidth; - double y = 1 - (crop.bottom / inputHeight); // OpenGL origin is bottom-left - double w = crop.width() / inputWidth; - double h = crop.height() / inputHeight; - - transform = AffineMatrix.reframe(x, y, w, h).multiply(transform); - size = new Size(crop.width(), crop.height()); - } - - public void addRotation(int ccwRotation) { - if (ccwRotation == 0) { - return; - } - - transform = AffineMatrix.rotateOrtho(ccwRotation).multiply(transform); - if (ccwRotation % 2 != 0) { - size = size.rotate(); - } - } - - public void addOrientation(Orientation captureOrientation) { - if (captureOrientation.isFlipped()) { - transform = AffineMatrix.hflip().multiply(transform); - } - int ccwRotation = (4 - captureOrientation.getRotation()) % 4; - addRotation(ccwRotation); - } - - public void addOrientation(int displayRotation, boolean locked, Orientation captureOrientation) { - if (locked) { - // flip/rotate the current display from the natural device orientation (i.e. where display rotation is 0) - int reverseDisplayRotation = (4 - displayRotation) % 4; - addRotation(reverseDisplayRotation); - } - addOrientation(captureOrientation); - } - - public void addAngle(double cwAngle) { - if (cwAngle == 0) { - return; - } - double ccwAngle = -cwAngle; - transform = AffineMatrix.rotate(ccwAngle).withAspectRatio(size).fromCenter().multiply(transform); - } - - public void addResize(Size targetSize) { - if (size.equals(targetSize)) { - return; - } - - if (transform == null) { - // The requested scaling is performed by the viewport (by changing the output size), but the OpenGL filter must still run, even if - // resizing is not performed by the shader. So transform MUST NOT be null. - transform = AffineMatrix.IDENTITY; - } - size = targetSize; - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/video/VirtualDisplayListener.java b/server/src/main/java/com/genymobile/scrcpy/video/VirtualDisplayListener.java deleted file mode 100644 index c079265e..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/video/VirtualDisplayListener.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.genymobile.scrcpy.video; - -import com.genymobile.scrcpy.control.PositionMapper; - -public interface VirtualDisplayListener { - void onNewVirtualDisplay(int displayId, PositionMapper positionMapper); -} diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java index 255483c6..bb1ca0d4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java @@ -1,14 +1,13 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; import android.annotation.TargetApi; -import android.content.IContentProvider; import android.content.Intent; import android.os.Binder; +import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.os.IInterface; @@ -64,8 +63,8 @@ public final class ActivityManager { return removeContentProviderExternalMethod; } - @TargetApi(AndroidVersions.API_29_ANDROID_10) - public IContentProvider getContentProviderExternal(String name, IBinder token) { + @TargetApi(Build.VERSION_CODES.Q) + private ContentProvider getContentProviderExternal(String name, IBinder token) { try { Method method = getGetContentProviderExternalMethod(); Object[] args; @@ -84,7 +83,11 @@ public final class ActivityManager { // IContentProvider provider = providerHolder.provider; Field providerField = providerHolder.getClass().getDeclaredField("provider"); providerField.setAccessible(true); - return (IContentProvider) providerField.get(providerHolder); + Object provider = providerField.get(providerHolder); + if (provider == null) { + return null; + } + return new ContentProvider(this, provider, name, token); } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); return null; @@ -101,12 +104,7 @@ public final class ActivityManager { } public ContentProvider createSettingsProvider() { - IBinder token = new Binder(); - IContentProvider provider = getContentProviderExternal("settings", token); - if (provider == null) { - return null; - } - return new ContentProvider(this, provider, "settings", token); + return getContentProviderExternal("settings", new Binder()); } private Method getStartActivityAsUserMethod() throws NoSuchMethodException, ClassNotFoundException { @@ -120,12 +118,8 @@ public final class ActivityManager { return startActivityAsUserMethod; } - public int startActivity(Intent intent) { - return startActivity(intent, null); - } - @SuppressWarnings("ConstantConditions") - public int startActivity(Intent intent, Bundle options) { + public int startActivity(Intent intent) { try { Method method = getStartActivityAsUserMethod(); return (int) method.invoke( @@ -139,7 +133,7 @@ public final class ActivityManager { /* requestCode */ 0, /* startFlags */ 0, /* profilerInfo */ null, - /* bOptions */ options, + /* bOptions */ null, /* userId */ /* UserHandle.USER_CURRENT */ -2); } catch (Throwable e) { Ln.e("Could not invoke method", e); diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java index 54936122..c5f007fe 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -1,43 +1,269 @@ package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.FakeContext; +import com.genymobile.scrcpy.util.Ln; import android.content.ClipData; -import android.content.Context; +import android.content.IOnPrimaryClipChangedListener; +import android.os.Build; +import android.os.IInterface; + +import java.lang.reflect.Method; public final class ClipboardManager { - private final android.content.ClipboardManager manager; + private final IInterface manager; + private Method getPrimaryClipMethod; + private Method setPrimaryClipMethod; + private Method addPrimaryClipChangedListener; + private int getMethodVersion; + private int setMethodVersion; + private int addListenerMethodVersion; static ClipboardManager create() { - android.content.ClipboardManager manager = (android.content.ClipboardManager) FakeContext.get().getSystemService(Context.CLIPBOARD_SERVICE); - if (manager == null) { + IInterface clipboard = ServiceManager.getService("clipboard", "android.content.IClipboard"); + if (clipboard == null) { // Some devices have no clipboard manager // // return null; } - return new ClipboardManager(manager); + return new ClipboardManager(clipboard); } - private ClipboardManager(android.content.ClipboardManager manager) { + private ClipboardManager(IInterface manager) { this.manager = manager; } + private Method getGetPrimaryClipMethod() throws NoSuchMethodException { + if (getPrimaryClipMethod == null) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class); + return getPrimaryClipMethod; + } + + try { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class); + getMethodVersion = 0; + return getPrimaryClipMethod; + } catch (NoSuchMethodException e) { + // fall-through + } + + try { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class); + getMethodVersion = 1; + return getPrimaryClipMethod; + } catch (NoSuchMethodException e) { + // fall-through + } + + try { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class); + getMethodVersion = 2; + return getPrimaryClipMethod; + } catch (NoSuchMethodException e) { + // fall-through + } + + try { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class, String.class); + getMethodVersion = 3; + return getPrimaryClipMethod; + } catch (NoSuchMethodException e) { + // fall-through + } + + try { + getPrimaryClipMethod = manager.getClass() + .getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, boolean.class); + getMethodVersion = 4; + return getPrimaryClipMethod; + } catch (NoSuchMethodException e) { + // fall-through + } + + try { + getPrimaryClipMethod = manager.getClass() + .getMethod("getPrimaryClip", String.class, String.class, String.class, String.class, int.class, int.class, boolean.class); + getMethodVersion = 5; + return getPrimaryClipMethod; + } catch (NoSuchMethodException e) { + // fall-through + } + + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, String.class); + getMethodVersion = 6; + } + return getPrimaryClipMethod; + } + + private Method getSetPrimaryClipMethod() throws NoSuchMethodException { + if (setPrimaryClipMethod == null) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class); + return setPrimaryClipMethod; + } + + try { + setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class); + setMethodVersion = 0; + return setPrimaryClipMethod; + } catch (NoSuchMethodException e1) { + // fall-through + } + + try { + setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class); + setMethodVersion = 1; + return setPrimaryClipMethod; + } catch (NoSuchMethodException e2) { + // fall-through + } + + try { + setPrimaryClipMethod = manager.getClass() + .getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class); + setMethodVersion = 2; + return setPrimaryClipMethod; + } catch (NoSuchMethodException e3) { + // fall-through + } + + setPrimaryClipMethod = manager.getClass() + .getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class, boolean.class); + setMethodVersion = 3; + } + return setPrimaryClipMethod; + } + + private static ClipData getPrimaryClip(Method method, int methodVersion, IInterface manager) throws ReflectiveOperationException { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME); + } + + switch (methodVersion) { + case 0: + return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID); + case 1: + return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID); + case 2: + return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0); + case 3: + return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID, null); + case 4: + // The last boolean parameter is "userOperate" + return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, true); + case 5: + return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, null, null, FakeContext.ROOT_UID, 0, true); + default: + return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, null); + } + } + + private static void setPrimaryClip(Method method, int methodVersion, IInterface manager, ClipData clipData) throws ReflectiveOperationException { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + method.invoke(manager, clipData, FakeContext.PACKAGE_NAME); + return; + } + + switch (methodVersion) { + case 0: + method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID); + break; + case 1: + method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID); + break; + case 2: + method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0); + break; + default: + // The last boolean parameter is "userOperate" + method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, true); + } + } + public CharSequence getText() { - ClipData clipData = manager.getPrimaryClip(); - if (clipData == null || clipData.getItemCount() == 0) { + try { + Method method = getGetPrimaryClipMethod(); + ClipData clipData = getPrimaryClip(method, getMethodVersion, manager); + if (clipData == null || clipData.getItemCount() == 0) { + return null; + } + return clipData.getItemAt(0).getText(); + } catch (ReflectiveOperationException e) { + Ln.e("Could not invoke method", e); return null; } - return clipData.getItemAt(0).getText(); } public boolean setText(CharSequence text) { - ClipData clipData = ClipData.newPlainText(null, text); - manager.setPrimaryClip(clipData); - return true; + try { + Method method = getSetPrimaryClipMethod(); + ClipData clipData = ClipData.newPlainText(null, text); + setPrimaryClip(method, setMethodVersion, manager, clipData); + return true; + } catch (ReflectiveOperationException e) { + Ln.e("Could not invoke method", e); + return false; + } } - public void addPrimaryClipChangedListener(android.content.ClipboardManager.OnPrimaryClipChangedListener listener) { - manager.addPrimaryClipChangedListener(listener); + private static void addPrimaryClipChangedListener(Method method, int methodVersion, IInterface manager, IOnPrimaryClipChangedListener listener) + throws ReflectiveOperationException { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + method.invoke(manager, listener, FakeContext.PACKAGE_NAME); + return; + } + + switch (methodVersion) { + case 0: + method.invoke(manager, listener, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID); + break; + case 1: + method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID); + break; + default: + method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0); + break; + } + } + + private Method getAddPrimaryClipChangedListener() throws NoSuchMethodException { + if (addPrimaryClipChangedListener == null) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + addPrimaryClipChangedListener = manager.getClass() + .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class); + } else { + try { + addPrimaryClipChangedListener = manager.getClass() + .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class); + addListenerMethodVersion = 0; + } catch (NoSuchMethodException e1) { + try { + addPrimaryClipChangedListener = manager.getClass() + .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class, + int.class); + addListenerMethodVersion = 1; + } catch (NoSuchMethodException e2) { + addPrimaryClipChangedListener = manager.getClass() + .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class, + int.class, int.class); + addListenerMethodVersion = 2; + } + } + } + } + return addPrimaryClipChangedListener; + } + + public boolean addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) { + try { + Method method = getAddPrimaryClipChangedListener(); + addPrimaryClipChangedListener(method, addListenerMethodVersion, manager, listener); + return true; + } catch (ReflectiveOperationException e) { + Ln.e("Could not invoke method", e); + return false; + } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java index f625b398..7e92ac50 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java @@ -1,6 +1,5 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.SettingsException; @@ -52,7 +51,7 @@ public final class ContentProvider implements Closeable { @SuppressLint("PrivateApi") private Method getCallMethod() throws NoSuchMethodException { if (callMethod == null) { - if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { callMethod = provider.getClass().getMethod("call", AttributionSource.class, String.class, String.class, String.class, Bundle.class); callMethodVersion = 0; } else { @@ -80,7 +79,7 @@ public final class ContentProvider implements Closeable { Method method = getCallMethod(); Object[] args; - if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12 && callMethodVersion == 0) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && callMethodVersion == 0) { args = new Object[]{FakeContext.get().getAttributionSource(), "settings", callMethod, arg, extras}; } else { switch (callMethodVersion) { diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java index 88ca3d3d..cc9d5526 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java @@ -1,17 +1,16 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; import android.annotation.TargetApi; +import android.os.Build; import android.os.IBinder; -import android.system.Os; import java.lang.reflect.Method; @SuppressLint({"PrivateApi", "SoonBlockedPrivateApi", "BlockedPrivateApi"}) -@TargetApi(AndroidVersions.API_34_ANDROID_14) +@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public final class DisplayControl { private static final Class CLASS; @@ -22,9 +21,7 @@ public final class DisplayControl { 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 classLoader = (ClassLoader) createClassLoaderMethod.invoke(null, "/system/framework/services.jar", null, null, ClassLoader.getSystemClassLoader(), 0, true, null); displayControlClass = classLoader.loadClass("com.android.server.display.DisplayControl"); diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java index a12470a4..dd92330c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -1,54 +1,24 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.AndroidVersions; -import com.genymobile.scrcpy.FakeContext; -import com.genymobile.scrcpy.device.DisplayInfo; -import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.Command; +import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.device.Size; import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.content.Context; import android.hardware.display.VirtualDisplay; -import android.os.Handler; import android.view.Display; import android.view.Surface; -import 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 { @@ -69,7 +39,7 @@ public final class DisplayManager { 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]+)", + + "rotation ([0-9]+).*?, layerStack ([0-9]+)", Pattern.MULTILINE); Matcher m = regex.matcher(dumpsysDisplayOutput); if (!m.find()) { @@ -79,10 +49,9 @@ public final class DisplayManager { 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)); + int layerStack = Integer.parseInt(m.group(5)); - return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, density, null); + return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags); } private static DisplayInfo getDisplayInfoFromDumpsysDisplay(int displayId) { @@ -96,12 +65,12 @@ public final class DisplayManager { } private static int parseDisplayFlags(String text) { + Pattern regex = Pattern.compile("FLAG_[A-Z_]+"); 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(); @@ -115,18 +84,9 @@ public final class DisplayManager { 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); + Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, displayId); if (displayInfo == null) { // fallback when displayInfo is null return getDisplayInfoFromDumpsysDisplay(displayId); @@ -138,9 +98,7 @@ public final class DisplayManager { 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); + return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags); } catch (ReflectiveOperationException e) { throw new AssertionError(e); } @@ -166,79 +124,4 @@ public final class DisplayManager { 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); - } - } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayWindowListener.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayWindowListener.java deleted file mode 100644 index f2ecb158..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayWindowListener.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.genymobile.scrcpy.wrappers; - -import android.content.res.Configuration; -import android.graphics.Rect; -import android.view.IDisplayWindowListener; - -import java.util.List; - -public class DisplayWindowListener extends IDisplayWindowListener.Stub { - @Override - public void onDisplayAdded(int displayId) { - // empty default implementation - } - - @Override - public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { - // empty default implementation - } - - @Override - public void onDisplayRemoved(int displayId) { - // empty default implementation - } - - @Override - public void onFixedRotationStarted(int displayId, int newRotation) { - // empty default implementation - } - - @Override - public void onFixedRotationFinished(int displayId) { - // empty default implementation - } - - @Override - public void onKeepClearAreasChanged(int displayId, List restricted, List unrestricted) { - // empty default implementation - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java index f55648d5..5c5ba56c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java @@ -1,15 +1,11 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.AndroidVersions; -import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; -import android.annotation.TargetApi; import android.view.InputEvent; import android.view.MotionEvent; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @SuppressLint("PrivateApi,DiscouragedPrivateApi") @@ -19,28 +15,39 @@ public final class InputManager { public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT = 1; public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2; - private final android.hardware.input.InputManager manager; - private long lastPermissionLogDate; + private final Object manager; + private Method injectInputEventMethod; - private static Method injectInputEventMethod; private static Method setDisplayIdMethod; private static Method setActionButtonMethod; - private static Method addUniqueIdAssociationByPortMethod; - private static Method removeUniqueIdAssociationByPortMethod; static InputManager create() { - android.hardware.input.InputManager manager = (android.hardware.input.InputManager) FakeContext.get() - .getSystemService(FakeContext.INPUT_SERVICE); - return new InputManager(manager); + try { + Class inputManagerClass = getInputManagerClass(); + Method getInstanceMethod = inputManagerClass.getDeclaredMethod("getInstance"); + Object im = getInstanceMethod.invoke(null); + return new InputManager(im); + } catch (ReflectiveOperationException e) { + throw new AssertionError(e); + } } - private InputManager(android.hardware.input.InputManager manager) { + private static Class getInputManagerClass() { + try { + // Parts of the InputManager class have been moved to a new InputManagerGlobal class in Android 14 preview + return Class.forName("android.hardware.input.InputManagerGlobal"); + } catch (ClassNotFoundException e) { + return android.hardware.input.InputManager.class; + } + } + + private InputManager(Object manager) { this.manager = manager; } - private static Method getInjectInputEventMethod() throws NoSuchMethodException { + private Method getInjectInputEventMethod() throws NoSuchMethodException { if (injectInputEventMethod == null) { - injectInputEventMethod = android.hardware.input.InputManager.class.getMethod("injectInputEvent", InputEvent.class, int.class); + injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class); } return injectInputEventMethod; } @@ -50,23 +57,6 @@ public final class InputManager { 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; } @@ -107,40 +97,4 @@ public final class InputManager { 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); - } - } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java index b5fefdd8..0a56f347 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java @@ -1,8 +1,8 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.util.Ln; +import android.annotation.SuppressLint; import android.os.Build; import android.os.IInterface; @@ -23,22 +23,16 @@ public final class PowerManager { 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"); - } + @SuppressLint("ObsoleteSdkInt") // we may lower minSdkVersion in the future + String methodName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH ? "isInteractive" : "isScreenOn"; + isScreenOnMethod = manager.getClass().getMethod(methodName); } return isScreenOnMethod; } - public boolean isScreenOn(int displayId) { - + public boolean isScreenOn() { 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); diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java index b1123b55..a8a56dab 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java @@ -54,8 +54,7 @@ public final class ServiceManager { return windowManager; } - // The DisplayManager may be used from both the Controller thread and the video (main) thread - public static synchronized DisplayManager getDisplayManager() { + public static DisplayManager getDisplayManager() { if (displayManager == null) { displayManager = DisplayManager.create(); } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java index 3bae4a37..038e7ca0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -1,6 +1,5 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; @@ -84,9 +83,9 @@ public final class SurfaceControl { private static Method getGetBuiltInDisplayMethod() throws NoSuchMethodException { if (getBuiltInDisplayMethod == null) { - // the method signature has changed in Android 10 + // the method signature has changed in Android Q // - if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { getBuiltInDisplayMethod = CLASS.getMethod("getBuiltInDisplay", int.class); } else { getBuiltInDisplayMethod = CLASS.getMethod("getInternalDisplayToken"); @@ -107,7 +106,7 @@ public final class SurfaceControl { public static IBinder getBuiltInDisplay() { try { Method method = getGetBuiltInDisplayMethod(); - if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { // call getBuiltInDisplay(0) return (IBinder) method.invoke(null, 0); } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java index 7ba5cc06..4c769e85 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -1,23 +1,15 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.util.Ln; import android.annotation.TargetApi; -import android.os.Build; import android.os.IInterface; -import android.view.IDisplayWindowListener; +import android.view.IDisplayFoldListener; +import android.view.IRotationWatcher; 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; @@ -30,9 +22,6 @@ public final class WindowManager { 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); @@ -191,77 +180,33 @@ public final class WindowManager { } } - @TargetApi(AndroidVersions.API_30_ANDROID_11) - public int[] registerDisplayWindowListener(IDisplayWindowListener listener) { + public void registerRotationWatcher(IRotationWatcher rotationWatcher, int displayId) { try { - return (int[]) manager.getClass().getMethod("registerDisplayWindowListener", IDisplayWindowListener.class).invoke(manager, listener); + Class cls = manager.getClass(); + try { + // display parameter added since this commit: + // https://android.googlesource.com/platform/frameworks/base/+/35fa3c26adcb5f6577849fd0df5228b1f67cf2c6%5E%21/#F1 + cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, displayId); + } catch (NoSuchMethodException e) { + // old version + if (displayId != 0) { + Ln.e("Secondary display rotation not supported on this device"); + return; + } + cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher); + } } catch (Exception e) { - Ln.e("Could not register display window listener", e); + Ln.e("Could not register rotation watcher", e); } - return null; } - @TargetApi(AndroidVersions.API_30_ANDROID_11) - public void unregisterDisplayWindowListener(IDisplayWindowListener listener) { + @TargetApi(29) + public void registerDisplayFoldListener(IDisplayFoldListener foldListener) { try { - manager.getClass().getMethod("unregisterDisplayWindowListener", IDisplayWindowListener.class).invoke(manager, listener); + Class cls = manager.getClass(); + cls.getMethod("registerDisplayFoldListener", IDisplayFoldListener.class).invoke(manager, foldListener); } 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); + Ln.e("Could not register display fold listener", e); } } } diff --git a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java index 0cc0a6b5..f29be2f4 100644 --- a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java @@ -1,5 +1,7 @@ package com.genymobile.scrcpy.control; +import com.genymobile.scrcpy.device.Device; + import android.view.KeyEvent; import android.view.MotionEvent; import org.junit.Assert; @@ -125,7 +127,7 @@ public class ControlMessageReaderTest { 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.writeShort(0x8000); // -1.0f encoded as i16 dos.writeInt(1); byte[] packet = bos.toByteArray(); @@ -139,7 +141,7 @@ public class ControlMessageReaderTest { 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(-1f, event.getVScroll(), 0f); Assert.assertEquals(1, event.getButtons()); Assert.assertEquals(-1, bis.read()); // EOS @@ -283,19 +285,19 @@ public class ControlMessageReaderTest { } @Test - public void testParseSetDisplayPower() throws IOException { + public void testParseSetScreenPowerMode() throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(ControlMessage.TYPE_SET_DISPLAY_POWER); - dos.writeBoolean(true); + dos.writeByte(ControlMessage.TYPE_SET_SCREEN_POWER_MODE); + dos.writeByte(Device.POWER_MODE_NORMAL); 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(ControlMessage.TYPE_SET_SCREEN_POWER_MODE, event.getType()); + Assert.assertEquals(Device.POWER_MODE_NORMAL, event.getAction()); Assert.assertEquals(-1, bis.read()); // EOS } @@ -322,8 +324,6 @@ public class ControlMessageReaderTest { 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}; @@ -337,8 +337,6 @@ public class ControlMessageReaderTest { 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()); @@ -401,27 +399,6 @@ public class ControlMessageReaderTest { 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();