Compare commits

..

4 Commits

Author SHA1 Message Date
5a2b929aac hack_virtual_display 2024-10-06 19:14:21 +02:00
9434718970 dpi 2024-10-06 19:05:36 +02:00
6ddcc98663 vdevents 2024-10-06 18:39:15 +02:00
19178e0df9 move to screencapture 2024-10-06 18:31:45 +02:00
124 changed files with 1563 additions and 5288 deletions

View File

@ -1,475 +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 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-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install dependencies
run: |
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 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: Workaround for old meson version run by Github Actions
run: sed -i 's/^pkg-config/pkgconfig/' cross_win32.txt
- 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 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: Workaround for old meson version run by Github Actions
run: sed -i 's/^pkg-config/pkgconfig/' cross_win64.txt
- 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: Checkout code
uses: actions/checkout@v4
- name: Install dependencies
run: |
brew install meson ninja nasm libiconv zlib automake autoconf \
libtool
- name: Build
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: 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
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-aarch64
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

View File

@ -2,7 +2,7 @@
source for the project. Do not download releases from random websites, even if source for the project. Do not download releases from random websites, even if
their name contains `scrcpy`.** their name contains `scrcpy`.**
# scrcpy (v3.0) # scrcpy (v2.7)
<img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" /> <img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />
@ -31,7 +31,6 @@ It focuses on:
Its features include: Its features include:
- [audio forwarding](doc/audio.md) (Android 11+) - [audio forwarding](doc/audio.md) (Android 11+)
- [recording](doc/recording.md) - [recording](doc/recording.md)
- [virtual display](doc/virtual_display.md)
- mirroring with [Android device screen off](doc/device.md#turn-screen-off) - mirroring with [Android device screen off](doc/device.md#turn-screen-off)
- [copy-paste](doc/control.md#copy-paste) in both directions - [copy-paste](doc/control.md#copy-paste) in both directions
- [configurable quality](doc/video.md) - [configurable quality](doc/video.md)
@ -74,7 +73,7 @@ Note that USB debugging is not required to run scrcpy in [OTG mode](doc/otg.md).
## Get the app ## Get the app
- [Linux](doc/linux.md) - [Linux](doc/linux.md)
- [Windows](doc/windows.md) (read [how to run](doc/windows.md#run)) - [Windows](doc/windows.md)
- [macOS](doc/macos.md) - [macOS](doc/macos.md)
@ -92,12 +91,6 @@ Here are just some common examples.
scrcpy --video-codec=h265 -m1920 --max-fps=60 --no-audio -K # short version 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 - Record the device camera in H.265 at 1920x1080 (and microphone) to an MP4
file: file:
@ -141,7 +134,6 @@ documented in the following pages:
- [Device](doc/device.md) - [Device](doc/device.md)
- [Window](doc/window.md) - [Window](doc/window.md)
- [Recording](doc/recording.md) - [Recording](doc/recording.md)
- [Virtual display](doc/virtual_display.md)
- [Tunnels](doc/tunnels.md) - [Tunnels](doc/tunnels.md)
- [OTG](doc/otg.md) - [OTG](doc/otg.md)
- [Camera](doc/camera.md) - [Camera](doc/camera.md)
@ -181,7 +173,6 @@ to your problem immediately.
You can also use: You can also use:
- Reddit: [`r/scrcpy`](https://www.reddit.com/r/scrcpy) - 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) - Twitter: [`@scrcpy_app`](https://twitter.com/scrcpy_app)

View File

@ -2,7 +2,6 @@ _scrcpy() {
local cur prev words cword local cur prev words cword
local opts=" local opts="
--always-on-top --always-on-top
--angle
--audio-bit-rate= --audio-bit-rate=
--audio-buffer= --audio-buffer=
--audio-codec= --audio-codec=
@ -18,10 +17,10 @@ _scrcpy() {
--camera-fps= --camera-fps=
--camera-high-speed --camera-high-speed
--camera-size= --camera-size=
--capture-orientation=
--crop= --crop=
-d --select-usb -d --select-usb
--disable-screensaver --disable-screensaver
--display-buffer=
--display-id= --display-id=
--display-orientation= --display-orientation=
-e --select-tcpip -e --select-tcpip
@ -34,11 +33,12 @@ _scrcpy() {
--keyboard= --keyboard=
--kill-adb-on-close --kill-adb-on-close
--legacy-paste --legacy-paste
--list-apps
--list-camera-sizes --list-camera-sizes
--list-cameras --list-cameras
--list-displays --list-displays
--list-encoders --list-encoders
--lock-video-orientation
--lock-video-orientation=
-m --max-size= -m --max-size=
-M -M
--max-fps= --max-fps=
@ -46,8 +46,6 @@ _scrcpy() {
--mouse-bind= --mouse-bind=
-n --no-control -n --no-control
-N --no-playback -N --no-playback
--new-display
--new-display=
--no-audio --no-audio
--no-audio-playback --no-audio-playback
--no-cleanup --no-cleanup
@ -57,7 +55,6 @@ _scrcpy() {
--no-mipmaps --no-mipmaps
--no-mouse-hover --no-mouse-hover
--no-power-on --no-power-on
--no-vd-system-decorations
--no-video --no-video
--no-video-playback --no-video-playback
--orientation= --orientation=
@ -78,9 +75,7 @@ _scrcpy() {
--rotation= --rotation=
-s --serial= -s --serial=
-S --turn-screen-off -S --turn-screen-off
--screen-off-timeout=
--shortcut-mod= --shortcut-mod=
--start-app=
-t --show-touches -t --show-touches
--tcpip --tcpip
--tcpip= --tcpip=
@ -91,7 +86,6 @@ _scrcpy() {
--v4l2-sink= --v4l2-sink=
-v --version -v --version
-V --verbosity= -V --verbosity=
--video-buffer=
--video-codec= --video-codec=
--video-codec-options= --video-codec-options=
--video-encoder= --video-encoder=
@ -139,10 +133,6 @@ _scrcpy() {
COMPREPLY=($(compgen -W 'disabled uhid aoa' -- "$cur")) COMPREPLY=($(compgen -W 'disabled uhid aoa' -- "$cur"))
return 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) --orientation|--display-orientation)
COMPREPLY=($(compgen -W '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur")) COMPREPLY=($(compgen -W '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur"))
return return
@ -151,6 +141,10 @@ _scrcpy() {
COMPREPLY=($(compgen -W '0 90 180 270' -- "$cur")) COMPREPLY=($(compgen -W '0 90 180 270' -- "$cur"))
return return
;; ;;
--lock-video-orientation)
COMPREPLY=($(compgen -W 'unlocked initial 0 90 180 270' -- "$cur"))
return
;;
--pause-on-exit) --pause-on-exit)
COMPREPLY=($(compgen -W 'true false if-error' -- "$cur")) COMPREPLY=($(compgen -W 'true false if-error' -- "$cur"))
return return
@ -193,9 +187,9 @@ _scrcpy() {
|--camera-size \ |--camera-size \
|--crop \ |--crop \
|--display-id \ |--display-id \
|--display-buffer \
|--max-fps \ |--max-fps \
|-m|--max-size \ |-m|--max-size \
|--new-display \
|-p|--port \ |-p|--port \
|--push-target \ |--push-target \
|--rotation \ |--rotation \
@ -203,7 +197,6 @@ _scrcpy() {
|--tunnel-port \ |--tunnel-port \
|--v4l2-buffer \ |--v4l2-buffer \
|--v4l2-sink \ |--v4l2-sink \
|--video-buffer \
|--video-codec-options \ |--video-codec-options \
|--video-encoder \ |--video-encoder \
|--tcpip \ |--tcpip \

View File

@ -1,6 +0,0 @@
#!/bin/bash
cd "$(dirname ${BASH_SOURCE[0]})"
export ADB="${ADB:-./adb}"
export SCRCPY_SERVER_PATH="${SCRCPY_SERVER_PATH:-./scrcpy-server}"
export SCRCPY_ICON_PATH="${SCRCPY_ICON_PATH:-./icon.png}"
./scrcpy_bin "$@"

View File

@ -9,7 +9,6 @@ local arguments
arguments=( arguments=(
'--always-on-top[Make scrcpy window always on top \(above other windows\)]' '--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-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=[Select the audio codec]:codec:(opus aac flac raw)'
@ -25,10 +24,10 @@ arguments=(
'--camera-facing=[Select the device camera by its facing direction]:facing:(front back external)' '--camera-facing=[Select the device camera by its facing direction]:facing:(front back external)'
'--camera-fps=[Specify the camera capture frame rate]' '--camera-fps=[Specify the camera capture frame rate]'
'--camera-size=[Specify an explicit camera capture size]' '--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]' '--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]'
{-d,--select-usb}'[Use USB device]' {-d,--select-usb}'[Use USB device]'
'--disable-screensaver[Disable screensaver while scrcpy is running]' '--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-id=[Specify the display id to mirror]'
'--display-orientation=[Set the initial display orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)' '--display-orientation=[Set the initial display orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)'
{-e,--select-tcpip}'[Use TCP/IP device]' {-e,--select-tcpip}'[Use TCP/IP device]'
@ -41,11 +40,11 @@ arguments=(
'--keyboard=[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)' '--keyboard=[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)'
'--kill-adb-on-close[Kill adb when scrcpy terminates]' '--kill-adb-on-close[Kill adb when scrcpy terminates]'
'--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]' '--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-camera-sizes[List the valid camera capture sizes]'
'--list-cameras[List cameras available on the device]' '--list-cameras[List cameras available on the device]'
'--list-displays[List displays available on the device]' '--list-displays[List displays available on the device]'
'--list-encoders[List video and audio encoders 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,--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]' '--max-fps=[Limit the frame rate of screen capture]'
@ -53,7 +52,6 @@ arguments=(
'--mouse-bind=[Configure bindings of secondary clicks]' '--mouse-bind=[Configure bindings of secondary clicks]'
{-n,--no-control}'[Disable device control \(mirror the device in read only\)]' {-n,--no-control}'[Disable device control \(mirror the device in read only\)]'
{-N,--no-playback}'[Disable video and audio playback]' {-N,--no-playback}'[Disable video and audio playback]'
'--new-display=[Create a new display]'
'--no-audio[Disable audio forwarding]' '--no-audio[Disable audio forwarding]'
'--no-audio-playback[Disable audio playback]' '--no-audio-playback[Disable audio playback]'
'--no-cleanup[Disable device cleanup actions on exit]' '--no-cleanup[Disable device cleanup actions on exit]'
@ -63,7 +61,6 @@ arguments=(
'--no-mipmaps[Disable the generation of mipmaps]' '--no-mipmaps[Disable the generation of mipmaps]'
'--no-mouse-hover[Do not forward mouse hover events]' '--no-mouse-hover[Do not forward mouse hover events]'
'--no-power-on[Do not power on the device on start]' '--no-power-on[Do not power on the device on start]'
'--no-vd-system-decorations[Disable virtual display system decorations flag]'
'--no-video[Disable video forwarding]' '--no-video[Disable video forwarding]'
'--no-video-playback[Disable video playback]' '--no-video-playback[Disable video playback]'
'--orientation=[Set the video orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)' '--orientation=[Set the video orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)'
@ -82,9 +79,7 @@ arguments=(
'--require-audio=[Make scrcpy fail if audio is enabled but does not work]' '--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,--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]' {-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)' '--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]' {-t,--show-touches}'[Show physical touches]'
'--tcpip[\(optional \[ip\:port\]\) Configure and connect the device over TCP/IP]' '--tcpip[\(optional \[ip\:port\]\) Configure and connect the device over TCP/IP]'
'--time-limit=[Set the maximum mirroring time, in seconds]' '--time-limit=[Set the maximum mirroring time, in seconds]'
@ -94,7 +89,6 @@ arguments=(
'--v4l2-sink=[\[\/dev\/videoN\] Output to v4l2loopback device]' '--v4l2-sink=[\[\/dev\/videoN\] Output to v4l2loopback device]'
{-v,--version}'[Print the version of scrcpy]' {-v,--version}'[Print the version of scrcpy]'
{-V,--verbosity=}'[Set the log level]:verbosity:(verbose debug info warn error)' {-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=[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-codec-options=[Set a list of comma-separated key\:type=value options for the device video encoder]'
'--video-encoder=[Use a specific MediaCodec video encoder]' '--video-encoder=[Use a specific MediaCodec video encoder]'

View File

@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR" cd "$DEPS_DIR"
. common . common
VERSION=35.0.2 VERSION=35.0.0
FILENAME=platform-tools_r$VERSION-win.zip FILENAME=platform-tools_r$VERSION-windows.zip
PROJECT_DIR=platform-tools-$VERSION-windows PROJECT_DIR=platform-tools-$VERSION
SHA256SUM=2975a3eac0b19182748d64195375ad056986561d994fffbdc64332a516300bb9 SHA256SUM=7ab78a8f8b305ae4d0de647d99c43599744de61a0838d3a47bda0cdffefee87e
cd "$SOURCES_DIR" cd "$SOURCES_DIR"
@ -27,6 +27,6 @@ else
rmdir "$ZIP_PREFIX" rmdir "$ZIP_PREFIX"
fi fi
mkdir -p "$INSTALL_DIR/adb-windows" mkdir -p "$INSTALL_DIR/$HOST/bin"
cd "$INSTALL_DIR/adb-windows" cd "$INSTALL_DIR/$HOST/bin"
cp -r "$SOURCES_DIR/$PROJECT_DIR"/. "$INSTALL_DIR/adb-windows/" cp -r "$SOURCES_DIR/$PROJECT_DIR"/. "$INSTALL_DIR/$HOST/bin/"

View File

@ -1,29 +0,0 @@
#!/usr/bin/env bash
set -ex
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
VERSION=35.0.2
FILENAME=platform-tools_r$VERSION-linux.zip
PROJECT_DIR=platform-tools-$VERSION-linux
SHA256SUM=acfdcccb123a8718c46c46c059b2f621140194e5ec1ac9d81715be3d6ab6cd0a
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/"

View File

@ -1,29 +0,0 @@
#!/usr/bin/env bash
set -ex
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
VERSION=35.0.2
FILENAME=platform-tools_r$VERSION-darwin.zip
PROJECT_DIR=platform-tools-$VERSION-darwin
SHA256SUM=1820078db90bf21628d257ff052528af1c61bb48f754b3555648f5652fa35d78
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/"

View File

@ -1,47 +1,25 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# This file is intended to be sourced by other scripts, not executed # This file is intended to be sourced by other scripts, not executed
process_args() { if [[ $# != 1 ]]
if [[ $# != 3 ]] then
then # <host>: win32 or win64
# <host>: win32 or win64 echo "Syntax: $0 <host>" >&2
# <build_type>: native or cross exit 1
# <link_type>: static or shared fi
echo "Syntax: $0 <host> <build_type> <link_type>" >&2
exit 1
fi
HOST="$1" HOST="$1"
BUILD_TYPE="$2" # native or cross
LINK_TYPE="$3" # static or shared
DIRNAME="$HOST-$BUILD_TYPE-$LINK_TYPE"
if [[ "$BUILD_TYPE" != native && "$BUILD_TYPE" != cross ]] if [[ "$HOST" = win32 ]]
then then
echo "Unsupported build type (expected native or cross): $BUILD_TYPE" >&2 HOST_TRIPLET=i686-w64-mingw32
exit 1 elif [[ "$HOST" = win64 ]]
fi then
HOST_TRIPLET=x86_64-w64-mingw32
if [[ "$LINK_TYPE" != static && "$LINK_TYPE" != shared ]] else
then echo "Unsupported host: $HOST" >&2
echo "Unsupported link type (expected static or shared): $LINK_TYPE" >&2 exit 1
exit 1 fi
fi
if [[ "$BUILD_TYPE" == cross ]]
then
if [[ "$HOST" = win32 ]]
then
HOST_TRIPLET=i686-w64-mingw32
elif [[ "$HOST" = win64 ]]
then
HOST_TRIPLET=x86_64-w64-mingw32
else
echo "Unsupported cross-build to host: $HOST" >&2
exit 1
fi
fi
}
DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR" cd "$DEPS_DIR"
@ -59,7 +37,7 @@ checksum() {
local file="$1" local file="$1"
local sum="$2" local sum="$2"
echo "$file: verifying checksum..." echo "$file: verifying checksum..."
echo "$sum $file" | shasum -a256 -c echo "$sum $file" | sha256sum -c
} }
get_file() { get_file() {

View File

@ -3,12 +3,11 @@ set -ex
DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR" cd "$DEPS_DIR"
. common . common
process_args "$@"
VERSION=7.1 VERSION=7.0.2
FILENAME=ffmpeg-$VERSION.tar.xz FILENAME=ffmpeg-$VERSION.tar.xz
PROJECT_DIR=ffmpeg-$VERSION PROJECT_DIR=ffmpeg-$VERSION
SHA256SUM=40973D44970DBC83EF302B0609F2E74982BE2D85916DD2EE7472D30678A7ABE6 SHA256SUM=8646515b638a3ad303e23af6a3587734447cb8fc0a0c064ecdb8e95c4fd8b389
cd "$SOURCES_DIR" cd "$SOURCES_DIR"
@ -23,121 +22,68 @@ fi
mkdir -p "$BUILD_DIR/$PROJECT_DIR" mkdir -p "$BUILD_DIR/$PROJECT_DIR"
cd "$BUILD_DIR/$PROJECT_DIR" cd "$BUILD_DIR/$PROJECT_DIR"
if [[ -d "$DIRNAME" ]] if [[ "$HOST" = win32 ]]
then then
echo "'$PWD/$DIRNAME' already exists, not reconfigured" ARCH=x86
cd "$DIRNAME" elif [[ "$HOST" = win64 ]]
then
ARCH=x86_64
else else
mkdir "$DIRNAME" echo "Unsupported host: $HOST" >&2
cd "$DIRNAME" exit 1
fi
if [[ "$HOST" == win* ]] # -static-libgcc to avoid missing libgcc_s_dw2-1.dll
then # -static to avoid dynamic dependency to zlib
# -static-libgcc to avoid missing libgcc_s_dw2-1.dll export CFLAGS='-static-libgcc -static'
# -static to avoid dynamic dependency to zlib export CXXFLAGS="$CFLAGS"
export CFLAGS='-static-libgcc -static' export LDFLAGS='-static-libgcc -static'
export CXXFLAGS="$CFLAGS"
export LDFLAGS='-static-libgcc -static'
elif [[ "$HOST" == "macos" ]]
then
export LDFLAGS="$LDFLAGS -L/opt/homebrew/opt/zlib/lib"
export CPPFLAGS="$CPPFLAGS -I/opt/homebrew/opt/zlib/include"
export LDFLAGS="$LDFLAGS-L/opt/homebrew/opt/libiconv/lib" if [[ -d "$HOST" ]]
export CPPFLAGS="$CPPFLAGS -I/opt/homebrew/opt/libiconv/include" then
export PKG_CONFIG_PATH="/opt/homebrew/opt/zlib/lib/pkgconfig" echo "'$PWD/$HOST' already exists, not reconfigured"
fi cd "$HOST"
else
mkdir "$HOST"
cd "$HOST"
conf=( "$SOURCES_DIR/$PROJECT_DIR"/configure \
--prefix="$INSTALL_DIR/$DIRNAME" --prefix="$INSTALL_DIR/$HOST" \
--extra-cflags="-O2 -fPIC" --enable-cross-compile \
--disable-programs --target-os=mingw32 \
--disable-doc --arch="$ARCH" \
--disable-swscale --cross-prefix="${HOST_TRIPLET}-" \
--disable-postproc --cc="${HOST_TRIPLET}-gcc" \
--disable-avfilter --extra-cflags="-O2 -fPIC" \
--disable-network --enable-shared \
--disable-everything --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-vulkan
--disable-vaapi
--disable-vdpau
--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
)
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 fi
make -j make -j

View File

@ -3,7 +3,6 @@ set -ex
DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR" cd "$DEPS_DIR"
. common . common
process_args "$@"
VERSION=1.0.27 VERSION=1.0.27
FILENAME=libusb-$VERSION.tar.gz FILENAME=libusb-$VERSION.tar.gz
@ -26,40 +25,20 @@ cd "$BUILD_DIR/$PROJECT_DIR"
export CFLAGS='-O2' export CFLAGS='-O2'
export CXXFLAGS="$CFLAGS" export CXXFLAGS="$CFLAGS"
if [[ -d "$DIRNAME" ]] if [[ -d "$HOST" ]]
then then
echo "'$PWD/$DIRNAME' already exists, not reconfigured" echo "'$PWD/$HOST' already exists, not reconfigured"
cd "$DIRNAME" cd "$HOST"
else else
mkdir "$DIRNAME" mkdir "$HOST"
cd "$DIRNAME" cd "$HOST"
conf=(
--prefix="$INSTALL_DIR/$DIRNAME"
)
if [[ "$LINK_TYPE" == static ]]
then
conf+=(
--enable-static
--disable-shared
)
else
conf+=(
--disable-static
--enable-shared
)
fi
if [[ "$BUILD_TYPE" == cross ]]
then
conf+=(
--host="$HOST_TRIPLET"
)
fi
"$SOURCES_DIR/$PROJECT_DIR"/bootstrap.sh "$SOURCES_DIR/$PROJECT_DIR"/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 fi
make -j make -j

View File

@ -3,12 +3,11 @@ set -ex
DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR" cd "$DEPS_DIR"
. common . common
process_args "$@"
VERSION=2.30.9 VERSION=2.30.7
FILENAME=SDL-$VERSION.tar.gz FILENAME=SDL-$VERSION.tar.gz
PROJECT_DIR=SDL-release-$VERSION PROJECT_DIR=SDL-release-$VERSION
SHA256SUM=682a055004081e37d81a7d4ce546c3ee3ef2e0e6a675ed2651e430ccd14eb407 SHA256SUM=1578c96f62c9ae36b64e431b2aa0e0b0fd07c275dedbc694afc38e19056688f5
cd "$SOURCES_DIR" cd "$SOURCES_DIR"
@ -26,54 +25,23 @@ cd "$BUILD_DIR/$PROJECT_DIR"
export CFLAGS='-O2' export CFLAGS='-O2'
export CXXFLAGS="$CFLAGS" export CXXFLAGS="$CFLAGS"
if [[ -d "$DIRNAME" ]] if [[ -d "$HOST" ]]
then then
echo "'$PWD/$HDIRNAME' already exists, not reconfigured" echo "'$PWD/$HOST' already exists, not reconfigured"
cd "$DIRNAME" cd "$HOST"
else else
mkdir "$DIRNAME" mkdir "$HOST"
cd "$DIRNAME" cd "$HOST"
conf=( "$SOURCES_DIR/$PROJECT_DIR"/configure \
--prefix="$INSTALL_DIR/$DIRNAME" --prefix="$INSTALL_DIR/$HOST" \
) --host="$HOST_TRIPLET" \
--enable-shared \
if [[ "$HOST" == linux ]] --disable-static
then
conf+=(
--enable-video-wayland
--enable-video-x11
)
fi
if [[ "$LINK_TYPE" == static ]]
then
conf+=(
--enable-static
--disable-shared
)
else
conf+=(
--disable-static
--enable-shared
)
fi
if [[ "$BUILD_TYPE" == cross ]]
then
conf+=(
--host="$HOST_TRIPLET"
)
fi
"$SOURCES_DIR/$PROJECT_DIR"/configure "${conf[@]}"
fi fi
make -j make -j
# There is no "make install-strip" # There is no "make install-strip"
make install make install
# Strip manually # Strip manually
if [[ "$LINK_TYPE" == shared && "$HOST" == win* ]] ${HOST_TRIPLET}-strip "$INSTALL_DIR/$HOST/bin/SDL2.dll"
then
${HOST_TRIPLET}-strip "$INSTALL_DIR/$DIRNAME/bin/SDL2.dll"
fi

View File

@ -109,22 +109,20 @@ endif
cc = meson.get_compiler('c') cc = meson.get_compiler('c')
static = get_option('static')
dependencies = [ dependencies = [
dependency('libavformat', version: '>= 57.33', static: static), dependency('libavformat', version: '>= 57.33'),
dependency('libavcodec', version: '>= 57.37', static: static), dependency('libavcodec', version: '>= 57.37'),
dependency('libavutil', static: static), dependency('libavutil'),
dependency('libswresample', static: static), dependency('libswresample'),
dependency('sdl2', version: '>= 2.0.5', static: static), dependency('sdl2', version: '>= 2.0.5'),
] ]
if v4l2_support if v4l2_support
dependencies += dependency('libavdevice', static: static) dependencies += dependency('libavdevice')
endif endif
if usb_support if usb_support
dependencies += dependency('libusb-1.0', static: static) dependencies += dependency('libusb-1.0')
endif endif
if host_machine.system() == 'windows' if host_machine.system() == 'windows'
@ -169,6 +167,9 @@ conf.set('DEFAULT_LOCAL_PORT_RANGE_LAST', '27199')
# run a server debugger and wait for a client to be attached # run a server debugger and wait for a client to be attached
conf.set('SERVER_DEBUGGER', get_option('server_debugger')) 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) # enable V4L2 support (linux only)
conf.set('HAVE_V4L2', v4l2_support) conf.set('HAVE_V4L2', v4l2_support)

View File

@ -13,7 +13,7 @@ BEGIN
VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "LegalCopyright", "Romain Vimont, Genymobile"
VALUE "OriginalFilename", "scrcpy.exe" VALUE "OriginalFilename", "scrcpy.exe"
VALUE "ProductName", "scrcpy" VALUE "ProductName", "scrcpy"
VALUE "ProductVersion", "3.0" VALUE "ProductVersion", "2.7"
END END
END END
BLOCK "VarFileInfo" BLOCK "VarFileInfo"

View File

@ -19,10 +19,6 @@ provides display and control of Android devices connected on USB (or over TCP/IP
.B \-\-always\-on\-top .B \-\-always\-on\-top
Make scrcpy window always on top (above other windows). 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 .TP
.BI "\-\-audio\-bit\-rate " value .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). Encode the audio at the given bit rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000).
@ -97,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"). 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 .TP
.B \-\-camera\-high\-speed .B \-\-camera\-high\-speed
Enable high-speed camera capture mode. Enable high-speed camera capture mode.
@ -122,26 +106,28 @@ Specify the device camera id to mirror.
The available camera ids can be listed by \fB\-\-list\-cameras\fR. The available camera ids can be listed by \fB\-\-list\-cameras\fR.
.TP .TP
.BI "\-\-camera\-size " width\fRx\fIheight .BI "\-\-camera\-facing " facing
Specify an explicit camera capture size. Select the device camera by its facing direction.
Possible values are "front", "back" and "external".
.TP .TP
.BI "\-\-capture\-orientation " value .BI "\-\-camera\-fps " fps
Possible values are 0, 90, 180, 270, flip0, flip90, flip180 and flip270, possibly prefixed by '@'. 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. .TP
.BI "\-\-camera\-size " width\fRx\fIheight
If '@' is passed alone, then the rotation is locked to the initial device orientation. Specify an explicit camera capture size.
Default is 0.
.TP .TP
.BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy .BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy
Crop the device screen on the server. 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 .TP
.B \-d, \-\-select\-usb .B \-d, \-\-select\-usb
@ -153,6 +139,12 @@ Also see \fB\-e\fR (\fB\-\-select\-tcpip\fR).
.BI "\-\-disable\-screensaver" .BI "\-\-disable\-screensaver"
Disable screensaver while scrcpy is running. 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 .TP
.BI "\-\-display\-id " id .BI "\-\-display\-id " id
Specify the device display id to mirror. Specify the device display id to mirror.
@ -235,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. 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 .TP
.B \-\-list\-camera\-sizes .B \-\-list\-camera\-sizes
List the valid camera capture sizes. List the valid camera capture sizes.
@ -255,6 +243,16 @@ List video and audio encoders available on the device.
.B \-\-list\-displays .B \-\-list\-displays
List displays available on the device. 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 .TP
.BI "\-m, \-\-max\-size " value .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. 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.
@ -316,17 +314,6 @@ Disable device control (mirror the device in read\-only).
.B \-N, \-\-no\-playback .B \-N, \-\-no\-playback
Disable video and audio playback on the computer (equivalent to \fB\-\-no\-video\-playback \-\-no\-audio\-playback\fR). 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 .TP
.B \-\-no\-audio .B \-\-no\-audio
Disable audio forwarding. Disable audio forwarding.
@ -369,10 +356,6 @@ Do not forward mouse hover (mouse motion without any clicks) events.
.B \-\-no\-power\-on .B \-\-no\-power\-on
Do not power on the device on start. Do not power on the device on start.
.TP
.B \-\-no\-vd\-system\-decorations
Disable virtual display system decorations flag.
.TP .TP
.B \-\-no\-video .B \-\-no\-video
Disable video forwarding. Disable video forwarding.
@ -495,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). 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 .TP
.B \-t, \-\-show\-touches .B \-t, \-\-show\-touches
Enable "show touches" on start, restore the initial value on exit. Enable "show touches" on start, restore the initial value on exit.
@ -555,19 +522,13 @@ Default is "info" for release builds, "debug" for debug builds.
.BI "\-\-v4l2-sink " /dev/videoN .BI "\-\-v4l2-sink " /dev/videoN
Output to v4l2loopback device. Output to v4l2loopback device.
It requires to lock the video orientation (see \fB\-\-lock\-video\-orientation\fR).
.TP .TP
.BI "\-\-v4l2-buffer " ms .BI "\-\-v4l2-buffer " ms
Add a buffering delay (in milliseconds) before pushing frames. This increases latency to compensate for jitter. 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. This option is similar to \fB\-\-display\-buffer\fR, but specific to V4L2 sink.
Default is 0 (no buffering).
.TP
.BI "\-\-video\-buffer " ms
Add a buffering delay (in milliseconds) before displaying video frames.
This increases latency to compensate for jitter.
Default is 0 (no buffering). Default is 0 (no buffering).
@ -676,10 +637,6 @@ Pause or re-pause display
.B MOD+Shift+z .B MOD+Shift+z
Unpause display Unpause display
.TP
.B MOD+Shift+r
Reset video capture/encoding
.TP .TP
.B MOD+g .B MOD+g
Resize window to 1:1 (pixel\-perfect) Resize window to 1:1 (pixel\-perfect)

View File

@ -739,21 +739,3 @@ sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags) {
return sc_adb_parse_device_ip(buf); 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;
}

View File

@ -114,10 +114,4 @@ sc_adb_getprop(struct sc_intr *intr, const char *serial, const char *prop,
char * char *
sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags); 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 #endif

View File

@ -288,7 +288,7 @@ sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) {
// Enable compensation when the difference exceeds +/- 4ms. // Enable compensation when the difference exceeds +/- 4ms.
// Disable compensation when the difference is lower than +/- 1ms. // Disable compensation when the difference is lower than +/- 1ms.
int threshold = ar->compensation_active int threshold = ar->compensation != 0
? ar->sample_rate / 1000 /* 1ms */ ? ar->sample_rate / 1000 /* 1ms */
: ar->sample_rate * 4 / 1000; /* 4ms */ : ar->sample_rate * 4 / 1000; /* 4ms */
@ -309,12 +309,14 @@ sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) {
LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32 LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32
" compensation=%d", ar->target_buffering, avg, can_read, diff); " compensation=%d", ar->target_buffering, avg, can_read, diff);
int ret = swr_set_compensation(swr_ctx, diff, distance); if (diff != ar->compensation) {
if (ret < 0) { int ret = swr_set_compensation(swr_ctx, diff, distance);
LOGW("Resampling compensation failed: %d", ret); if (ret < 0) {
// not fatal LOGW("Resampling compensation failed: %d", ret);
} else { // not fatal
ar->compensation_active = diff != 0; } else {
ar->compensation = diff;
}
} }
} }
@ -390,7 +392,7 @@ sc_audio_regulator_init(struct sc_audio_regulator *ar, size_t sample_size,
atomic_init(&ar->played, false); atomic_init(&ar->played, false);
atomic_init(&ar->received, false); atomic_init(&ar->received, false);
atomic_init(&ar->underflow, 0); atomic_init(&ar->underflow, 0);
ar->compensation_active = false; ar->compensation = 0;
return true; return true;

View File

@ -44,8 +44,8 @@ struct sc_audio_regulator {
// Number of silence samples inserted since the last received packet // Number of silence samples inserted since the last received packet
atomic_uint_least32_t underflow; atomic_uint_least32_t underflow;
// Non-zero compensation applied (only used by the receiver thread) // Current applied compensation value (only used by the receiver thread)
bool compensation_active; int compensation;
// Set to true the first time a sample is received // Set to true the first time a sample is received
atomic_bool received; atomic_bool received;

View File

@ -50,7 +50,6 @@ enum {
OPT_POWER_OFF_ON_CLOSE, OPT_POWER_OFF_ON_CLOSE,
OPT_V4L2_SINK, OPT_V4L2_SINK,
OPT_DISPLAY_BUFFER, OPT_DISPLAY_BUFFER,
OPT_VIDEO_BUFFER,
OPT_V4L2_BUFFER, OPT_V4L2_BUFFER,
OPT_TUNNEL_HOST, OPT_TUNNEL_HOST,
OPT_TUNNEL_PORT, OPT_TUNNEL_PORT,
@ -103,13 +102,6 @@ enum {
OPT_NO_MOUSE_HOVER, OPT_NO_MOUSE_HOVER,
OPT_AUDIO_DUP, OPT_AUDIO_DUP,
OPT_GAMEPAD, 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,
}; };
struct sc_option { struct sc_option {
@ -151,13 +143,6 @@ static const struct sc_option options[] = {
.longopt = "always-on-top", .longopt = "always-on-top",
.text = "Make scrcpy window always on top (above other windows).", .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_id = OPT_AUDIO_BIT_RATE,
.longopt = "audio-bit-rate", .longopt = "audio-bit-rate",
@ -255,6 +240,14 @@ static const struct sc_option options[] = {
"ratio), \"<num>:<den>\" (e.g. \"4:3\") or \"<value>\" (e.g. " "ratio), \"<num>:<den>\" (e.g. \"4:3\") or \"<value>\" (e.g. "
"\"1.6\")." "\"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_id = OPT_CAMERA_FACING,
.longopt = "camera-facing", .longopt = "camera-facing",
@ -262,14 +255,6 @@ static const struct sc_option options[] = {
.text = "Select the device camera by its facing direction.\n" .text = "Select the device camera by its facing direction.\n"
"Possible values are \"front\", \"back\" and \"external\".", "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_id = OPT_CAMERA_HIGH_SPEED,
.longopt = "camera-high-speed", .longopt = "camera-high-speed",
@ -277,14 +262,6 @@ static const struct sc_option options[] = {
"This mode is restricted to specific resolutions and frame " "This mode is restricted to specific resolutions and frame "
"rates, listed by --list-camera-sizes.", "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_id = OPT_CAMERA_SIZE,
.longopt = "camera-size", .longopt = "camera-size",
@ -292,21 +269,12 @@ static const struct sc_option options[] = {
.text = "Specify an explicit camera capture size.", .text = "Specify an explicit camera capture size.",
}, },
{ {
.longopt_id = OPT_CAPTURE_ORIENTATION, .longopt_id = OPT_CAMERA_FPS,
.longopt = "capture-orientation", .longopt = "camera-fps",
.argdesc = "value", .argdesc = "value",
.text = "Set the capture video orientation.\n" .text = "Specify the camera capture frame rate.\n"
"Possible values are 0, 90, 180, 270, flip0, flip90, flip180 " "If not specified, Android's default frame rate (30 fps) is "
"and flip270, possibly prefixed by '@'.\n" "used.",
"The number represents the clockwise rotation in degrees; the "
"flip\" keyword applies a horizontal flip before the "
"rotation.\n"
"If a leading '@' is passed (@90) for display capture, then "
"the rotation is locked, and is relative to the natural device "
"orientation.\n"
"If '@' is passed alone, then the rotation is locked to the "
"initial device orientation.\n"
"Default is 0.",
}, },
{ {
// Not really deprecated (--codec has never been released), but without // Not really deprecated (--codec has never been released), but without
@ -329,7 +297,8 @@ static const struct sc_option options[] = {
.argdesc = "width:height:x:y", .argdesc = "width:height:x:y",
.text = "Crop the device screen on the server.\n" .text = "Crop the device screen on the server.\n"
"The values are expressed in the device natural orientation " "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', .shortopt = 'd',
@ -349,10 +318,12 @@ static const struct sc_option options[] = {
.argdesc = "id", .argdesc = "id",
}, },
{ {
// deprecated
.longopt_id = OPT_DISPLAY_BUFFER, .longopt_id = OPT_DISPLAY_BUFFER,
.longopt = "display-buffer", .longopt = "display-buffer",
.argdesc = "ms", .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, .longopt_id = OPT_DISPLAY_ID,
@ -471,11 +442,6 @@ static const struct sc_option options[] = {
"This is a workaround for some devices not behaving as " "This is a workaround for some devices not behaving as "
"expected when setting the device clipboard programmatically.", "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_id = OPT_LIST_CAMERAS,
.longopt = "list-cameras", .longopt = "list-cameras",
@ -497,10 +463,18 @@ static const struct sc_option options[] = {
.text = "List video and audio encoders available on the device.", .text = "List video and audio encoders available on the device.",
}, },
{ {
// deprecated
.longopt_id = OPT_LOCK_VIDEO_ORIENTATION, .longopt_id = OPT_LOCK_VIDEO_ORIENTATION,
.longopt = "lock-video-orientation", .longopt = "lock-video-orientation",
.argdesc = "value", .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', .shortopt = 'm',
@ -583,20 +557,6 @@ static const struct sc_option options[] = {
.text = "Disable video and audio playback on the computer (equivalent " .text = "Disable video and audio playback on the computer (equivalent "
"to --no-video-playback --no-audio-playback).", "to --no-video-playback --no-audio-playback).",
}, },
{
.longopt_id = OPT_NEW_DISPLAY,
.longopt = "new-display",
.argdesc = "[<width>x<height>][/<dpi>]",
.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_id = OPT_NO_AUDIO,
.longopt = "no-audio", .longopt = "no-audio",
@ -659,11 +619,6 @@ static const struct sc_option options[] = {
.longopt = "no-power-on", .longopt = "no-power-on",
.text = "Do not power on the device on start.", .text = "Do not power on the device on start.",
}, },
{
.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_id = OPT_NO_VIDEO,
.longopt = "no-video", .longopt = "no-video",
@ -816,13 +771,6 @@ static const struct sc_option options[] = {
.longopt = "turn-screen-off", .longopt = "turn-screen-off",
.text = "Turn the device screen off immediately.", .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_id = OPT_SHORTCUT_MOD,
.longopt = "shortcut-mod", .longopt = "shortcut-mod",
@ -836,20 +784,6 @@ static const struct sc_option options[] = {
"shortcuts, pass \"lctrl,lsuper\".\n" "shortcuts, pass \"lctrl,lsuper\".\n"
"Default is \"lalt,lsuper\" (left-Alt or left-Super).", "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', .shortopt = 't',
.longopt = "show-touches", .longopt = "show-touches",
@ -917,6 +851,8 @@ static const struct sc_option options[] = {
.longopt = "v4l2-sink", .longopt = "v4l2-sink",
.argdesc = "/dev/videoN", .argdesc = "/dev/videoN",
.text = "Output to v4l2loopback device.\n" .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.", "This feature is only available on Linux.",
}, },
{ {
@ -925,20 +861,11 @@ static const struct sc_option options[] = {
.argdesc = "ms", .argdesc = "ms",
.text = "Add a buffering delay (in milliseconds) before pushing " .text = "Add a buffering delay (in milliseconds) before pushing "
"frames. This increases latency to compensate for jitter.\n" "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" "V4L2 sink.\n"
"Default is 0 (no buffering).\n" "Default is 0 (no buffering).\n"
"This option is only available on Linux.", "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_id = OPT_VIDEO_CODEC,
.longopt = "video-codec", .longopt = "video-codec",
@ -1050,10 +977,6 @@ static const struct sc_shortcut shortcuts[] = {
.shortcuts = { "MOD+Shift+z" }, .shortcuts = { "MOD+Shift+z" },
.text = "Unpause display", .text = "Unpause display",
}, },
{
.shortcuts = { "MOD+Shift+r" },
.text = "Reset video capture/encoding",
},
{ {
.shortcuts = { "MOD+g" }, .shortcuts = { "MOD+g" },
.text = "Resize window to 1:1 (pixel-perfect)", .text = "Resize window to 1:1 (pixel-perfect)",
@ -1602,6 +1525,78 @@ parse_audio_output_buffer(const char *s, sc_tick *tick) {
return true; return true;
} }
static bool
parse_lock_video_orientation(const char *s,
enum sc_lock_video_orientation *lock_mode) {
if (!s || !strcmp(s, "initial")) {
// Without argument, lock the initial orientation
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_INITIAL;
return true;
}
if (!strcmp(s, "unlocked")) {
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED;
return true;
}
if (!strcmp(s, "0")) {
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_0;
return true;
}
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 static bool
parse_orientation(const char *s, enum sc_orientation *orientation) { parse_orientation(const char *s, enum sc_orientation *orientation) {
if (!strcmp(s, "0")) { if (!strcmp(s, "0")) {
@ -1641,32 +1636,6 @@ parse_orientation(const char *s, enum sc_orientation *orientation) {
return false; 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 static bool
parse_window_position(const char *s, int16_t *position) { parse_window_position(const char *s, int16_t *position) {
// special value for "auto" // special value for "auto"
@ -2137,20 +2106,6 @@ parse_time_limit(const char *s, sc_tick *tick) {
return true; 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 static bool
parse_pause_on_exit(const char *s, enum sc_pause_on_exit *pause_on_exit) { parse_pause_on_exit(const char *s, enum sc_pause_on_exit *pause_on_exit) {
if (!s || !strcmp(s, "true")) { if (!s || !strcmp(s, "true")) {
@ -2276,8 +2231,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
opts->crop = optarg; opts->crop = optarg;
break; break;
case OPT_DISPLAY: case OPT_DISPLAY:
LOGE("--display has been removed, use --display-id instead."); LOGW("--display is deprecated, use --display-id instead.");
return false; // fall through
case OPT_DISPLAY_ID: case OPT_DISPLAY_ID:
if (!parse_display_id(optarg, &opts->display_id)) { if (!parse_display_id(optarg, &opts->display_id)) {
return false; return false;
@ -2341,13 +2296,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
"--mouse=uhid instead."); "--mouse=uhid instead.");
return false; return false;
case OPT_LOCK_VIDEO_ORIENTATION: case OPT_LOCK_VIDEO_ORIENTATION:
LOGE("--lock-video-orientation has been removed, use " if (!parse_lock_video_orientation(optarg,
"--capture-orientation instead."); &opts->lock_video_orientation)) {
return false;
case OPT_CAPTURE_ORIENTATION:
if (!parse_capture_orientation(optarg,
&opts->capture_orientation,
&opts->capture_orientation_lock)) {
return false; return false;
} }
break; break;
@ -2365,9 +2315,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
opts->control = false; opts->control = false;
break; break;
case OPT_NO_DISPLAY: case OPT_NO_DISPLAY:
LOGE("--no-display has been removed, use --no-playback " LOGW("--no-display is deprecated, use --no-playback instead.");
"instead."); // fall through
return false;
case 'N': case 'N':
opts->video_playback = false; opts->video_playback = false;
opts->audio_playback = false; opts->audio_playback = false;
@ -2453,9 +2402,32 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
opts->key_inject_mode = SC_KEY_INJECT_MODE_RAW; opts->key_inject_mode = SC_KEY_INJECT_MODE_RAW;
break; break;
case OPT_ROTATION: case OPT_ROTATION:
LOGE("--rotation has been removed, use --orientation or " LOGW("--rotation is deprecated, use --display-orientation "
"--capture-orientation instead."); "instead.");
return false; 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: case OPT_DISPLAY_ORIENTATION:
if (!parse_orientation(optarg, &opts->display_orientation)) { if (!parse_orientation(optarg, &opts->display_orientation)) {
return false; return false;
@ -2516,9 +2488,23 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
} }
break; break;
case OPT_FORWARD_ALL_CLICKS: case OPT_FORWARD_ALL_CLICKS:
LOGE("--forward-all-clicks has been removed, " LOGW("--forward-all-clicks is deprecated, "
"use --mouse-bind=++++ instead."); "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: case OPT_LEGACY_PASTE:
opts->legacy_paste = true; opts->legacy_paste = true;
break; break;
@ -2526,11 +2512,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
opts->power_off_on_close = true; opts->power_off_on_close = true;
break; break;
case OPT_DISPLAY_BUFFER: case OPT_DISPLAY_BUFFER:
LOGE("--display-buffer has been removed, use --video-buffer " if (!parse_buffering_time(optarg, &opts->display_buffer)) {
"instead.");
return false;
case OPT_VIDEO_BUFFER:
if (!parse_buffering_time(optarg, &opts->video_buffer)) {
return false; return false;
} }
break; break;
@ -2613,9 +2595,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
case OPT_LIST_CAMERA_SIZES: case OPT_LIST_CAMERA_SIZES:
opts->list |= SC_OPTION_LIST_CAMERA_SIZES; opts->list |= SC_OPTION_LIST_CAMERA_SIZES;
break; break;
case OPT_LIST_APPS:
opts->list |= SC_OPTION_LIST_APPS;
break;
case OPT_REQUIRE_AUDIO: case OPT_REQUIRE_AUDIO:
opts->require_audio = true; opts->require_audio = true;
break; break;
@ -2689,24 +2668,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
return false; return false;
} }
break; 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_SYSTEM_DECORATIONS:
opts->vd_system_decorations = optarg;
break;
default: default:
// getopt prints the error message on stderr // getopt prints the error message on stderr
return false; return false;
@ -2801,6 +2762,13 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
return false; 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. // V4L2 could not handle size change.
// Do not log because downsizing on error is the default behavior, // Do not log because downsizing on error is the default behavior,
// not an explicit request from the user. // not an explicit request from the user.
@ -2808,7 +2776,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
} }
if (opts->v4l2_buffer && !opts->v4l2_device) { if (opts->v4l2_buffer && !opts->v4l2_device) {
LOGE("V4L2 buffer value without V4L2 sink"); LOGE("V4L2 buffer value without V4L2 sink\n");
return false; return false;
} }
#endif #endif
@ -2827,8 +2795,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
if (otg) { if (otg) {
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_AOA; opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_AOA;
} else if (!opts->video_playback) { } else if (!opts->video_playback) {
LOGI("No video mirroring, SDK mouse disabled"); LOGI("No video mirroring, mouse mode switched to UHID");
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_DISABLED; opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_UHID;
} else { } else {
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_SDK; opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_SDK;
} }
@ -2880,18 +2848,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 (otg) {
if (!opts->control) { if (!opts->control) {
LOGE("--no-control is not allowed in OTG mode"); LOGE("--no-control is not allowed in OTG mode");
@ -2998,11 +2954,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
return false; return false;
} }
if (opts->display_id != 0 && opts->new_display) {
LOGE("Cannot specify both --display-id and --new-display");
return false;
}
if (opts->audio && opts->audio_source == SC_AUDIO_SOURCE_AUTO) { if (opts->audio && opts->audio_source == SC_AUDIO_SOURCE_AUTO) {
// Select the audio source according to the video source // Select the audio source according to the video source
if (opts->video_source == SC_VIDEO_SOURCE_DISPLAY) { if (opts->video_source == SC_VIDEO_SOURCE_DISPLAY) {
@ -3135,10 +3086,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"); LOGE("Cannot request power off on close if control is disabled");
return false; return false;
} }
if (opts->start_app) {
LOGE("Cannot start an Android app if control is disabled");
return false;
}
} }
# ifdef _WIN32 # ifdef _WIN32

View File

@ -22,6 +22,9 @@
#define MOTIONEVENT_ACTION_LABEL(value) \ #define MOTIONEVENT_ACTION_LABEL(value) \
ENUM_TO_LABEL(android_motionevent_action_labels, 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[] = { static const char *const android_keyevent_action_labels[] = {
"down", "down",
"up", "up",
@ -44,6 +47,14 @@ static const char *const android_motionevent_action_labels[] = {
"btn-release", "btn-release",
}; };
static const char *const screen_power_mode_labels[] = {
"off",
"doze",
"normal",
"doze-suspend",
"suspend",
};
static const char *const copy_key_labels[] = { static const char *const copy_key_labels[] = {
"none", "none",
"copy", "copy",
@ -147,8 +158,8 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) {
size_t len = write_string(&buf[10], msg->set_clipboard.text, size_t len = write_string(&buf[10], msg->set_clipboard.text,
SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH); SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH);
return 10 + len; return 10 + len;
case SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER: case SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE:
buf[1] = msg->set_display_power.on; buf[1] = msg->set_screen_power_mode.mode;
return 2; return 2;
case SC_CONTROL_MSG_TYPE_UHID_CREATE: case SC_CONTROL_MSG_TYPE_UHID_CREATE:
sc_write16be(&buf[1], msg->uhid_create.id); sc_write16be(&buf[1], msg->uhid_create.id);
@ -172,16 +183,11 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) {
case SC_CONTROL_MSG_TYPE_UHID_DESTROY: case SC_CONTROL_MSG_TYPE_UHID_DESTROY:
sc_write16be(&buf[1], msg->uhid_destroy.id); sc_write16be(&buf[1], msg->uhid_destroy.id);
return 3; 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_NOTIFICATION_PANEL:
case SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL: case SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL:
case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS: case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS:
case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE: case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE:
case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS: case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
case SC_CONTROL_MSG_TYPE_RESET_VIDEO:
// no additional data // no additional data
return 1; return 1;
default: default:
@ -258,9 +264,9 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
msg->set_clipboard.paste ? "paste" : "nopaste", msg->set_clipboard.paste ? "paste" : "nopaste",
msg->set_clipboard.text); msg->set_clipboard.text);
break; break;
case SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER: case SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE:
LOG_CMSG("display power %s", LOG_CMSG("power mode %s",
msg->set_display_power.on ? "on" : "off"); SCREEN_POWER_MODE_LABEL(msg->set_screen_power_mode.mode));
break; break;
case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL:
LOG_CMSG("expand notification panel"); LOG_CMSG("expand notification panel");
@ -302,12 +308,6 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS: case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
LOG_CMSG("open hard keyboard settings"); LOG_CMSG("open hard keyboard settings");
break; 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: default:
LOG_CMSG("unknown type: %u", (unsigned) msg->type); LOG_CMSG("unknown type: %u", (unsigned) msg->type);
break; break;
@ -333,9 +333,6 @@ sc_control_msg_destroy(struct sc_control_msg *msg) {
case SC_CONTROL_MSG_TYPE_SET_CLIPBOARD: case SC_CONTROL_MSG_TYPE_SET_CLIPBOARD:
free(msg->set_clipboard.text); free(msg->set_clipboard.text);
break; break;
case SC_CONTROL_MSG_TYPE_START_APP:
free(msg->start_app.name);
break;
default: default:
// do nothing // do nothing
break; break;

View File

@ -35,14 +35,18 @@ enum sc_control_msg_type {
SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS, SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS,
SC_CONTROL_MSG_TYPE_GET_CLIPBOARD, SC_CONTROL_MSG_TYPE_GET_CLIPBOARD,
SC_CONTROL_MSG_TYPE_SET_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_ROTATE_DEVICE,
SC_CONTROL_MSG_TYPE_UHID_CREATE, SC_CONTROL_MSG_TYPE_UHID_CREATE,
SC_CONTROL_MSG_TYPE_UHID_INPUT, SC_CONTROL_MSG_TYPE_UHID_INPUT,
SC_CONTROL_MSG_TYPE_UHID_DESTROY, SC_CONTROL_MSG_TYPE_UHID_DESTROY,
SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS, 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 <https://android.googlesource.com/platform/frameworks/base.git/+/pie-release-2/core/java/android/view/SurfaceControl.java#305>
SC_SCREEN_POWER_MODE_OFF = 0,
SC_SCREEN_POWER_MODE_NORMAL = 2,
}; };
enum sc_copy_key { enum sc_copy_key {
@ -90,8 +94,8 @@ struct sc_control_msg {
bool paste; bool paste;
} set_clipboard; } set_clipboard;
struct { struct {
bool on; enum sc_screen_power_mode mode;
} set_display_power; } set_screen_power_mode;
struct { struct {
uint16_t id; uint16_t id;
const char *name; // pointer to static data const char *name; // pointer to static data
@ -106,9 +110,6 @@ struct sc_control_msg {
struct { struct {
uint16_t id; uint16_t id;
} uhid_destroy; } uhid_destroy;
struct {
char *name;
} start_app;
}; };
}; };

View File

@ -5,7 +5,7 @@
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#include <SDL2/SDL_events.h> #include <SDL_events.h>
enum { enum {
SC_EVENT_NEW_FRAME = SDL_USEREVENT, SC_EVENT_NEW_FRAME = SDL_USEREVENT,

View File

@ -203,12 +203,13 @@ set_device_clipboard(struct sc_input_manager *im, bool paste,
} }
static void 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); assert(im->controller);
struct sc_control_msg msg; struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER; msg.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE;
msg.set_display_power.on = on; msg.set_screen_power_mode.mode = mode;
if (!sc_controller_push_msg(im->controller, &msg)) { if (!sc_controller_push_msg(im->controller, &msg)) {
LOGW("Could not request 'set screen power mode'"); LOGW("Could not request 'set screen power mode'");
@ -284,18 +285,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 static void
apply_orientation_transform(struct sc_input_manager *im, apply_orientation_transform(struct sc_input_manager *im,
enum sc_orientation transform) { enum sc_orientation transform) {
@ -426,8 +415,10 @@ sc_input_manager_process_key(struct sc_input_manager *im,
return; return;
case SDLK_o: case SDLK_o:
if (control && !repeat && down && !paused) { if (control && !repeat && down && !paused) {
bool on = shift; enum sc_screen_power_mode mode = shift
set_display_power(im, on); ? SC_SCREEN_POWER_MODE_NORMAL
: SC_SCREEN_POWER_MODE_OFF;
set_screen_power_mode(im, mode);
} }
return; return;
case SDLK_z: case SDLK_z:
@ -533,12 +524,8 @@ sc_input_manager_process_key(struct sc_input_manager *im,
} }
return; return;
case SDLK_r: case SDLK_r:
if (control && !repeat && down && !paused) { if (control && !shift && !repeat && down && !paused) {
if (shift) { rotate_device(im);
reset_video(im);
} else {
rotate_device(im);
}
} }
return; return;
case SDLK_k: case SDLK_k:

View File

@ -92,8 +92,8 @@ sc_mouse_capture_set_active(struct sc_mouse_capture *mc, bool capture) {
SDL_GetGlobalMouseState(&mouse_x, &mouse_y); SDL_GetGlobalMouseState(&mouse_x, &mouse_y);
int x, y, w, h; int x, y, w, h;
SDL_GetWindowPosition(mc->window, &x, &y); SDL_GetWindowPosition(window, &x, &y);
SDL_GetWindowSize(mc->window, &w, &h); SDL_GetWindowSize(window, &w, &h);
bool outside_window = mouse_x < x || mouse_x >= x + w bool outside_window = mouse_x < x || mouse_x >= x + w
|| mouse_y < y || mouse_y >= y + h; || mouse_y < y || mouse_y >= y + h;

View File

@ -50,8 +50,7 @@ const struct scrcpy_options scrcpy_options_default = {
.video_bit_rate = 0, .video_bit_rate = 0,
.audio_bit_rate = 0, .audio_bit_rate = 0,
.max_fps = NULL, .max_fps = NULL,
.capture_orientation = SC_ORIENTATION_0, .lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED,
.capture_orientation_lock = SC_ORIENTATION_UNLOCKED,
.display_orientation = SC_ORIENTATION_0, .display_orientation = SC_ORIENTATION_0,
.record_orientation = SC_ORIENTATION_0, .record_orientation = SC_ORIENTATION_0,
.window_x = SC_WINDOW_POSITION_UNDEFINED, .window_x = SC_WINDOW_POSITION_UNDEFINED,
@ -59,11 +58,10 @@ const struct scrcpy_options scrcpy_options_default = {
.window_width = 0, .window_width = 0,
.window_height = 0, .window_height = 0,
.display_id = 0, .display_id = 0,
.video_buffer = 0, .display_buffer = 0,
.audio_buffer = -1, // depends on the audio format, .audio_buffer = -1, // depends on the audio format,
.audio_output_buffer = SC_TICK_FROM_MS(5), .audio_output_buffer = SC_TICK_FROM_MS(5),
.time_limit = 0, .time_limit = 0,
.screen_off_timeout = -1,
#ifdef HAVE_V4L2 #ifdef HAVE_V4L2
.v4l2_device = NULL, .v4l2_device = NULL,
.v4l2_buffer = 0, .v4l2_buffer = 0,
@ -105,10 +103,6 @@ const struct scrcpy_options scrcpy_options_default = {
.window = true, .window = true,
.mouse_hover = true, .mouse_hover = true,
.audio_dup = false, .audio_dup = false,
.new_display = NULL,
.start_app = NULL,
.angle = NULL,
.vd_system_decorations = true,
}; };
enum sc_orientation enum sc_orientation

View File

@ -84,12 +84,6 @@ enum sc_orientation { // v v v
SC_ORIENTATION_FLIP_270, // 1 1 1 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
};
static inline bool static inline bool
sc_orientation_is_mirror(enum sc_orientation orientation) { sc_orientation_is_mirror(enum sc_orientation orientation) {
assert(!(orientation & ~7)); assert(!(orientation & ~7));
@ -136,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 { enum sc_keyboard_input_mode {
SC_KEYBOARD_INPUT_MODE_AUTO, SC_KEYBOARD_INPUT_MODE_AUTO,
SC_KEYBOARD_INPUT_MODE_UHID_OR_AOA, // normal vs otg mode SC_KEYBOARD_INPUT_MODE_UHID_OR_AOA, // normal vs otg mode
@ -247,9 +251,7 @@ struct scrcpy_options {
uint32_t video_bit_rate; uint32_t video_bit_rate;
uint32_t audio_bit_rate; uint32_t audio_bit_rate;
const char *max_fps; // float to be parsed by the server const char *max_fps; // float to be parsed by the server
const char *angle; // float to be parsed by the server enum sc_lock_video_orientation lock_video_orientation;
enum sc_orientation capture_orientation;
enum sc_orientation_lock capture_orientation_lock;
enum sc_orientation display_orientation; enum sc_orientation display_orientation;
enum sc_orientation record_orientation; enum sc_orientation record_orientation;
int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto" int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto"
@ -257,11 +259,10 @@ struct scrcpy_options {
uint16_t window_width; uint16_t window_width;
uint16_t window_height; uint16_t window_height;
uint32_t display_id; uint32_t display_id;
sc_tick video_buffer; sc_tick display_buffer;
sc_tick audio_buffer; sc_tick audio_buffer;
sc_tick audio_output_buffer; sc_tick audio_output_buffer;
sc_tick time_limit; sc_tick time_limit;
sc_tick screen_off_timeout;
#ifdef HAVE_V4L2 #ifdef HAVE_V4L2
const char *v4l2_device; const char *v4l2_device;
sc_tick v4l2_buffer; sc_tick v4l2_buffer;
@ -303,14 +304,10 @@ struct scrcpy_options {
#define SC_OPTION_LIST_DISPLAYS 0x2 #define SC_OPTION_LIST_DISPLAYS 0x2
#define SC_OPTION_LIST_CAMERAS 0x4 #define SC_OPTION_LIST_CAMERAS 0x4
#define SC_OPTION_LIST_CAMERA_SIZES 0x8 #define SC_OPTION_LIST_CAMERA_SIZES 0x8
#define SC_OPTION_LIST_APPS 0x10
uint8_t list; uint8_t list;
bool window; bool window;
bool mouse_hover; bool mouse_hover;
bool audio_dup; bool audio_dup;
const char *new_display; // [<width>x<height>][/<dpi>] parsed by the server
const char *start_app;
bool vd_system_decorations;
}; };
extern const struct scrcpy_options scrcpy_options_default; extern const struct scrcpy_options scrcpy_options_default;

View File

@ -143,14 +143,8 @@ sc_recorder_open_output_file(struct sc_recorder *recorder) {
return false; return false;
} }
char *file_url = sc_str_concat("file:", recorder->filename); int ret = avio_open(&recorder->ctx->pb, recorder->filename,
if (!file_url) { AVIO_FLAG_WRITE);
avformat_free_context(recorder->ctx);
return false;
}
int ret = avio_open(&recorder->ctx->pb, file_url, AVIO_FLAG_WRITE);
free(file_url);
if (ret < 0) { if (ret < 0) {
LOGE("Failed to open output file: %s", recorder->filename); LOGE("Failed to open output file: %s", recorder->filename);
avformat_free_context(recorder->ctx); avformat_free_context(recorder->ctx);

View File

@ -53,7 +53,7 @@ struct scrcpy {
struct sc_decoder video_decoder; struct sc_decoder video_decoder;
struct sc_decoder audio_decoder; struct sc_decoder audio_decoder;
struct sc_recorder recorder; struct sc_recorder recorder;
struct sc_delay_buffer video_buffer; struct sc_delay_buffer display_buffer;
#ifdef HAVE_V4L2 #ifdef HAVE_V4L2
struct sc_v4l2_sink v4l2_sink; struct sc_v4l2_sink v4l2_sink;
struct sc_delay_buffer v4l2_buffer; struct sc_delay_buffer v4l2_buffer;
@ -428,13 +428,9 @@ scrcpy(struct scrcpy_options *options) {
.video_bit_rate = options->video_bit_rate, .video_bit_rate = options->video_bit_rate,
.audio_bit_rate = options->audio_bit_rate, .audio_bit_rate = options->audio_bit_rate,
.max_fps = options->max_fps, .max_fps = options->max_fps,
.angle = options->angle, .lock_video_orientation = options->lock_video_orientation,
.screen_off_timeout = options->screen_off_timeout,
.capture_orientation = options->capture_orientation,
.capture_orientation_lock = options->capture_orientation_lock,
.control = options->control, .control = options->control,
.display_id = options->display_id, .display_id = options->display_id,
.new_display = options->new_display,
.video = options->video, .video = options->video,
.audio = options->audio, .audio = options->audio,
.audio_dup = options->audio_dup, .audio_dup = options->audio_dup,
@ -458,7 +454,6 @@ scrcpy(struct scrcpy_options *options) {
.power_on = options->power_on, .power_on = options->power_on,
.kill_adb_on_close = options->kill_adb_on_close, .kill_adb_on_close = options->kill_adb_on_close,
.camera_high_speed = options->camera_high_speed, .camera_high_speed = options->camera_high_speed,
.vd_system_decorations = options->vd_system_decorations,
.list = options->list, .list = options->list,
}; };
@ -819,11 +814,11 @@ aoa_complete:
if (options->video_playback) { if (options->video_playback) {
struct sc_frame_source *src = &s->video_decoder.frame_source; struct sc_frame_source *src = &s->video_decoder.frame_source;
if (options->video_buffer) { if (options->display_buffer) {
sc_delay_buffer_init(&s->video_buffer, sc_delay_buffer_init(&s->display_buffer,
options->video_buffer, true); options->display_buffer, true);
sc_frame_source_add_sink(src, &s->video_buffer.frame_sink); sc_frame_source_add_sink(src, &s->display_buffer.frame_sink);
src = &s->video_buffer.frame_source; src = &s->display_buffer.frame_source;
} }
sc_frame_source_add_sink(src, &s->screen.frame_sink); sc_frame_source_add_sink(src, &s->screen.frame_sink);
@ -877,11 +872,11 @@ aoa_complete:
// everything is set up // everything is set up
if (options->control && options->turn_screen_off) { if (options->control && options->turn_screen_off) {
struct sc_control_msg msg; struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER; msg.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE;
msg.set_display_power.on = false; msg.set_screen_power_mode.mode = SC_SCREEN_POWER_MODE_OFF;
if (!sc_controller_push_msg(&s->controller, &msg)) { if (!sc_controller_push_msg(&s->controller, &msg)) {
LOGW("Could not request 'set display power'"); LOGW("Could not request 'set screen power mode'");
} }
} }
@ -911,25 +906,6 @@ aoa_complete:
init_sdl_gamepads(); 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); ret = event_loop(s);
terminate_event_loop(); terminate_event_loop();
LOGD("quit..."); LOGD("quit...");

View File

@ -66,6 +66,56 @@ get_server_path(void) {
return server_path; 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 static bool
push_server(struct sc_intr *intr, const char *serial) { push_server(struct sc_intr *intr, const char *serial) {
char *server_path = get_server_path(); char *server_path = get_server_path();
@ -201,31 +251,18 @@ execute_server(struct sc_server *server,
cmd[count++] = "app_process"; cmd[count++] = "app_process";
#ifdef SERVER_DEBUGGER #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" # define SERVER_DEBUGGER_PORT "5005"
const char *dbg; cmd[count++] =
if (sdk_version < 28) { # ifdef SERVER_DEBUGGER_METHOD_NEW
// Android < 9 /* Android 9 and above */
dbg = "-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address=" "-XjdwpProvider:internal -XjdwpOptions:transport=dt_socket,suspend=y,"
SERVER_DEBUGGER_PORT; "server=y,address="
} else if (sdk_version < 30) { # else
// Android >= 9 && Android < 11 /* Android 8 and below */
dbg = "-XjdwpProvider:internal -XjdwpOptions:transport=dt_socket," "-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address="
"suspend=y,server=y,address=" SERVER_DEBUGGER_PORT; # endif
} else { SERVER_DEBUGGER_PORT;
// Android >= 11
// Contrary to the other methods, this does not suspend on start.
// <https://github.com/Genymobile/scrcpy/pull/5466>
dbg = "-XjdwpProvider:adbconnection";
}
cmd[count++] = dbg;
#endif #endif
cmd[count++] = "/"; // unused cmd[count++] = "/"; // unused
cmd[count++] = "com.genymobile.scrcpy.Server"; cmd[count++] = "com.genymobile.scrcpy.Server";
cmd[count++] = SCRCPY_VERSION; cmd[count++] = SCRCPY_VERSION;
@ -287,21 +324,9 @@ execute_server(struct sc_server *server,
VALIDATE_STRING(params->max_fps); VALIDATE_STRING(params->max_fps);
ADD_PARAM("max_fps=%s", params->max_fps); ADD_PARAM("max_fps=%s", params->max_fps);
} }
if (params->angle) { if (params->lock_video_orientation != SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) {
VALIDATE_STRING(params->angle); ADD_PARAM("lock_video_orientation=%" PRIi8,
ADD_PARAM("angle=%s", params->angle); params->lock_video_orientation);
}
if (params->capture_orientation_lock != SC_ORIENTATION_UNLOCKED
|| params->capture_orientation != SC_ORIENTATION_0) {
if (params->capture_orientation_lock == SC_ORIENTATION_LOCKED_INITIAL) {
ADD_PARAM("capture_orientation=@");
} else {
const char *orient =
sc_orientation_get_name(params->capture_orientation);
bool locked =
params->capture_orientation_lock != SC_ORIENTATION_UNLOCKED;
ADD_PARAM("capture_orientation=%s%s", locked ? "@" : "", orient);
}
} }
if (server->tunnel.forward) { if (server->tunnel.forward) {
ADD_PARAM("tunnel_forward=true"); ADD_PARAM("tunnel_forward=true");
@ -345,11 +370,6 @@ execute_server(struct sc_server *server,
if (params->stay_awake) { if (params->stay_awake) {
ADD_PARAM("stay_awake=true"); 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) { if (params->video_codec_options) {
VALIDATE_STRING(params->video_codec_options); VALIDATE_STRING(params->video_codec_options);
ADD_PARAM("video_codec_options=%s", params->video_codec_options); ADD_PARAM("video_codec_options=%s", params->video_codec_options);
@ -385,13 +405,6 @@ execute_server(struct sc_server *server,
// By default, power_on is true // By default, power_on is true
ADD_PARAM("power_on=false"); ADD_PARAM("power_on=false");
} }
if (params->new_display) {
VALIDATE_STRING(params->new_display);
ADD_PARAM("new_display=%s", params->new_display);
}
if (!params->vd_system_decorations) {
ADD_PARAM("vd_system_decorations=false");
}
if (params->list & SC_OPTION_LIST_ENCODERS) { if (params->list & SC_OPTION_LIST_ENCODERS) {
ADD_PARAM("list_encoders=true"); ADD_PARAM("list_encoders=true");
} }
@ -404,23 +417,16 @@ execute_server(struct sc_server *server,
if (params->list & SC_OPTION_LIST_CAMERA_SIZES) { if (params->list & SC_OPTION_LIST_CAMERA_SIZES) {
ADD_PARAM("list_camera_sizes=true"); ADD_PARAM("list_camera_sizes=true");
} }
if (params->list & SC_OPTION_LIST_APPS) {
ADD_PARAM("list_apps=true");
}
#undef ADD_PARAM #undef ADD_PARAM
cmd[count++] = NULL; cmd[count++] = NULL;
#ifdef SERVER_DEBUGGER #ifdef SERVER_DEBUGGER
LOGI("Server debugger listening%s...", LOGI("Server debugger waiting for a client on device port "
sdk_version < 30 ? " on port " SERVER_DEBUGGER_PORT : ""); SERVER_DEBUGGER_PORT "...");
// For Android < 11, from the computer: // From the computer, run
// - run `adb forward tcp:5005 tcp:5005` // adb forward tcp:5005 tcp:5005
// For Android >= 11:
// - execute `adb jdwp` to get the jdwp port
// - run `adb forward tcp:5005 jdwp:XXXX` (replace XXXX)
//
// Then, from Android Studio: Run > Debug > Edit configurations... // Then, from Android Studio: Run > Debug > Edit configurations...
// On the left, click on '+', "Remote", with: // On the left, click on '+', "Remote", with:
// Host: localhost // Host: localhost
@ -493,18 +499,22 @@ connect_to_server(struct sc_server *server, unsigned attempts, sc_tick delay,
bool bool
sc_server_init(struct sc_server *server, const struct sc_server_params *params, sc_server_init(struct sc_server *server, const struct sc_server_params *params,
const struct sc_server_callbacks *cbs, void *cbs_userdata) { const struct sc_server_callbacks *cbs, void *cbs_userdata) {
// The allocated data in params (const char *) must remain valid until the bool ok = sc_server_params_copy(&server->params, params);
// end of the program
server->params = *params;
bool ok = sc_mutex_init(&server->mutex);
if (!ok) { if (!ok) {
LOG_OOM();
return false;
}
ok = sc_mutex_init(&server->mutex);
if (!ok) {
sc_server_params_destroy(&server->params);
return false; return false;
} }
ok = sc_cond_init(&server->cond_stopped); ok = sc_cond_init(&server->cond_stopped);
if (!ok) { if (!ok) {
sc_mutex_destroy(&server->mutex); sc_mutex_destroy(&server->mutex);
sc_server_params_destroy(&server->params);
return false; return false;
} }
@ -512,6 +522,7 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params,
if (!ok) { if (!ok) {
sc_cond_destroy(&server->cond_stopped); sc_cond_destroy(&server->cond_stopped);
sc_mutex_destroy(&server->mutex); sc_mutex_destroy(&server->mutex);
sc_server_params_destroy(&server->params);
return false; return false;
} }
@ -1150,6 +1161,7 @@ sc_server_destroy(struct sc_server *server) {
free(server->serial); free(server->serial);
free(server->device_socket_name); free(server->device_socket_name);
sc_server_params_destroy(&server->params);
sc_intr_destroy(&server->intr); sc_intr_destroy(&server->intr);
sc_cond_destroy(&server->cond_stopped); sc_cond_destroy(&server->cond_stopped);
sc_mutex_destroy(&server->mutex); sc_mutex_destroy(&server->mutex);

View File

@ -45,13 +45,9 @@ struct sc_server_params {
uint32_t video_bit_rate; uint32_t video_bit_rate;
uint32_t audio_bit_rate; uint32_t audio_bit_rate;
const char *max_fps; // float to be parsed by the server const char *max_fps; // float to be parsed by the server
const char *angle; // float to be parsed by the server int8_t lock_video_orientation;
sc_tick screen_off_timeout;
enum sc_orientation capture_orientation;
enum sc_orientation_lock capture_orientation_lock;
bool control; bool control;
uint32_t display_id; uint32_t display_id;
const char *new_display;
bool video; bool video;
bool audio; bool audio;
bool audio_dup; bool audio_dup;
@ -69,7 +65,6 @@ struct sc_server_params {
bool power_on; bool power_on;
bool kill_adb_on_close; bool kill_adb_on_close;
bool camera_high_speed; bool camera_high_speed;
bool vd_system_decorations;
uint8_t list; uint8_t list;
}; };

View File

@ -64,26 +64,6 @@ sc_str_quote(const char *src) {
return quoted; 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 bool
sc_str_parse_integer(const char *s, long *out) { sc_str_parse_integer(const char *s, long *out) {
char *endptr; char *endptr;

View File

@ -38,15 +38,6 @@ sc_str_join(char *dst, const char *const tokens[], char sep, size_t n);
char * char *
sc_str_quote(const char *src); 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` * Parse `s` as an integer into `out`
* *

View File

@ -10,14 +10,14 @@ typedef int64_t sc_tick;
#define SC_TICK_FREQ 1000000 // microsecond #define SC_TICK_FREQ 1000000 // microsecond
// To be adapted if SC_TICK_FREQ changes // To be adapted if SC_TICK_FREQ changes
#define SC_TICK_TO_NS(tick) ((sc_tick) (tick) * 1000) #define SC_TICK_TO_NS(tick) ((tick) * 1000)
#define SC_TICK_TO_US(tick) ((sc_tick) tick) #define SC_TICK_TO_US(tick) (tick)
#define SC_TICK_TO_MS(tick) ((sc_tick) (tick) / 1000) #define SC_TICK_TO_MS(tick) ((tick) / 1000)
#define SC_TICK_TO_SEC(tick) ((sc_tick) (tick) / 1000000) #define SC_TICK_TO_SEC(tick) ((tick) / 1000000)
#define SC_TICK_FROM_NS(ns) ((sc_tick) (ns) / 1000) #define SC_TICK_FROM_NS(ns) ((ns) / 1000)
#define SC_TICK_FROM_US(us) ((sc_tick) us) #define SC_TICK_FROM_US(us) (us)
#define SC_TICK_FROM_MS(ms) ((sc_tick) (ms) * 1000) #define SC_TICK_FROM_MS(ms) ((ms) * 1000)
#define SC_TICK_FROM_SEC(sec) ((sc_tick) (sec) * 1000000) #define SC_TICK_FROM_SEC(sec) ((sec) * 1000000)
sc_tick sc_tick
sc_tick_now(void); sc_tick_now(void);

View File

@ -62,7 +62,6 @@ void
sc_timeout_stop(struct sc_timeout *timeout) { sc_timeout_stop(struct sc_timeout *timeout) {
sc_mutex_lock(&timeout->mutex); sc_mutex_lock(&timeout->mutex);
timeout->stopped = true; timeout->stopped = true;
sc_cond_signal(&timeout->cond);
sc_mutex_unlock(&timeout->mutex); sc_mutex_unlock(&timeout->mutex);
} }

View File

@ -51,6 +51,7 @@ static void test_options(void) {
"--fullscreen", "--fullscreen",
"--max-fps", "30", "--max-fps", "30",
"--max-size", "1024", "--max-size", "1024",
"--lock-video-orientation=2", // optional arguments require '='
// "--no-control" is not compatible with "--turn-screen-off" // "--no-control" is not compatible with "--turn-screen-off"
// "--no-playback" is not compatible with "--fulscreen" // "--no-playback" is not compatible with "--fulscreen"
"--port", "1234:1236", "--port", "1234:1236",
@ -79,6 +80,7 @@ static void test_options(void) {
assert(opts->fullscreen); assert(opts->fullscreen);
assert(!strcmp(opts->max_fps, "30")); assert(!strcmp(opts->max_fps, "30"));
assert(opts->max_size == 1024); assert(opts->max_size == 1024);
assert(opts->lock_video_orientation == 2);
assert(opts->port_range.first == 1234); assert(opts->port_range.first == 1234);
assert(opts->port_range.last == 1236); assert(opts->port_range.last == 1236);
assert(!strcmp(opts->push_target, "/sdcard/Movies")); assert(!strcmp(opts->push_target, "/sdcard/Movies"));

View File

@ -289,11 +289,11 @@ static void test_serialize_set_clipboard_long(void) {
assert(!memcmp(buf, expected, sizeof(expected))); 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 = { struct sc_control_msg msg = {
.type = SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER, .type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE,
.set_display_power = { .set_screen_power_mode = {
.on = true, .mode = SC_SCREEN_POWER_MODE_NORMAL,
}, },
}; };
@ -302,8 +302,8 @@ static void test_serialize_set_display_power(void) {
assert(size == 2); assert(size == 2);
const uint8_t expected[] = { const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER, SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE,
0x01, // true 0x02, // SC_SCREEN_POWER_MODE_NORMAL
}; };
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
} }
@ -407,21 +407,6 @@ static void test_serialize_open_hard_keyboard(void) {
assert(!memcmp(buf, expected, sizeof(expected))); 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[]) { int main(int argc, char *argv[]) {
(void) argc; (void) argc;
(void) argv; (void) argv;
@ -438,12 +423,11 @@ int main(int argc, char *argv[]) {
test_serialize_get_clipboard(); test_serialize_get_clipboard();
test_serialize_set_clipboard(); test_serialize_set_clipboard();
test_serialize_set_clipboard_long(); test_serialize_set_clipboard_long();
test_serialize_set_display_power(); test_serialize_set_screen_power_mode();
test_serialize_rotate_device(); test_serialize_rotate_device();
test_serialize_uhid_create(); test_serialize_uhid_create();
test_serialize_uhid_input(); test_serialize_uhid_input();
test_serialize_uhid_destroy(); test_serialize_uhid_destroy();
test_serialize_open_hard_keyboard(); test_serialize_open_hard_keyboard();
test_serialize_reset_video();
return 0; return 0;
} }

View File

@ -141,16 +141,6 @@ static void test_quote(void) {
free(out); free(out);
} }
static void test_concat(void) {
const char *s = "2024:11";
char *out = sc_str_concat("my-prefix:", s);
// contains the concat
assert(!strcmp("my-prefix:2024:11", out));
free(out);
}
static void test_utf8_truncate(void) { static void test_utf8_truncate(void) {
const char *s = "aÉbÔc"; const char *s = "aÉbÔc";
assert(strlen(s) == 7); // É and Ô are 2 bytes-wide 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_before_sep();
test_join_truncated_after_sep(); test_join_truncated_after_sep();
test_quote(); test_quote();
test_concat();
test_utf8_truncate(); test_utf8_truncate();
test_parse_integer(); test_parse_integer();
test_parse_integers(); test_parse_integers();

View File

@ -7,7 +7,7 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { 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 // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files

View File

@ -170,7 +170,7 @@ latency (for both [video](video.md#buffering) and audio) might be preferable to
avoid glitches and smooth the playback: 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), It is also possible to configure another audio buffer (the audio output buffer),

View File

@ -77,7 +77,7 @@ pip3 install meson
sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm
# client build dependencies # 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 # server build dependencies
sudo dnf install java-devel sudo dnf install java-devel
@ -233,10 +233,10 @@ install` must be run as root)._
#### Option 2: Use prebuilt server #### Option 2: Use prebuilt server
- [`scrcpy-server-v3.0`][direct-scrcpy-server] - [`scrcpy-server-v2.7`][direct-scrcpy-server]
<sub>SHA-256: `800044c62a94d5fc16f5ab9c86d45b1050eae3eb436514d1b0d2fe2646b894ea`</sub> <sub>SHA-256: `a23c5659f36c260f105c022d27bcb3eafffa26070e7baa9eda66d01377a1adba`</sub>
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-server-v3.0 [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 Download the prebuilt server somewhere, and specify its path during the Meson
configuration: configuration:

View File

@ -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) 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 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 decodes the video frames, and displays them as soon as possible, without
buffering (unless `--video-buffer=delay` is specified) to minimize latency. The buffering (unless `--display-buffer=delay` is specified) to minimize latency.
client is not aware of the device rotation (which is handled by the server), it The client is not aware of the device rotation (which is handled by the server),
just knows the dimensions of the video frames it receives. 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 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 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 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: Redirect that port to the computer:
```bash ```bash
adb forward tcp:5005 tcp:5005 adb forward tcp:5005 tcp:5005
``` ```
For Android >= 11, first find the listening port: In Android Studio, _Run_ > _Debug_ > _Edit configurations..._ On the left, click on
`+`, _Remote_, and fill the form:
```bash
adb jdwp
# press Ctrl+C to interrupt
```
Then redirect the resulting PID:
```bash
adb forward tcp:5005 jdwp:XXXX # replace XXXX
```
In Android Studio, _Run_ > _Debug_ > _Edit configurations..._ On the left, click
on `+`, _Remote_, and fill the form:
- Host: `localhost` - Host: `localhost`
- Port: `5005` - Port: `5005`

View File

@ -18,21 +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), If the device is not plugged in (i.e. only connected over TCP/IP),
`--stay-awake` has no effect (this is the Android behavior). `--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
```
## Turn screen off ## Turn screen off
@ -61,40 +46,6 @@ scrcpy --turn-screen-off --stay-awake
scrcpy -Sw # short version 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
```
## 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.
## Show touches ## Show touches
@ -111,16 +62,6 @@ scrcpy -t # short version
Note that it only shows _physical_ touches (by a finger on the device). 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 ## Power off on close
@ -137,48 +78,3 @@ By default, on start, the device is powered on. To prevent this behavior:
```bash ```bash
scrcpy --no-power-on 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
```

View File

@ -2,23 +2,6 @@
## Install ## Install
### From the official release
Download a static build of the [latest release]:
- [`scrcpy-linux-v3.0.tar.gz`][direct-linux] (x86_64)
<sub>SHA-256: `06cb74e22f758228c944cea048b78e42b2925c2affe2b5aca901cfd6a649e503`</sub>
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
[direct-linux]: https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-linux-v3.0.tar.gz
and extract it.
_Static builds of scrcpy for Linux are still experimental._
### From your package manager
<a href="https://repology.org/project/scrcpy/versions"><img src="https://repology.org/badge/vertical-allrepos/scrcpy.svg" alt="Packaging status" align="right"></a> <a href="https://repology.org/project/scrcpy/versions"><img src="https://repology.org/badge/vertical-allrepos/scrcpy.svg" alt="Packaging status" align="right"></a>
Scrcpy is packaged in several distributions and package managers: Scrcpy is packaged in several distributions and package managers:
@ -30,10 +13,10 @@ Scrcpy is packaged in several distributions and package managers:
- Snap: `snap install scrcpy` - Snap: `snap install scrcpy`
- … (see [repology](https://repology.org/project/scrcpy/versions)) - … (see [repology](https://repology.org/project/scrcpy/versions))
### Latest version
### From an install script However, the packaged version is not always the latest release. To install the
latest release from `master`, follow this simplified process.
To install the latest release from `master`, follow this simplified process.
First, you need to install the required packages: First, you need to install the required packages:

View File

@ -2,23 +2,6 @@
## Install ## Install
### From the official release
Download a static build of the [latest release]:
- [`scrcpy-macos-v3.0.tar.gz`][direct-macos] (arm64)
<sub>SHA-256: `5db9821918537eb3aaf0333cdd05baf85babdd851972d5f1b71f86da0530b4bf`</sub>
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
[direct-macos]: https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-macos-v3.0.tar.gz
and extract it.
_Static builds of scrcpy for macOS are still experimental._
### From a package manager
Scrcpy is available in [Homebrew]: Scrcpy is available in [Homebrew]:
```bash ```bash
@ -30,7 +13,7 @@ brew install scrcpy
You need `adb`, accessible from your `PATH`. If you don't have it yet: You need `adb`, accessible from your `PATH`. If you don't have it yet:
```bash ```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: Alternatively, Scrcpy is also available in [MacPorts], which sets up `adb` for you:

View File

@ -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 In these modes, the computer mouse is "captured": the mouse pointer disappears
from the computer and appears on the Android device instead. from the computer and appears on the Android device instead.
The [shortcut mod](shortcuts.md) (either <kbd>Alt</kbd> or <kbd>Super</kbd> by Special capture keys, either <kbd>Alt</kbd> or <kbd>Super</kbd>, toggle
default) toggle (disable or enable) the mouse capture. Use one of them to give (disable or enable) the mouse capture. Use one of them to give the control of
the control of the mouse back to the computer. the mouse back to the computer.
### UHID ### UHID

View File

@ -30,7 +30,6 @@ _<kbd>[Super]</kbd> is typically the <kbd>Windows</kbd> or <kbd>Cmd</kbd> key._
| Flip display vertically | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd></kbd> _(up)_ \| <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd></kbd> _(down)_ | Flip display vertically | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd></kbd> _(up)_ \| <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd></kbd> _(down)_
| Pause or re-pause display | <kbd>MOD</kbd>+<kbd>z</kbd> | Pause or re-pause display | <kbd>MOD</kbd>+<kbd>z</kbd>
| Unpause display | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>z</kbd> | Unpause display | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>z</kbd>
| Reset video capture/encoding | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>r</kbd>
| Resize window to 1:1 (pixel-perfect) | <kbd>MOD</kbd>+<kbd>g</kbd> | Resize window to 1:1 (pixel-perfect) | <kbd>MOD</kbd>+<kbd>g</kbd>
| Resize window to remove black borders | <kbd>MOD</kbd>+<kbd>w</kbd> \| _Double-left-click¹_ | Resize window to remove black borders | <kbd>MOD</kbd>+<kbd>w</kbd> \| _Double-left-click¹_
| Click on `HOME` | <kbd>MOD</kbd>+<kbd>h</kbd> \| _Middle-click_ | Click on `HOME` | <kbd>MOD</kbd>+<kbd>h</kbd> \| _Middle-click_

View File

@ -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 If encoding fails, scrcpy automatically tries again with a lower definition
(unless `--no-downsize-on-error` is enabled). (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 ## Bit rate
@ -96,7 +93,7 @@ Sometimes, the default encoder may have issues or even crash, so it is useful to
try another one: try another one:
```bash ```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) <kbd>MOD</kbd>+<kbd>r</kbd> requests the - The [shortcut](shortcuts.md) <kbd>MOD</kbd>+<kbd>r</kbd> requests the
device to switch between portrait and landscape (the current running app may device to switch between portrait and landscape (the current running app may
refuse, if it does not support the requested orientation). 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 of the video sent from the device to the computer). This affects the
recording. recording.
- `--orientation` is applied on the client side, and affects display and - `--orientation` is applied on the client side, and affects display and
recording. For the display, it can be changed dynamically using recording. For the display, it can be changed dynamically using
[shortcuts](shortcuts.md). [shortcuts](shortcuts.md).
To capture the video with a specific orientation: To lock the mirroring orientation (on the capture side):
```bash ```bash
scrcpy --capture-orientation=0 scrcpy --lock-video-orientation # initial (current) orientation
scrcpy --capture-orientation=90 # 90° clockwise scrcpy --lock-video-orientation=0 # natural orientation
scrcpy --capture-orientation=180 # 180° scrcpy --lock-video-orientation=90 # 90° clockwise
scrcpy --capture-orientation=270 # 270° clockwise scrcpy --lock-video-orientation=180 # 180°
scrcpy --capture-orientation=flip0 # hflip scrcpy --lock-video-orientation=270 # 270° clockwise
scrcpy --capture-orientation=flip90 # hflip + 90° clockwise
scrcpy --capture-orientation=flip180 # hflip + 180°
scrcpy --capture-orientation=flip270 # hflip + 270° clockwise
``` ```
The capture orientation can be locked by using `@`, so that a physical device To orient the video (on the rendering side):
rotation does not change the captured video orientation:
```bash
scrcpy --capture-orientation=@ # locked to the initial orientation
scrcpy --capture-orientation=@0 # locked to 0°
scrcpy --capture-orientation=@90 # locked to 90° clockwise
scrcpy --capture-orientation=@180 # locked to 180°
scrcpy --capture-orientation=@270 # locked to 270° clockwise
scrcpy --capture-orientation=@flip0 # locked to hflip
scrcpy --capture-orientation=@flip90 # locked to hflip + 90° clockwise
scrcpy --capture-orientation=@flip180 # locked to hflip + 180°
scrcpy --capture-orientation=@flip270 # locked to hflip + 270° clockwise
```
The capture orientation transform is applied after `--crop`, but before
`--angle`.
To orient the video (on the client side):
```bash ```bash
scrcpy --orientation=0 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. 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 ## Crop
The device screen may be cropped to mirror only part of the screen. 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 The values are expressed in the device natural orientation (portrait for a
phone, landscape for a tablet). phone, landscape for a tablet).
Cropping is performed before `--capture-orientation` and `--angle`. If `--max-size` is also specified, resizing is applied after cropping.
For display mirroring, `--max-size` is applied after cropping. For camera,
`--max-size` is applied first (because it selects the source size rather than
resizing the content).
## Display ## Display
@ -216,8 +175,6 @@ scrcpy --list-displays
A secondary display may only be controlled if the device runs at least Android A secondary display may only be controlled if the device runs at least Android
10 (otherwise it is mirrored as read-only). 10 (otherwise it is mirrored as read-only).
It is also possible to create a [virtual display](virtual_display.md).
## Buffering ## Buffering
@ -232,15 +189,15 @@ The configuration is available independently for the display,
[v4l2 sinks](video.md#video4linux) and [audio](audio.md#buffering) playback. [v4l2 sinks](video.md#video4linux) and [audio](audio.md#buffering) playback.
```bash ```bash
scrcpy --video-buffer=50 # add 50ms buffering for video playback scrcpy --display-buffer=50 # add 50ms buffering for display
scrcpy --audio-buffer=200 # set 200ms buffering for audio playback
scrcpy --v4l2-buffer=300 # add 300ms buffering for v4l2 sink 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: They can be applied simultaneously:
```bash ```bash
scrcpy --video-buffer=50 --v4l2-buffer=300 scrcpy --display-buffer=50 --v4l2-buffer=300
``` ```

View File

@ -1,35 +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
```
## Start app
On some devices, a launcher is available in the virtual display.
When no launcher is available, 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
```
## System decorations
By default, virtual display system decorations are enabled. But some devices
might display a broken UI;
Use `--no-vd-system-decorations` to disable it.
Note that if no app is started, no content will be rendered, so no video frame
will be produced at all.

View File

@ -2,32 +2,27 @@
## Install ## Install
### From the official release
Download the [latest release]: Download the [latest release]:
- [`scrcpy-win64-v3.0.zip`][direct-win64] (64-bit) - [`scrcpy-win64-v2.7.zip`][direct-win64] (64-bit)
<sub>SHA-256: `dfbe8a8fef6535197acc506936bfd59d0aa0427e9b44fb2e5c550eae642f72be`</sub> <sub>SHA-256: `5910bc18d5a16f42d84185ddc7e16a4cee6a6f5f33451559c1a1d6d0099bd5f5`</sub>
- [`scrcpy-win32-v3.0.zip`][direct-win32] (32-bit) - [`scrcpy-win32-v2.7.zip`][direct-win32] (32-bit)
<sub>SHA-256: `7cbf8d7a6ebfdca7b3b161e29a481c11088305f3e0a89d28e8e62f70c7bd0028`</sub> <sub>SHA-256: `ef4daf89d500f33d78b830625536ecb18481429dd94433e7634c824292059d06`</sub>
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest [latest release]: https://github.com/Genymobile/scrcpy/releases/latest
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-win64-v3.0.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/v3.0/scrcpy-win32-v3.0.zip [direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-win32-v2.7.zip
and extract it. and extract it.
Alternatively, you could install it from packages manager, like [Chocolatey]:
### From a package manager
From [Chocolatey]:
```bash ```bash
choco install scrcpy choco install scrcpy
choco install adb # if you don't have it yet choco install adb # if you don't have it yet
``` ```
From [Scoop]: or [Scoop]:
```bash ```bash
@ -35,6 +30,7 @@ scoop install scrcpy
scoop install adb # if you don't have it yet scoop install adb # if you don't have it yet
``` ```
[Winget]: https://github.com/microsoft/winget-cli
[Chocolatey]: https://chocolatey.org/ [Chocolatey]: https://chocolatey.org/
[Scoop]: https://scoop.sh [Scoop]: https://scoop.sh

View File

@ -1,7 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
# https://gradle.org/release-checksums/
distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@ -2,8 +2,8 @@
set -e set -e
BUILDDIR=build-auto BUILDDIR=build-auto
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-server-v3.0 PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-server-v2.7
PREBUILT_SERVER_SHA256=800044c62a94d5fc16f5ab9c86d45b1050eae3eb436514d1b0d2fe2646b894ea PREBUILT_SERVER_SHA256=a23c5659f36c260f105c022d27bcb3eafffa26070e7baa9eda66d01377a1adba
echo "[scrcpy] Downloading prebuilt server..." echo "[scrcpy] Downloading prebuilt server..."
wget "$PREBUILT_SERVER_URL" -O scrcpy-server wget "$PREBUILT_SERVER_URL" -O scrcpy-server

View File

@ -1,5 +1,5 @@
project('scrcpy', 'c', project('scrcpy', 'c',
version: '3.0', version: '2.7',
meson_version: '>= 0.48', meson_version: '>= 0.48',
default_options: [ default_options: [
'c_std=c11', 'c_std=c11',

View File

@ -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('compile_server', type: 'boolean', value: true, description: 'Build the server')
option('prebuilt_server', type: 'string', description: 'Path of the prebuilt 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('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', 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('v4l2', type: 'boolean', value: true, description: 'Enable V4L2 feature when supported')
option('usb', type: 'boolean', value: true, description: 'Enable HID/OTG features when supported') option('usb', type: 'boolean', value: true, description: 'Enable HID/OTG features when supported')

131
release.mk Normal file
View File

@ -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)"

2
release.sh Executable file
View File

@ -0,0 +1,2 @@
#!/bin/bash
make -f release.mk

2
release/.gitignore vendored
View File

@ -1,2 +0,0 @@
/work
/output

View File

@ -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)}"

View File

@ -1,43 +0,0 @@
#!/bin/bash
set -ex
cd "$(dirname ${BASH_SOURCE[0]})"
. build_common
cd .. # root project dir
ARCH="$1"
if [[ $# != 1 ]]
then
echo "Syntax: $0 <arch>" >&2
exit 1
fi
LINUX_BUILD_DIR="$WORK_DIR/build-linux-$ARCH"
app/deps/adb_linux.sh
app/deps/sdl.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/scrcpy_bin"
cp app/data/icon.png "$LINUX_BUILD_DIR/dist/"
cp app/data/scrcpy_static_wrapper.sh "$LINUX_BUILD_DIR/dist/scrcpy"
cp app/scrcpy.1 "$LINUX_BUILD_DIR/dist/"
cp -r "$ADB_INSTALL_DIR"/. "$LINUX_BUILD_DIR/dist/"

View File

@ -1,43 +0,0 @@
#!/bin/bash
set -ex
cd "$(dirname ${BASH_SOURCE[0]})"
. build_common
cd .. # root project dir
ARCH="$1"
if [[ $# != 1 ]]
then
echo "Syntax: $0 <arch>" >&2
exit 1
fi
MACOS_BUILD_DIR="$WORK_DIR/build-macos-$ARCH"
app/deps/adb_macos.sh
app/deps/sdl.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/scrcpy_bin"
cp app/data/icon.png "$MACOS_BUILD_DIR/dist/"
cp app/data/scrcpy_static_wrapper.sh "$MACOS_BUILD_DIR/dist/scrcpy"
cp app/scrcpy.1 "$MACOS_BUILD_DIR/dist/"
cp -r "$ADB_INSTALL_DIR"/. "$MACOS_BUILD_DIR/dist/"

View File

@ -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"

View File

@ -1,52 +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/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/"

View File

@ -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"

View File

@ -1,52 +0,0 @@
#!/bin/bash
set -ex
cd "$(dirname ${BASH_SOURCE[0]})"
. build_common
cd .. # root project dir
if [[ $# != 2 ]]
then
# <target_name>: for example win64
# <format>: zip or tar.gz
echo "Syntax: $0 <target> <format>" >&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 cvf "$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'"

View File

@ -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'"

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -2,13 +2,13 @@ apply plugin: 'com.android.application'
android { android {
namespace 'com.genymobile.scrcpy' namespace 'com.genymobile.scrcpy'
compileSdk 35 compileSdk 34
defaultConfig { defaultConfig {
applicationId "com.genymobile.scrcpy" applicationId "com.genymobile.scrcpy"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 35 targetSdkVersion 34
versionCode 30000 versionCode 20700
versionName "3.0" versionName "2.7"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
} }
buildTypes { buildTypes {

View File

@ -12,11 +12,10 @@
set -e set -e
SCRCPY_DEBUG=false SCRCPY_DEBUG=false
SCRCPY_VERSION_NAME=3.0 SCRCPY_VERSION_NAME=2.7
PLATFORM=${ANDROID_PLATFORM:-35} PLATFORM=${ANDROID_PLATFORM:-34}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.0.0} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-34.0.0}
PLATFORM_TOOLS="$ANDROID_HOME/platforms/android-$PLATFORM"
BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS" BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS"
BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})" BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})"
@ -24,8 +23,7 @@ CLASSES_DIR="$BUILD_DIR/classes"
GEN_DIR="$BUILD_DIR/gen" GEN_DIR="$BUILD_DIR/gen"
SERVER_DIR=$(dirname "$0") SERVER_DIR=$(dirname "$0")
SERVER_BINARY=scrcpy-server SERVER_BINARY=scrcpy-server
ANDROID_JAR="$PLATFORM_TOOLS/android.jar" ANDROID_JAR="$ANDROID_HOME/platforms/android-$PLATFORM/android.jar"
ANDROID_AIDL="$PLATFORM_TOOLS/framework.aidl"
LAMBDA_JAR="$BUILD_TOOLS_DIR/core-lambda-stubs.jar" LAMBDA_JAR="$BUILD_TOOLS_DIR/core-lambda-stubs.jar"
echo "Platform: android-$PLATFORM" echo "Platform: android-$PLATFORM"
@ -47,24 +45,16 @@ EOF
echo "Generating java from aidl..." echo "Generating java from aidl..."
cd "$SERVER_DIR/src/main/aidl" cd "$SERVER_DIR/src/main/aidl"
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. android/view/IRotationWatcher.aidl "$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" android/view/IRotationWatcher.aidl
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. \ "$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" \
android/content/IOnPrimaryClipChangedListener.aidl android/content/IOnPrimaryClipChangedListener.aidl
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. android/view/IDisplayFoldListener.aidl "$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" android/view/IDisplayFoldListener.aidl
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. -p "$ANDROID_AIDL" \
android/view/IDisplayWindowListener.aidl
# Fake sources to expose hidden Android types to the project
FAKE_SRC=( \
android/content/*java \
)
SRC=( \ SRC=( \
com/genymobile/scrcpy/*.java \ com/genymobile/scrcpy/*.java \
com/genymobile/scrcpy/audio/*.java \ com/genymobile/scrcpy/audio/*.java \
com/genymobile/scrcpy/control/*.java \ com/genymobile/scrcpy/control/*.java \
com/genymobile/scrcpy/device/*.java \ com/genymobile/scrcpy/device/*.java \
com/genymobile/scrcpy/opengl/*.java \
com/genymobile/scrcpy/util/*.java \ com/genymobile/scrcpy/util/*.java \
com/genymobile/scrcpy/video/*.java \ com/genymobile/scrcpy/video/*.java \
com/genymobile/scrcpy/wrappers/*.java \ com/genymobile/scrcpy/wrappers/*.java \
@ -78,11 +68,10 @@ done
echo "Compiling java sources..." echo "Compiling java sources..."
cd ../java cd ../java
javac -encoding UTF-8 -bootclasspath "$ANDROID_JAR" \ javac -bootclasspath "$ANDROID_JAR" \
-cp "$LAMBDA_JAR:$GEN_DIR" \ -cp "$LAMBDA_JAR:$GEN_DIR" \
-d "$CLASSES_DIR" \ -d "$CLASSES_DIR" \
-source 1.8 -target 1.8 \ -source 1.8 -target 1.8 \
${FAKE_SRC[@]} \
${SRC[@]} ${SRC[@]}
echo "Dexing..." echo "Dexing..."

View File

@ -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<Rect> restricted, in List<Rect> unrestricted);
}

View File

@ -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
}

View File

@ -1,32 +0,0 @@
package com.genymobile.scrcpy;
import android.os.Build;
/**
* Android version code constants, done right.
* <p/>
* <a href="https://apilevels.com/">API levels</a>
*/
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;
}

View File

@ -5,8 +5,6 @@ import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.Settings; import com.genymobile.scrcpy.util.Settings;
import com.genymobile.scrcpy.util.SettingsException; import com.genymobile.scrcpy.util.SettingsException;
import android.os.BatteryManager;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
@ -18,132 +16,59 @@ import java.io.OutputStream;
*/ */
public final class CleanUp { public final class CleanUp {
// Dynamic options private static final int MSG_TYPE_MASK = 0b11;
private static final int PENDING_CHANGE_DISPLAY_POWER = 1 << 0; private static final int MSG_TYPE_RESTORE_STAY_ON = 0;
private int pendingChanges; private static final int MSG_TYPE_DISABLE_SHOW_TOUCHES = 1;
private boolean pendingRestoreDisplayPower; 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 static final int MSG_PARAM_SHIFT = 2;
private CleanUp(Options options) { private final OutputStream out;
thread = new Thread(() -> runCleanUp(options), "cleanup");
thread.start(); public CleanUp(OutputStream out) {
this.out = out;
} }
public static CleanUp start(Options options) { public static CleanUp configure(int displayId) throws IOException {
return new CleanUp(options); String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(displayId)};
}
public void interrupt() {
thread.interrupt();
}
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);
}
}
boolean powerOffScreen = options.getPowerOffScreenOnClose();
int displayId = options.getDisplayId();
try {
run(displayId, restoreStayOn, disableShowTouches, powerOffScreen, restoreScreenOffTimeout);
} catch (InterruptedException e) {
// ignore
} catch (IOException e) {
Ln.e("Clean up I/O exception", e);
}
}
private void run(int displayId, int restoreStayOn, boolean disableShowTouches, boolean powerOffScreen, int restoreScreenOffTimeout)
throws IOException, InterruptedException {
String[] cmd = {
"app_process",
"/",
CleanUp.class.getName(),
String.valueOf(displayId),
String.valueOf(restoreStayOn),
String.valueOf(disableShowTouches),
String.valueOf(powerOffScreen),
String.valueOf(restoreScreenOffTimeout),
};
ProcessBuilder builder = new ProcessBuilder(cmd); ProcessBuilder builder = new ProcessBuilder(cmd);
builder.environment().put("CLASSPATH", Server.SERVER_PATH); builder.environment().put("CLASSPATH", Server.SERVER_PATH);
Process process = builder.start(); Process process = builder.start();
OutputStream out = process.getOutputStream(); return new CleanUp(process.getOutputStream());
}
while (true) { private boolean sendMessage(int type, int param) {
int localPendingChanges; assert (type & ~MSG_TYPE_MASK) == 0;
boolean localPendingRestoreDisplayPower; int msg = type | param << MSG_PARAM_SHIFT;
synchronized (this) { try {
while (pendingChanges == 0) { out.write(msg);
wait(); out.flush();
} return true;
localPendingChanges = pendingChanges; } catch (IOException e) {
localPendingRestoreDisplayPower = pendingRestoreDisplayPower; Ln.w("Could not configure cleanup (type=" + type + ", param=" + param + ")", e);
pendingChanges = 0; return false;
}
if ((localPendingChanges & PENDING_CHANGE_DISPLAY_POWER) != 0) {
out.write(localPendingRestoreDisplayPower ? 1 : 0);
out.flush();
}
} }
} }
public synchronized void setRestoreDisplayPower(boolean restoreDisplayPower) { public boolean setRestoreStayOn(int restoreValue) {
pendingRestoreDisplayPower = restoreDisplayPower; // Restore the value (between 0 and 7), -1 to not restore
pendingChanges |= PENDING_CHANGE_DISPLAY_POWER; // <https://developer.android.com/reference/android/provider/Settings.Global#STAY_ON_WHILE_PLUGGED_IN>
notify(); 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() { public static void unlinkSelf() {
@ -158,21 +83,35 @@ public final class CleanUp {
unlinkSelf(); unlinkSelf();
int displayId = Integer.parseInt(args[0]); 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]);
// Dynamic option int restoreStayOn = -1;
boolean restoreDisplayPower = false; boolean disableShowTouches = false;
boolean restoreNormalPowerMode = false;
boolean powerOffScreen = false;
try { try {
// Wait for the server to die // Wait for the server to die
int msg; int msg;
while ((msg = System.in.read()) != -1) { while ((msg = System.in.read()) != -1) {
// Only restore display power int type = msg & MSG_TYPE_MASK;
assert msg == 0 || msg == 1; int param = msg >> MSG_PARAM_SHIFT;
restoreDisplayPower = msg != 0; 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) { } catch (IOException e) {
// Expected when the server is dead // Expected when the server is dead
@ -198,22 +137,13 @@ public final class CleanUp {
} }
} }
if (restoreScreenOffTimeout != -1) { if (Device.isScreenOn()) {
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 (displayId != Device.DISPLAY_ID_NONE && Device.isScreenOn(displayId)) {
if (powerOffScreen) { if (powerOffScreen) {
Ln.i("Power off screen"); Ln.i("Power off screen");
Device.powerOffScreen(displayId); Device.powerOffScreen(displayId);
} else if (restoreDisplayPower) { } else if (restoreNormalPowerMode) {
Ln.i("Restoring display power"); Ln.i("Restoring normal power mode");
Device.setDisplayPower(displayId, true); Device.setScreenPowerMode(Device.POWER_MODE_NORMAL);
} }
} }

View File

@ -1,14 +1,10 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.AttributionSource; import android.content.AttributionSource;
import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.ContextWrapper; import android.content.ContextWrapper;
import android.content.IContentProvider; import android.os.Build;
import android.os.Binder;
import android.os.Process; import android.os.Process;
public final class FakeContext extends ContextWrapper { public final class FakeContext extends ContextWrapper {
@ -22,38 +18,6 @@ public final class FakeContext extends ContextWrapper {
return INSTANCE; 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() { private FakeContext() {
super(Workarounds.getSystemContext()); super(Workarounds.getSystemContext());
} }
@ -68,7 +32,7 @@ public final class FakeContext extends ContextWrapper {
return PACKAGE_NAME; return PACKAGE_NAME;
} }
@TargetApi(AndroidVersions.API_31_ANDROID_12) @TargetApi(Build.VERSION_CODES.S)
@Override @Override
public AttributionSource getAttributionSource() { public AttributionSource getAttributionSource() {
AttributionSource.Builder builder = new AttributionSource.Builder(Process.SHELL_UID); AttributionSource.Builder builder = new AttributionSource.Builder(Process.SHELL_UID);
@ -86,9 +50,4 @@ public final class FakeContext extends ContextWrapper {
public Context getApplicationContext() { public Context getApplicationContext() {
return this; return this;
} }
@Override
public ContentResolver getContentResolver() {
return contentResolver;
}
} }

View File

@ -2,9 +2,6 @@ package com.genymobile.scrcpy;
import com.genymobile.scrcpy.audio.AudioCodec; import com.genymobile.scrcpy.audio.AudioCodec;
import com.genymobile.scrcpy.audio.AudioSource; 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.device.Size;
import com.genymobile.scrcpy.util.CodecOption; import com.genymobile.scrcpy.util.CodecOption;
import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.Ln;
@ -14,7 +11,6 @@ import com.genymobile.scrcpy.video.VideoCodec;
import com.genymobile.scrcpy.video.VideoSource; import com.genymobile.scrcpy.video.VideoSource;
import android.graphics.Rect; import android.graphics.Rect;
import android.util.Pair;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
@ -34,7 +30,7 @@ public class Options {
private int videoBitRate = 8000000; private int videoBitRate = 8000000;
private int audioBitRate = 128000; private int audioBitRate = 128000;
private float maxFps; private float maxFps;
private float angle; private int lockVideoOrientation = -1;
private boolean tunnelForward; private boolean tunnelForward;
private Rect crop; private Rect crop;
private boolean control = true; private boolean control = true;
@ -47,7 +43,6 @@ public class Options {
private boolean cameraHighSpeed; private boolean cameraHighSpeed;
private boolean showTouches; private boolean showTouches;
private boolean stayAwake; private boolean stayAwake;
private int screenOffTimeout = -1;
private List<CodecOption> videoCodecOptions; private List<CodecOption> videoCodecOptions;
private List<CodecOption> audioCodecOptions; private List<CodecOption> audioCodecOptions;
@ -59,17 +54,10 @@ public class Options {
private boolean cleanup = true; private boolean cleanup = true;
private boolean powerOn = true; private boolean powerOn = true;
private NewDisplay newDisplay;
private boolean vdSystemDecorations = true;
private Orientation.Lock captureOrientationLock = Orientation.Lock.Unlocked;
private Orientation captureOrientation = Orientation.Orient0;
private boolean listEncoders; private boolean listEncoders;
private boolean listDisplays; private boolean listDisplays;
private boolean listCameras; private boolean listCameras;
private boolean listCameraSizes; private boolean listCameraSizes;
private boolean listApps;
// Options not used by the scrcpy client, but useful to use scrcpy-server directly // Options not used by the scrcpy client, but useful to use scrcpy-server directly
private boolean sendDeviceMeta = true; // send device name and size private boolean sendDeviceMeta = true; // send device name and size
@ -129,8 +117,8 @@ public class Options {
return maxFps; return maxFps;
} }
public float getAngle() { public int getLockVideoOrientation() {
return angle; return lockVideoOrientation;
} }
public boolean isTunnelForward() { public boolean isTunnelForward() {
@ -181,10 +169,6 @@ public class Options {
return stayAwake; return stayAwake;
} }
public int getScreenOffTimeout() {
return screenOffTimeout;
}
public List<CodecOption> getVideoCodecOptions() { public List<CodecOption> getVideoCodecOptions() {
return videoCodecOptions; return videoCodecOptions;
} }
@ -221,24 +205,8 @@ public class Options {
return powerOn; return powerOn;
} }
public NewDisplay getNewDisplay() {
return newDisplay;
}
public Orientation getCaptureOrientation() {
return captureOrientation;
}
public Orientation.Lock getCaptureOrientationLock() {
return captureOrientationLock;
}
public boolean getVDSystemDecorations() {
return vdSystemDecorations;
}
public boolean getList() { public boolean getList() {
return listEncoders || listDisplays || listCameras || listCameraSizes || listApps; return listEncoders || listDisplays || listCameras || listCameraSizes;
} }
public boolean getListEncoders() { public boolean getListEncoders() {
@ -257,10 +225,6 @@ public class Options {
return listCameraSizes; return listCameraSizes;
} }
public boolean getListApps() {
return listApps;
}
public boolean getSendDeviceMeta() { public boolean getSendDeviceMeta() {
return sendDeviceMeta; return sendDeviceMeta;
} }
@ -359,8 +323,8 @@ public class Options {
case "max_fps": case "max_fps":
options.maxFps = parseFloat("max_fps", value); options.maxFps = parseFloat("max_fps", value);
break; break;
case "angle": case "lock_video_orientation":
options.angle = parseFloat("angle", value); options.lockVideoOrientation = Integer.parseInt(value);
break; break;
case "tunnel_forward": case "tunnel_forward":
options.tunnelForward = Boolean.parseBoolean(value); options.tunnelForward = Boolean.parseBoolean(value);
@ -382,12 +346,6 @@ public class Options {
case "stay_awake": case "stay_awake":
options.stayAwake = Boolean.parseBoolean(value); options.stayAwake = Boolean.parseBoolean(value);
break; 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": case "video_codec_options":
options.videoCodecOptions = CodecOption.parse(value); options.videoCodecOptions = CodecOption.parse(value);
break; break;
@ -430,9 +388,6 @@ public class Options {
case "list_camera_sizes": case "list_camera_sizes":
options.listCameraSizes = Boolean.parseBoolean(value); options.listCameraSizes = Boolean.parseBoolean(value);
break; break;
case "list_apps":
options.listApps = Boolean.parseBoolean(value);
break;
case "camera_id": case "camera_id":
if (!value.isEmpty()) { if (!value.isEmpty()) {
options.cameraId = value; options.cameraId = value;
@ -463,17 +418,6 @@ public class Options {
case "camera_high_speed": case "camera_high_speed":
options.cameraHighSpeed = Boolean.parseBoolean(value); options.cameraHighSpeed = Boolean.parseBoolean(value);
break; break;
case "new_display":
options.newDisplay = parseNewDisplay(value);
break;
case "vd_system_decorations":
options.vdSystemDecorations = Boolean.parseBoolean(value);
break;
case "capture_orientation":
Pair<Orientation.Lock, Orientation> pair = parseCaptureOrientation(value);
options.captureOrientationLock = pair.first;
options.captureOrientation = pair.second;
break;
case "send_device_meta": case "send_device_meta":
options.sendDeviceMeta = Boolean.parseBoolean(value); options.sendDeviceMeta = Boolean.parseBoolean(value);
break; break;
@ -501,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; return options;
} }
@ -536,9 +475,6 @@ public class Options {
} }
int width = Integer.parseInt(tokens[0]); int width = Integer.parseInt(tokens[0]);
int height = Integer.parseInt(tokens[1]); 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); return new Size(width, height);
} }
@ -565,57 +501,4 @@ public class Options {
throw new IllegalArgumentException("Invalid float value for " + key + ": \"" + value + "\""); throw new IllegalArgumentException("Invalid float value for " + key + ": \"" + value + "\"");
} }
} }
private static NewDisplay parseNewDisplay(String newDisplay) {
// Possible inputs:
// - "" (empty string)
// - "<width>x<height>/<dpi>"
// - "<width>x<height>"
// - "/<dpi>"
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<Orientation.Lock, Orientation> 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));
}
} }

View File

@ -9,21 +9,22 @@ import com.genymobile.scrcpy.audio.AudioRawRecorder;
import com.genymobile.scrcpy.audio.AudioSource; import com.genymobile.scrcpy.audio.AudioSource;
import com.genymobile.scrcpy.control.ControlChannel; import com.genymobile.scrcpy.control.ControlChannel;
import com.genymobile.scrcpy.control.Controller; import com.genymobile.scrcpy.control.Controller;
import com.genymobile.scrcpy.control.DeviceMessage;
import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.device.ConfigurationException;
import com.genymobile.scrcpy.device.DesktopConnection; import com.genymobile.scrcpy.device.DesktopConnection;
import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.Device;
import com.genymobile.scrcpy.device.NewDisplay;
import com.genymobile.scrcpy.device.Streamer; import com.genymobile.scrcpy.device.Streamer;
import com.genymobile.scrcpy.opengl.OpenGLRunner;
import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.LogUtils; 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.CameraCapture;
import com.genymobile.scrcpy.video.NewDisplayCapture;
import com.genymobile.scrcpy.video.ScreenCapture; import com.genymobile.scrcpy.video.ScreenCapture;
import com.genymobile.scrcpy.video.SurfaceCapture; import com.genymobile.scrcpy.video.SurfaceCapture;
import com.genymobile.scrcpy.video.SurfaceEncoder; import com.genymobile.scrcpy.video.SurfaceEncoder;
import com.genymobile.scrcpy.video.VideoSource; import com.genymobile.scrcpy.video.VideoSource;
import android.os.BatteryManager;
import android.os.Build; import android.os.Build;
import java.io.File; import java.io.File;
@ -74,21 +75,63 @@ public final class Server {
// not instantiable // 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 { 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"); Ln.e("Camera mirroring is not supported before Android 12");
throw new ConfigurationException("Camera mirroring is not supported"); throw new ConfigurationException("Camera mirroring is not supported");
} }
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10 && options.getNewDisplay() != null) {
Ln.e("New virtual display is not supported before Android 10");
throw new ConfigurationException("New virtual display is not supported");
}
CleanUp cleanUp = null; CleanUp cleanUp = null;
Thread initThread = null;
if (options.getCleanup()) { if (options.getCleanup()) {
cleanUp = CleanUp.start(options); cleanUp = CleanUp.configure(options.getDisplayId());
initThread = startInitThread(options, cleanUp);
} }
int scid = options.getScid(); int scid = options.getScid();
@ -97,6 +140,9 @@ public final class Server {
boolean video = options.getVideo(); boolean video = options.getVideo();
boolean audio = options.getAudio(); boolean audio = options.getAudio();
boolean sendDummyByte = options.getSendDummyByte(); boolean sendDummyByte = options.getSendDummyByte();
boolean camera = video && options.getVideoSource() == VideoSource.CAMERA;
final Device device = camera ? null : new Device(options);
Workarounds.apply(); Workarounds.apply();
@ -108,11 +154,13 @@ public final class Server {
connection.sendDeviceMeta(Device.getDeviceName()); connection.sendDeviceMeta(Device.getDeviceName());
} }
Controller controller = null;
if (control) { if (control) {
ControlChannel controlChannel = connection.getControlChannel(); 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); asyncProcessors.add(controller);
} }
@ -131,7 +179,8 @@ public final class Server {
if (audioCodec == AudioCodec.RAW) { if (audioCodec == AudioCodec.RAW) {
audioRecorder = new AudioRawRecorder(audioCapture, audioStreamer); audioRecorder = new AudioRawRecorder(audioCapture, audioStreamer);
} else { } else {
audioRecorder = new AudioEncoder(audioCapture, audioStreamer, options); audioRecorder = new AudioEncoder(audioCapture, audioStreamer, options.getAudioBitRate(), options.getAudioCodecOptions(),
options.getAudioEncoder());
} }
asyncProcessors.add(audioRecorder); asyncProcessors.add(audioRecorder);
} }
@ -141,22 +190,15 @@ public final class Server {
options.getSendFrameMeta()); options.getSendFrameMeta());
SurfaceCapture surfaceCapture; SurfaceCapture surfaceCapture;
if (options.getVideoSource() == VideoSource.DISPLAY) { if (options.getVideoSource() == VideoSource.DISPLAY) {
NewDisplay newDisplay = options.getNewDisplay(); surfaceCapture = new ScreenCapture(device, options.getDisplayId(), options.getMaxSize(), options.getCrop(),
if (newDisplay != null) { options.getLockVideoOrientation());
surfaceCapture = new NewDisplayCapture(controller, options);
} else {
assert options.getDisplayId() != Device.DISPLAY_ID_NONE;
surfaceCapture = new ScreenCapture(controller, options);
}
} else { } 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); asyncProcessors.add(surfaceEncoder);
if (controller != null) {
controller.setSurfaceCapture(surfaceCapture);
}
} }
Completion completion = new Completion(asyncProcessors.size()); Completion completion = new Completion(asyncProcessors.size());
@ -168,25 +210,22 @@ public final class Server {
completion.await(); completion.await();
} finally { } finally {
if (cleanUp != null) { if (initThread != null) {
cleanUp.interrupt(); initThread.interrupt();
} }
for (AsyncProcessor asyncProcessor : asyncProcessors) { for (AsyncProcessor asyncProcessor : asyncProcessors) {
asyncProcessor.stop(); asyncProcessor.stop();
} }
OpenGLRunner.quit(); // quit the OpenGL thread, if any
connection.shutdown(); connection.shutdown();
try { try {
if (cleanUp != null) { if (initThread != null) {
cleanUp.join(); initThread.join();
} }
for (AsyncProcessor asyncProcessor : asyncProcessors) { for (AsyncProcessor asyncProcessor : asyncProcessors) {
asyncProcessor.join(); asyncProcessor.join();
} }
OpenGLRunner.join();
} catch (InterruptedException e) { } catch (InterruptedException e) {
// ignore // ignore
} }
@ -195,6 +234,12 @@ public final class Server {
} }
} }
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) { public static void main(String... args) {
int status = 0; int status = 0;
try { try {
@ -238,11 +283,6 @@ public final class Server {
Workarounds.apply(); Workarounds.apply();
Ln.i(LogUtils.buildCameraListMessage(options.getListCameraSizes())); 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 // Just print the requested data, do not mirror
return; return;
} }

View File

@ -52,7 +52,7 @@ public final class Workarounds {
} }
public static void apply() { 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(), // On some Samsung devices, DisplayManagerGlobal.getDisplayInfoLocked() calls ActivityThread.currentActivityThread().getConfiguration(),
// which requires a non-null ConfigurationController. // which requires a non-null ConfigurationController.
// ConfigurationController was introduced in Android 12, so do not attempt to set it on lower versions. // ConfigurationController was introduced in Android 12, so do not attempt to set it on lower versions.
@ -155,7 +155,7 @@ public final class Workarounds {
} }
} }
@TargetApi(AndroidVersions.API_30_ANDROID_11) @TargetApi(Build.VERSION_CODES.R)
@SuppressLint("WrongConstant,MissingPermission") @SuppressLint("WrongConstant,MissingPermission")
public static AudioRecord createAudioRecord(int source, int sampleRate, int channelConfig, int channels, int channelMask, int encoding) throws public static AudioRecord createAudioRecord(int source, int sampleRate, int channelConfig, int channels, int channelMask, int encoding) throws
AudioCaptureException { AudioCaptureException {
@ -226,7 +226,7 @@ public final class Workarounds {
int[] session = new int[]{AudioManager.AUDIO_SESSION_ID_GENERATE}; int[] session = new int[]{AudioManager.AUDIO_SESSION_ID_GENERATE};
int initResult; 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, // private native final int native_setup(Object audiorecord_this,
// Object /*AudioAttributes*/ attributes, // Object /*AudioAttributes*/ attributes,
// int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat, // int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat,
@ -252,7 +252,7 @@ public final class Workarounds {
Method getParcelMethod = attributionSourceState.getClass().getDeclaredMethod("getParcel"); Method getParcelMethod = attributionSourceState.getClass().getDeclaredMethod("getParcel");
Parcel attributionSourceParcel = (Parcel) getParcelMethod.invoke(attributionSourceState); 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, // private native int native_setup(Object audiorecordThis,
// Object /*AudioAttributes*/ attributes, // Object /*AudioAttributes*/ attributes,
// int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat, // int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat,

View File

@ -1,6 +1,5 @@
package com.genymobile.scrcpy.audio; package com.genymobile.scrcpy.audio;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.Workarounds; import com.genymobile.scrcpy.Workarounds;
import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.Ln;
@ -46,11 +45,11 @@ public class AudioDirectCapture implements AudioCapture {
} }
} }
@TargetApi(AndroidVersions.API_23_ANDROID_6_0) @TargetApi(Build.VERSION_CODES.M)
@SuppressLint({"WrongConstant", "MissingPermission"}) @SuppressLint({"WrongConstant", "MissingPermission"})
private static AudioRecord createAudioRecord(int audioSource) { private static AudioRecord createAudioRecord(int audioSource) {
AudioRecord.Builder builder = new AudioRecord.Builder(); 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 // On older APIs, Workarounds.fillAppInfo() must be called beforehand
builder.setContext(FakeContext.get()); builder.setContext(FakeContext.get());
} }
@ -118,7 +117,7 @@ public class AudioDirectCapture implements AudioCapture {
@Override @Override
public void checkCompatibility() throws AudioCaptureException { 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"); Ln.w("Audio disabled: it is not supported before Android 11");
throw new AudioCaptureException(); throw new AudioCaptureException();
} }
@ -126,7 +125,7 @@ public class AudioDirectCapture implements AudioCapture {
@Override @Override
public void start() throws AudioCaptureException { 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(); startWorkaroundAndroid11();
try { try {
tryStartRecording(5, 100); tryStartRecording(5, 100);
@ -147,7 +146,7 @@ public class AudioDirectCapture implements AudioCapture {
} }
@Override @Override
@TargetApi(AndroidVersions.API_24_ANDROID_7_0) @TargetApi(Build.VERSION_CODES.N)
public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) { public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) {
return reader.read(outDirectBuffer, outBufferInfo); return reader.read(outDirectBuffer, outBufferInfo);
} }

View File

@ -1,8 +1,6 @@
package com.genymobile.scrcpy.audio; package com.genymobile.scrcpy.audio;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.AsyncProcessor; import com.genymobile.scrcpy.AsyncProcessor;
import com.genymobile.scrcpy.Options;
import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.device.ConfigurationException;
import com.genymobile.scrcpy.device.Streamer; import com.genymobile.scrcpy.device.Streamer;
import com.genymobile.scrcpy.util.Codec; import com.genymobile.scrcpy.util.Codec;
@ -68,12 +66,12 @@ public final class AudioEncoder implements AsyncProcessor {
private boolean ended; private boolean ended;
public AudioEncoder(AudioCapture capture, Streamer streamer, Options options) { public AudioEncoder(AudioCapture capture, Streamer streamer, int bitRate, List<CodecOption> codecOptions, String encoderName) {
this.capture = capture; this.capture = capture;
this.streamer = streamer; this.streamer = streamer;
this.bitRate = options.getAudioBitRate(); this.bitRate = bitRate;
this.codecOptions = options.getAudioCodecOptions(); this.codecOptions = codecOptions;
this.encoderName = options.getAudioEncoder(); this.encoderName = encoderName;
} }
private static MediaFormat createFormat(String mimeType, int bitRate, List<CodecOption> codecOptions) { private static MediaFormat createFormat(String mimeType, int bitRate, List<CodecOption> codecOptions) {
@ -95,7 +93,7 @@ public final class AudioEncoder implements AsyncProcessor {
return format; return format;
} }
@TargetApi(AndroidVersions.API_24_ANDROID_7_0) @TargetApi(Build.VERSION_CODES.N)
private void inputThread(MediaCodec mediaCodec, AudioCapture capture) throws IOException, InterruptedException { private void inputThread(MediaCodec mediaCodec, AudioCapture capture) throws IOException, InterruptedException {
final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
@ -177,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 { 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"); Ln.w("Audio disabled: it is not supported before Android 11");
streamer.writeDisableStream(false); streamer.writeDisableStream(false);
return; return;
@ -316,7 +314,7 @@ public final class AudioEncoder implements AsyncProcessor {
} }
private final class EncoderCallback extends MediaCodec.Callback { private final class EncoderCallback extends MediaCodec.Callback {
@TargetApi(AndroidVersions.API_24_ANDROID_7_0) @TargetApi(Build.VERSION_CODES.N)
@Override @Override
public void onInputBufferAvailable(MediaCodec codec, int index) { public void onInputBufferAvailable(MediaCodec codec, int index) {
try { try {

View File

@ -1,6 +1,5 @@
package com.genymobile.scrcpy.audio; package com.genymobile.scrcpy.audio;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.Ln;
@ -109,7 +108,7 @@ public final class AudioPlaybackCapture implements AudioCapture {
@Override @Override
public void checkCompatibility() throws AudioCaptureException { 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"); Ln.w("Audio disabled: audio playback capture source not supported before Android 13");
throw new AudioCaptureException(); throw new AudioCaptureException();
} }
@ -131,7 +130,7 @@ public final class AudioPlaybackCapture implements AudioCapture {
} }
@Override @Override
@TargetApi(AndroidVersions.API_24_ANDROID_7_0) @TargetApi(Build.VERSION_CODES.N)
public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) { public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) {
return reader.read(outDirectBuffer, outBufferInfo); return reader.read(outDirectBuffer, outBufferInfo);
} }

View File

@ -1,6 +1,5 @@
package com.genymobile.scrcpy.audio; package com.genymobile.scrcpy.audio;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.AsyncProcessor; import com.genymobile.scrcpy.AsyncProcessor;
import com.genymobile.scrcpy.device.Streamer; import com.genymobile.scrcpy.device.Streamer;
import com.genymobile.scrcpy.util.IO; import com.genymobile.scrcpy.util.IO;
@ -25,7 +24,7 @@ public final class AudioRawRecorder implements AsyncProcessor {
} }
private void record() throws IOException, AudioCaptureException { 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"); Ln.w("Audio disabled: it is not supported before Android 11");
streamer.writeDisableStream(false); streamer.writeDisableStream(false);
return; return;

View File

@ -1,12 +1,12 @@
package com.genymobile.scrcpy.audio; package com.genymobile.scrcpy.audio;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.Ln;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.media.AudioRecord; import android.media.AudioRecord;
import android.media.AudioTimestamp; import android.media.AudioTimestamp;
import android.media.MediaCodec; import android.media.MediaCodec;
import android.os.Build;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@ -26,7 +26,7 @@ public class AudioRecordReader {
this.recorder = recorder; this.recorder = recorder;
} }
@TargetApi(AndroidVersions.API_24_ANDROID_7_0) @TargetApi(Build.VERSION_CODES.N)
public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) { public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) {
int r = recorder.read(outDirectBuffer, AudioConfig.MAX_READ_SIZE); int r = recorder.read(outDirectBuffer, AudioConfig.MAX_READ_SIZE);
if (r <= 0) { if (r <= 0) {

View File

@ -17,14 +17,12 @@ public final class ControlMessage {
public static final int TYPE_COLLAPSE_PANELS = 7; public static final int TYPE_COLLAPSE_PANELS = 7;
public static final int TYPE_GET_CLIPBOARD = 8; public static final int TYPE_GET_CLIPBOARD = 8;
public static final int TYPE_SET_CLIPBOARD = 9; 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_ROTATE_DEVICE = 11;
public static final int TYPE_UHID_CREATE = 12; public static final int TYPE_UHID_CREATE = 12;
public static final int TYPE_UHID_INPUT = 13; public static final int TYPE_UHID_INPUT = 13;
public static final int TYPE_UHID_DESTROY = 14; public static final int TYPE_UHID_DESTROY = 14;
public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 15; public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 15;
public static final int TYPE_START_APP = 16;
public static final int TYPE_RESET_VIDEO = 17;
public static final long SEQUENCE_INVALID = 0; public static final long SEQUENCE_INVALID = 0;
@ -35,7 +33,7 @@ public final class ControlMessage {
private int type; private int type;
private String text; private String text;
private int metaState; // KeyEvent.META_* 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 keycode; // KeyEvent.KEYCODE_*
private int actionButton; // MotionEvent.BUTTON_* private int actionButton; // MotionEvent.BUTTON_*
private int buttons; // MotionEvent.BUTTON_* private int buttons; // MotionEvent.BUTTON_*
@ -50,7 +48,6 @@ public final class ControlMessage {
private long sequence; private long sequence;
private int id; private int id;
private byte[] data; private byte[] data;
private boolean on;
private ControlMessage() { private ControlMessage() {
} }
@ -118,10 +115,13 @@ public final class ControlMessage {
return msg; 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(); ControlMessage msg = new ControlMessage();
msg.type = TYPE_SET_DISPLAY_POWER; msg.type = TYPE_SET_SCREEN_POWER_MODE;
msg.on = on; msg.action = mode;
return msg; return msg;
} }
@ -155,13 +155,6 @@ public final class ControlMessage {
return msg; 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() { public int getType() {
return type; return type;
} }
@ -233,8 +226,4 @@ public final class ControlMessage {
public byte[] getData() { public byte[] getData() {
return data; return data;
} }
public boolean getOn() {
return on;
}
} }

View File

@ -39,14 +39,13 @@ public class ControlMessageReader {
return parseGetClipboard(); return parseGetClipboard();
case ControlMessage.TYPE_SET_CLIPBOARD: case ControlMessage.TYPE_SET_CLIPBOARD:
return parseSetClipboard(); return parseSetClipboard();
case ControlMessage.TYPE_SET_DISPLAY_POWER: case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
return parseSetDisplayPower(); return parseSetScreenPowerMode();
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL: case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL:
case ControlMessage.TYPE_COLLAPSE_PANELS: case ControlMessage.TYPE_COLLAPSE_PANELS:
case ControlMessage.TYPE_ROTATE_DEVICE: case ControlMessage.TYPE_ROTATE_DEVICE:
case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS: case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
case ControlMessage.TYPE_RESET_VIDEO:
return ControlMessage.createEmpty(type); return ControlMessage.createEmpty(type);
case ControlMessage.TYPE_UHID_CREATE: case ControlMessage.TYPE_UHID_CREATE:
return parseUhidCreate(); return parseUhidCreate();
@ -54,8 +53,6 @@ public class ControlMessageReader {
return parseUhidInput(); return parseUhidInput();
case ControlMessage.TYPE_UHID_DESTROY: case ControlMessage.TYPE_UHID_DESTROY:
return parseUhidDestroy(); return parseUhidDestroy();
case ControlMessage.TYPE_START_APP:
return parseStartApp();
default: default:
throw new ControlProtocolException("Unknown event type: " + type); throw new ControlProtocolException("Unknown event type: " + type);
} }
@ -135,9 +132,9 @@ public class ControlMessageReader {
return ControlMessage.createSetClipboard(sequence, text, paste); return ControlMessage.createSetClipboard(sequence, text, paste);
} }
private ControlMessage parseSetDisplayPower() throws IOException { private ControlMessage parseSetScreenPowerMode() throws IOException {
boolean on = dis.readBoolean(); int mode = dis.readUnsignedByte();
return ControlMessage.createSetDisplayPower(on); return ControlMessage.createSetScreenPowerMode(mode);
} }
private ControlMessage parseUhidCreate() throws IOException { private ControlMessage parseUhidCreate() throws IOException {
@ -158,11 +155,6 @@ public class ControlMessageReader {
return ControlMessage.createUhidDestroy(id); return ControlMessage.createUhidDestroy(id);
} }
private ControlMessage parseStartApp() throws IOException {
String name = parseString(1);
return ControlMessage.createStartApp(name);
}
private Position parsePosition() throws IOException { private Position parsePosition() throws IOException {
int x = dis.readInt(); int x = dis.readInt();
int y = dis.readInt(); int y = dis.readInt();

View File

@ -1,23 +1,14 @@
package com.genymobile.scrcpy.control; package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.AsyncProcessor; import com.genymobile.scrcpy.AsyncProcessor;
import com.genymobile.scrcpy.CleanUp; import com.genymobile.scrcpy.CleanUp;
import com.genymobile.scrcpy.Options;
import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.Device;
import com.genymobile.scrcpy.device.DeviceApp;
import com.genymobile.scrcpy.device.Point; import com.genymobile.scrcpy.device.Point;
import com.genymobile.scrcpy.device.Position; import com.genymobile.scrcpy.device.Position;
import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.util.Ln; 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.InputManager;
import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.content.IOnPrimaryClipChangedListener;
import android.content.Intent; import android.content.Intent;
import android.os.Build; import android.os.Build;
import android.os.SystemClock; import android.os.SystemClock;
@ -27,40 +18,11 @@ import android.view.KeyEvent;
import android.view.MotionEvent; import android.view.MotionEvent;
import java.io.IOException; import java.io.IOException;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
public class Controller implements AsyncProcessor, VirtualDisplayListener { public class Controller implements AsyncProcessor {
/*
* For event injection, there are two display ids:
* - the displayId passed to the constructor (which comes from --display-id passed by the client, 0 for the main display);
* - the virtualDisplayId used for mirroring, notified by the capture instance via the VirtualDisplayListener interface.
*
* (In case the ScreenCapture uses the "SurfaceControl API", then both ids are equals, but this is an implementation detail.)
*
* In order to make events work correctly in all cases:
* - virtualDisplayId must be used for events relative to the display (mouse and touch events with coordinates);
* - displayId must be used for other events (like key events).
*
* If a new separate virtual display is created (using --new-display), then displayId == Device.DISPLAY_ID_NONE. In that case, all events are
* sent to the virtual display id.
*/
private static final class DisplayData {
private final int virtualDisplayId;
private final PositionMapper positionMapper;
private DisplayData(int virtualDisplayId, PositionMapper positionMapper) {
this.virtualDisplayId = virtualDisplayId;
this.positionMapper = positionMapper;
}
}
private static final int DEFAULT_DEVICE_ID = 0; private static final int DEFAULT_DEVICE_ID = 0;
@ -68,14 +30,12 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
private static final int POINTER_ID_MOUSE = -1; private static final int POINTER_ID_MOUSE = -1;
private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor(); private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor();
private ExecutorService startAppExecutor;
private Thread thread; private Thread thread;
private UhidManager uhidManager; private UhidManager uhidManager;
private final int displayId; private final Device device;
private final boolean supportsInputEvents;
private final ControlChannel controlChannel; private final ControlChannel controlChannel;
private final CleanUp cleanUp; private final CleanUp cleanUp;
private final DeviceMessageSender sender; private final DeviceMessageSender sender;
@ -84,73 +44,21 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
private final AtomicBoolean isSettingClipboard = new AtomicBoolean();
private final AtomicReference<DisplayData> displayData = new AtomicReference<>();
private final Object displayDataAvailable = new Object(); // condition variable
private long lastTouchDown; private long lastTouchDown;
private final PointersState pointersState = new PointersState(); private final PointersState pointersState = new PointersState();
private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS]; private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS];
private final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[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 public Controller(Device device, ControlChannel controlChannel, CleanUp cleanUp, boolean clipboardAutosync, boolean powerOn) {
private SurfaceCapture surfaceCapture; this.device = device;
public Controller(ControlChannel controlChannel, CleanUp cleanUp, Options options) {
this.displayId = options.getDisplayId();
this.controlChannel = controlChannel; this.controlChannel = controlChannel;
this.cleanUp = cleanUp; this.cleanUp = cleanUp;
this.clipboardAutosync = options.getClipboardAutosync(); this.clipboardAutosync = clipboardAutosync;
this.powerOn = options.getPowerOn(); this.powerOn = powerOn;
initPointers(); initPointers();
sender = new DeviceMessageSender(controlChannel); sender = new DeviceMessageSender(controlChannel);
supportsInputEvents = Device.supportsInputEvents(displayId);
if (!supportsInputEvents) {
Ln.w("Input events are not supported for secondary displays before Android 10");
}
if (clipboardAutosync) {
// 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;
}
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() { private UhidManager getUhidManager() {
@ -176,8 +84,8 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
private void control() throws IOException { private void control() throws IOException {
// on start, power on the device // on start, power on the device
if (powerOn && displayId == 0 && !Device.isScreenOn(displayId)) { if (powerOn && !Device.isScreenOn()) {
Device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC); device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC);
// dirty hack // dirty hack
// After POWER is injected, the device is powered on asynchronously. // After POWER is injected, the device is powered on asynchronously.
@ -230,6 +138,10 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
sender.join(); sender.join();
} }
public DeviceMessageSender getSender() {
return sender;
}
private boolean handleEvent() throws IOException { private boolean handleEvent() throws IOException {
ControlMessage msg; ControlMessage msg;
try { try {
@ -241,27 +153,27 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
switch (msg.getType()) { switch (msg.getType()) {
case ControlMessage.TYPE_INJECT_KEYCODE: case ControlMessage.TYPE_INJECT_KEYCODE:
if (supportsInputEvents) { if (device.supportsInputEvents()) {
injectKeycode(msg.getAction(), msg.getKeycode(), msg.getRepeat(), msg.getMetaState()); injectKeycode(msg.getAction(), msg.getKeycode(), msg.getRepeat(), msg.getMetaState());
} }
break; break;
case ControlMessage.TYPE_INJECT_TEXT: case ControlMessage.TYPE_INJECT_TEXT:
if (supportsInputEvents) { if (device.supportsInputEvents()) {
injectText(msg.getText()); injectText(msg.getText());
} }
break; break;
case ControlMessage.TYPE_INJECT_TOUCH_EVENT: case ControlMessage.TYPE_INJECT_TOUCH_EVENT:
if (supportsInputEvents) { if (device.supportsInputEvents()) {
injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getActionButton(), msg.getButtons()); injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getActionButton(), msg.getButtons());
} }
break; break;
case ControlMessage.TYPE_INJECT_SCROLL_EVENT: case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
if (supportsInputEvents) { if (device.supportsInputEvents()) {
injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll(), msg.getButtons()); injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll(), msg.getButtons());
} }
break; break;
case ControlMessage.TYPE_BACK_OR_SCREEN_ON: case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
if (supportsInputEvents) { if (device.supportsInputEvents()) {
pressBackOrTurnScreenOn(msg.getAction()); pressBackOrTurnScreenOn(msg.getAction());
} }
break; break;
@ -280,13 +192,22 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
case ControlMessage.TYPE_SET_CLIPBOARD: case ControlMessage.TYPE_SET_CLIPBOARD:
setClipboard(msg.getText(), msg.getPaste(), msg.getSequence()); setClipboard(msg.getText(), msg.getPaste(), msg.getSequence());
break; break;
case ControlMessage.TYPE_SET_DISPLAY_POWER: case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
if (supportsInputEvents && displayId != Device.DISPLAY_ID_NONE) { if (device.supportsInputEvents()) {
setDisplayPower(msg.getOn()); 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; break;
case ControlMessage.TYPE_ROTATE_DEVICE: case ControlMessage.TYPE_ROTATE_DEVICE:
Device.rotateDevice(getActionDisplayId()); device.rotateDevice();
break; break;
case ControlMessage.TYPE_UHID_CREATE: case ControlMessage.TYPE_UHID_CREATE:
getUhidManager().open(msg.getId(), msg.getText(), msg.getData()); getUhidManager().open(msg.getId(), msg.getText(), msg.getData());
@ -300,12 +221,6 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS: case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
openHardKeyboardSettings(); openHardKeyboardSettings();
break; break;
case ControlMessage.TYPE_START_APP:
startAppAsync(msg.getText());
break;
case ControlMessage.TYPE_RESET_VIDEO:
resetVideo();
break;
default: default:
// do nothing // do nothing
} }
@ -314,11 +229,10 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
} }
private boolean injectKeycode(int action, int keycode, int repeat, int metaState) { 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)) { if (keepPowerModeOff && action == KeyEvent.ACTION_UP && (keycode == KeyEvent.KEYCODE_POWER || keycode == KeyEvent.KEYCODE_WAKEUP)) {
assert displayId != Device.DISPLAY_ID_NONE; schedulePowerModeOff();
scheduleDisplayPowerOff(displayId);
} }
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) { private boolean injectChar(char c) {
@ -328,10 +242,8 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
if (events == null) { if (events == null) {
return false; return false;
} }
int actionDisplayId = getActionDisplayId();
for (KeyEvent event : events) { for (KeyEvent event : events) {
if (!Device.injectEvent(event, actionDisplayId, Device.INJECT_MODE_ASYNC)) { if (!device.injectMainDisplayEvent(event, Device.INJECT_MODE_ASYNC)) {
return false; return false;
} }
} }
@ -353,18 +265,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
private boolean injectTouch(int action, long pointerId, Position position, float pressure, int actionButton, int buttons) { private boolean injectTouch(int action, long pointerId, Position position, float pressure, int actionButton, int buttons) {
long now = SystemClock.uptimeMillis(); long now = SystemClock.uptimeMillis();
// it hides the field on purpose, to read it with atomic access Point point = device.getPhysicalPoint(position);
@SuppressWarnings("checkstyle:HiddenField")
DisplayData displayData = this.displayData.get();
assert displayData != null : "Cannot receive a touch event without a display";
Point point = displayData.positionMapper.map(position);
if (point == null) { if (point == null) {
if (Ln.isEnabled(Ln.Level.VERBOSE)) { Ln.w("Ignore touch event, it was generated for a different device size");
Size eventSize = position.getScreenSize();
Size currentSize = displayData.positionMapper.getVideoSize();
Ln.v("Ignore touch event generated for size " + eventSize + " (current size is " + currentSize + ")");
}
return false; return false;
} }
@ -415,13 +318,13 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
* *
* Otherwise, Chrome does not work properly: <https://github.com/Genymobile/scrcpy/issues/3635> * Otherwise, Chrome does not work properly: <https://github.com/Genymobile/scrcpy/issues/3635>
*/ */
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 (action == MotionEvent.ACTION_DOWN) {
if (actionButton == buttons) { if (actionButton == buttons) {
// First button pressed: ACTION_DOWN // First button pressed: ACTION_DOWN
MotionEvent downEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_DOWN, pointerCount, pointerProperties, MotionEvent downEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_DOWN, pointerCount, pointerProperties,
pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0);
if (!Device.injectEvent(downEvent, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC)) { if (!device.injectVirtualDisplayEvent(downEvent, Device.INJECT_MODE_ASYNC)) {
return false; return false;
} }
} }
@ -432,7 +335,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
if (!InputManager.setActionButton(pressEvent, actionButton)) { if (!InputManager.setActionButton(pressEvent, actionButton)) {
return false; return false;
} }
if (!Device.injectEvent(pressEvent, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC)) { if (!device.injectVirtualDisplayEvent(pressEvent, Device.INJECT_MODE_ASYNC)) {
return false; return false;
} }
@ -446,7 +349,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
if (!InputManager.setActionButton(releaseEvent, actionButton)) { if (!InputManager.setActionButton(releaseEvent, actionButton)) {
return false; return false;
} }
if (!Device.injectEvent(releaseEvent, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC)) { if (!device.injectVirtualDisplayEvent(releaseEvent, Device.INJECT_MODE_ASYNC)) {
return false; return false;
} }
@ -454,7 +357,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
// Last button released: ACTION_UP // Last button released: ACTION_UP
MotionEvent upEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_UP, pointerCount, pointerProperties, MotionEvent upEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_UP, pointerCount, pointerProperties,
pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0);
if (!Device.injectEvent(upEvent, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC)) { if (!device.injectVirtualDisplayEvent(upEvent, Device.INJECT_MODE_ASYNC)) {
return false; return false;
} }
} }
@ -465,24 +368,14 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f,
DEFAULT_DEVICE_ID, 0, source, 0); DEFAULT_DEVICE_ID, 0, source, 0);
return Device.injectEvent(event, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC); return device.injectVirtualDisplayEvent(event, Device.INJECT_MODE_ASYNC);
} }
private boolean injectScroll(Position position, float hScroll, float vScroll, int buttons) { private boolean injectScroll(Position position, float hScroll, float vScroll, int buttons) {
long now = SystemClock.uptimeMillis(); long now = SystemClock.uptimeMillis();
Point point = device.getPhysicalPoint(position);
// it hides the field on purpose, to read it with atomic access
@SuppressWarnings("checkstyle:HiddenField")
DisplayData displayData = this.displayData.get();
assert displayData != null : "Cannot receive a scroll event without a display";
Point point = displayData.positionMapper.map(position);
if (point == null) { if (point == null) {
if (Ln.isEnabled(Ln.Level.VERBOSE)) { // ignore event
Size eventSize = position.getScreenSize();
Size currentSize = displayData.positionMapper.getVideoSize();
Ln.v("Ignore scroll event generated for size " + eventSize + " (current size is " + currentSize + ")");
}
return false; return false;
} }
@ -497,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, MotionEvent event = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, buttons, 1f, 1f,
DEFAULT_DEVICE_ID, 0, InputDevice.SOURCE_MOUSE, 0); DEFAULT_DEVICE_ID, 0, InputDevice.SOURCE_MOUSE, 0);
return Device.injectEvent(event, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC); return device.injectVirtualDisplayEvent(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(() -> { EXECUTOR.schedule(() -> {
Ln.i("Forcing display off"); Ln.i("Forcing screen off");
Device.setDisplayPower(displayId, false); Device.setScreenPowerMode(Device.POWER_MODE_OFF);
}, 200, TimeUnit.MILLISECONDS); }, 200, TimeUnit.MILLISECONDS);
} }
private boolean pressBackOrTurnScreenOn(int action) { private boolean pressBackOrTurnScreenOn(int action) {
if (displayId == Device.DISPLAY_ID_NONE || Device.isScreenOn(displayId)) { if (Device.isScreenOn()) {
return injectKeyEvent(action, KeyEvent.KEYCODE_BACK, 0, 0, Device.INJECT_MODE_ASYNC); return device.injectKeyEvent(action, KeyEvent.KEYCODE_BACK, 0, 0, Device.INJECT_MODE_ASYNC);
} }
// Screen is off // Screen is off
@ -522,19 +415,18 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
return true; return true;
} }
if (keepDisplayPowerOff) { if (keepPowerModeOff) {
assert displayId != Device.DISPLAY_ID_NONE; schedulePowerModeOff();
scheduleDisplayPowerOff(displayId);
} }
return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC); return device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC);
} }
private void getClipboard(int copyKey) { private void getClipboard(int copyKey) {
// On Android >= 7, press the COPY or CUT key if requested // 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; 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 // 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 // If clipboard autosync is enabled, then the device clipboard is synchronized to the computer clipboard whenever it changes, in
@ -550,16 +442,14 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
} }
private boolean setClipboard(String text, boolean paste, long sequence) { private boolean setClipboard(String text, boolean paste, long sequence) {
isSettingClipboard.set(true); boolean ok = device.setClipboardText(text);
boolean ok = Device.setClipboardText(text);
isSettingClipboard.set(false);
if (ok) { if (ok) {
Ln.i("Device clipboard set"); Ln.i("Device clipboard set");
} }
// On Android >= 7, also press the PASTE key if requested // On Android >= 7, also press the PASTE key if requested
if (paste && Build.VERSION.SDK_INT >= AndroidVersions.API_24_ANDROID_7_0 && supportsInputEvents) { if (paste && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) {
pressReleaseKeycode(KeyEvent.KEYCODE_PASTE, Device.INJECT_MODE_ASYNC); device.pressReleaseKeycode(KeyEvent.KEYCODE_PASTE, Device.INJECT_MODE_ASYNC);
} }
if (sequence != ControlMessage.SEQUENCE_INVALID) { if (sequence != ControlMessage.SEQUENCE_INVALID) {
@ -575,137 +465,4 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
Intent intent = new Intent("android.settings.HARD_KEYBOARD_SETTINGS"); Intent intent = new Intent("android.settings.HARD_KEYBOARD_SETTINGS");
ServiceManager.getActivityManager().startActivity(intent); 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<DeviceApp> 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;
}
displayDataAvailable.wait(timeout);
data = displayData.get();
}
return data;
}
}
private void setDisplayPower(boolean on) {
boolean setDisplayPowerOk = Device.setDisplayPower(displayId, on);
if (setDisplayPowerOk) {
keepDisplayPowerOff = !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();
}
}
} }

View File

@ -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;
}
}

View File

@ -1,6 +1,5 @@
package com.genymobile.scrcpy.control; package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.StringUtils; import com.genymobile.scrcpy.util.StringUtils;
@ -39,7 +38,7 @@ public final class UhidManager {
public UhidManager(DeviceMessageSender sender) { public UhidManager(DeviceMessageSender sender) {
this.sender = sender; this.sender = sender;
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"); HandlerThread thread = new HandlerThread("UHidManager");
thread.start(); thread.start();
queue = thread.getLooper().getQueue(); queue = thread.getLooper().getQueue();
@ -72,7 +71,7 @@ public final class UhidManager {
} }
private void registerUhidListener(int id, FileDescriptor fd) { 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) -> { queue.addOnFileDescriptorEventListener(fd, MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT, (fd2, events) -> {
try { try {
buffer.clear(); buffer.clear();
@ -98,7 +97,7 @@ public final class UhidManager {
} }
private void unregisterUhidListener(FileDescriptor fd) { 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); queue.removeOnFileDescriptorEventListener(fd);
} }
} }

View File

@ -1,9 +1,8 @@
package com.genymobile.scrcpy.device; package com.genymobile.scrcpy.device;
import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.Options;
import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.wrappers.ActivityManager; import com.genymobile.scrcpy.video.ScreenInfo;
import com.genymobile.scrcpy.wrappers.ClipboardManager; import com.genymobile.scrcpy.wrappers.ClipboardManager;
import com.genymobile.scrcpy.wrappers.DisplayControl; import com.genymobile.scrcpy.wrappers.DisplayControl;
import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.InputManager;
@ -11,13 +10,9 @@ import com.genymobile.scrcpy.wrappers.ServiceManager;
import com.genymobile.scrcpy.wrappers.SurfaceControl; import com.genymobile.scrcpy.wrappers.SurfaceControl;
import com.genymobile.scrcpy.wrappers.WindowManager; import com.genymobile.scrcpy.wrappers.WindowManager;
import android.annotation.SuppressLint; import android.content.IOnPrimaryClipChangedListener;
import android.content.Intent; import android.graphics.Rect;
import android.app.ActivityOptions;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Build; import android.os.Build;
import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
import android.os.SystemClock; import android.os.SystemClock;
import android.view.InputDevice; import android.view.InputDevice;
@ -25,14 +20,10 @@ import android.view.InputEvent;
import android.view.KeyCharacterMap; import android.view.KeyCharacterMap;
import android.view.KeyEvent; import android.view.KeyEvent;
import java.util.ArrayList; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.List;
import java.util.Locale;
public final class Device { 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_OFF = SurfaceControl.POWER_MODE_OFF;
public static final int POWER_MODE_NORMAL = SurfaceControl.POWER_MODE_NORMAL; public static final int POWER_MODE_NORMAL = SurfaceControl.POWER_MODE_NORMAL;
@ -40,8 +31,90 @@ 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_RESULT = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT;
public static final int INJECT_MODE_WAIT_FOR_FINISH = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH; public static final int INJECT_MODE_WAIT_FOR_FINISH = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH;
private Device() { public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1;
// not instantiable public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2;
public interface ClipboardListener {
void onClipboardTextChanged(String text);
}
private ClipboardListener clipboardListener;
private final AtomicBoolean isSettingClipboard = new AtomicBoolean();
/**
* Logical display identifier
*/
private final int displayId;
private final boolean supportsInputEvents;
// set by the ScreenCapture instance
private ScreenInfo screenInfo;
private int virtualDisplayId;
public Device(Options options) {
displayId = options.getDisplayId();
virtualDisplayId = displayId; // by default
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");
}
}
// main display or any display on Android >= Q
supportsInputEvents = options.getDisplayId() == 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 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
if (screenInfo == null) {
return null;
}
// 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() { public static String getDeviceName() {
@ -49,8 +122,27 @@ public final class Device {
} }
public static boolean supportsInputEvents(int displayId) { public static boolean supportsInputEvents(int displayId) {
// main display or any display on Android >= 10 return displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
return displayId == 0 || Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10; }
public boolean supportsInputEvents() {
return supportsInputEvents;
}
private synchronized ScreenInfo getScreenInfo() {
return screenInfo;
}
public synchronized void setScreenInfo(ScreenInfo screenInfo) {
this.screenInfo = screenInfo;
}
private synchronized int getVirtualDisplayId() {
return virtualDisplayId;
}
public synchronized void setVirtualDisplayId(int virtualDisplayId) {
this.virtualDisplayId = virtualDisplayId;
} }
public static boolean injectEvent(InputEvent inputEvent, int displayId, int injectMode) { public static boolean injectEvent(InputEvent inputEvent, int displayId, int injectMode) {
@ -65,6 +157,14 @@ public final class Device {
return ServiceManager.getInputManager().injectInputEvent(inputEvent, injectMode); return ServiceManager.getInputManager().injectInputEvent(inputEvent, injectMode);
} }
public boolean injectMainDisplayEvent(InputEvent event, int injectMode) {
return injectEvent(event, displayId, injectMode);
}
public boolean injectVirtualDisplayEvent(InputEvent event, int injectMode) {
return injectEvent(event, virtualDisplayId, injectMode);
}
public static boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int displayId, int injectMode) { public static boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int displayId, int injectMode) {
long now = SystemClock.uptimeMillis(); long now = SystemClock.uptimeMillis();
KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
@ -72,14 +172,25 @@ public final class Device {
return injectEvent(event, displayId, injectMode); 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) { public static boolean pressReleaseKeycode(int keyCode, int displayId, int injectMode) {
return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0, displayId, injectMode) return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0, displayId, injectMode)
&& injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0, displayId, injectMode); && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0, displayId, injectMode);
} }
public static boolean isScreenOn(int displayId) { public boolean pressReleaseKeycode(int keyCode, int injectMode) {
assert displayId != DISPLAY_ID_NONE; return pressReleaseKeycode(keyCode, displayId, injectMode);
return ServiceManager.getPowerManager().isScreenOn(displayId); }
public static boolean isScreenOn() {
return ServiceManager.getPowerManager().isScreenOn();
}
public synchronized void setClipboardListener(ClipboardListener clipboardListener) {
this.clipboardListener = clipboardListener;
} }
public static void expandNotificationPanel() { public static void expandNotificationPanel() {
@ -106,7 +217,7 @@ public final class Device {
return s.toString(); return s.toString();
} }
public static boolean setClipboardText(String text) { public boolean setClipboardText(String text) {
ClipboardManager clipboardManager = ServiceManager.getClipboardManager(); ClipboardManager clipboardManager = ServiceManager.getClipboardManager();
if (clipboardManager == null) { if (clipboardManager == null) {
return false; return false;
@ -121,20 +232,20 @@ public final class Device {
return false; 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; * @param mode one of the {@code POWER_MODE_*} constants
*/
if (Build.VERSION.SDK_INT >= AndroidVersions.API_35_ANDROID_15) { public static boolean setScreenPowerMode(int mode) {
return ServiceManager.getDisplayManager().requestDisplayPower(displayId, on); boolean applyToMultiPhysicalDisplays = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
}
boolean applyToMultiPhysicalDisplays = Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10;
if (applyToMultiPhysicalDisplays 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") && Build.BRAND.equalsIgnoreCase("honor")
&& SurfaceControl.hasGetBuildInDisplayMethod()) { && SurfaceControl.hasGetBuildInDisplayMethod()) {
// Workaround for Honor devices with Android 14: // Workaround for Honor devices with Android 14:
@ -143,11 +254,10 @@ public final class Device {
applyToMultiPhysicalDisplays = false; applyToMultiPhysicalDisplays = false;
} }
int mode = on ? POWER_MODE_NORMAL : POWER_MODE_OFF;
if (applyToMultiPhysicalDisplays) { if (applyToMultiPhysicalDisplays) {
// On Android 14, these internal methods have been moved to DisplayControl // On Android 14, these internal methods have been moved to DisplayControl
boolean useDisplayControl = 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 // Change the power mode for all physical displays
long[] physicalDisplayIds = useDisplayControl ? DisplayControl.getPhysicalDisplayIds() : SurfaceControl.getPhysicalDisplayIds(); long[] physicalDisplayIds = useDisplayControl ? DisplayControl.getPhysicalDisplayIds() : SurfaceControl.getPhysicalDisplayIds();
@ -175,9 +285,7 @@ public final class Device {
} }
public static boolean powerOffScreen(int displayId) { public static boolean powerOffScreen(int displayId) {
assert displayId != DISPLAY_ID_NONE; if (!isScreenOn()) {
if (!isScreenOn(displayId)) {
return true; return true;
} }
return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC); return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC);
@ -186,9 +294,7 @@ public final class Device {
/** /**
* Disable auto-rotation (if enabled), set the screen rotation and re-enable auto-rotation (if it was enabled). * Disable auto-rotation (if enabled), set the screen rotation and re-enable auto-rotation (if it was enabled).
*/ */
public static void rotateDevice(int displayId) { public void rotateDevice() {
assert displayId != DISPLAY_ID_NONE;
WindowManager wm = ServiceManager.getWindowManager(); WindowManager wm = ServiceManager.getWindowManager();
boolean accelerometerRotation = !wm.isRotationFrozen(displayId); boolean accelerometerRotation = !wm.isRotationFrozen(displayId);
@ -207,8 +313,6 @@ public final class Device {
} }
private static int getCurrentRotation(int displayId) { private static int getCurrentRotation(int displayId) {
assert displayId != DISPLAY_ID_NONE;
if (displayId == 0) { if (displayId == 0) {
return ServiceManager.getWindowManager().getRotation(); return ServiceManager.getWindowManager().getRotation();
} }
@ -216,96 +320,4 @@ public final class Device {
DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId); DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId);
return displayInfo.getRotation(); return displayInfo.getRotation();
} }
public static List<DeviceApp> listApps() {
List<DeviceApp> 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<ApplicationInfo> getLaunchableApps(PackageManager pm) {
List<ApplicationInfo> 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<DeviceApp> findByName(String searchName) {
List<DeviceApp> 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);
}
} }

View File

@ -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;
}
}

View File

@ -6,17 +6,17 @@ public final class DisplayInfo {
private final int rotation; private final int rotation;
private final int layerStack; private final int layerStack;
private final int flags; private final int flags;
private final int dpi; private final int logicalDensityDpi;
public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 0x00000001; public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 0x00000001;
public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags, int dpi) { public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags, int logicalDensityDpi) {
this.displayId = displayId; this.displayId = displayId;
this.size = size; this.size = size;
this.rotation = rotation; this.rotation = rotation;
this.layerStack = layerStack; this.layerStack = layerStack;
this.flags = flags; this.flags = flags;
this.dpi = dpi; this.logicalDensityDpi = logicalDensityDpi;
} }
public int getDisplayId() { public int getDisplayId() {
@ -39,8 +39,8 @@ public final class DisplayInfo {
return flags; return flags;
} }
public int getDpi() { public int getLogicalDensityDpi() {
return dpi; return logicalDensityDpi;
} }
} }

View File

@ -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;
}
}

View File

@ -1,47 +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 rotation) {
assert rotation >= 0 && rotation < 4;
return values()[rotation];
}
public boolean isFlipped() {
return (ordinal() & 4) != 0;
}
public int getRotation() {
return ordinal() & 3;
}
}

View File

@ -21,69 +21,10 @@ public final class Size {
return height; return height;
} }
public int getMax() {
return Math.max(width, height);
}
public Size rotate() { public Size rotate() {
return new Size(height, width); 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() { public Rect toRect() {
return new Rect(0, 0, width, height); return new Rect(0, 0, width, height);
} }
@ -107,6 +48,6 @@ public final class Size {
@Override @Override
public String toString() { public String toString() {
return width + "x" + height; return "Size{" + "width=" + width + ", height=" + height + '}';
} }
} }

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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();
}

View File

@ -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 <https://github.com/Genymobile/scrcpy/issues/5444>
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();
}
}
}

View File

@ -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):
*
* <pre>
* / a c e \
* | b d f |
* \ 0 0 1 /
* </pre>
* <p>
* Or, a 4x4 matrix if we add a z axis:
*
* <pre>
* / a c 0 e \
* | b d 0 f |
* | 0 0 1 0 |
* \ 0 0 0 1 /
* </pre>
*/
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:
*
* <pre>
* / a c e \
* | b d f |
* \ 0 0 1 /
* </pre>
*/
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 <code>this * rhs</code>.
*
* @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).
* <p/>
* <code>(x, y)</code> is the bottom-left corner, <code>(w, h)</code> 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;
}
}

Some files were not shown because too many files have changed in this diff Show More