Compare commits
86 Commits
server_deb
...
tmp
Author | SHA1 | Date | |
---|---|---|---|
bed5f45cf8 | |||
9555d3a537 | |||
aea6a371aa | |||
dc6c279b1e | |||
6d0ac3626d | |||
beee42fb06 | |||
131372d2c4 | |||
0fd7534bd5 | |||
36574d2ee7 | |||
3b2b3625e4 | |||
b2cdaa4bdc | |||
d01373c03c | |||
ff06b6dcc1 | |||
017a3672a4 | |||
c1351b250e | |||
618a978f5b | |||
acddd811bf | |||
ee9f7126ff | |||
a18ed1ee7a | |||
678025b316 | |||
3e689020ba | |||
3d1f036c04 | |||
3d5294c1e5 | |||
1d2f16dbb5 | |||
7fef051976 | |||
da8ade88fd | |||
74aecc00b5 | |||
5e05f2a25b | |||
3d478d7d5b | |||
54e1f8e060 | |||
d40224f299 | |||
0628ffcb0b | |||
6f9520f3e2 | |||
a7efb180b9 | |||
28c372e838 | |||
cb19686d79 | |||
93da693e8c | |||
179c664e2b | |||
360936248c | |||
98d2065d6d | |||
6a81fc438b | |||
cf0098abf0 | |||
73b595c806 | |||
d74f564f56 | |||
7fc6943284 | |||
a57180047c | |||
5df218d8f9 | |||
26bf209617 | |||
dc82425769 | |||
9f39a5f2d6 | |||
24588cb637 | |||
0e50d1e7db | |||
264110fd70 | |||
4608a19a13 | |||
f1f2711626 | |||
eeb04292a4 | |||
2ec30bdf80 | |||
145b823b1d | |||
28d64ef319 | |||
36d61f9ecd | |||
f95a5f97b1 | |||
adb674a5c8 | |||
d19045628e | |||
443f315f60 | |||
0904880816 | |||
4348f12194 | |||
371ff31225 | |||
456fa510f2 | |||
45382e3f01 | |||
9b03bfc3ae | |||
39d51ff2cc | |||
d72686c867 | |||
06385ce83b | |||
9fb0a3dac1 | |||
23960ca11a | |||
904f86152e | |||
e226950cfa | |||
019ce5eea4 | |||
d6033d28f5 | |||
89518f49ad | |||
2a04858a22 | |||
e411b74a16 | |||
5694562a74 | |||
04dd72b594 | |||
762816cac6 | |||
67d4dfb5ff |
425
.github/workflows/release.yml
vendored
425
.github/workflows/release.yml
vendored
@ -6,11 +6,15 @@ on:
|
||||
name:
|
||||
description: 'Version name (default is ref name)'
|
||||
|
||||
env:
|
||||
# $VERSION is used by release scripts
|
||||
VERSION: ${{ github.event.inputs.name || github.ref_name }}
|
||||
|
||||
jobs:
|
||||
build-scrcpy-server:
|
||||
test-scrcpy-server:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GRADLE: gradle # use native gradle instead of ./gradlew in release.mk
|
||||
GRADLE: gradle # use native gradle instead of ./gradlew in scripts
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@ -22,16 +26,45 @@ jobs:
|
||||
java-version: '17'
|
||||
|
||||
- name: Test scrcpy-server
|
||||
run: make -f release.mk test-server
|
||||
run: release/test_server.sh
|
||||
|
||||
- name: Build scrcpy-server
|
||||
run: make -f release.mk build-server
|
||||
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: Upload scrcpy-server artifact
|
||||
- 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: build-server/server/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
|
||||
@ -41,18 +74,52 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \
|
||||
libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \
|
||||
libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
meson setup d -Db_sanitize=address,undefined
|
||||
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: Check architecture
|
||||
run: |
|
||||
meson test -Cd
|
||||
arch=$(uname -m)
|
||||
if [[ "$arch" != x86_64 ]]
|
||||
then
|
||||
echo "Unexpected architecture: $arch" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt 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
|
||||
@ -62,7 +129,6 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \
|
||||
libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \
|
||||
libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \
|
||||
@ -71,14 +137,22 @@ jobs:
|
||||
- name: Workaround for old meson version run by Github Actions
|
||||
run: sed -i 's/^pkg-config/pkgconfig/' cross_win32.txt
|
||||
|
||||
- name: Build scrcpy win32
|
||||
run: make -f release.mk build-win32
|
||||
- name: Build
|
||||
run: release/build_windows.sh 32
|
||||
|
||||
- name: Upload build-win32 artifact
|
||||
# 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: build-win32/dist/
|
||||
path: release/work/build-win32/dist-tar/
|
||||
|
||||
build-win64:
|
||||
runs-on: ubuntu-latest
|
||||
@ -88,7 +162,6 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \
|
||||
libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \
|
||||
libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \
|
||||
@ -97,24 +170,101 @@ jobs:
|
||||
- name: Workaround for old meson version run by Github Actions
|
||||
run: sed -i 's/^pkg-config/pkgconfig/' cross_win64.txt
|
||||
|
||||
- name: Build scrcpy win64
|
||||
run: make -f release.mk build-win64
|
||||
- name: Build
|
||||
run: release/build_windows.sh 64
|
||||
|
||||
- name: Upload build-win64 artifact
|
||||
# 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: build-win64/dist/
|
||||
path: release/work/build-win64/dist-tar/
|
||||
|
||||
package:
|
||||
build-macos-aarch64:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Check architecture
|
||||
run: |
|
||||
arch=$(uname -m)
|
||||
if [[ "$arch" != arm64 ]]
|
||||
then
|
||||
echo "Unexpected architecture: $arch" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
brew install meson ninja nasm libiconv zlib automake autoconf \
|
||||
libtool
|
||||
|
||||
- name: Build
|
||||
run: release/build_macos.sh aarch64
|
||||
|
||||
# upload-artifact does not preserve permissions
|
||||
- name: Tar
|
||||
run: |
|
||||
cd release/work/build-macos-aarch64
|
||||
mkdir dist-tar
|
||||
cd dist-tar
|
||||
tar -C .. -cvf dist.tar.gz dist/
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-macos-aarch64-intermediate
|
||||
path: release/work/build-macos-aarch64/dist-tar/
|
||||
|
||||
build-macos-x86_64:
|
||||
runs-on: macos-13
|
||||
steps:
|
||||
- name: Check architecture
|
||||
run: |
|
||||
arch=$(uname -m)
|
||||
if [[ "$arch" != x86_64 ]]
|
||||
then
|
||||
echo "Unexpected architecture: $arch" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: brew install meson ninja nasm libiconv zlib automake
|
||||
# autoconf and libtool are already installed on macos-13
|
||||
|
||||
- name: Build
|
||||
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-win32
|
||||
- build-win64
|
||||
- build-linux-x86_64
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
# $VERSION is used by release.mk
|
||||
VERSION: ${{ github.event.inputs.name || github.ref_name }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@ -123,25 +273,230 @@ jobs:
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: scrcpy-server
|
||||
path: build-server/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: build-win32/dist/
|
||||
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: build-win64/dist/
|
||||
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: make -f release.mk package
|
||||
run: release/package_client.sh win64 zip
|
||||
|
||||
- name: Upload release
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-win64
|
||||
path: release/output
|
||||
|
||||
package-macos-aarch64:
|
||||
needs:
|
||||
- build-scrcpy-server
|
||||
- build-macos-aarch64
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download scrcpy-server
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: scrcpy-server
|
||||
path: release/work/build-server/server/
|
||||
|
||||
- name: Download build-macos-aarch64
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build-macos-aarch64-intermediate
|
||||
path: release/work/build-macos-aarch64/dist-tar/
|
||||
|
||||
# upload-artifact does not preserve permissions
|
||||
- name: Detar
|
||||
run: |
|
||||
cd release/work/build-macos-aarch64
|
||||
tar xf dist-tar/dist.tar.gz
|
||||
|
||||
- name: Package
|
||||
run: release/package_client.sh macos-aarch64 tar.gz
|
||||
|
||||
- name: Upload release
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-macos-aarch64
|
||||
path: release/output/
|
||||
|
||||
package-macos-x86_64:
|
||||
needs:
|
||||
- build-scrcpy-server
|
||||
- build-macos-x86_64
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download scrcpy-server
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: scrcpy-server
|
||||
path: release/work/build-server/server/
|
||||
|
||||
- name: Download build-macos
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build-macos-x86_64-intermediate
|
||||
path: release/work/build-macos-x86_64/dist-tar/
|
||||
|
||||
# upload-artifact does not preserve permissions
|
||||
- name: Detar
|
||||
run: |
|
||||
cd release/work/build-macos-x86_64
|
||||
tar xf dist-tar/dist.tar.gz
|
||||
|
||||
- name: Package
|
||||
run: release/package_client.sh macos-x86_64 tar.gz
|
||||
|
||||
- name: Upload release
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-macos-x86_64
|
||||
path: release/output/
|
||||
|
||||
release:
|
||||
needs:
|
||||
- build-scrcpy-server
|
||||
- package-linux-x86_64
|
||||
- package-win32
|
||||
- package-win64
|
||||
- package-macos-aarch64
|
||||
- package-macos-x86_64
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download scrcpy-server
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: scrcpy-server
|
||||
path: release/work/build-server/server/
|
||||
|
||||
- name: Download release-linux-x86_64
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: release-linux-x86_64
|
||||
path: release/output/
|
||||
|
||||
- name: Download release-win32
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: release-win32
|
||||
path: release/output/
|
||||
|
||||
- name: Download release-win64
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: release-win64
|
||||
path: release/output/
|
||||
|
||||
- name: Download release-macos-aarch64
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: release-macos-aarch64
|
||||
path: release/output/
|
||||
|
||||
- name: Download release-macos-x86_64
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: release-macos-x86_64
|
||||
path: release/output/
|
||||
|
||||
- name: Package server
|
||||
run: release/package_server.sh
|
||||
|
||||
- name: Generate checksums
|
||||
run: release/generate_checksums.sh
|
||||
|
||||
- name: Upload release artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: scrcpy-release-${{ env.VERSION }}
|
||||
path: release-${{ env.VERSION }}
|
||||
path: release/output
|
||||
|
@ -2,7 +2,7 @@
|
||||
source for the project. Do not download releases from random websites, even if
|
||||
their name contains `scrcpy`.**
|
||||
|
||||
# scrcpy (v2.7)
|
||||
# scrcpy (v3.0)
|
||||
|
||||
<img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />
|
||||
|
||||
@ -74,7 +74,7 @@ Note that USB debugging is not required to run scrcpy in [OTG mode](doc/otg.md).
|
||||
## Get the app
|
||||
|
||||
- [Linux](doc/linux.md)
|
||||
- [Windows](doc/windows.md)
|
||||
- [Windows](doc/windows.md) (read [how to run](doc/windows.md#run))
|
||||
- [macOS](doc/macos.md)
|
||||
|
||||
|
||||
@ -141,7 +141,7 @@ documented in the following pages:
|
||||
- [Device](doc/device.md)
|
||||
- [Window](doc/window.md)
|
||||
- [Recording](doc/recording.md)
|
||||
- [Virtual display](doc/virtual_displays.md)
|
||||
- [Virtual display](doc/virtual_display.md)
|
||||
- [Tunnels](doc/tunnels.md)
|
||||
- [OTG](doc/otg.md)
|
||||
- [Camera](doc/camera.md)
|
||||
@ -181,6 +181,7 @@ to your problem immediately.
|
||||
You can also use:
|
||||
|
||||
- Reddit: [`r/scrcpy`](https://www.reddit.com/r/scrcpy)
|
||||
- BlueSky: [`@scrcpy.bsky.social`](https://bsky.app/profile/scrcpy.bsky.social)
|
||||
- Twitter: [`@scrcpy_app`](https://twitter.com/scrcpy_app)
|
||||
|
||||
|
||||
|
@ -2,6 +2,7 @@ _scrcpy() {
|
||||
local cur prev words cword
|
||||
local opts="
|
||||
--always-on-top
|
||||
--angle
|
||||
--audio-bit-rate=
|
||||
--audio-buffer=
|
||||
--audio-codec=
|
||||
@ -17,6 +18,7 @@ _scrcpy() {
|
||||
--camera-fps=
|
||||
--camera-high-speed
|
||||
--camera-size=
|
||||
--capture-orientation=
|
||||
--crop=
|
||||
-d --select-usb
|
||||
--disable-screensaver
|
||||
@ -37,8 +39,6 @@ _scrcpy() {
|
||||
--list-cameras
|
||||
--list-displays
|
||||
--list-encoders
|
||||
--lock-video-orientation
|
||||
--lock-video-orientation=
|
||||
-m --max-size=
|
||||
-M
|
||||
--max-fps=
|
||||
@ -57,6 +57,7 @@ _scrcpy() {
|
||||
--no-mipmaps
|
||||
--no-mouse-hover
|
||||
--no-power-on
|
||||
--no-vd-system-decorations
|
||||
--no-video
|
||||
--no-video-playback
|
||||
--orientation=
|
||||
@ -138,6 +139,10 @@ _scrcpy() {
|
||||
COMPREPLY=($(compgen -W 'disabled uhid aoa' -- "$cur"))
|
||||
return
|
||||
;;
|
||||
--capture-orientation)
|
||||
COMPREPLY=($(compgen -W '0 90 180 270 flip0 flip90 flip180 flip270 @0 @90 @180 @270 @flip0 @flip90 @flip180 @flip270' -- "$cur"))
|
||||
return
|
||||
;;
|
||||
--orientation|--display-orientation)
|
||||
COMPREPLY=($(compgen -W '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur"))
|
||||
return
|
||||
@ -146,10 +151,6 @@ _scrcpy() {
|
||||
COMPREPLY=($(compgen -W '0 90 180 270' -- "$cur"))
|
||||
return
|
||||
;;
|
||||
--lock-video-orientation)
|
||||
COMPREPLY=($(compgen -W 'unlocked initial 0 90 180 270' -- "$cur"))
|
||||
return
|
||||
;;
|
||||
--pause-on-exit)
|
||||
COMPREPLY=($(compgen -W 'true false if-error' -- "$cur"))
|
||||
return
|
||||
@ -194,6 +195,7 @@ _scrcpy() {
|
||||
|--display-id \
|
||||
|--max-fps \
|
||||
|-m|--max-size \
|
||||
|--new-display \
|
||||
|-p|--port \
|
||||
|--push-target \
|
||||
|--rotation \
|
||||
|
@ -9,6 +9,7 @@ local arguments
|
||||
|
||||
arguments=(
|
||||
'--always-on-top[Make scrcpy window always on top \(above other windows\)]'
|
||||
'--angle=[Rotate the video content by a custom angle, in degrees]'
|
||||
'--audio-bit-rate=[Encode the audio at the given bit-rate]'
|
||||
'--audio-buffer=[Configure the audio buffering delay (in milliseconds)]'
|
||||
'--audio-codec=[Select the audio codec]:codec:(opus aac flac raw)'
|
||||
@ -24,6 +25,7 @@ arguments=(
|
||||
'--camera-facing=[Select the device camera by its facing direction]:facing:(front back external)'
|
||||
'--camera-fps=[Specify the camera capture frame rate]'
|
||||
'--camera-size=[Specify an explicit camera capture size]'
|
||||
'--capture-orientation=[Set the capture video orientation]:orientation:(0 90 180 270 flip0 flip90 flip180 flip270 @0 @90 @180 @270 @flip0 @flip90 @flip180 @flip270)'
|
||||
'--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]'
|
||||
{-d,--select-usb}'[Use USB device]'
|
||||
'--disable-screensaver[Disable screensaver while scrcpy is running]'
|
||||
@ -44,7 +46,6 @@ arguments=(
|
||||
'--list-cameras[List cameras available on the device]'
|
||||
'--list-displays[List displays available on the device]'
|
||||
'--list-encoders[List video and audio encoders available on the device]'
|
||||
'--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 90 180 270)'
|
||||
{-m,--max-size=}'[Limit both the width and height of the video to value]'
|
||||
'-M[Use UHID/AOA mouse (same as --mouse=uhid or --mouse=aoa, depending on OTG mode)]'
|
||||
'--max-fps=[Limit the frame rate of screen capture]'
|
||||
@ -62,6 +63,7 @@ arguments=(
|
||||
'--no-mipmaps[Disable the generation of mipmaps]'
|
||||
'--no-mouse-hover[Do not forward mouse hover events]'
|
||||
'--no-power-on[Do not power on the device on start]'
|
||||
'--no-vd-system-decorations[Disable virtual display system decorations flag]'
|
||||
'--no-video[Disable video forwarding]'
|
||||
'--no-video-playback[Disable video playback]'
|
||||
'--orientation=[Set the video orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)'
|
||||
|
29
app/deps/adb_linux.sh
Executable file
29
app/deps/adb_linux.sh
Executable file
@ -0,0 +1,29 @@
|
||||
#!/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/"
|
29
app/deps/adb_macos.sh
Executable file
29
app/deps/adb_macos.sh
Executable file
@ -0,0 +1,29 @@
|
||||
#!/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/"
|
@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
|
||||
cd "$DEPS_DIR"
|
||||
. common
|
||||
|
||||
VERSION=35.0.0
|
||||
FILENAME=platform-tools_r$VERSION-windows.zip
|
||||
PROJECT_DIR=platform-tools-$VERSION
|
||||
SHA256SUM=7ab78a8f8b305ae4d0de647d99c43599744de61a0838d3a47bda0cdffefee87e
|
||||
VERSION=35.0.2
|
||||
FILENAME=platform-tools_r$VERSION-win.zip
|
||||
PROJECT_DIR=platform-tools-$VERSION-windows
|
||||
SHA256SUM=2975a3eac0b19182748d64195375ad056986561d994fffbdc64332a516300bb9
|
||||
|
||||
cd "$SOURCES_DIR"
|
||||
|
||||
@ -27,6 +27,6 @@ else
|
||||
rmdir "$ZIP_PREFIX"
|
||||
fi
|
||||
|
||||
mkdir -p "$INSTALL_DIR/$HOST/bin"
|
||||
cd "$INSTALL_DIR/$HOST/bin"
|
||||
cp -r "$SOURCES_DIR/$PROJECT_DIR"/. "$INSTALL_DIR/$HOST/bin/"
|
||||
mkdir -p "$INSTALL_DIR/adb-windows"
|
||||
cd "$INSTALL_DIR/adb-windows"
|
||||
cp -r "$SOURCES_DIR/$PROJECT_DIR"/. "$INSTALL_DIR/adb-windows/"
|
@ -1,25 +1,47 @@
|
||||
#!/usr/bin/env bash
|
||||
# This file is intended to be sourced by other scripts, not executed
|
||||
|
||||
if [[ $# != 1 ]]
|
||||
then
|
||||
# <host>: win32 or win64
|
||||
echo "Syntax: $0 <host>" >&2
|
||||
exit 1
|
||||
fi
|
||||
process_args() {
|
||||
if [[ $# != 3 ]]
|
||||
then
|
||||
# <host>: win32 or win64
|
||||
# <build_type>: native or cross
|
||||
# <link_type>: static or shared
|
||||
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 [[ "$HOST" = win32 ]]
|
||||
then
|
||||
HOST_TRIPLET=i686-w64-mingw32
|
||||
elif [[ "$HOST" = win64 ]]
|
||||
then
|
||||
HOST_TRIPLET=x86_64-w64-mingw32
|
||||
else
|
||||
echo "Unsupported host: $HOST" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$BUILD_TYPE" != native && "$BUILD_TYPE" != cross ]]
|
||||
then
|
||||
echo "Unsupported build type (expected native or cross): $BUILD_TYPE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$LINK_TYPE" != static && "$LINK_TYPE" != shared ]]
|
||||
then
|
||||
echo "Unsupported link type (expected static or shared): $LINK_TYPE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$BUILD_TYPE" == cross ]]
|
||||
then
|
||||
if [[ "$HOST" = win32 ]]
|
||||
then
|
||||
HOST_TRIPLET=i686-w64-mingw32
|
||||
elif [[ "$HOST" = win64 ]]
|
||||
then
|
||||
HOST_TRIPLET=x86_64-w64-mingw32
|
||||
else
|
||||
echo "Unsupported cross-build to host: $HOST" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
|
||||
cd "$DEPS_DIR"
|
||||
@ -37,7 +59,7 @@ checksum() {
|
||||
local file="$1"
|
||||
local sum="$2"
|
||||
echo "$file: verifying checksum..."
|
||||
echo "$sum $file" | sha256sum -c
|
||||
echo "$sum $file" | shasum -a256 -c
|
||||
}
|
||||
|
||||
get_file() {
|
||||
|
@ -3,11 +3,12 @@ set -ex
|
||||
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
|
||||
cd "$DEPS_DIR"
|
||||
. common
|
||||
process_args "$@"
|
||||
|
||||
VERSION=7.0.2
|
||||
VERSION=7.1
|
||||
FILENAME=ffmpeg-$VERSION.tar.xz
|
||||
PROJECT_DIR=ffmpeg-$VERSION
|
||||
SHA256SUM=8646515b638a3ad303e23af6a3587734447cb8fc0a0c064ecdb8e95c4fd8b389
|
||||
SHA256SUM=40973D44970DBC83EF302B0609F2E74982BE2D85916DD2EE7472D30678A7ABE6
|
||||
|
||||
cd "$SOURCES_DIR"
|
||||
|
||||
@ -22,68 +23,121 @@ fi
|
||||
mkdir -p "$BUILD_DIR/$PROJECT_DIR"
|
||||
cd "$BUILD_DIR/$PROJECT_DIR"
|
||||
|
||||
if [[ "$HOST" = win32 ]]
|
||||
if [[ -d "$DIRNAME" ]]
|
||||
then
|
||||
ARCH=x86
|
||||
elif [[ "$HOST" = win64 ]]
|
||||
then
|
||||
ARCH=x86_64
|
||||
echo "'$PWD/$DIRNAME' already exists, not reconfigured"
|
||||
cd "$DIRNAME"
|
||||
else
|
||||
echo "Unsupported host: $HOST" >&2
|
||||
exit 1
|
||||
fi
|
||||
mkdir "$DIRNAME"
|
||||
cd "$DIRNAME"
|
||||
|
||||
# -static-libgcc to avoid missing libgcc_s_dw2-1.dll
|
||||
# -static to avoid dynamic dependency to zlib
|
||||
export CFLAGS='-static-libgcc -static'
|
||||
export CXXFLAGS="$CFLAGS"
|
||||
export LDFLAGS='-static-libgcc -static'
|
||||
if [[ "$HOST" == win* ]]
|
||||
then
|
||||
# -static-libgcc to avoid missing libgcc_s_dw2-1.dll
|
||||
# -static to avoid dynamic dependency to zlib
|
||||
export CFLAGS='-static-libgcc -static'
|
||||
export CXXFLAGS="$CFLAGS"
|
||||
export LDFLAGS='-static-libgcc -static'
|
||||
elif [[ "$HOST" == "macos" ]]
|
||||
then
|
||||
export LDFLAGS="$LDFLAGS -L/opt/homebrew/opt/zlib/lib"
|
||||
export CPPFLAGS="$CPPFLAGS -I/opt/homebrew/opt/zlib/include"
|
||||
|
||||
if [[ -d "$HOST" ]]
|
||||
then
|
||||
echo "'$PWD/$HOST' already exists, not reconfigured"
|
||||
cd "$HOST"
|
||||
else
|
||||
mkdir "$HOST"
|
||||
cd "$HOST"
|
||||
export LDFLAGS="$LDFLAGS-L/opt/homebrew/opt/libiconv/lib"
|
||||
export CPPFLAGS="$CPPFLAGS -I/opt/homebrew/opt/libiconv/include"
|
||||
export PKG_CONFIG_PATH="/opt/homebrew/opt/zlib/lib/pkgconfig"
|
||||
fi
|
||||
|
||||
"$SOURCES_DIR/$PROJECT_DIR"/configure \
|
||||
--prefix="$INSTALL_DIR/$HOST" \
|
||||
--enable-cross-compile \
|
||||
--target-os=mingw32 \
|
||||
--arch="$ARCH" \
|
||||
--cross-prefix="${HOST_TRIPLET}-" \
|
||||
--cc="${HOST_TRIPLET}-gcc" \
|
||||
--extra-cflags="-O2 -fPIC" \
|
||||
--enable-shared \
|
||||
--disable-static \
|
||||
--disable-programs \
|
||||
--disable-doc \
|
||||
--disable-swscale \
|
||||
--disable-postproc \
|
||||
--disable-avfilter \
|
||||
--disable-avdevice \
|
||||
--disable-network \
|
||||
--disable-everything \
|
||||
--enable-swresample \
|
||||
--enable-decoder=h264 \
|
||||
--enable-decoder=hevc \
|
||||
--enable-decoder=av1 \
|
||||
--enable-decoder=pcm_s16le \
|
||||
--enable-decoder=opus \
|
||||
--enable-decoder=aac \
|
||||
--enable-decoder=flac \
|
||||
--enable-decoder=png \
|
||||
--enable-protocol=file \
|
||||
--enable-demuxer=image2 \
|
||||
--enable-parser=png \
|
||||
--enable-zlib \
|
||||
--enable-muxer=matroska \
|
||||
--enable-muxer=mp4 \
|
||||
--enable-muxer=opus \
|
||||
--enable-muxer=flac \
|
||||
--enable-muxer=wav \
|
||||
conf=(
|
||||
--prefix="$INSTALL_DIR/$DIRNAME"
|
||||
--extra-cflags="-O2 -fPIC"
|
||||
--disable-programs
|
||||
--disable-doc
|
||||
--disable-swscale
|
||||
--disable-postproc
|
||||
--disable-avfilter
|
||||
--disable-network
|
||||
--disable-everything
|
||||
--disable-vulkan
|
||||
--disable-vaapi
|
||||
--disable-vdpau
|
||||
--enable-swresample
|
||||
--enable-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
|
||||
|
||||
make -j
|
||||
|
@ -3,6 +3,7 @@ set -ex
|
||||
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
|
||||
cd "$DEPS_DIR"
|
||||
. common
|
||||
process_args "$@"
|
||||
|
||||
VERSION=1.0.27
|
||||
FILENAME=libusb-$VERSION.tar.gz
|
||||
@ -25,20 +26,40 @@ cd "$BUILD_DIR/$PROJECT_DIR"
|
||||
export CFLAGS='-O2'
|
||||
export CXXFLAGS="$CFLAGS"
|
||||
|
||||
if [[ -d "$HOST" ]]
|
||||
if [[ -d "$DIRNAME" ]]
|
||||
then
|
||||
echo "'$PWD/$HOST' already exists, not reconfigured"
|
||||
cd "$HOST"
|
||||
echo "'$PWD/$DIRNAME' already exists, not reconfigured"
|
||||
cd "$DIRNAME"
|
||||
else
|
||||
mkdir "$HOST"
|
||||
cd "$HOST"
|
||||
mkdir "$DIRNAME"
|
||||
cd "$DIRNAME"
|
||||
|
||||
conf=(
|
||||
--prefix="$INSTALL_DIR/$DIRNAME"
|
||||
)
|
||||
|
||||
if [[ "$LINK_TYPE" == static ]]
|
||||
then
|
||||
conf+=(
|
||||
--enable-static
|
||||
--disable-shared
|
||||
)
|
||||
else
|
||||
conf+=(
|
||||
--disable-static
|
||||
--enable-shared
|
||||
)
|
||||
fi
|
||||
|
||||
if [[ "$BUILD_TYPE" == cross ]]
|
||||
then
|
||||
conf+=(
|
||||
--host="$HOST_TRIPLET"
|
||||
)
|
||||
fi
|
||||
|
||||
"$SOURCES_DIR/$PROJECT_DIR"/bootstrap.sh
|
||||
"$SOURCES_DIR/$PROJECT_DIR"/configure \
|
||||
--prefix="$INSTALL_DIR/$HOST" \
|
||||
--host="$HOST_TRIPLET" \
|
||||
--enable-shared \
|
||||
--disable-static
|
||||
"$SOURCES_DIR/$PROJECT_DIR"/configure "${conf[@]}"
|
||||
fi
|
||||
|
||||
make -j
|
||||
|
@ -3,11 +3,12 @@ set -ex
|
||||
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
|
||||
cd "$DEPS_DIR"
|
||||
. common
|
||||
process_args "$@"
|
||||
|
||||
VERSION=2.30.7
|
||||
VERSION=2.30.9
|
||||
FILENAME=SDL-$VERSION.tar.gz
|
||||
PROJECT_DIR=SDL-release-$VERSION
|
||||
SHA256SUM=1578c96f62c9ae36b64e431b2aa0e0b0fd07c275dedbc694afc38e19056688f5
|
||||
SHA256SUM=682a055004081e37d81a7d4ce546c3ee3ef2e0e6a675ed2651e430ccd14eb407
|
||||
|
||||
cd "$SOURCES_DIR"
|
||||
|
||||
@ -25,23 +26,54 @@ cd "$BUILD_DIR/$PROJECT_DIR"
|
||||
export CFLAGS='-O2'
|
||||
export CXXFLAGS="$CFLAGS"
|
||||
|
||||
if [[ -d "$HOST" ]]
|
||||
if [[ -d "$DIRNAME" ]]
|
||||
then
|
||||
echo "'$PWD/$HOST' already exists, not reconfigured"
|
||||
cd "$HOST"
|
||||
echo "'$PWD/$HDIRNAME' already exists, not reconfigured"
|
||||
cd "$DIRNAME"
|
||||
else
|
||||
mkdir "$HOST"
|
||||
cd "$HOST"
|
||||
mkdir "$DIRNAME"
|
||||
cd "$DIRNAME"
|
||||
|
||||
"$SOURCES_DIR/$PROJECT_DIR"/configure \
|
||||
--prefix="$INSTALL_DIR/$HOST" \
|
||||
--host="$HOST_TRIPLET" \
|
||||
--enable-shared \
|
||||
--disable-static
|
||||
conf=(
|
||||
--prefix="$INSTALL_DIR/$DIRNAME"
|
||||
)
|
||||
|
||||
if [[ "$HOST" == linux ]]
|
||||
then
|
||||
conf+=(
|
||||
--enable-video-wayland
|
||||
--enable-video-x11
|
||||
)
|
||||
fi
|
||||
|
||||
if [[ "$LINK_TYPE" == static ]]
|
||||
then
|
||||
conf+=(
|
||||
--enable-static
|
||||
--disable-shared
|
||||
)
|
||||
else
|
||||
conf+=(
|
||||
--disable-static
|
||||
--enable-shared
|
||||
)
|
||||
fi
|
||||
|
||||
if [[ "$BUILD_TYPE" == cross ]]
|
||||
then
|
||||
conf+=(
|
||||
--host="$HOST_TRIPLET"
|
||||
)
|
||||
fi
|
||||
|
||||
"$SOURCES_DIR/$PROJECT_DIR"/configure "${conf[@]}"
|
||||
fi
|
||||
|
||||
make -j
|
||||
# There is no "make install-strip"
|
||||
make install
|
||||
# Strip manually
|
||||
${HOST_TRIPLET}-strip "$INSTALL_DIR/$HOST/bin/SDL2.dll"
|
||||
if [[ "$LINK_TYPE" == shared && "$HOST" == win* ]]
|
||||
then
|
||||
${HOST_TRIPLET}-strip "$INSTALL_DIR/$DIRNAME/bin/SDL2.dll"
|
||||
fi
|
||||
|
@ -46,6 +46,7 @@ src = [
|
||||
'src/util/acksync.c',
|
||||
'src/util/audiobuf.c',
|
||||
'src/util/average.c',
|
||||
'src/util/env.c',
|
||||
'src/util/file.c',
|
||||
'src/util/intmap.c',
|
||||
'src/util/intr.c',
|
||||
@ -109,20 +110,22 @@ endif
|
||||
|
||||
cc = meson.get_compiler('c')
|
||||
|
||||
static = get_option('static')
|
||||
|
||||
dependencies = [
|
||||
dependency('libavformat', version: '>= 57.33'),
|
||||
dependency('libavcodec', version: '>= 57.37'),
|
||||
dependency('libavutil'),
|
||||
dependency('libswresample'),
|
||||
dependency('sdl2', version: '>= 2.0.5'),
|
||||
dependency('libavformat', version: '>= 57.33', static: static),
|
||||
dependency('libavcodec', version: '>= 57.37', static: static),
|
||||
dependency('libavutil', static: static),
|
||||
dependency('libswresample', static: static),
|
||||
dependency('sdl2', version: '>= 2.0.5', static: static),
|
||||
]
|
||||
|
||||
if v4l2_support
|
||||
dependencies += dependency('libavdevice')
|
||||
dependencies += dependency('libavdevice', static: static)
|
||||
endif
|
||||
|
||||
if usb_support
|
||||
dependencies += dependency('libusb-1.0')
|
||||
dependencies += dependency('libusb-1.0', static: static)
|
||||
endif
|
||||
|
||||
if host_machine.system() == 'windows'
|
||||
|
@ -13,7 +13,7 @@ BEGIN
|
||||
VALUE "LegalCopyright", "Romain Vimont, Genymobile"
|
||||
VALUE "OriginalFilename", "scrcpy.exe"
|
||||
VALUE "ProductName", "scrcpy"
|
||||
VALUE "ProductVersion", "2.7"
|
||||
VALUE "ProductVersion", "3.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
63
app/scrcpy.1
63
app/scrcpy.1
@ -19,6 +19,10 @@ provides display and control of Android devices connected on USB (or over TCP/IP
|
||||
.B \-\-always\-on\-top
|
||||
Make scrcpy window always on top (above other windows).
|
||||
|
||||
.TP
|
||||
.BI "\-\-angle " degrees
|
||||
Rotate the video content by a custom angle, in degrees (clockwise).
|
||||
|
||||
.TP
|
||||
.BI "\-\-audio\-bit\-rate " value
|
||||
Encode the audio at the given bit rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000).
|
||||
@ -93,18 +97,6 @@ Select the camera size by its aspect ratio (+/- 10%).
|
||||
|
||||
Possible values are "sensor" (use the camera sensor aspect ratio), "\fInum\fR:\fIden\fR" (e.g. "4:3") and "\fIvalue\fR" (e.g. "1.6").
|
||||
|
||||
.TP
|
||||
.B \-\-camera\-high\-speed
|
||||
Enable high-speed camera capture mode.
|
||||
|
||||
This mode is restricted to specific resolutions and frame rates, listed by \fB\-\-list\-camera\-sizes\fR.
|
||||
|
||||
.TP
|
||||
.BI "\-\-camera\-id " id
|
||||
Specify the device camera id to mirror.
|
||||
|
||||
The available camera ids can be listed by \fB\-\-list\-cameras\fR.
|
||||
|
||||
.TP
|
||||
.BI "\-\-camera\-facing " facing
|
||||
Select the device camera by its facing direction.
|
||||
@ -117,17 +109,39 @@ Specify the camera capture frame rate.
|
||||
|
||||
If not specified, Android's default frame rate (30 fps) is used.
|
||||
|
||||
.TP
|
||||
.B \-\-camera\-high\-speed
|
||||
Enable high-speed camera capture mode.
|
||||
|
||||
This mode is restricted to specific resolutions and frame rates, listed by \fB\-\-list\-camera\-sizes\fR.
|
||||
|
||||
.TP
|
||||
.BI "\-\-camera\-id " id
|
||||
Specify the device camera id to mirror.
|
||||
|
||||
The available camera ids can be listed by \fB\-\-list\-cameras\fR.
|
||||
|
||||
.TP
|
||||
.BI "\-\-camera\-size " width\fRx\fIheight
|
||||
Specify an explicit camera capture size.
|
||||
|
||||
.TP
|
||||
.BI "\-\-capture\-orientation " value
|
||||
Possible values are 0, 90, 180, 270, flip0, flip90, flip180 and flip270, possibly prefixed by '@'.
|
||||
|
||||
The number represents the clockwise rotation in degrees; the "flip" keyword applies a horizontal flip before the rotation.
|
||||
|
||||
If a leading '@' is passed (@90) for display capture, then the rotation is locked, and is relative to the natural device orientation.
|
||||
|
||||
If '@' is passed alone, then the rotation is locked to the initial device orientation.
|
||||
|
||||
Default is 0.
|
||||
|
||||
.TP
|
||||
.BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy
|
||||
Crop the device screen on the server.
|
||||
|
||||
The values are expressed in the device natural orientation (typically, portrait for a phone, landscape for a tablet). Any
|
||||
.B \-\-max\-size
|
||||
value is computed on the cropped size.
|
||||
The values are expressed in the device natural orientation (typically, portrait for a phone, landscape for a tablet).
|
||||
|
||||
.TP
|
||||
.B \-d, \-\-select\-usb
|
||||
@ -241,16 +255,6 @@ List video and audio encoders available on the device.
|
||||
.B \-\-list\-displays
|
||||
List displays available on the device.
|
||||
|
||||
.TP
|
||||
\fB\-\-lock\-video\-orientation\fR[=\fIvalue\fR]
|
||||
Lock capture video orientation to \fIvalue\fR.
|
||||
|
||||
Possible values are "unlocked", "initial" (locked to the initial orientation), 0, 90, 180, and 270. The values represent the clockwise rotation from the natural device orientation, in degrees.
|
||||
|
||||
Default is "unlocked".
|
||||
|
||||
Passing the option without argument is equivalent to passing "initial".
|
||||
|
||||
.TP
|
||||
.BI "\-m, \-\-max\-size " value
|
||||
Limit both the width and height of the video to \fIvalue\fR. The other dimension is computed so that the device aspect\-ratio is preserved.
|
||||
@ -314,14 +318,13 @@ Disable video and audio playback on the computer (equivalent to \fB\-\-no\-video
|
||||
|
||||
.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, and \fB\-\-max\-size\fR is considered.
|
||||
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 -m1920 # scaled to fit a max size of 1920
|
||||
\-\-new\-display=/240 # main display size and 240 dpi
|
||||
|
||||
.TP
|
||||
@ -366,6 +369,10 @@ Do not forward mouse hover (mouse motion without any clicks) events.
|
||||
.B \-\-no\-power\-on
|
||||
Do not power on the device on start.
|
||||
|
||||
.TP
|
||||
.B \-\-no\-vd\-system\-decorations
|
||||
Disable virtual display system decorations flag.
|
||||
|
||||
.TP
|
||||
.B \-\-no\-video
|
||||
Disable video forwarding.
|
||||
@ -548,8 +555,6 @@ Default is "info" for release builds, "debug" for debug builds.
|
||||
.BI "\-\-v4l2-sink " /dev/videoN
|
||||
Output to v4l2loopback device.
|
||||
|
||||
It requires to lock the video orientation (see \fB\-\-lock\-video\-orientation\fR).
|
||||
|
||||
.TP
|
||||
.BI "\-\-v4l2-buffer " ms
|
||||
Add a buffering delay (in milliseconds) before pushing frames. This increases latency to compensate for jitter.
|
||||
|
@ -7,6 +7,7 @@
|
||||
|
||||
#include "adb_device.h"
|
||||
#include "adb_parser.h"
|
||||
#include "util/env.h"
|
||||
#include "util/file.h"
|
||||
#include "util/log.h"
|
||||
#include "util/process_intr.h"
|
||||
@ -24,15 +25,45 @@
|
||||
*/
|
||||
#define SC_ADB_COMMAND(...) { sc_adb_get_executable(), __VA_ARGS__, NULL }
|
||||
|
||||
static const char *adb_executable;
|
||||
static char *adb_executable;
|
||||
|
||||
bool
|
||||
sc_adb_init(void) {
|
||||
adb_executable = sc_get_env("ADB");
|
||||
if (adb_executable) {
|
||||
LOGD("Using adb: %s", adb_executable);
|
||||
return true;
|
||||
}
|
||||
|
||||
#if !defined(PORTABLE) || defined(_WIN32)
|
||||
adb_executable = strdup("adb");
|
||||
if (!adb_executable) {
|
||||
LOG_OOM();
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
// For portable builds, use the absolute path to the adb executable
|
||||
// in the same directory as scrcpy (except on Windows, where "adb"
|
||||
// is sufficient)
|
||||
adb_executable = sc_file_get_local_path("adb");
|
||||
if (!adb_executable) {
|
||||
// Error already logged
|
||||
return false;
|
||||
}
|
||||
|
||||
LOGD("Using adb (portable): %s", adb_executable);
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
sc_adb_destroy(void) {
|
||||
free(adb_executable);
|
||||
}
|
||||
|
||||
const char *
|
||||
sc_adb_get_executable(void) {
|
||||
if (!adb_executable) {
|
||||
adb_executable = getenv("ADB");
|
||||
if (!adb_executable)
|
||||
adb_executable = "adb";
|
||||
}
|
||||
return adb_executable;
|
||||
}
|
||||
|
||||
@ -739,3 +770,21 @@ sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags) {
|
||||
|
||||
return sc_adb_parse_device_ip(buf);
|
||||
}
|
||||
|
||||
uint16_t
|
||||
sc_adb_get_device_sdk_version(struct sc_intr *intr, const char *serial) {
|
||||
char *sdk_version =
|
||||
sc_adb_getprop(intr, serial, "ro.build.version.sdk", SC_ADB_SILENT);
|
||||
if (!sdk_version) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
long value;
|
||||
bool ok = sc_str_parse_integer(sdk_version, &value);
|
||||
free(sdk_version);
|
||||
if (!ok || value < 0 || value > 0xFFFF) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
@ -15,6 +15,12 @@
|
||||
|
||||
#define SC_ADB_SILENT (SC_ADB_NO_STDOUT | SC_ADB_NO_STDERR | SC_ADB_NO_LOGERR)
|
||||
|
||||
bool
|
||||
sc_adb_init(void);
|
||||
|
||||
void
|
||||
sc_adb_destroy(void);
|
||||
|
||||
const char *
|
||||
sc_adb_get_executable(void);
|
||||
|
||||
@ -114,4 +120,10 @@ sc_adb_getprop(struct sc_intr *intr, const char *serial, const char *prop,
|
||||
char *
|
||||
sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags);
|
||||
|
||||
/**
|
||||
* Return the device SDK version.
|
||||
*/
|
||||
uint16_t
|
||||
sc_adb_get_device_sdk_version(struct sc_intr *intr, const char *serial);
|
||||
|
||||
#endif
|
||||
|
264
app/src/cli.c
264
app/src/cli.c
@ -107,6 +107,9 @@ enum {
|
||||
OPT_LIST_APPS,
|
||||
OPT_START_APP,
|
||||
OPT_SCREEN_OFF_TIMEOUT,
|
||||
OPT_CAPTURE_ORIENTATION,
|
||||
OPT_ANGLE,
|
||||
OPT_NO_VD_SYSTEM_DECORATIONS,
|
||||
};
|
||||
|
||||
struct sc_option {
|
||||
@ -148,6 +151,13 @@ static const struct sc_option options[] = {
|
||||
.longopt = "always-on-top",
|
||||
.text = "Make scrcpy window always on top (above other windows).",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_ANGLE,
|
||||
.longopt = "angle",
|
||||
.argdesc = "degrees",
|
||||
.text = "Rotate the video content by a custom angle, in degrees "
|
||||
"(clockwise).",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_AUDIO_BIT_RATE,
|
||||
.longopt = "audio-bit-rate",
|
||||
@ -245,14 +255,6 @@ static const struct sc_option options[] = {
|
||||
"ratio), \"<num>:<den>\" (e.g. \"4:3\") or \"<value>\" (e.g. "
|
||||
"\"1.6\")."
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_CAMERA_ID,
|
||||
.longopt = "camera-id",
|
||||
.argdesc = "id",
|
||||
.text = "Specify the device camera id to mirror.\n"
|
||||
"The available camera ids can be listed by:\n"
|
||||
" scrcpy --list-cameras",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_CAMERA_FACING,
|
||||
.longopt = "camera-facing",
|
||||
@ -260,6 +262,14 @@ static const struct sc_option options[] = {
|
||||
.text = "Select the device camera by its facing direction.\n"
|
||||
"Possible values are \"front\", \"back\" and \"external\".",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_CAMERA_FPS,
|
||||
.longopt = "camera-fps",
|
||||
.argdesc = "value",
|
||||
.text = "Specify the camera capture frame rate.\n"
|
||||
"If not specified, Android's default frame rate (30 fps) is "
|
||||
"used.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_CAMERA_HIGH_SPEED,
|
||||
.longopt = "camera-high-speed",
|
||||
@ -267,6 +277,14 @@ static const struct sc_option options[] = {
|
||||
"This mode is restricted to specific resolutions and frame "
|
||||
"rates, listed by --list-camera-sizes.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_CAMERA_ID,
|
||||
.longopt = "camera-id",
|
||||
.argdesc = "id",
|
||||
.text = "Specify the device camera id to mirror.\n"
|
||||
"The available camera ids can be listed by:\n"
|
||||
" scrcpy --list-cameras",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_CAMERA_SIZE,
|
||||
.longopt = "camera-size",
|
||||
@ -274,12 +292,21 @@ static const struct sc_option options[] = {
|
||||
.text = "Specify an explicit camera capture size.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_CAMERA_FPS,
|
||||
.longopt = "camera-fps",
|
||||
.longopt_id = OPT_CAPTURE_ORIENTATION,
|
||||
.longopt = "capture-orientation",
|
||||
.argdesc = "value",
|
||||
.text = "Specify the camera capture frame rate.\n"
|
||||
"If not specified, Android's default frame rate (30 fps) is "
|
||||
"used.",
|
||||
.text = "Set the capture video orientation.\n"
|
||||
"Possible values are 0, 90, 180, 270, flip0, flip90, flip180 "
|
||||
"and flip270, possibly prefixed by '@'.\n"
|
||||
"The number represents the clockwise rotation in degrees; the "
|
||||
"flip\" keyword applies a horizontal flip before the "
|
||||
"rotation.\n"
|
||||
"If a leading '@' is passed (@90) for display capture, then "
|
||||
"the rotation is locked, and is relative to the natural device "
|
||||
"orientation.\n"
|
||||
"If '@' is passed alone, then the rotation is locked to the "
|
||||
"initial device orientation.\n"
|
||||
"Default is 0.",
|
||||
},
|
||||
{
|
||||
// Not really deprecated (--codec has never been released), but without
|
||||
@ -302,8 +329,7 @@ static const struct sc_option options[] = {
|
||||
.argdesc = "width:height:x:y",
|
||||
.text = "Crop the device screen on the server.\n"
|
||||
"The values are expressed in the device natural orientation "
|
||||
"(typically, portrait for a phone, landscape for a tablet). "
|
||||
"Any --max-size value is computed on the cropped size.",
|
||||
"(typically, portrait for a phone, landscape for a tablet).",
|
||||
},
|
||||
{
|
||||
.shortopt = 'd',
|
||||
@ -471,18 +497,10 @@ static const struct sc_option options[] = {
|
||||
.text = "List video and audio encoders available on the device.",
|
||||
},
|
||||
{
|
||||
// deprecated
|
||||
.longopt_id = OPT_LOCK_VIDEO_ORIENTATION,
|
||||
.longopt = "lock-video-orientation",
|
||||
.argdesc = "value",
|
||||
.optional_arg = true,
|
||||
.text = "Lock capture video orientation to value.\n"
|
||||
"Possible values are \"unlocked\", \"initial\" (locked to the "
|
||||
"initial orientation), 0, 90, 180 and 270. The values "
|
||||
"represent the clockwise rotation from the natural device "
|
||||
"orientation, in degrees.\n"
|
||||
"Default is \"unlocked\".\n"
|
||||
"Passing the option without argument is equivalent to passing "
|
||||
"\"initial\".",
|
||||
},
|
||||
{
|
||||
.shortopt = 'm',
|
||||
@ -572,12 +590,11 @@ static const struct sc_option options[] = {
|
||||
.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, and --max-size is considered.\n"
|
||||
"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 -m1920 # scaled to fit a max size of 1920\n"
|
||||
" --new-display=/240 # main display size and 240 dpi",
|
||||
},
|
||||
{
|
||||
@ -642,6 +659,11 @@ static const struct sc_option options[] = {
|
||||
.longopt = "no-power-on",
|
||||
.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 = "no-video",
|
||||
@ -895,8 +917,6 @@ static const struct sc_option options[] = {
|
||||
.longopt = "v4l2-sink",
|
||||
.argdesc = "/dev/videoN",
|
||||
.text = "Output to v4l2loopback device.\n"
|
||||
"It requires to lock the video orientation (see "
|
||||
"--lock-video-orientation).\n"
|
||||
"This feature is only available on Linux.",
|
||||
},
|
||||
{
|
||||
@ -1582,78 +1602,6 @@ parse_audio_output_buffer(const char *s, sc_tick *tick) {
|
||||
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
|
||||
parse_orientation(const char *s, enum sc_orientation *orientation) {
|
||||
if (!strcmp(s, "0")) {
|
||||
@ -1693,6 +1641,32 @@ parse_orientation(const char *s, enum sc_orientation *orientation) {
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool
|
||||
parse_capture_orientation(const char *s, enum sc_orientation *orientation,
|
||||
enum sc_orientation_lock *lock) {
|
||||
if (*s == '\0') {
|
||||
LOGE("Capture orientation may not be empty (expected 0, 90, 180, 270, "
|
||||
"flip0, flip90, flip180 or flip270, possibly prefixed by '@')");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Lock the orientation by a leading '@'
|
||||
if (s[0] == '@') {
|
||||
// Consume '@'
|
||||
++s;
|
||||
if (*s == '\0') {
|
||||
// Only '@': lock to the initial orientation (orientation is unused)
|
||||
*lock = SC_ORIENTATION_LOCKED_INITIAL;
|
||||
return true;
|
||||
}
|
||||
*lock = SC_ORIENTATION_LOCKED_VALUE;
|
||||
} else {
|
||||
*lock = SC_ORIENTATION_UNLOCKED;
|
||||
}
|
||||
|
||||
return parse_orientation(s, orientation);
|
||||
}
|
||||
|
||||
static bool
|
||||
parse_window_position(const char *s, int16_t *position) {
|
||||
// special value for "auto"
|
||||
@ -2302,8 +2276,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
opts->crop = optarg;
|
||||
break;
|
||||
case OPT_DISPLAY:
|
||||
LOGW("--display is deprecated, use --display-id instead.");
|
||||
// fall through
|
||||
LOGE("--display has been removed, use --display-id instead.");
|
||||
return false;
|
||||
case OPT_DISPLAY_ID:
|
||||
if (!parse_display_id(optarg, &opts->display_id)) {
|
||||
return false;
|
||||
@ -2367,8 +2341,13 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
"--mouse=uhid instead.");
|
||||
return false;
|
||||
case OPT_LOCK_VIDEO_ORIENTATION:
|
||||
if (!parse_lock_video_orientation(optarg,
|
||||
&opts->lock_video_orientation)) {
|
||||
LOGE("--lock-video-orientation has been removed, use "
|
||||
"--capture-orientation instead.");
|
||||
return false;
|
||||
case OPT_CAPTURE_ORIENTATION:
|
||||
if (!parse_capture_orientation(optarg,
|
||||
&opts->capture_orientation,
|
||||
&opts->capture_orientation_lock)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
@ -2386,8 +2365,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
opts->control = false;
|
||||
break;
|
||||
case OPT_NO_DISPLAY:
|
||||
LOGW("--no-display is deprecated, use --no-playback instead.");
|
||||
// fall through
|
||||
LOGE("--no-display has been removed, use --no-playback "
|
||||
"instead.");
|
||||
return false;
|
||||
case 'N':
|
||||
opts->video_playback = false;
|
||||
opts->audio_playback = false;
|
||||
@ -2473,32 +2453,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
opts->key_inject_mode = SC_KEY_INJECT_MODE_RAW;
|
||||
break;
|
||||
case OPT_ROTATION:
|
||||
LOGW("--rotation is deprecated, use --display-orientation "
|
||||
"instead.");
|
||||
uint8_t rotation;
|
||||
if (!parse_rotation(optarg, &rotation)) {
|
||||
return false;
|
||||
}
|
||||
assert(rotation <= 3);
|
||||
switch (rotation) {
|
||||
case 0:
|
||||
opts->display_orientation = SC_ORIENTATION_0;
|
||||
break;
|
||||
case 1:
|
||||
// rotation 1 was 90° counterclockwise, but orientation
|
||||
// is expressed clockwise
|
||||
opts->display_orientation = SC_ORIENTATION_270;
|
||||
break;
|
||||
case 2:
|
||||
opts->display_orientation = SC_ORIENTATION_180;
|
||||
break;
|
||||
case 3:
|
||||
// rotation 3 was 270° counterclockwise, but orientation
|
||||
// is expressed clockwise
|
||||
opts->display_orientation = SC_ORIENTATION_90;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
LOGE("--rotation has been removed, use --orientation or "
|
||||
"--capture-orientation instead.");
|
||||
return false;
|
||||
case OPT_DISPLAY_ORIENTATION:
|
||||
if (!parse_orientation(optarg, &opts->display_orientation)) {
|
||||
return false;
|
||||
@ -2559,23 +2516,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
}
|
||||
break;
|
||||
case OPT_FORWARD_ALL_CLICKS:
|
||||
LOGW("--forward-all-clicks is deprecated, "
|
||||
LOGE("--forward-all-clicks has been removed, "
|
||||
"use --mouse-bind=++++ instead.");
|
||||
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;
|
||||
return false;
|
||||
case OPT_LEGACY_PASTE:
|
||||
opts->legacy_paste = true;
|
||||
break;
|
||||
@ -2583,9 +2526,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
opts->power_off_on_close = true;
|
||||
break;
|
||||
case OPT_DISPLAY_BUFFER:
|
||||
LOGW("--display-buffer is deprecated, use --video-buffer "
|
||||
LOGE("--display-buffer has been removed, use --video-buffer "
|
||||
"instead.");
|
||||
// fall through
|
||||
return false;
|
||||
case OPT_VIDEO_BUFFER:
|
||||
if (!parse_buffering_time(optarg, &opts->video_buffer)) {
|
||||
return false;
|
||||
@ -2758,6 +2701,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case OPT_ANGLE:
|
||||
opts->angle = optarg;
|
||||
break;
|
||||
case OPT_NO_VD_SYSTEM_DECORATIONS:
|
||||
opts->vd_system_decorations = optarg;
|
||||
break;
|
||||
default:
|
||||
// getopt prints the error message on stderr
|
||||
return false;
|
||||
@ -2852,14 +2801,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
return false;
|
||||
}
|
||||
|
||||
if (opts->lock_video_orientation ==
|
||||
SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) {
|
||||
LOGI("Video orientation is locked for v4l2 sink. "
|
||||
"See --lock-video-orientation.");
|
||||
opts->lock_video_orientation =
|
||||
SC_LOCK_VIDEO_ORIENTATION_INITIAL_AUTO;
|
||||
}
|
||||
|
||||
// V4L2 could not handle size change.
|
||||
// Do not log because downsizing on error is the default behavior,
|
||||
// not an explicit request from the user.
|
||||
@ -2949,13 +2890,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
LOGE("--new-display is incompatible with --no-video");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (opts->max_size && opts->new_display[0] != '\0'
|
||||
&& opts->new_display[0] != '/') {
|
||||
// An explicit size is defined (not "" nor "/<dpi>")
|
||||
LOGE("Cannot specify both --new-display size and -m/--max-size");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (otg) {
|
||||
|
@ -9,6 +9,7 @@
|
||||
|
||||
#include "config.h"
|
||||
#include "compat.h"
|
||||
#include "util/env.h"
|
||||
#include "util/file.h"
|
||||
#include "util/log.h"
|
||||
#include "util/str.h"
|
||||
@ -19,35 +20,22 @@
|
||||
|
||||
static char *
|
||||
get_icon_path(void) {
|
||||
#ifdef __WINDOWS__
|
||||
const wchar_t *icon_path_env = _wgetenv(L"SCRCPY_ICON_PATH");
|
||||
#else
|
||||
const char *icon_path_env = getenv("SCRCPY_ICON_PATH");
|
||||
#endif
|
||||
if (icon_path_env) {
|
||||
char *icon_path = sc_get_env("SCRCPY_ICON_PATH");
|
||||
if (icon_path) {
|
||||
// if the envvar is set, use it
|
||||
#ifdef __WINDOWS__
|
||||
char *icon_path = sc_str_from_wchars(icon_path_env);
|
||||
#else
|
||||
char *icon_path = strdup(icon_path_env);
|
||||
#endif
|
||||
if (!icon_path) {
|
||||
LOG_OOM();
|
||||
return NULL;
|
||||
}
|
||||
LOGD("Using SCRCPY_ICON_PATH: %s", icon_path);
|
||||
return icon_path;
|
||||
}
|
||||
|
||||
#ifndef PORTABLE
|
||||
LOGD("Using icon: " SCRCPY_DEFAULT_ICON_PATH);
|
||||
char *icon_path = strdup(SCRCPY_DEFAULT_ICON_PATH);
|
||||
icon_path = strdup(SCRCPY_DEFAULT_ICON_PATH);
|
||||
if (!icon_path) {
|
||||
LOG_OOM();
|
||||
return NULL;
|
||||
}
|
||||
#else
|
||||
char *icon_path = sc_file_get_local_path(SCRCPY_PORTABLE_ICON_FILENAME);
|
||||
icon_path = sc_file_get_local_path(SCRCPY_PORTABLE_ICON_FILENAME);
|
||||
if (!icon_path) {
|
||||
LOGE("Could not get icon path");
|
||||
return NULL;
|
||||
|
@ -50,7 +50,8 @@ const struct scrcpy_options scrcpy_options_default = {
|
||||
.video_bit_rate = 0,
|
||||
.audio_bit_rate = 0,
|
||||
.max_fps = NULL,
|
||||
.lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED,
|
||||
.capture_orientation = SC_ORIENTATION_0,
|
||||
.capture_orientation_lock = SC_ORIENTATION_UNLOCKED,
|
||||
.display_orientation = SC_ORIENTATION_0,
|
||||
.record_orientation = SC_ORIENTATION_0,
|
||||
.window_x = SC_WINDOW_POSITION_UNDEFINED,
|
||||
@ -106,6 +107,8 @@ const struct scrcpy_options scrcpy_options_default = {
|
||||
.audio_dup = false,
|
||||
.new_display = NULL,
|
||||
.start_app = NULL,
|
||||
.angle = NULL,
|
||||
.vd_system_decorations = true,
|
||||
};
|
||||
|
||||
enum sc_orientation
|
||||
|
@ -84,6 +84,12 @@ enum sc_orientation { // v v v
|
||||
SC_ORIENTATION_FLIP_270, // 1 1 1
|
||||
};
|
||||
|
||||
enum sc_orientation_lock {
|
||||
SC_ORIENTATION_UNLOCKED,
|
||||
SC_ORIENTATION_LOCKED_VALUE, // lock to specified orientation
|
||||
SC_ORIENTATION_LOCKED_INITIAL, // lock to initial device orientation
|
||||
};
|
||||
|
||||
static inline bool
|
||||
sc_orientation_is_mirror(enum sc_orientation orientation) {
|
||||
assert(!(orientation & ~7));
|
||||
@ -130,18 +136,6 @@ 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,
|
||||
// like SC_LOCK_VIDEO_ORIENTATION_INITIAL, but set automatically
|
||||
SC_LOCK_VIDEO_ORIENTATION_INITIAL_AUTO = -3,
|
||||
SC_LOCK_VIDEO_ORIENTATION_0 = 0,
|
||||
SC_LOCK_VIDEO_ORIENTATION_90 = 3,
|
||||
SC_LOCK_VIDEO_ORIENTATION_180 = 2,
|
||||
SC_LOCK_VIDEO_ORIENTATION_270 = 1,
|
||||
};
|
||||
|
||||
enum sc_keyboard_input_mode {
|
||||
SC_KEYBOARD_INPUT_MODE_AUTO,
|
||||
SC_KEYBOARD_INPUT_MODE_UHID_OR_AOA, // normal vs otg mode
|
||||
@ -253,7 +247,9 @@ struct scrcpy_options {
|
||||
uint32_t video_bit_rate;
|
||||
uint32_t audio_bit_rate;
|
||||
const char *max_fps; // float to be parsed by the server
|
||||
enum sc_lock_video_orientation lock_video_orientation;
|
||||
const char *angle; // float to be parsed by the server
|
||||
enum sc_orientation capture_orientation;
|
||||
enum sc_orientation_lock capture_orientation_lock;
|
||||
enum sc_orientation display_orientation;
|
||||
enum sc_orientation record_orientation;
|
||||
int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto"
|
||||
@ -314,6 +310,7 @@ struct scrcpy_options {
|
||||
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;
|
||||
|
@ -143,8 +143,14 @@ sc_recorder_open_output_file(struct sc_recorder *recorder) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int ret = avio_open(&recorder->ctx->pb, recorder->filename,
|
||||
AVIO_FLAG_WRITE);
|
||||
char *file_url = sc_str_concat("file:", recorder->filename);
|
||||
if (!file_url) {
|
||||
avformat_free_context(recorder->ctx);
|
||||
return false;
|
||||
}
|
||||
|
||||
int ret = avio_open(&recorder->ctx->pb, file_url, AVIO_FLAG_WRITE);
|
||||
free(file_url);
|
||||
if (ret < 0) {
|
||||
LOGE("Failed to open output file: %s", recorder->filename);
|
||||
avformat_free_context(recorder->ctx);
|
||||
|
@ -428,8 +428,10 @@ scrcpy(struct scrcpy_options *options) {
|
||||
.video_bit_rate = options->video_bit_rate,
|
||||
.audio_bit_rate = options->audio_bit_rate,
|
||||
.max_fps = options->max_fps,
|
||||
.angle = options->angle,
|
||||
.screen_off_timeout = options->screen_off_timeout,
|
||||
.lock_video_orientation = options->lock_video_orientation,
|
||||
.capture_orientation = options->capture_orientation,
|
||||
.capture_orientation_lock = options->capture_orientation_lock,
|
||||
.control = options->control,
|
||||
.display_id = options->display_id,
|
||||
.new_display = options->new_display,
|
||||
@ -456,6 +458,7 @@ scrcpy(struct scrcpy_options *options) {
|
||||
.power_on = options->power_on,
|
||||
.kill_adb_on_close = options->kill_adb_on_close,
|
||||
.camera_high_speed = options->camera_high_speed,
|
||||
.vd_system_decorations = options->vd_system_decorations,
|
||||
.list = options->list,
|
||||
};
|
||||
|
||||
|
@ -9,6 +9,7 @@
|
||||
|
||||
#include "adb/adb.h"
|
||||
#include "util/binary.h"
|
||||
#include "util/env.h"
|
||||
#include "util/file.h"
|
||||
#include "util/log.h"
|
||||
#include "util/net_intr.h"
|
||||
@ -25,35 +26,22 @@
|
||||
|
||||
static char *
|
||||
get_server_path(void) {
|
||||
#ifdef __WINDOWS__
|
||||
const wchar_t *server_path_env = _wgetenv(L"SCRCPY_SERVER_PATH");
|
||||
#else
|
||||
const char *server_path_env = getenv("SCRCPY_SERVER_PATH");
|
||||
#endif
|
||||
if (server_path_env) {
|
||||
char *server_path = sc_get_env("SCRCPY_SERVER_PATH");
|
||||
if (server_path) {
|
||||
// if the envvar is set, use it
|
||||
#ifdef __WINDOWS__
|
||||
char *server_path = sc_str_from_wchars(server_path_env);
|
||||
#else
|
||||
char *server_path = strdup(server_path_env);
|
||||
#endif
|
||||
if (!server_path) {
|
||||
LOG_OOM();
|
||||
return NULL;
|
||||
}
|
||||
LOGD("Using SCRCPY_SERVER_PATH: %s", server_path);
|
||||
return server_path;
|
||||
}
|
||||
|
||||
#ifndef PORTABLE
|
||||
LOGD("Using server: " SC_SERVER_PATH_DEFAULT);
|
||||
char *server_path = strdup(SC_SERVER_PATH_DEFAULT);
|
||||
server_path = strdup(SC_SERVER_PATH_DEFAULT);
|
||||
if (!server_path) {
|
||||
LOG_OOM();
|
||||
return NULL;
|
||||
}
|
||||
#else
|
||||
char *server_path = sc_file_get_local_path(SC_SERVER_FILENAME);
|
||||
server_path = sc_file_get_local_path(SC_SERVER_FILENAME);
|
||||
if (!server_path) {
|
||||
LOGE("Could not get local file path, "
|
||||
"using " SC_SERVER_FILENAME " from current directory");
|
||||
@ -183,27 +171,6 @@ validate_string(const char *s) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static uint16_t
|
||||
get_device_sdk_version(struct sc_server *server) {
|
||||
struct sc_intr *intr = &server->intr;
|
||||
|
||||
char *sdk_version =
|
||||
sc_adb_getprop(intr, server->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;
|
||||
}
|
||||
|
||||
static sc_pid
|
||||
execute_server(struct sc_server *server,
|
||||
const struct sc_server_params *params) {
|
||||
@ -222,7 +189,7 @@ execute_server(struct sc_server *server,
|
||||
cmd[count++] = "app_process";
|
||||
|
||||
#ifdef SERVER_DEBUGGER
|
||||
uint16_t sdk_version = get_device_sdk_version(server);
|
||||
uint16_t sdk_version = sc_adb_get_device_sdk_version(&server->intr, serial);
|
||||
if (!sdk_version) {
|
||||
LOGE("Could not determine SDK version");
|
||||
return 0;
|
||||
@ -234,10 +201,15 @@ execute_server(struct sc_server *server,
|
||||
// Android < 9
|
||||
dbg = "-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address="
|
||||
SERVER_DEBUGGER_PORT;
|
||||
} else {
|
||||
// Android >= 9
|
||||
} else if (sdk_version < 30) {
|
||||
// Android >= 9 && Android < 11
|
||||
dbg = "-XjdwpProvider:internal -XjdwpOptions:transport=dt_socket,"
|
||||
"suspend=y,server=y,address=" SERVER_DEBUGGER_PORT;
|
||||
} else {
|
||||
// Android >= 11
|
||||
// Contrary to the other methods, this does not suspend on start.
|
||||
// <https://github.com/Genymobile/scrcpy/pull/5466>
|
||||
dbg = "-XjdwpProvider:adbconnection";
|
||||
}
|
||||
cmd[count++] = dbg;
|
||||
#endif
|
||||
@ -303,9 +275,21 @@ execute_server(struct sc_server *server,
|
||||
VALIDATE_STRING(params->max_fps);
|
||||
ADD_PARAM("max_fps=%s", params->max_fps);
|
||||
}
|
||||
if (params->lock_video_orientation != SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) {
|
||||
ADD_PARAM("lock_video_orientation=%" PRIi8,
|
||||
params->lock_video_orientation);
|
||||
if (params->angle) {
|
||||
VALIDATE_STRING(params->angle);
|
||||
ADD_PARAM("angle=%s", params->angle);
|
||||
}
|
||||
if (params->capture_orientation_lock != SC_ORIENTATION_UNLOCKED
|
||||
|| params->capture_orientation != SC_ORIENTATION_0) {
|
||||
if (params->capture_orientation_lock == SC_ORIENTATION_LOCKED_INITIAL) {
|
||||
ADD_PARAM("capture_orientation=@");
|
||||
} else {
|
||||
const char *orient =
|
||||
sc_orientation_get_name(params->capture_orientation);
|
||||
bool locked =
|
||||
params->capture_orientation_lock != SC_ORIENTATION_UNLOCKED;
|
||||
ADD_PARAM("capture_orientation=%s%s", locked ? "@" : "", orient);
|
||||
}
|
||||
}
|
||||
if (server->tunnel.forward) {
|
||||
ADD_PARAM("tunnel_forward=true");
|
||||
@ -393,6 +377,9 @@ execute_server(struct sc_server *server,
|
||||
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) {
|
||||
ADD_PARAM("list_encoders=true");
|
||||
}
|
||||
@ -414,10 +401,14 @@ execute_server(struct sc_server *server,
|
||||
cmd[count++] = NULL;
|
||||
|
||||
#ifdef SERVER_DEBUGGER
|
||||
LOGI("Server debugger waiting for a client on device port "
|
||||
SERVER_DEBUGGER_PORT "...");
|
||||
// From the computer, run
|
||||
// adb forward tcp:5005 tcp:5005
|
||||
LOGI("Server debugger listening%s...",
|
||||
sdk_version < 30 ? " on port " SERVER_DEBUGGER_PORT : "");
|
||||
// For Android < 11, from the computer:
|
||||
// - run `adb forward tcp:5005 tcp:5005`
|
||||
// For Android >= 11:
|
||||
// - execute `adb jdwp` to get the jdwp port
|
||||
// - run `adb forward tcp:5005 jdwp:XXXX` (replace XXXX)
|
||||
//
|
||||
// Then, from Android Studio: Run > Debug > Edit configurations...
|
||||
// On the left, click on '+', "Remote", with:
|
||||
// Host: localhost
|
||||
@ -494,14 +485,21 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params,
|
||||
// end of the program
|
||||
server->params = *params;
|
||||
|
||||
bool ok = sc_mutex_init(&server->mutex);
|
||||
bool ok = sc_adb_init();
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ok = sc_mutex_init(&server->mutex);
|
||||
if (!ok) {
|
||||
sc_adb_destroy();
|
||||
return false;
|
||||
}
|
||||
|
||||
ok = sc_cond_init(&server->cond_stopped);
|
||||
if (!ok) {
|
||||
sc_mutex_destroy(&server->mutex);
|
||||
sc_adb_destroy();
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -509,6 +507,7 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params,
|
||||
if (!ok) {
|
||||
sc_cond_destroy(&server->cond_stopped);
|
||||
sc_mutex_destroy(&server->mutex);
|
||||
sc_adb_destroy();
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -1150,4 +1149,6 @@ sc_server_destroy(struct sc_server *server) {
|
||||
sc_intr_destroy(&server->intr);
|
||||
sc_cond_destroy(&server->cond_stopped);
|
||||
sc_mutex_destroy(&server->mutex);
|
||||
|
||||
sc_adb_destroy();
|
||||
}
|
||||
|
@ -45,8 +45,10 @@ struct sc_server_params {
|
||||
uint32_t video_bit_rate;
|
||||
uint32_t audio_bit_rate;
|
||||
const char *max_fps; // float to be parsed by the server
|
||||
const char *angle; // float to be parsed by the server
|
||||
sc_tick screen_off_timeout;
|
||||
int8_t lock_video_orientation;
|
||||
enum sc_orientation capture_orientation;
|
||||
enum sc_orientation_lock capture_orientation_lock;
|
||||
bool control;
|
||||
uint32_t display_id;
|
||||
const char *new_display;
|
||||
@ -67,6 +69,7 @@ struct sc_server_params {
|
||||
bool power_on;
|
||||
bool kill_adb_on_close;
|
||||
bool camera_high_speed;
|
||||
bool vd_system_decorations;
|
||||
uint8_t list;
|
||||
};
|
||||
|
||||
|
@ -6,6 +6,9 @@
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#ifdef __APPLE__
|
||||
# include <mach-o/dyld.h> // for _NSGetExecutablePath()
|
||||
#endif
|
||||
|
||||
#include "util/log.h"
|
||||
|
||||
@ -60,11 +63,22 @@ sc_file_get_executable_path(void) {
|
||||
}
|
||||
buf[len] = '\0';
|
||||
return strdup(buf);
|
||||
#elif defined(__APPLE__)
|
||||
char buf[PATH_MAX];
|
||||
uint32_t bufsize = PATH_MAX;
|
||||
if (_NSGetExecutablePath(buf, &bufsize) != 0) {
|
||||
LOGE("Executable path buffer too small; need %u bytes", bufsize);
|
||||
return NULL;
|
||||
}
|
||||
return realpath(buf, NULL);
|
||||
#else
|
||||
// in practice, we only need this feature for portable builds, only used on
|
||||
// Windows, so we don't care implementing it for every platform
|
||||
// (it's useful to have a working version on Linux for debugging though)
|
||||
return NULL;
|
||||
// "_" is often used to store the full path of the command being executed
|
||||
char *path = getenv("_");
|
||||
if (!path) {
|
||||
LOGE("Could not determine executable path");
|
||||
return NULL;
|
||||
}
|
||||
return strdup(path);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -95,9 +95,14 @@ scrcpy_otg(struct scrcpy_options *options) {
|
||||
// On Windows, only one process could open a USB device
|
||||
// <https://github.com/Genymobile/scrcpy/issues/2773>
|
||||
LOGI("Killing adb server (if any)...");
|
||||
unsigned flags = SC_ADB_NO_STDOUT | SC_ADB_NO_STDERR | SC_ADB_NO_LOGERR;
|
||||
// uninterruptible (intr == NULL), but in practice it's very quick
|
||||
sc_adb_kill_server(NULL, flags);
|
||||
if (sc_adb_init()) {
|
||||
unsigned flags = SC_ADB_NO_STDOUT | SC_ADB_NO_STDERR | SC_ADB_NO_LOGERR;
|
||||
// uninterruptible (intr == NULL), but in practice it's very quick
|
||||
sc_adb_kill_server(NULL, flags);
|
||||
sc_adb_destroy();
|
||||
} else {
|
||||
LOGW("Could not call adb executable, adb server not killed");
|
||||
}
|
||||
#endif
|
||||
|
||||
static const struct sc_usb_callbacks cbs = {
|
||||
|
29
app/src/util/env.c
Normal file
29
app/src/util/env.c
Normal file
@ -0,0 +1,29 @@
|
||||
#include "env.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "util/str.h"
|
||||
|
||||
char *
|
||||
sc_get_env(const char *varname) {
|
||||
#ifdef _WIN32
|
||||
wchar_t *w_varname = sc_str_to_wchars(varname);
|
||||
if (!w_varname) {
|
||||
return NULL;
|
||||
}
|
||||
const wchar_t *value = _wgetenv(w_varname);
|
||||
free(w_varname);
|
||||
if (!value) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return sc_str_from_wchars(value);
|
||||
#else
|
||||
const char *value = getenv(varname);
|
||||
if (!value) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return strdup(value);
|
||||
#endif
|
||||
}
|
12
app/src/util/env.h
Normal file
12
app/src/util/env.h
Normal file
@ -0,0 +1,12 @@
|
||||
#ifndef SC_ENV_H
|
||||
#define SC_ENV_H
|
||||
|
||||
#include "common.h"
|
||||
|
||||
// Return the value of the environment variable (may be NULL).
|
||||
//
|
||||
// The returned value must be freed by the caller.
|
||||
char *
|
||||
sc_get_env(const char *varname);
|
||||
|
||||
#endif
|
@ -9,8 +9,6 @@
|
||||
#ifdef _WIN32
|
||||
# include <ws2tcpip.h>
|
||||
typedef int socklen_t;
|
||||
typedef SOCKET sc_raw_socket;
|
||||
# define SC_RAW_SOCKET_NONE INVALID_SOCKET
|
||||
#else
|
||||
# include <sys/types.h>
|
||||
# include <sys/socket.h>
|
||||
@ -23,8 +21,6 @@
|
||||
typedef struct sockaddr_in SOCKADDR_IN;
|
||||
typedef struct sockaddr SOCKADDR;
|
||||
typedef struct in_addr IN_ADDR;
|
||||
typedef int sc_raw_socket;
|
||||
# define SC_RAW_SOCKET_NONE -1
|
||||
#endif
|
||||
|
||||
bool
|
||||
@ -47,17 +43,26 @@ net_cleanup(void) {
|
||||
#endif
|
||||
}
|
||||
|
||||
static inline bool
|
||||
sc_raw_socket_close(sc_raw_socket raw_sock) {
|
||||
#ifndef _WIN32
|
||||
return !close(raw_sock);
|
||||
#else
|
||||
return !closesocket(raw_sock);
|
||||
#endif
|
||||
}
|
||||
|
||||
static inline sc_socket
|
||||
wrap(sc_raw_socket sock) {
|
||||
#ifdef _WIN32
|
||||
if (sock == INVALID_SOCKET) {
|
||||
#ifdef SC_SOCKET_CLOSE_ON_INTERRUPT
|
||||
if (sock == SC_RAW_SOCKET_NONE) {
|
||||
return SC_SOCKET_NONE;
|
||||
}
|
||||
|
||||
struct sc_socket_windows *socket = malloc(sizeof(*socket));
|
||||
struct sc_socket_wrapper *socket = malloc(sizeof(*socket));
|
||||
if (!socket) {
|
||||
LOG_OOM();
|
||||
closesocket(sock);
|
||||
sc_raw_socket_close(sock);
|
||||
return SC_SOCKET_NONE;
|
||||
}
|
||||
|
||||
@ -72,9 +77,9 @@ wrap(sc_raw_socket sock) {
|
||||
|
||||
static inline sc_raw_socket
|
||||
unwrap(sc_socket socket) {
|
||||
#ifdef _WIN32
|
||||
#ifdef SC_SOCKET_CLOSE_ON_INTERRUPT
|
||||
if (socket == SC_SOCKET_NONE) {
|
||||
return INVALID_SOCKET;
|
||||
return SC_RAW_SOCKET_NONE;
|
||||
}
|
||||
|
||||
return socket->socket;
|
||||
@ -83,17 +88,6 @@ unwrap(sc_socket socket) {
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifndef HAVE_SOCK_CLOEXEC // avoid unused-function warning
|
||||
static inline bool
|
||||
sc_raw_socket_close(sc_raw_socket raw_sock) {
|
||||
#ifndef _WIN32
|
||||
return !close(raw_sock);
|
||||
#else
|
||||
return !closesocket(raw_sock);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifndef HAVE_SOCK_CLOEXEC
|
||||
// If SOCK_CLOEXEC does not exist, the flag must be set manually once the
|
||||
// socket is created
|
||||
@ -248,9 +242,9 @@ net_interrupt(sc_socket socket) {
|
||||
|
||||
sc_raw_socket raw_sock = unwrap(socket);
|
||||
|
||||
#ifdef _WIN32
|
||||
#ifdef SC_SOCKET_CLOSE_ON_INTERRUPT
|
||||
if (!atomic_flag_test_and_set(&socket->closed)) {
|
||||
return !closesocket(raw_sock);
|
||||
return sc_raw_socket_close(raw_sock);
|
||||
}
|
||||
return true;
|
||||
#else
|
||||
@ -262,15 +256,15 @@ bool
|
||||
net_close(sc_socket socket) {
|
||||
sc_raw_socket raw_sock = unwrap(socket);
|
||||
|
||||
#ifdef _WIN32
|
||||
#ifdef SC_SOCKET_CLOSE_ON_INTERRUPT
|
||||
bool ret = true;
|
||||
if (!atomic_flag_test_and_set(&socket->closed)) {
|
||||
ret = !closesocket(raw_sock);
|
||||
ret = sc_raw_socket_close(raw_sock);
|
||||
}
|
||||
free(socket);
|
||||
return ret;
|
||||
#else
|
||||
return !close(raw_sock);
|
||||
return sc_raw_socket_close(raw_sock);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -7,21 +7,36 @@
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
# include <winsock2.h>
|
||||
typedef SOCKET sc_raw_socket;
|
||||
# define SC_RAW_SOCKET_NONE INVALID_SOCKET
|
||||
#else // not _WIN32
|
||||
# include <sys/socket.h>
|
||||
typedef int sc_raw_socket;
|
||||
# define SC_RAW_SOCKET_NONE -1
|
||||
#endif
|
||||
|
||||
#if defined(_WIN32) || defined(__APPLE__)
|
||||
// On Windows and macOS, shutdown() does not interrupt accept() or read()
|
||||
// calls, so net_interrupt() must call close() instead, and net_close() must
|
||||
// behave accordingly.
|
||||
// This causes a small race condition (once the socket is closed, its
|
||||
// handle becomes invalid and may in theory be reassigned before another
|
||||
// thread calls accept() or read()), but it is deemed acceptable as a
|
||||
// workaround.
|
||||
# define SC_SOCKET_CLOSE_ON_INTERRUPT
|
||||
#endif
|
||||
|
||||
#ifdef SC_SOCKET_CLOSE_ON_INTERRUPT
|
||||
# include <stdatomic.h>
|
||||
# define SC_SOCKET_NONE NULL
|
||||
typedef struct sc_socket_windows {
|
||||
SOCKET socket;
|
||||
typedef struct sc_socket_wrapper {
|
||||
sc_raw_socket socket;
|
||||
atomic_flag closed;
|
||||
} *sc_socket;
|
||||
|
||||
#else // not _WIN32
|
||||
|
||||
# include <sys/socket.h>
|
||||
#else
|
||||
# define SC_SOCKET_NONE -1
|
||||
typedef int sc_socket;
|
||||
|
||||
typedef sc_raw_socket sc_socket;
|
||||
#endif
|
||||
|
||||
#define IPV4_LOCALHOST 0x7F000001
|
||||
|
@ -64,6 +64,26 @@ sc_str_quote(const char *src) {
|
||||
return quoted;
|
||||
}
|
||||
|
||||
char *
|
||||
sc_str_concat(const char *start, const char *end) {
|
||||
assert(start);
|
||||
assert(end);
|
||||
|
||||
size_t start_len = strlen(start);
|
||||
size_t end_len = strlen(end);
|
||||
|
||||
char *result = malloc(start_len + end_len + 1);
|
||||
if (!result) {
|
||||
LOG_OOM();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
memcpy(result, start, start_len);
|
||||
memcpy(result + start_len, end, end_len + 1);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool
|
||||
sc_str_parse_integer(const char *s, long *out) {
|
||||
char *endptr;
|
||||
|
@ -38,6 +38,15 @@ sc_str_join(char *dst, const char *const tokens[], char sep, size_t n);
|
||||
char *
|
||||
sc_str_quote(const char *src);
|
||||
|
||||
/**
|
||||
* Concat two strings
|
||||
*
|
||||
* Return a new allocated string, contanining the concatenation of the two
|
||||
* input strings.
|
||||
*/
|
||||
char *
|
||||
sc_str_concat(const char *start, const char *end);
|
||||
|
||||
/**
|
||||
* Parse `s` as an integer into `out`
|
||||
*
|
||||
|
@ -51,7 +51,6 @@ static void test_options(void) {
|
||||
"--fullscreen",
|
||||
"--max-fps", "30",
|
||||
"--max-size", "1024",
|
||||
"--lock-video-orientation=2", // optional arguments require '='
|
||||
// "--no-control" is not compatible with "--turn-screen-off"
|
||||
// "--no-playback" is not compatible with "--fulscreen"
|
||||
"--port", "1234:1236",
|
||||
@ -80,7 +79,6 @@ static void test_options(void) {
|
||||
assert(opts->fullscreen);
|
||||
assert(!strcmp(opts->max_fps, "30"));
|
||||
assert(opts->max_size == 1024);
|
||||
assert(opts->lock_video_orientation == 2);
|
||||
assert(opts->port_range.first == 1234);
|
||||
assert(opts->port_range.last == 1236);
|
||||
assert(!strcmp(opts->push_target, "/sdcard/Movies"));
|
||||
|
@ -141,6 +141,16 @@ static void test_quote(void) {
|
||||
free(out);
|
||||
}
|
||||
|
||||
static void test_concat(void) {
|
||||
const char *s = "2024:11";
|
||||
char *out = sc_str_concat("my-prefix:", s);
|
||||
|
||||
// contains the concat
|
||||
assert(!strcmp("my-prefix:2024:11", out));
|
||||
|
||||
free(out);
|
||||
}
|
||||
|
||||
static void test_utf8_truncate(void) {
|
||||
const char *s = "aÉbÔc";
|
||||
assert(strlen(s) == 7); // É and Ô are 2 bytes-wide
|
||||
@ -389,6 +399,7 @@ int main(int argc, char *argv[]) {
|
||||
test_join_truncated_before_sep();
|
||||
test_join_truncated_after_sep();
|
||||
test_quote();
|
||||
test_concat();
|
||||
test_utf8_truncate();
|
||||
test_parse_integer();
|
||||
test_parse_integers();
|
||||
|
@ -77,7 +77,7 @@ pip3 install meson
|
||||
sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm
|
||||
|
||||
# client build dependencies
|
||||
sudo dnf install SDL2-devel ffms2-devel libusb1-devel meson gcc make
|
||||
sudo dnf install SDL2-devel ffms2-devel libusb1-devel libavdevice-free-devel meson gcc make
|
||||
|
||||
# server build dependencies
|
||||
sudo dnf install java-devel
|
||||
@ -233,10 +233,10 @@ install` must be run as root)._
|
||||
|
||||
#### Option 2: Use prebuilt server
|
||||
|
||||
- [`scrcpy-server-v2.7`][direct-scrcpy-server]
|
||||
<sub>SHA-256: `a23c5659f36c260f105c022d27bcb3eafffa26070e7baa9eda66d01377a1adba`</sub>
|
||||
- [`scrcpy-server-v3.0`][direct-scrcpy-server]
|
||||
<sub>SHA-256: `800044c62a94d5fc16f5ab9c86d45b1050eae3eb436514d1b0d2fe2646b894ea`</sub>
|
||||
|
||||
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-server-v2.7
|
||||
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-server-v3.0
|
||||
|
||||
Download the prebuilt server somewhere, and specify its path during the Meson
|
||||
configuration:
|
||||
|
@ -23,14 +23,20 @@ To control the device without mirroring:
|
||||
scrcpy --no-video --no-audio
|
||||
```
|
||||
|
||||
By default, mouse mode is switched to UHID if video mirroring is disabled (a
|
||||
relative mouse mode is required).
|
||||
By default, the mouse is disabled when video playback is turned off.
|
||||
|
||||
To control the device using a relative mouse, enable UHID mouse mode:
|
||||
|
||||
```bash
|
||||
scrcpy --no-video --no-audio --mouse=uhid
|
||||
scrcpy --no-video --no-audio -M # short version
|
||||
```
|
||||
|
||||
To also use a UHID keyboard, set it explicitly:
|
||||
|
||||
```bash
|
||||
scrcpy --no-video --no-audio --keyboard=uhid
|
||||
scrcpy --no-video --no-audio -K # short version
|
||||
scrcpy --no-video --no-audio --mouse=uhid --keyboard=uhid
|
||||
scrcpy --no-video --no-audio -MK # short version
|
||||
```
|
||||
|
||||
To use AOA instead (over USB only):
|
||||
|
@ -461,26 +461,30 @@ meson setup x -Dserver_debugger=true
|
||||
meson configure x -Dserver_debugger=true
|
||||
```
|
||||
|
||||
If your device runs Android 8 or below, set the `server_debugger_method` to
|
||||
`old` in addition:
|
||||
Then recompile, and run scrcpy.
|
||||
|
||||
```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.
|
||||
For Android < 11, it will start a debugger on port 5005 on the device and wait:
|
||||
Redirect that port to the computer:
|
||||
|
||||
```bash
|
||||
adb forward tcp:5005 tcp:5005
|
||||
```
|
||||
|
||||
In Android Studio, _Run_ > _Debug_ > _Edit configurations..._ On the left, click on
|
||||
`+`, _Remote_, and fill the form:
|
||||
For Android >= 11, first find the listening port:
|
||||
|
||||
```bash
|
||||
adb jdwp
|
||||
# press Ctrl+C to interrupt
|
||||
```
|
||||
|
||||
Then redirect the resulting PID:
|
||||
|
||||
```bash
|
||||
adb forward tcp:5005 jdwp:XXXX # replace XXXX
|
||||
```
|
||||
|
||||
In Android Studio, _Run_ > _Debug_ > _Edit configurations..._ On the left, click
|
||||
on `+`, _Remote_, and fill the form:
|
||||
|
||||
- Host: `localhost`
|
||||
- Port: `5005`
|
||||
|
23
doc/linux.md
23
doc/linux.md
@ -2,6 +2,23 @@
|
||||
|
||||
## 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>
|
||||
|
||||
Scrcpy is packaged in several distributions and package managers:
|
||||
@ -13,10 +30,10 @@ Scrcpy is packaged in several distributions and package managers:
|
||||
- Snap: `snap install scrcpy`
|
||||
- … (see [repology](https://repology.org/project/scrcpy/versions))
|
||||
|
||||
### Latest version
|
||||
|
||||
However, the packaged version is not always the latest release. To install the
|
||||
latest release from `master`, follow this simplified process.
|
||||
### From an install script
|
||||
|
||||
To install the latest release from `master`, follow this simplified process.
|
||||
|
||||
First, you need to install the required packages:
|
||||
|
||||
|
19
doc/macos.md
19
doc/macos.md
@ -2,6 +2,23 @@
|
||||
|
||||
## 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]:
|
||||
|
||||
```bash
|
||||
@ -13,7 +30,7 @@ brew install scrcpy
|
||||
You need `adb`, accessible from your `PATH`. If you don't have it yet:
|
||||
|
||||
```bash
|
||||
brew install android-platform-tools
|
||||
brew install --cask android-platform-tools
|
||||
```
|
||||
|
||||
Alternatively, Scrcpy is also available in [MacPorts], which sets up `adb` for you:
|
||||
|
63
doc/video.md
63
doc/video.md
@ -27,6 +27,9 @@ preserved. That way, a device in 1920×1080 will be mirrored at 1024×576.
|
||||
If encoding fails, scrcpy automatically tries again with a lower definition
|
||||
(unless `--no-downsize-on-error` is enabled).
|
||||
|
||||
For camera mirroring, the `--max-size` value is used to select the camera source
|
||||
size instead (among the available resolutions).
|
||||
|
||||
|
||||
## Bit rate
|
||||
|
||||
@ -93,7 +96,7 @@ Sometimes, the default encoder may have issues or even crash, so it is useful to
|
||||
try another one:
|
||||
|
||||
```bash
|
||||
scrcpy --video-codec=h264 --video-encoder='OMX.qcom.video.encoder.avc'
|
||||
scrcpy --video-codec=h264 --video-encoder=OMX.qcom.video.encoder.avc
|
||||
```
|
||||
|
||||
|
||||
@ -103,24 +106,45 @@ The orientation may be applied at 3 different levels:
|
||||
- The [shortcut](shortcuts.md) <kbd>MOD</kbd>+<kbd>r</kbd> requests the
|
||||
device to switch between portrait and landscape (the current running app may
|
||||
refuse, if it does not support the requested orientation).
|
||||
- `--lock-video-orientation` changes the mirroring orientation (the orientation
|
||||
- `--capture-orientation` changes the mirroring orientation (the orientation
|
||||
of the video sent from the device to the computer). This affects the
|
||||
recording.
|
||||
- `--orientation` is applied on the client side, and affects display and
|
||||
recording. For the display, it can be changed dynamically using
|
||||
[shortcuts](shortcuts.md).
|
||||
|
||||
To lock the mirroring orientation (on the capture side):
|
||||
To capture the video with a specific orientation:
|
||||
|
||||
```bash
|
||||
scrcpy --lock-video-orientation # initial (current) orientation
|
||||
scrcpy --lock-video-orientation=0 # natural orientation
|
||||
scrcpy --lock-video-orientation=90 # 90° clockwise
|
||||
scrcpy --lock-video-orientation=180 # 180°
|
||||
scrcpy --lock-video-orientation=270 # 270° clockwise
|
||||
scrcpy --capture-orientation=0
|
||||
scrcpy --capture-orientation=90 # 90° clockwise
|
||||
scrcpy --capture-orientation=180 # 180°
|
||||
scrcpy --capture-orientation=270 # 270° clockwise
|
||||
scrcpy --capture-orientation=flip0 # hflip
|
||||
scrcpy --capture-orientation=flip90 # hflip + 90° clockwise
|
||||
scrcpy --capture-orientation=flip180 # hflip + 180°
|
||||
scrcpy --capture-orientation=flip270 # hflip + 270° clockwise
|
||||
```
|
||||
|
||||
To orient the video (on the rendering side):
|
||||
The capture orientation can be locked by using `@`, so that a physical device
|
||||
rotation does not change the captured video orientation:
|
||||
|
||||
```bash
|
||||
scrcpy --capture-orientation=@ # locked to the initial orientation
|
||||
scrcpy --capture-orientation=@0 # locked to 0°
|
||||
scrcpy --capture-orientation=@90 # locked to 90° clockwise
|
||||
scrcpy --capture-orientation=@180 # locked to 180°
|
||||
scrcpy --capture-orientation=@270 # locked to 270° clockwise
|
||||
scrcpy --capture-orientation=@flip0 # locked to hflip
|
||||
scrcpy --capture-orientation=@flip90 # locked to hflip + 90° clockwise
|
||||
scrcpy --capture-orientation=@flip180 # locked to hflip + 180°
|
||||
scrcpy --capture-orientation=@flip270 # locked to hflip + 270° clockwise
|
||||
```
|
||||
|
||||
The capture orientation transform is applied after `--crop`, but before
|
||||
`--angle`.
|
||||
|
||||
To orient the video (on the client side):
|
||||
|
||||
```bash
|
||||
scrcpy --orientation=0
|
||||
@ -141,6 +165,19 @@ to the MP4 or MKV target file. Flipping is not supported, so only the 4 first
|
||||
values are allowed when recording.
|
||||
|
||||
|
||||
## Angle
|
||||
|
||||
To rotate the video content by a custom angle (in degrees, clockwise):
|
||||
|
||||
```
|
||||
scrcpy --angle=23
|
||||
```
|
||||
|
||||
The center of rotation is the center of the visible area.
|
||||
|
||||
This transformation is applied after `--crop` and `--capture-orientation`.
|
||||
|
||||
|
||||
## Crop
|
||||
|
||||
The device screen may be cropped to mirror only part of the screen.
|
||||
@ -154,7 +191,11 @@ scrcpy --crop=1224:1440:0:0 # 1224x1440 at offset (0,0)
|
||||
The values are expressed in the device natural orientation (portrait for a
|
||||
phone, landscape for a tablet).
|
||||
|
||||
If `--max-size` is also specified, resizing is applied after cropping.
|
||||
Cropping is performed before `--capture-orientation` and `--angle`.
|
||||
|
||||
For display mirroring, `--max-size` is applied after cropping. For camera,
|
||||
`--max-size` is applied first (because it selects the source size rather than
|
||||
resizing the content).
|
||||
|
||||
|
||||
## Display
|
||||
@ -175,6 +216,8 @@ scrcpy --list-displays
|
||||
A secondary display may only be controlled if the device runs at least Android
|
||||
10 (otherwise it is mirrored as read-only).
|
||||
|
||||
It is also possible to create a [virtual display](virtual_display.md).
|
||||
|
||||
|
||||
## Buffering
|
||||
|
||||
|
@ -8,7 +8,6 @@ To mirror a new virtual display instead of the device screen:
|
||||
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 -m1920 # ... scaled to fit a max size of 1920
|
||||
scrcpy --new-display=/240 # use the main display size and 240 dpi
|
||||
```
|
||||
|
||||
@ -16,11 +15,38 @@ scrcpy --new-display=/240 # use the main display size and 240 dpi
|
||||
|
||||
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).
|
||||
When no launcher is available (or if is explicitly disabled by
|
||||
[`--no-vd-system-decorations`](#system-decorations)), the virtual display is
|
||||
empty. In that case, you must [start an Android
|
||||
app](device.md#start-android-app).
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
scrcpy --new-display=1920x1080 --start-app=org.videolan.vlc
|
||||
```
|
||||
|
||||
The app may itself be a launcher. For example, to run the open source [Fossify
|
||||
Launcher]:
|
||||
|
||||
```bash
|
||||
scrcpy --new-display=1920x1080 --no-vd-system-decorations --start-app=org.fossify.home
|
||||
```
|
||||
|
||||
[Fossify Launcher]: https://f-droid.org/en/packages/org.fossify.home/
|
||||
|
||||
|
||||
## System decorations
|
||||
|
||||
By default, virtual display system decorations are enabled. To disable them, use
|
||||
`--no-vd-system-decorations`:
|
||||
|
||||
```
|
||||
scrcpy --new-display --no-vd-system-decorations
|
||||
```
|
||||
|
||||
This is useful for some devices which might display a broken UI, or to disable
|
||||
any default launcher UI available in virtual displays.
|
||||
|
||||
Note that if no app is started, no content will be rendered, so no video frame
|
||||
will be produced at all.
|
||||
|
@ -2,27 +2,32 @@
|
||||
|
||||
## Install
|
||||
|
||||
### From the official release
|
||||
|
||||
Download the [latest release]:
|
||||
|
||||
- [`scrcpy-win64-v2.7.zip`][direct-win64] (64-bit)
|
||||
<sub>SHA-256: `5910bc18d5a16f42d84185ddc7e16a4cee6a6f5f33451559c1a1d6d0099bd5f5`</sub>
|
||||
- [`scrcpy-win32-v2.7.zip`][direct-win32] (32-bit)
|
||||
<sub>SHA-256: `ef4daf89d500f33d78b830625536ecb18481429dd94433e7634c824292059d06`</sub>
|
||||
- [`scrcpy-win64-v3.0.zip`][direct-win64] (64-bit)
|
||||
<sub>SHA-256: `dfbe8a8fef6535197acc506936bfd59d0aa0427e9b44fb2e5c550eae642f72be`</sub>
|
||||
- [`scrcpy-win32-v3.0.zip`][direct-win32] (32-bit)
|
||||
<sub>SHA-256: `7cbf8d7a6ebfdca7b3b161e29a481c11088305f3e0a89d28e8e62f70c7bd0028`</sub>
|
||||
|
||||
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
|
||||
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-win64-v2.7.zip
|
||||
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-win32-v2.7.zip
|
||||
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-win64-v3.0.zip
|
||||
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-win32-v3.0.zip
|
||||
|
||||
and extract it.
|
||||
|
||||
Alternatively, you could install it from packages manager, like [Chocolatey]:
|
||||
|
||||
### From a package manager
|
||||
|
||||
From [Chocolatey]:
|
||||
|
||||
```bash
|
||||
choco install scrcpy
|
||||
choco install adb # if you don't have it yet
|
||||
```
|
||||
|
||||
or [Scoop]:
|
||||
From [Scoop]:
|
||||
|
||||
|
||||
```bash
|
||||
@ -30,7 +35,6 @@ scoop install scrcpy
|
||||
scoop install adb # if you don't have it yet
|
||||
```
|
||||
|
||||
[Winget]: https://github.com/microsoft/winget-cli
|
||||
[Chocolatey]: https://chocolatey.org/
|
||||
[Scoop]: https://scoop.sh
|
||||
|
||||
|
@ -2,8 +2,8 @@
|
||||
set -e
|
||||
|
||||
BUILDDIR=build-auto
|
||||
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-server-v2.7
|
||||
PREBUILT_SERVER_SHA256=a23c5659f36c260f105c022d27bcb3eafffa26070e7baa9eda66d01377a1adba
|
||||
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-server-v3.0
|
||||
PREBUILT_SERVER_SHA256=800044c62a94d5fc16f5ab9c86d45b1050eae3eb436514d1b0d2fe2646b894ea
|
||||
|
||||
echo "[scrcpy] Downloading prebuilt server..."
|
||||
wget "$PREBUILT_SERVER_URL" -O scrcpy-server
|
||||
|
@ -1,5 +1,5 @@
|
||||
project('scrcpy', 'c',
|
||||
version: '2.7',
|
||||
version: '3.0',
|
||||
meson_version: '>= 0.48',
|
||||
default_options: [
|
||||
'c_std=c11',
|
||||
|
@ -2,6 +2,7 @@ option('compile_app', type: 'boolean', value: true, description: 'Build the clie
|
||||
option('compile_server', type: 'boolean', value: true, description: 'Build the server')
|
||||
option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server')
|
||||
option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server from the same directory as the scrcpy executable')
|
||||
option('static', type: 'boolean', value: false, description: 'Use static dependencies')
|
||||
option('server_debugger', type: 'boolean', value: false, description: 'Run a server debugger and wait for a client to be attached')
|
||||
option('v4l2', type: 'boolean', value: true, description: 'Enable V4L2 feature when supported')
|
||||
option('usb', type: 'boolean', value: true, description: 'Enable HID/OTG features when supported')
|
||||
|
141
release.mk
141
release.mk
@ -1,141 +0,0 @@
|
||||
# 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 test-client test-server \
|
||||
build-server \
|
||||
prepare-deps-win32 prepare-deps-win64 \
|
||||
build-win32 build-win64 \
|
||||
zip-win32 zip-win64 \
|
||||
package 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)
|
||||
|
||||
ZIP := zip
|
||||
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 build-win32 build-win64 package
|
||||
|
||||
clean:
|
||||
$(GRADLE) clean
|
||||
rm -rf "$(ZIP)" "$(TEST_BUILD_DIR)" "$(SERVER_BUILD_DIR)" \
|
||||
"$(WIN32_BUILD_DIR)" "$(WIN64_BUILD_DIR)"
|
||||
|
||||
test-client:
|
||||
[ -d "$(TEST_BUILD_DIR)" ] || ( mkdir "$(TEST_BUILD_DIR)" && \
|
||||
meson setup "$(TEST_BUILD_DIR)" -Db_sanitize=address )
|
||||
ninja -C "$(TEST_BUILD_DIR)"
|
||||
|
||||
test-server:
|
||||
$(GRADLE) -p server check
|
||||
|
||||
test: test-client test-server
|
||||
|
||||
build-server:
|
||||
$(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"
|
||||
|
||||
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)"
|
||||
# Group intermediate outputs into a 'dist' directory
|
||||
mkdir -p "$(WIN32_BUILD_DIR)/dist"
|
||||
cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(WIN32_BUILD_DIR)/dist/"
|
||||
cp app/data/scrcpy-console.bat "$(WIN32_BUILD_DIR)/dist/"
|
||||
cp app/data/scrcpy-noconsole.vbs "$(WIN32_BUILD_DIR)/dist/"
|
||||
cp app/data/icon.png "$(WIN32_BUILD_DIR)/dist/"
|
||||
cp app/data/open_a_terminal_here.bat "$(WIN32_BUILD_DIR)/dist/"
|
||||
cp app/deps/work/install/win32/bin/*.dll "$(WIN32_BUILD_DIR)/dist/"
|
||||
cp app/deps/work/install/win32/bin/adb.exe "$(WIN32_BUILD_DIR)/dist/"
|
||||
|
||||
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)"
|
||||
# Group intermediate outputs into a 'dist' directory
|
||||
mkdir -p "$(WIN64_BUILD_DIR)/dist"
|
||||
cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(WIN64_BUILD_DIR)/dist/"
|
||||
cp app/data/scrcpy-console.bat "$(WIN64_BUILD_DIR)/dist/"
|
||||
cp app/data/scrcpy-noconsole.vbs "$(WIN64_BUILD_DIR)/dist/"
|
||||
cp app/data/icon.png "$(WIN64_BUILD_DIR)/dist/"
|
||||
cp app/data/open_a_terminal_here.bat "$(WIN64_BUILD_DIR)/dist/"
|
||||
cp app/deps/work/install/win64/bin/*.dll "$(WIN64_BUILD_DIR)/dist/"
|
||||
cp app/deps/work/install/win64/bin/adb.exe "$(WIN64_BUILD_DIR)/dist/"
|
||||
|
||||
zip-win32:
|
||||
mkdir -p "$(ZIP)/$(WIN32_TARGET_DIR)"
|
||||
cp -r "$(WIN32_BUILD_DIR)/dist/." "$(ZIP)/$(WIN32_TARGET_DIR)/"
|
||||
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(ZIP)/$(WIN32_TARGET_DIR)/"
|
||||
cd "$(ZIP)"; \
|
||||
zip -r "$(WIN32_TARGET)" "$(WIN32_TARGET_DIR)"
|
||||
rm -rf "$(ZIP)/$(WIN32_TARGET_DIR)"
|
||||
|
||||
zip-win64:
|
||||
mkdir -p "$(ZIP)/$(WIN64_TARGET_DIR)"
|
||||
cp -r "$(WIN64_BUILD_DIR)/dist/." "$(ZIP)/$(WIN64_TARGET_DIR)/"
|
||||
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(ZIP)/$(WIN64_TARGET_DIR)/"
|
||||
cd "$(ZIP)"; \
|
||||
zip -r "$(WIN64_TARGET)" "$(WIN64_TARGET_DIR)"
|
||||
rm -rf "$(ZIP)/$(WIN64_TARGET_DIR)"
|
||||
|
||||
package: zip-win32 zip-win64
|
||||
mkdir -p "$(RELEASE_DIR)"
|
||||
cp "$(SERVER_BUILD_DIR)/server/scrcpy-server" \
|
||||
"$(RELEASE_DIR)/scrcpy-server-$(VERSION)"
|
||||
cp "$(ZIP)/$(WIN32_TARGET)" "$(RELEASE_DIR)"
|
||||
cp "$(ZIP)/$(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)/"
|
@ -1,2 +0,0 @@
|
||||
#!/bin/bash
|
||||
make -f release.mk
|
2
release/.gitignore
vendored
Normal file
2
release/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/work
|
||||
/output
|
5
release/build_common
Normal file
5
release/build_common
Normal file
@ -0,0 +1,5 @@
|
||||
# This file must be sourced from the release scripts directory
|
||||
WORK_DIR="$PWD/work"
|
||||
OUTPUT_DIR="$PWD/output"
|
||||
|
||||
VERSION="${VERSION:-$(git describe --tags --always)}"
|
42
release/build_linux.sh
Executable file
42
release/build_linux.sh
Executable file
@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
set -ex
|
||||
cd "$(dirname ${BASH_SOURCE[0]})"
|
||||
. build_common
|
||||
cd .. # root project dir
|
||||
|
||||
if [[ $# != 1 ]]
|
||||
then
|
||||
echo "Syntax: $0 <arch>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ARCH="$1"
|
||||
LINUX_BUILD_DIR="$WORK_DIR/build-linux-$ARCH"
|
||||
|
||||
app/deps/adb_linux.sh
|
||||
app/deps/sdl.sh linux native static
|
||||
app/deps/ffmpeg.sh linux native static
|
||||
app/deps/libusb.sh linux native static
|
||||
|
||||
DEPS_INSTALL_DIR="$PWD/app/deps/work/install/linux-native-static"
|
||||
ADB_INSTALL_DIR="$PWD/app/deps/work/install/adb-linux"
|
||||
|
||||
rm -rf "$LINUX_BUILD_DIR"
|
||||
meson setup "$LINUX_BUILD_DIR" \
|
||||
--pkg-config-path="$DEPS_INSTALL_DIR/lib/pkgconfig" \
|
||||
-Dc_args="-I$DEPS_INSTALL_DIR/include" \
|
||||
-Dc_link_args="-L$DEPS_INSTALL_DIR/lib" \
|
||||
--buildtype=release \
|
||||
--strip \
|
||||
-Db_lto=true \
|
||||
-Dcompile_server=false \
|
||||
-Dportable=true \
|
||||
-Dstatic=true
|
||||
ninja -C "$LINUX_BUILD_DIR"
|
||||
|
||||
# Group intermediate outputs into a 'dist' directory
|
||||
mkdir -p "$LINUX_BUILD_DIR/dist"
|
||||
cp "$LINUX_BUILD_DIR"/app/scrcpy "$LINUX_BUILD_DIR/dist/"
|
||||
cp app/data/icon.png "$LINUX_BUILD_DIR/dist/"
|
||||
cp app/scrcpy.1 "$LINUX_BUILD_DIR/dist/"
|
||||
cp -r "$ADB_INSTALL_DIR"/. "$LINUX_BUILD_DIR/dist/"
|
42
release/build_macos.sh
Executable file
42
release/build_macos.sh
Executable file
@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
set -ex
|
||||
cd "$(dirname ${BASH_SOURCE[0]})"
|
||||
. build_common
|
||||
cd .. # root project dir
|
||||
|
||||
if [[ $# != 1 ]]
|
||||
then
|
||||
echo "Syntax: $0 <arch>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ARCH="$1"
|
||||
MACOS_BUILD_DIR="$WORK_DIR/build-macos-$ARCH"
|
||||
|
||||
app/deps/adb_macos.sh
|
||||
app/deps/sdl.sh macos native static
|
||||
app/deps/ffmpeg.sh macos native static
|
||||
app/deps/libusb.sh macos native static
|
||||
|
||||
DEPS_INSTALL_DIR="$PWD/app/deps/work/install/macos-native-static"
|
||||
ADB_INSTALL_DIR="$PWD/app/deps/work/install/adb-macos"
|
||||
|
||||
rm -rf "$MACOS_BUILD_DIR"
|
||||
meson setup "$MACOS_BUILD_DIR" \
|
||||
--pkg-config-path="$DEPS_INSTALL_DIR/lib/pkgconfig" \
|
||||
-Dc_args="-I$DEPS_INSTALL_DIR/include" \
|
||||
-Dc_link_args="-L$DEPS_INSTALL_DIR/lib" \
|
||||
--buildtype=release \
|
||||
--strip \
|
||||
-Db_lto=true \
|
||||
-Dcompile_server=false \
|
||||
-Dportable=true \
|
||||
-Dstatic=true
|
||||
ninja -C "$MACOS_BUILD_DIR"
|
||||
|
||||
# Group intermediate outputs into a 'dist' directory
|
||||
mkdir -p "$MACOS_BUILD_DIR/dist"
|
||||
cp "$MACOS_BUILD_DIR"/app/scrcpy "$MACOS_BUILD_DIR/dist/"
|
||||
cp app/data/icon.png "$MACOS_BUILD_DIR/dist/"
|
||||
cp app/scrcpy.1 "$MACOS_BUILD_DIR/dist/"
|
||||
cp -r "$ADB_INSTALL_DIR"/. "$MACOS_BUILD_DIR/dist/"
|
14
release/build_server.sh
Executable file
14
release/build_server.sh
Executable file
@ -0,0 +1,14 @@
|
||||
#!/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"
|
52
release/build_windows.sh
Executable file
52
release/build_windows.sh
Executable file
@ -0,0 +1,52 @@
|
||||
#!/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/"
|
14
release/generate_checksums.sh
Executable file
14
release/generate_checksums.sh
Executable file
@ -0,0 +1,14 @@
|
||||
#!/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"
|
52
release/package_client.sh
Executable file
52
release/package_client.sh
Executable file
@ -0,0 +1,52 @@
|
||||
#!/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 cvzf "$OUTPUT_DIR/$TARGET_DIRNAME.tar.gz" "$TARGET_DIRNAME"
|
||||
;;
|
||||
*)
|
||||
echo "Invalid format (expected zip or tar.gz): $FORMAT" >&2
|
||||
exit 1
|
||||
esac
|
||||
|
||||
rm -rf "$TARGET_DIRNAME"
|
||||
cd -
|
||||
echo "Generated '$OUTPUT_DIR/$TARGET_DIRNAME.$FORMAT'"
|
10
release/package_server.sh
Executable file
10
release/package_server.sh
Executable file
@ -0,0 +1,10 @@
|
||||
#!/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'"
|
24
release/release.sh
Executable file
24
release/release.sh
Executable file
@ -0,0 +1,24 @@
|
||||
#!/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"
|
12
release/test_client.sh
Executable file
12
release/test_client.sh
Executable file
@ -0,0 +1,12 @@
|
||||
#!/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
|
9
release/test_server.sh
Executable file
9
release/test_server.sh
Executable file
@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
set -ex
|
||||
cd "$(dirname ${BASH_SOURCE[0]})"
|
||||
. build_common
|
||||
cd .. # root project dir
|
||||
|
||||
GRADLE="${GRADLE:-./gradlew}"
|
||||
|
||||
"$GRADLE" -p server check
|
@ -7,8 +7,8 @@ android {
|
||||
applicationId "com.genymobile.scrcpy"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 35
|
||||
versionCode 20700
|
||||
versionName "2.7"
|
||||
versionCode 30000
|
||||
versionName "3.0"
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
buildTypes {
|
||||
|
@ -12,10 +12,11 @@
|
||||
set -e
|
||||
|
||||
SCRCPY_DEBUG=false
|
||||
SCRCPY_VERSION_NAME=2.7
|
||||
SCRCPY_VERSION_NAME=3.0
|
||||
|
||||
PLATFORM=${ANDROID_PLATFORM:-35}
|
||||
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.0.0}
|
||||
PLATFORM_TOOLS="$ANDROID_HOME/platforms/android-$PLATFORM"
|
||||
BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS"
|
||||
|
||||
BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})"
|
||||
@ -23,7 +24,8 @@ CLASSES_DIR="$BUILD_DIR/classes"
|
||||
GEN_DIR="$BUILD_DIR/gen"
|
||||
SERVER_DIR=$(dirname "$0")
|
||||
SERVER_BINARY=scrcpy-server
|
||||
ANDROID_JAR="$ANDROID_HOME/platforms/android-$PLATFORM/android.jar"
|
||||
ANDROID_JAR="$PLATFORM_TOOLS/android.jar"
|
||||
ANDROID_AIDL="$PLATFORM_TOOLS/framework.aidl"
|
||||
LAMBDA_JAR="$BUILD_TOOLS_DIR/core-lambda-stubs.jar"
|
||||
|
||||
echo "Platform: android-$PLATFORM"
|
||||
@ -49,6 +51,8 @@ cd "$SERVER_DIR/src/main/aidl"
|
||||
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. \
|
||||
android/content/IOnPrimaryClipChangedListener.aidl
|
||||
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. 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=( \
|
||||
@ -60,6 +64,7 @@ SRC=( \
|
||||
com/genymobile/scrcpy/audio/*.java \
|
||||
com/genymobile/scrcpy/control/*.java \
|
||||
com/genymobile/scrcpy/device/*.java \
|
||||
com/genymobile/scrcpy/opengl/*.java \
|
||||
com/genymobile/scrcpy/util/*.java \
|
||||
com/genymobile/scrcpy/video/*.java \
|
||||
com/genymobile/scrcpy/wrappers/*.java \
|
||||
|
@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
@ -207,13 +207,15 @@ public final class CleanUp {
|
||||
}
|
||||
}
|
||||
|
||||
if (displayId != Device.DISPLAY_ID_NONE && Device.isScreenOn(displayId)) {
|
||||
// Change the power of the main display when mirroring a virtual display
|
||||
int targetDisplayId = displayId != Device.DISPLAY_ID_NONE ? displayId : 0;
|
||||
if (Device.isScreenOn(targetDisplayId)) {
|
||||
if (powerOffScreen) {
|
||||
Ln.i("Power off screen");
|
||||
Device.powerOffScreen(displayId);
|
||||
Device.powerOffScreen(targetDisplayId);
|
||||
} else if (restoreDisplayPower) {
|
||||
Ln.i("Restoring display power");
|
||||
Device.setDisplayPower(displayId, true);
|
||||
Device.setDisplayPower(targetDisplayId, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ import com.genymobile.scrcpy.audio.AudioCodec;
|
||||
import com.genymobile.scrcpy.audio.AudioSource;
|
||||
import com.genymobile.scrcpy.device.Device;
|
||||
import com.genymobile.scrcpy.device.NewDisplay;
|
||||
import com.genymobile.scrcpy.device.Orientation;
|
||||
import com.genymobile.scrcpy.device.Size;
|
||||
import com.genymobile.scrcpy.util.CodecOption;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
@ -13,6 +14,7 @@ import com.genymobile.scrcpy.video.VideoCodec;
|
||||
import com.genymobile.scrcpy.video.VideoSource;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.util.Pair;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@ -32,7 +34,7 @@ public class Options {
|
||||
private int videoBitRate = 8000000;
|
||||
private int audioBitRate = 128000;
|
||||
private float maxFps;
|
||||
private int lockVideoOrientation = Device.LOCK_VIDEO_ORIENTATION_UNLOCKED;
|
||||
private float angle;
|
||||
private boolean tunnelForward;
|
||||
private Rect crop;
|
||||
private boolean control = true;
|
||||
@ -58,6 +60,10 @@ public class Options {
|
||||
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 listDisplays;
|
||||
@ -123,8 +129,8 @@ public class Options {
|
||||
return maxFps;
|
||||
}
|
||||
|
||||
public int getLockVideoOrientation() {
|
||||
return lockVideoOrientation;
|
||||
public float getAngle() {
|
||||
return angle;
|
||||
}
|
||||
|
||||
public boolean isTunnelForward() {
|
||||
@ -219,6 +225,18 @@ public class Options {
|
||||
return newDisplay;
|
||||
}
|
||||
|
||||
public Orientation getCaptureOrientation() {
|
||||
return captureOrientation;
|
||||
}
|
||||
|
||||
public Orientation.Lock getCaptureOrientationLock() {
|
||||
return captureOrientationLock;
|
||||
}
|
||||
|
||||
public boolean getVDSystemDecorations() {
|
||||
return vdSystemDecorations;
|
||||
}
|
||||
|
||||
public boolean getList() {
|
||||
return listEncoders || listDisplays || listCameras || listCameraSizes || listApps;
|
||||
}
|
||||
@ -259,10 +277,6 @@ public class Options {
|
||||
return sendCodecMeta;
|
||||
}
|
||||
|
||||
public void resetLockVideoOrientation() {
|
||||
this.lockVideoOrientation = Device.LOCK_VIDEO_ORIENTATION_UNLOCKED;
|
||||
}
|
||||
|
||||
@SuppressWarnings("MethodLength")
|
||||
public static Options parse(String... args) {
|
||||
if (args.length < 1) {
|
||||
@ -345,8 +359,8 @@ public class Options {
|
||||
case "max_fps":
|
||||
options.maxFps = parseFloat("max_fps", value);
|
||||
break;
|
||||
case "lock_video_orientation":
|
||||
options.lockVideoOrientation = Integer.parseInt(value);
|
||||
case "angle":
|
||||
options.angle = parseFloat("angle", value);
|
||||
break;
|
||||
case "tunnel_forward":
|
||||
options.tunnelForward = Boolean.parseBoolean(value);
|
||||
@ -452,6 +466,14 @@ public class Options {
|
||||
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":
|
||||
options.sendDeviceMeta = Boolean.parseBoolean(value);
|
||||
break;
|
||||
@ -575,4 +597,25 @@ public class Options {
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import com.genymobile.scrcpy.device.DesktopConnection;
|
||||
import com.genymobile.scrcpy.device.Device;
|
||||
import com.genymobile.scrcpy.device.NewDisplay;
|
||||
import com.genymobile.scrcpy.device.Streamer;
|
||||
import com.genymobile.scrcpy.opengl.OpenGLRunner;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
import com.genymobile.scrcpy.util.LogUtils;
|
||||
import com.genymobile.scrcpy.video.CameraCapture;
|
||||
@ -84,23 +85,6 @@ public final class Server {
|
||||
throw new ConfigurationException("New virtual display is not supported");
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14) {
|
||||
int lockVideoOrientation = options.getLockVideoOrientation();
|
||||
if (lockVideoOrientation != Device.LOCK_VIDEO_ORIENTATION_UNLOCKED) {
|
||||
if (lockVideoOrientation != Device.LOCK_VIDEO_ORIENTATION_INITIAL_AUTO) {
|
||||
Ln.e("--lock-video-orientation is broken on Android >= 14: <https://github.com/Genymobile/scrcpy/issues/4011>");
|
||||
throw new ConfigurationException("--lock-video-orientation is broken on Android >= 14");
|
||||
} else {
|
||||
// If the flag has been set automatically (because v4l2 sink is enabled), do not fail
|
||||
Ln.w("--lock-video-orientation is ignored on Android >= 14: <https://github.com/Genymobile/scrcpy/issues/4011>");
|
||||
}
|
||||
}
|
||||
if (options.getCrop() != null) {
|
||||
Ln.e("--crop is broken on Android >= 14: <https://github.com/Genymobile/scrcpy/issues/4162>");
|
||||
throw new ConfigurationException("Crop is not broken on Android >= 14");
|
||||
}
|
||||
}
|
||||
|
||||
CleanUp cleanUp = null;
|
||||
|
||||
if (options.getCleanup()) {
|
||||
@ -191,6 +175,8 @@ public final class Server {
|
||||
asyncProcessor.stop();
|
||||
}
|
||||
|
||||
OpenGLRunner.quit(); // quit the OpenGL thread, if any
|
||||
|
||||
connection.shutdown();
|
||||
|
||||
try {
|
||||
@ -200,6 +186,7 @@ public final class Server {
|
||||
for (AsyncProcessor asyncProcessor : asyncProcessors) {
|
||||
asyncProcessor.join();
|
||||
}
|
||||
OpenGLRunner.join();
|
||||
} catch (InterruptedException e) {
|
||||
// ignore
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import com.genymobile.scrcpy.device.Device;
|
||||
import com.genymobile.scrcpy.device.DeviceApp;
|
||||
import com.genymobile.scrcpy.device.Point;
|
||||
import com.genymobile.scrcpy.device.Position;
|
||||
import com.genymobile.scrcpy.device.Size;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
import com.genymobile.scrcpy.util.LogUtils;
|
||||
import com.genymobile.scrcpy.video.SurfaceCapture;
|
||||
@ -20,6 +21,7 @@ import android.content.IOnPrimaryClipChangedListener;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Pair;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyCharacterMap;
|
||||
import android.view.KeyEvent;
|
||||
@ -280,7 +282,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
setClipboard(msg.getText(), msg.getPaste(), msg.getSequence());
|
||||
break;
|
||||
case ControlMessage.TYPE_SET_DISPLAY_POWER:
|
||||
if (supportsInputEvents && displayId != Device.DISPLAY_ID_NONE) {
|
||||
if (supportsInputEvents) {
|
||||
setDisplayPower(msg.getOn());
|
||||
}
|
||||
break;
|
||||
@ -349,20 +351,47 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
return successCount;
|
||||
}
|
||||
|
||||
private boolean injectTouch(int action, long pointerId, Position position, float pressure, int actionButton, int buttons) {
|
||||
long now = SystemClock.uptimeMillis();
|
||||
|
||||
private Pair<Point, Integer> getEventPointAndDisplayId(Position 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 touch event without a display";
|
||||
// In scrcpy, displayData should never be null (a touch event can only be generated from the client when a video frame is present).
|
||||
// However, it is possible to send events without video playback when using scrcpy-server alone (except for virtual displays).
|
||||
assert displayData != null || displayId != Device.DISPLAY_ID_NONE : "Cannot receive a positional event without a display";
|
||||
|
||||
Point point = displayData.positionMapper.map(position);
|
||||
if (point == null) {
|
||||
Ln.w("Ignore touch event, it was generated for a different device size");
|
||||
Point point;
|
||||
int targetDisplayId;
|
||||
if (displayData != null) {
|
||||
point = displayData.positionMapper.map(position);
|
||||
if (point == null) {
|
||||
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
|
||||
Size eventSize = position.getScreenSize();
|
||||
Size currentSize = displayData.positionMapper.getVideoSize();
|
||||
Ln.v("Ignore positional event generated for size " + eventSize + " (current size is " + currentSize + ")");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
targetDisplayId = displayData.virtualDisplayId;
|
||||
} else {
|
||||
// No display, use the raw coordinates
|
||||
point = position.getPoint();
|
||||
targetDisplayId = displayId;
|
||||
}
|
||||
|
||||
return Pair.create(point, targetDisplayId);
|
||||
}
|
||||
|
||||
private boolean injectTouch(int action, long pointerId, Position position, float pressure, int actionButton, int buttons) {
|
||||
long now = SystemClock.uptimeMillis();
|
||||
|
||||
Pair<Point, Integer> pair = getEventPointAndDisplayId(position);
|
||||
if (pair == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Point point = pair.first;
|
||||
int targetDisplayId = pair.second;
|
||||
|
||||
int pointerIndex = pointersState.getPointerIndex(pointerId);
|
||||
if (pointerIndex == -1) {
|
||||
Ln.w("Too many pointers for touch event");
|
||||
@ -416,7 +445,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
// First button pressed: ACTION_DOWN
|
||||
MotionEvent downEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_DOWN, pointerCount, pointerProperties,
|
||||
pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0);
|
||||
if (!Device.injectEvent(downEvent, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC)) {
|
||||
if (!Device.injectEvent(downEvent, targetDisplayId, Device.INJECT_MODE_ASYNC)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -427,7 +456,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
if (!InputManager.setActionButton(pressEvent, actionButton)) {
|
||||
return false;
|
||||
}
|
||||
if (!Device.injectEvent(pressEvent, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC)) {
|
||||
if (!Device.injectEvent(pressEvent, targetDisplayId, Device.INJECT_MODE_ASYNC)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -441,7 +470,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
if (!InputManager.setActionButton(releaseEvent, actionButton)) {
|
||||
return false;
|
||||
}
|
||||
if (!Device.injectEvent(releaseEvent, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC)) {
|
||||
if (!Device.injectEvent(releaseEvent, targetDisplayId, Device.INJECT_MODE_ASYNC)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -449,7 +478,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
// Last button released: ACTION_UP
|
||||
MotionEvent upEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_UP, pointerCount, pointerProperties,
|
||||
pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0);
|
||||
if (!Device.injectEvent(upEvent, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC)) {
|
||||
if (!Device.injectEvent(upEvent, targetDisplayId, Device.INJECT_MODE_ASYNC)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -460,23 +489,20 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
|
||||
MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f,
|
||||
DEFAULT_DEVICE_ID, 0, source, 0);
|
||||
return Device.injectEvent(event, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC);
|
||||
return Device.injectEvent(event, targetDisplayId, Device.INJECT_MODE_ASYNC);
|
||||
}
|
||||
|
||||
private boolean injectScroll(Position position, float hScroll, float vScroll, int buttons) {
|
||||
long now = SystemClock.uptimeMillis();
|
||||
|
||||
// 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) {
|
||||
Ln.w("Ignore scroll event, it was generated for a different device size");
|
||||
Pair<Point, Integer> pair = getEventPointAndDisplayId(position);
|
||||
if (pair == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Point point = pair.first;
|
||||
int targetDisplayId = pair.second;
|
||||
|
||||
MotionEvent.PointerProperties props = pointerProperties[0];
|
||||
props.id = 0;
|
||||
|
||||
@ -488,7 +514,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
|
||||
MotionEvent event = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, buttons, 1f, 1f,
|
||||
DEFAULT_DEVICE_ID, 0, InputDevice.SOURCE_MOUSE, 0);
|
||||
return Device.injectEvent(event, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC);
|
||||
return Device.injectEvent(event, targetDisplayId, Device.INJECT_MODE_ASYNC);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -682,9 +708,12 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
}
|
||||
|
||||
private void setDisplayPower(boolean on) {
|
||||
boolean setDisplayPowerOk = Device.setDisplayPower(displayId, on);
|
||||
// Change the power of the main display when mirroring a virtual display
|
||||
int targetDisplayId = displayId != Device.DISPLAY_ID_NONE ? displayId : 0;
|
||||
boolean setDisplayPowerOk = Device.setDisplayPower(targetDisplayId, on);
|
||||
if (setDisplayPowerOk) {
|
||||
keepDisplayPowerOff = !on;
|
||||
// Do not keep display power off for virtual displays: MOD+p must wake up the physical device
|
||||
keepDisplayPowerOff = displayId != Device.DISPLAY_ID_NONE && !on;
|
||||
Ln.i("Device display turned " + (on ? "on" : "off"));
|
||||
if (cleanUp != null) {
|
||||
boolean mustRestoreOnExit = !on;
|
||||
|
@ -3,46 +3,46 @@ 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.video.ScreenInfo;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import com.genymobile.scrcpy.util.AffineMatrix;
|
||||
|
||||
public final class PositionMapper {
|
||||
|
||||
private final Size videoSize;
|
||||
private final Rect contentRect;
|
||||
private final int coordsRotation;
|
||||
private final AffineMatrix videoToDeviceMatrix;
|
||||
|
||||
public PositionMapper(Size videoSize, Rect contentRect, int videoRotation) {
|
||||
public PositionMapper(Size videoSize, AffineMatrix videoToDeviceMatrix) {
|
||||
this.videoSize = videoSize;
|
||||
this.contentRect = contentRect;
|
||||
this.coordsRotation = reverseRotation(videoRotation);
|
||||
this.videoToDeviceMatrix = videoToDeviceMatrix;
|
||||
}
|
||||
|
||||
public static PositionMapper from(ScreenInfo screenInfo) {
|
||||
// ignore the locked video orientation, the events will apply in coordinates considered in the physical device orientation
|
||||
Size videoSize = screenInfo.getUnlockedVideoSize();
|
||||
return new PositionMapper(videoSize, screenInfo.getContentRect(), screenInfo.getVideoRotation());
|
||||
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);
|
||||
}
|
||||
|
||||
private static int reverseRotation(int rotation) {
|
||||
return (4 - rotation) % 4;
|
||||
public Size getVideoSize() {
|
||||
return videoSize;
|
||||
}
|
||||
|
||||
public Point map(Position position) {
|
||||
// reverse the video rotation to apply the events
|
||||
Position devicePosition = position.rotate(coordsRotation);
|
||||
|
||||
Size clientVideoSize = devicePosition.getScreenSize();
|
||||
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 = devicePosition.getPoint();
|
||||
int convertedX = contentRect.left + point.getX() * contentRect.width() / videoSize.getWidth();
|
||||
int convertedY = contentRect.top + point.getY() * contentRect.height() / videoSize.getHeight();
|
||||
return new Point(convertedX, convertedY);
|
||||
Point point = position.getPoint();
|
||||
if (videoToDeviceMatrix != null) {
|
||||
point = videoToDeviceMatrix.apply(point);
|
||||
}
|
||||
return point;
|
||||
}
|
||||
}
|
||||
|
@ -40,10 +40,9 @@ public final class Device {
|
||||
public static final int INJECT_MODE_WAIT_FOR_RESULT = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT;
|
||||
public static final int INJECT_MODE_WAIT_FOR_FINISH = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH;
|
||||
|
||||
public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1;
|
||||
public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2;
|
||||
// like SC_LOCK_VIDEO_ORIENTATION_INITIAL, but set automatically
|
||||
public static final int LOCK_VIDEO_ORIENTATION_INITIAL_AUTO = -3;
|
||||
// The new display power method introduced in Android 15 does not work as expected:
|
||||
// <https://github.com/Genymobile/scrcpy/issues/5530>
|
||||
private static final boolean USE_ANDROID_15_DISPLAY_POWER = false;
|
||||
|
||||
private Device() {
|
||||
// not instantiable
|
||||
@ -132,7 +131,7 @@ public final class Device {
|
||||
public static boolean setDisplayPower(int displayId, boolean on) {
|
||||
assert displayId != Device.DISPLAY_ID_NONE;
|
||||
|
||||
if (Build.VERSION.SDK_INT >= AndroidVersions.API_35_ANDROID_15) {
|
||||
if (USE_ANDROID_15_DISPLAY_POWER && Build.VERSION.SDK_INT >= AndroidVersions.API_35_ANDROID_15) {
|
||||
return ServiceManager.getDisplayManager().requestDisplayPower(displayId, on);
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,47 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -29,6 +29,61 @@ public final class Size {
|
||||
return new Size(height, width);
|
||||
}
|
||||
|
||||
public Size limit(int maxSize) {
|
||||
assert maxSize >= 0 : "Max size may not be negative";
|
||||
assert maxSize % 8 == 0 : "Max size must be a multiple of 8";
|
||||
|
||||
if (maxSize == 0) {
|
||||
// No limit
|
||||
return this;
|
||||
}
|
||||
|
||||
boolean portrait = height > width;
|
||||
int major = portrait ? height : width;
|
||||
if (major <= maxSize) {
|
||||
return this;
|
||||
}
|
||||
|
||||
int minor = portrait ? width : height;
|
||||
|
||||
int newMajor = maxSize;
|
||||
int newMinor = maxSize * minor / major;
|
||||
|
||||
int w = portrait ? newMinor : newMajor;
|
||||
int h = portrait ? newMajor : newMinor;
|
||||
return new Size(w, h);
|
||||
}
|
||||
|
||||
/**
|
||||
* Round both dimensions of this size to be a multiple of 8 (as required by many encoders).
|
||||
*
|
||||
* @return The current size rounded.
|
||||
*/
|
||||
public Size round8() {
|
||||
if (isMultipleOf8()) {
|
||||
// Already a multiple of 8
|
||||
return this;
|
||||
}
|
||||
|
||||
boolean portrait = height > width;
|
||||
int major = portrait ? height : width;
|
||||
int minor = portrait ? width : height;
|
||||
|
||||
major &= ~7; // round down to not exceed the initial size
|
||||
minor = (minor + 4) & ~7; // round to the nearest to minimize aspect ratio distortion
|
||||
if (minor > major) {
|
||||
minor = major;
|
||||
}
|
||||
|
||||
int w = portrait ? minor : major;
|
||||
int h = portrait ? major : minor;
|
||||
return new Size(w, h);
|
||||
}
|
||||
|
||||
public boolean isMultipleOf8() {
|
||||
return (width & 7) == 0 && (height & 7) == 0;
|
||||
}
|
||||
|
||||
public Rect toRect() {
|
||||
return new Rect(0, 0, width, height);
|
||||
}
|
||||
@ -52,6 +107,6 @@ public final class Size {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Size{" + width + 'x' + height + '}';
|
||||
return width + "x" + height;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,135 @@
|
||||
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();
|
||||
}
|
||||
}
|
124
server/src/main/java/com/genymobile/scrcpy/opengl/GLUtils.java
Normal file
124
server/src/main/java/com/genymobile/scrcpy/opengl/GLUtils.java
Normal file
@ -0,0 +1,124 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
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();
|
||||
}
|
@ -0,0 +1,258 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,368 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -3,7 +3,12 @@ package com.genymobile.scrcpy.video;
|
||||
import com.genymobile.scrcpy.AndroidVersions;
|
||||
import com.genymobile.scrcpy.Options;
|
||||
import com.genymobile.scrcpy.device.ConfigurationException;
|
||||
import com.genymobile.scrcpy.device.Orientation;
|
||||
import com.genymobile.scrcpy.device.Size;
|
||||
import com.genymobile.scrcpy.opengl.AffineOpenGLFilter;
|
||||
import com.genymobile.scrcpy.opengl.OpenGLFilter;
|
||||
import com.genymobile.scrcpy.opengl.OpenGLRunner;
|
||||
import com.genymobile.scrcpy.util.AffineMatrix;
|
||||
import com.genymobile.scrcpy.util.HandlerExecutor;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
import com.genymobile.scrcpy.util.LogUtils;
|
||||
@ -41,6 +46,13 @@ import java.util.stream.Stream;
|
||||
|
||||
public class CameraCapture extends SurfaceCapture {
|
||||
|
||||
public static final float[] VFLIP_MATRIX = {
|
||||
1, 0, 0, 0, // column 1
|
||||
0, -1, 0, 0, // column 2
|
||||
0, 0, 1, 0, // column 3
|
||||
0, 1, 0, 1, // column 4
|
||||
};
|
||||
|
||||
private final String explicitCameraId;
|
||||
private final CameraFacing cameraFacing;
|
||||
private final Size explicitSize;
|
||||
@ -48,9 +60,16 @@ public class CameraCapture extends SurfaceCapture {
|
||||
private final CameraAspectRatio aspectRatio;
|
||||
private final int fps;
|
||||
private final boolean highSpeed;
|
||||
private final Rect crop;
|
||||
private final Orientation captureOrientation;
|
||||
private final float angle;
|
||||
|
||||
private String cameraId;
|
||||
private Size size;
|
||||
private Size captureSize;
|
||||
private Size videoSize; // after OpenGL transforms
|
||||
|
||||
private AffineMatrix transform;
|
||||
private OpenGLRunner glRunner;
|
||||
|
||||
private HandlerThread cameraThread;
|
||||
private Handler cameraHandler;
|
||||
@ -67,6 +86,10 @@ public class CameraCapture extends SurfaceCapture {
|
||||
this.aspectRatio = options.getCameraAspectRatio();
|
||||
this.fps = options.getCameraFps();
|
||||
this.highSpeed = options.getCameraHighSpeed();
|
||||
this.crop = options.getCrop();
|
||||
this.captureOrientation = options.getCaptureOrientation();
|
||||
assert captureOrientation != null;
|
||||
this.angle = options.getAngle();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -92,13 +115,28 @@ public class CameraCapture extends SurfaceCapture {
|
||||
@Override
|
||||
public void prepare() throws IOException {
|
||||
try {
|
||||
size = selectSize(cameraId, explicitSize, maxSize, aspectRatio, highSpeed);
|
||||
if (size == null) {
|
||||
captureSize = selectSize(cameraId, explicitSize, maxSize, aspectRatio, highSpeed);
|
||||
if (captureSize == null) {
|
||||
throw new IOException("Could not select camera size");
|
||||
}
|
||||
} catch (CameraAccessException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
||||
VideoFilter filter = new VideoFilter(captureSize);
|
||||
|
||||
if (crop != null) {
|
||||
filter.addCrop(crop, false);
|
||||
}
|
||||
|
||||
if (captureOrientation != Orientation.Orient0) {
|
||||
filter.addOrientation(captureOrientation);
|
||||
}
|
||||
|
||||
filter.addAngle(angle);
|
||||
|
||||
transform = filter.getInverseTransform();
|
||||
videoSize = filter.getOutputSize().limit(maxSize).round8();
|
||||
}
|
||||
|
||||
private static String selectCamera(String explicitCameraId, CameraFacing cameraFacing) throws CameraAccessException, ConfigurationException {
|
||||
@ -214,15 +252,33 @@ public class CameraCapture extends SurfaceCapture {
|
||||
|
||||
@Override
|
||||
public void start(Surface surface) throws IOException {
|
||||
if (transform != null) {
|
||||
assert glRunner == null;
|
||||
OpenGLFilter glFilter = new AffineOpenGLFilter(transform);
|
||||
// The transform matrix returned by SurfaceTexture is incorrect for camera capture (it often contains an additional unexpected 90°
|
||||
// rotation). Use a vertical flip transform matrix instead.
|
||||
glRunner = new OpenGLRunner(glFilter, VFLIP_MATRIX);
|
||||
surface = glRunner.start(captureSize, videoSize, surface);
|
||||
}
|
||||
|
||||
try {
|
||||
CameraCaptureSession session = createCaptureSession(cameraDevice, surface);
|
||||
CaptureRequest request = createCaptureRequest(surface);
|
||||
setRepeatingRequest(session, request);
|
||||
} catch (CameraAccessException | InterruptedException e) {
|
||||
stop();
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
if (glRunner != null) {
|
||||
glRunner.stopAndRelease();
|
||||
glRunner = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
if (cameraDevice != null) {
|
||||
@ -235,7 +291,7 @@ public class CameraCapture extends SurfaceCapture {
|
||||
|
||||
@Override
|
||||
public Size getSize() {
|
||||
return size;
|
||||
return videoSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -18,7 +18,11 @@ public class CaptureReset implements SurfaceCapture.CaptureListener {
|
||||
public synchronized void reset() {
|
||||
reset.set(true);
|
||||
if (runningMediaCodec != null) {
|
||||
runningMediaCodec.signalEndOfInputStream();
|
||||
try {
|
||||
runningMediaCodec.signalEndOfInputStream();
|
||||
} catch (IllegalStateException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,139 @@
|
||||
package com.genymobile.scrcpy.video;
|
||||
|
||||
import com.genymobile.scrcpy.AndroidVersions;
|
||||
import com.genymobile.scrcpy.device.Device;
|
||||
import com.genymobile.scrcpy.device.DisplayInfo;
|
||||
import com.genymobile.scrcpy.device.Size;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
import com.genymobile.scrcpy.wrappers.DisplayManager;
|
||||
import com.genymobile.scrcpy.wrappers.DisplayWindowListener;
|
||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.view.IDisplayWindowListener;
|
||||
|
||||
public class DisplaySizeMonitor {
|
||||
|
||||
public interface Listener {
|
||||
void onDisplaySizeChanged();
|
||||
}
|
||||
|
||||
// On Android 14, DisplayListener may be broken (it never sends events). This is fixed in recent Android 14 upgrades, but we can't really
|
||||
// detect it directly, so register a DisplayWindowListener (introduced in Android 11) to listen to configuration changes instead.
|
||||
private static final boolean USE_DEFAULT_METHOD = Build.VERSION.SDK_INT != AndroidVersions.API_34_ANDROID_14;
|
||||
|
||||
private DisplayManager.DisplayListenerHandle displayListenerHandle;
|
||||
private HandlerThread handlerThread;
|
||||
|
||||
private IDisplayWindowListener displayWindowListener;
|
||||
|
||||
private int displayId = Device.DISPLAY_ID_NONE;
|
||||
|
||||
private Size sessionDisplaySize;
|
||||
|
||||
private Listener listener;
|
||||
|
||||
public void start(int displayId, Listener listener) {
|
||||
// Once started, the listener and the displayId must never change
|
||||
assert listener != null;
|
||||
this.listener = listener;
|
||||
|
||||
assert this.displayId == Device.DISPLAY_ID_NONE;
|
||||
this.displayId = displayId;
|
||||
|
||||
if (USE_DEFAULT_METHOD) {
|
||||
handlerThread = new HandlerThread("DisplayListener");
|
||||
handlerThread.start();
|
||||
Handler handler = new Handler(handlerThread.getLooper());
|
||||
displayListenerHandle = ServiceManager.getDisplayManager().registerDisplayListener(eventDisplayId -> {
|
||||
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
|
||||
Ln.v("DisplaySizeMonitor: onDisplayChanged(" + eventDisplayId + ")");
|
||||
}
|
||||
|
||||
if (eventDisplayId == displayId) {
|
||||
checkDisplaySizeChanged();
|
||||
}
|
||||
}, handler);
|
||||
} else {
|
||||
displayWindowListener = new DisplayWindowListener() {
|
||||
@Override
|
||||
public void onDisplayConfigurationChanged(int eventDisplayId, Configuration newConfig) {
|
||||
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
|
||||
Ln.v("DisplaySizeMonitor: onDisplayConfigurationChanged(" + eventDisplayId + ")");
|
||||
}
|
||||
|
||||
if (eventDisplayId == displayId) {
|
||||
checkDisplaySizeChanged();
|
||||
}
|
||||
}
|
||||
};
|
||||
ServiceManager.getWindowManager().registerDisplayWindowListener(displayWindowListener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop and release the monitor.
|
||||
* <p/>
|
||||
* It must not be used anymore.
|
||||
* It is ok to call this method even if {@link #start(int, Listener)} was not called.
|
||||
*/
|
||||
public void stopAndRelease() {
|
||||
if (USE_DEFAULT_METHOD) {
|
||||
// displayListenerHandle may be null if registration failed
|
||||
if (displayListenerHandle != null) {
|
||||
ServiceManager.getDisplayManager().unregisterDisplayListener(displayListenerHandle);
|
||||
displayListenerHandle = null;
|
||||
}
|
||||
|
||||
if (handlerThread != null) {
|
||||
handlerThread.quitSafely();
|
||||
}
|
||||
} else if (displayWindowListener != null) {
|
||||
ServiceManager.getWindowManager().unregisterDisplayWindowListener(displayWindowListener);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized Size getSessionDisplaySize() {
|
||||
return sessionDisplaySize;
|
||||
}
|
||||
|
||||
public synchronized void setSessionDisplaySize(Size sessionDisplaySize) {
|
||||
this.sessionDisplaySize = sessionDisplaySize;
|
||||
}
|
||||
|
||||
private void checkDisplaySizeChanged() {
|
||||
DisplayInfo di = ServiceManager.getDisplayManager().getDisplayInfo(displayId);
|
||||
if (di == null) {
|
||||
Ln.w("DisplayInfo for " + displayId + " cannot be retrieved");
|
||||
// We can't compare with the current size, so reset unconditionally
|
||||
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
|
||||
Ln.v("DisplaySizeMonitor: requestReset(): " + getSessionDisplaySize() + " -> (unknown)");
|
||||
}
|
||||
setSessionDisplaySize(null);
|
||||
listener.onDisplaySizeChanged();
|
||||
} else {
|
||||
Size size = di.getSize();
|
||||
|
||||
// The field is hidden on purpose, to read it with synchronization
|
||||
@SuppressWarnings("checkstyle:HiddenField")
|
||||
Size sessionDisplaySize = getSessionDisplaySize(); // synchronized
|
||||
|
||||
// .equals() also works if sessionDisplaySize == null
|
||||
if (!size.equals(sessionDisplaySize)) {
|
||||
// Reset only if the size is different
|
||||
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
|
||||
Ln.v("DisplaySizeMonitor: requestReset(): " + sessionDisplaySize + " -> " + size);
|
||||
}
|
||||
// Set the new size immediately, so that a future onDisplayChanged() event called before the asynchronous prepare()
|
||||
// considers that the current size is the requested size (to avoid a duplicate requestReset())
|
||||
setSessionDisplaySize(size);
|
||||
listener.onDisplaySizeChanged();
|
||||
} else if (Ln.isEnabled(Ln.Level.VERBOSE)) {
|
||||
Ln.v("DisplaySizeMonitor: Size not changed (" + size + "): do not requestReset()");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -5,12 +5,16 @@ import com.genymobile.scrcpy.Options;
|
||||
import com.genymobile.scrcpy.control.PositionMapper;
|
||||
import com.genymobile.scrcpy.device.DisplayInfo;
|
||||
import com.genymobile.scrcpy.device.NewDisplay;
|
||||
import com.genymobile.scrcpy.device.Orientation;
|
||||
import com.genymobile.scrcpy.device.Size;
|
||||
import com.genymobile.scrcpy.opengl.AffineOpenGLFilter;
|
||||
import com.genymobile.scrcpy.opengl.OpenGLFilter;
|
||||
import com.genymobile.scrcpy.opengl.OpenGLRunner;
|
||||
import com.genymobile.scrcpy.util.AffineMatrix;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.hardware.display.DisplayManager;
|
||||
import android.hardware.display.VirtualDisplay;
|
||||
import android.os.Build;
|
||||
import android.view.Surface;
|
||||
@ -20,8 +24,8 @@ import java.io.IOException;
|
||||
public class NewDisplayCapture extends SurfaceCapture {
|
||||
|
||||
// Internal fields copied from android.hardware.display.DisplayManager
|
||||
private static final int VIRTUAL_DISPLAY_FLAG_PUBLIC = DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC;
|
||||
private static final int VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY = DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
|
||||
private static final int VIRTUAL_DISPLAY_FLAG_PUBLIC = android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC;
|
||||
private static final int VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY = android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
|
||||
private static final int VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH = 1 << 6;
|
||||
private static final int VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT = 1 << 7;
|
||||
private static final int VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL = 1 << 8;
|
||||
@ -36,12 +40,26 @@ public class NewDisplayCapture extends SurfaceCapture {
|
||||
private final VirtualDisplayListener vdListener;
|
||||
private final NewDisplay newDisplay;
|
||||
|
||||
private final DisplaySizeMonitor displaySizeMonitor = new DisplaySizeMonitor();
|
||||
|
||||
private AffineMatrix displayTransform;
|
||||
private AffineMatrix eventTransform;
|
||||
private OpenGLRunner glRunner;
|
||||
|
||||
private Size mainDisplaySize;
|
||||
private int mainDisplayDpi;
|
||||
private int maxSize; // only used if newDisplay.getSize() != null
|
||||
private int maxSize;
|
||||
private final Rect crop;
|
||||
private final boolean captureOrientationLocked;
|
||||
private final Orientation captureOrientation;
|
||||
private final float angle;
|
||||
private final boolean vdSystemDecorations;
|
||||
|
||||
private VirtualDisplay virtualDisplay;
|
||||
private Size size;
|
||||
private Size videoSize;
|
||||
private Size displaySize; // the logical size of the display (including rotation)
|
||||
private Size physicalSize; // the physical size of the display (without rotation)
|
||||
|
||||
private int dpi;
|
||||
|
||||
public NewDisplayCapture(VirtualDisplayListener vdListener, Options options) {
|
||||
@ -49,16 +67,26 @@ public class NewDisplayCapture extends SurfaceCapture {
|
||||
this.newDisplay = options.getNewDisplay();
|
||||
assert newDisplay != null;
|
||||
this.maxSize = options.getMaxSize();
|
||||
this.crop = options.getCrop();
|
||||
assert options.getCaptureOrientationLock() != null;
|
||||
this.captureOrientationLocked = options.getCaptureOrientationLock() != Orientation.Lock.Unlocked;
|
||||
this.captureOrientation = options.getCaptureOrientation();
|
||||
assert captureOrientation != null;
|
||||
this.angle = options.getAngle();
|
||||
this.vdSystemDecorations = options.getVDSystemDecorations();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
size = newDisplay.getSize();
|
||||
displaySize = newDisplay.getSize();
|
||||
dpi = newDisplay.getDpi();
|
||||
if (size == null || dpi == 0) {
|
||||
if (displaySize == null || dpi == 0) {
|
||||
DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(0);
|
||||
if (displayInfo != null) {
|
||||
mainDisplaySize = displayInfo.getSize();
|
||||
if ((displayInfo.getRotation() % 2) != 0) {
|
||||
mainDisplaySize = mainDisplaySize.rotate(); // Use the natural device orientation (at rotation 0), not the current one
|
||||
}
|
||||
mainDisplayDpi = displayInfo.getDpi();
|
||||
} else {
|
||||
Ln.w("Main display not found, fallback to 1920x1080 240dpi");
|
||||
@ -70,12 +98,67 @@ public class NewDisplayCapture extends SurfaceCapture {
|
||||
|
||||
@Override
|
||||
public void prepare() {
|
||||
if (!newDisplay.hasExplicitSize()) {
|
||||
size = ScreenInfo.computeVideoSize(mainDisplaySize.getWidth(), mainDisplaySize.getHeight(), maxSize);
|
||||
int displayRotation;
|
||||
if (virtualDisplay == null) {
|
||||
if (!newDisplay.hasExplicitSize()) {
|
||||
displaySize = mainDisplaySize;
|
||||
}
|
||||
if (!newDisplay.hasExplicitDpi()) {
|
||||
dpi = scaleDpi(mainDisplaySize, mainDisplayDpi, displaySize);
|
||||
}
|
||||
|
||||
videoSize = displaySize;
|
||||
displayRotation = 0;
|
||||
// Set the current display size to avoid an unnecessary call to invalidate()
|
||||
displaySizeMonitor.setSessionDisplaySize(displaySize);
|
||||
} else {
|
||||
DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(virtualDisplay.getDisplay().getDisplayId());
|
||||
displaySize = displayInfo.getSize();
|
||||
dpi = displayInfo.getDpi();
|
||||
displayRotation = displayInfo.getRotation();
|
||||
}
|
||||
if (!newDisplay.hasExplicitDpi()) {
|
||||
dpi = scaleDpi(mainDisplaySize, mainDisplayDpi, size);
|
||||
|
||||
VideoFilter filter = new VideoFilter(displaySize);
|
||||
|
||||
if (crop != null) {
|
||||
boolean transposed = (displayRotation % 2) != 0;
|
||||
filter.addCrop(crop, transposed);
|
||||
}
|
||||
|
||||
filter.addOrientation(displayRotation, captureOrientationLocked, captureOrientation);
|
||||
filter.addAngle(angle);
|
||||
|
||||
Size filteredSize = filter.getOutputSize();
|
||||
if (!filteredSize.isMultipleOf8() || (maxSize != 0 && filteredSize.getMax() > maxSize)) {
|
||||
if (maxSize != 0) {
|
||||
filteredSize = filteredSize.limit(maxSize);
|
||||
}
|
||||
filteredSize = filteredSize.round8();
|
||||
filter.addResize(filteredSize);
|
||||
}
|
||||
|
||||
eventTransform = filter.getInverseTransform();
|
||||
|
||||
// DisplayInfo gives the oriented size (so videoSize includes the display rotation)
|
||||
videoSize = filter.getOutputSize();
|
||||
|
||||
// But the virtual display video always remains in the origin orientation (the video itself is not rotated, so it must rotated manually).
|
||||
// This additional display rotation must not be included in the input events transform (the expected coordinates are already in the
|
||||
// physical display size)
|
||||
if ((displayRotation % 2) == 0) {
|
||||
physicalSize = displaySize;
|
||||
} else {
|
||||
physicalSize = displaySize.rotate();
|
||||
}
|
||||
VideoFilter displayFilter = new VideoFilter(physicalSize);
|
||||
displayFilter.addRotation(displayRotation);
|
||||
AffineMatrix displayRotationMatrix = displayFilter.getInverseTransform();
|
||||
|
||||
// Take care of multiplication order:
|
||||
// displayTransform = (FILTER_MATRIX * DISPLAY_FILTER_MATRIX)⁻¹
|
||||
// = DISPLAY_FILTER_MATRIX⁻¹ * FILTER_MATRIX⁻¹
|
||||
// = displayRotationMatrix * eventTransform
|
||||
displayTransform = AffineMatrix.multiplyAll(displayRotationMatrix, eventTransform);
|
||||
}
|
||||
|
||||
public void startNew(Surface surface) {
|
||||
@ -85,8 +168,10 @@ public class NewDisplayCapture extends SurfaceCapture {
|
||||
| VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
|
||||
| VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH
|
||||
| VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT
|
||||
| VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL
|
||||
| VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS;
|
||||
| VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL;
|
||||
if (vdSystemDecorations) {
|
||||
flags |= VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS;
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= AndroidVersions.API_33_ANDROID_13) {
|
||||
flags |= VIRTUAL_DISPLAY_FLAG_TRUSTED
|
||||
| VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP
|
||||
@ -98,32 +183,50 @@ public class NewDisplayCapture extends SurfaceCapture {
|
||||
}
|
||||
}
|
||||
virtualDisplay = ServiceManager.getDisplayManager()
|
||||
.createNewVirtualDisplay("scrcpy", size.getWidth(), size.getHeight(), dpi, surface, flags);
|
||||
.createNewVirtualDisplay("scrcpy", displaySize.getWidth(), displaySize.getHeight(), dpi, surface, flags);
|
||||
virtualDisplayId = virtualDisplay.getDisplay().getDisplayId();
|
||||
Ln.i("New display: " + size.getWidth() + "x" + size.getHeight() + "/" + dpi + " (id=" + virtualDisplayId + ")");
|
||||
Ln.i("New display: " + displaySize.getWidth() + "x" + displaySize.getHeight() + "/" + dpi + " (id=" + virtualDisplayId + ")");
|
||||
|
||||
displaySizeMonitor.start(virtualDisplayId, this::invalidate);
|
||||
} catch (Exception e) {
|
||||
Ln.e("Could not create display", e);
|
||||
throw new AssertionError("Could not create display");
|
||||
}
|
||||
|
||||
if (vdListener != null) {
|
||||
Rect contentRect = new Rect(0, 0, size.getWidth(), size.getHeight());
|
||||
PositionMapper positionMapper = new PositionMapper(size, contentRect, 0);
|
||||
vdListener.onNewVirtualDisplay(virtualDisplayId, positionMapper);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Surface surface) throws IOException {
|
||||
if (displayTransform != null) {
|
||||
assert glRunner == null;
|
||||
OpenGLFilter glFilter = new AffineOpenGLFilter(displayTransform);
|
||||
glRunner = new OpenGLRunner(glFilter);
|
||||
surface = glRunner.start(physicalSize, videoSize, surface);
|
||||
}
|
||||
|
||||
if (virtualDisplay == null) {
|
||||
startNew(surface);
|
||||
} else {
|
||||
virtualDisplay.setSurface(surface);
|
||||
}
|
||||
|
||||
if (vdListener != null) {
|
||||
PositionMapper positionMapper = PositionMapper.create(videoSize, eventTransform, displaySize);
|
||||
vdListener.onNewVirtualDisplay(virtualDisplay.getDisplay().getDisplayId(), positionMapper);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
if (glRunner != null) {
|
||||
glRunner.stopAndRelease();
|
||||
glRunner = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
displaySizeMonitor.stopAndRelease();
|
||||
|
||||
if (virtualDisplay != null) {
|
||||
virtualDisplay.release();
|
||||
virtualDisplay = null;
|
||||
@ -132,16 +235,11 @@ public class NewDisplayCapture extends SurfaceCapture {
|
||||
|
||||
@Override
|
||||
public synchronized Size getSize() {
|
||||
return size;
|
||||
return videoSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized boolean setMaxSize(int newMaxSize) {
|
||||
if (newDisplay.hasExplicitSize()) {
|
||||
// Cannot retry with a different size if the display size was explicitly provided
|
||||
return false;
|
||||
}
|
||||
|
||||
maxSize = newMaxSize;
|
||||
return true;
|
||||
}
|
||||
|
@ -6,49 +6,45 @@ import com.genymobile.scrcpy.control.PositionMapper;
|
||||
import com.genymobile.scrcpy.device.ConfigurationException;
|
||||
import com.genymobile.scrcpy.device.Device;
|
||||
import com.genymobile.scrcpy.device.DisplayInfo;
|
||||
import com.genymobile.scrcpy.device.Orientation;
|
||||
import com.genymobile.scrcpy.device.Size;
|
||||
import com.genymobile.scrcpy.opengl.AffineOpenGLFilter;
|
||||
import com.genymobile.scrcpy.opengl.OpenGLFilter;
|
||||
import com.genymobile.scrcpy.opengl.OpenGLRunner;
|
||||
import com.genymobile.scrcpy.util.AffineMatrix;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
import com.genymobile.scrcpy.util.LogUtils;
|
||||
import com.genymobile.scrcpy.wrappers.DisplayManager;
|
||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||
import com.genymobile.scrcpy.wrappers.SurfaceControl;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.hardware.display.VirtualDisplay;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.IBinder;
|
||||
import android.view.IDisplayFoldListener;
|
||||
import android.view.IRotationWatcher;
|
||||
import android.view.Surface;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class ScreenCapture extends SurfaceCapture {
|
||||
|
||||
private final VirtualDisplayListener vdListener;
|
||||
private final int displayId;
|
||||
private int maxSize;
|
||||
private final Rect crop;
|
||||
private final int lockVideoOrientation;
|
||||
private Orientation.Lock captureOrientationLock;
|
||||
private Orientation captureOrientation;
|
||||
private final float angle;
|
||||
|
||||
private DisplayInfo displayInfo;
|
||||
private ScreenInfo screenInfo;
|
||||
private Size videoSize;
|
||||
|
||||
// Source display size (before resizing/crop) for the current session
|
||||
private Size sessionDisplaySize;
|
||||
private final DisplaySizeMonitor displaySizeMonitor = new DisplaySizeMonitor();
|
||||
|
||||
private IBinder display;
|
||||
private VirtualDisplay virtualDisplay;
|
||||
|
||||
private DisplayManager.DisplayListenerHandle displayListenerHandle;
|
||||
private HandlerThread handlerThread;
|
||||
|
||||
// On Android 14, the DisplayListener may be broken (it never sends events). This is fixed in recent Android 14 upgrades, but we can't really
|
||||
// detect it directly, so register a RotationWatcher and a DisplayFoldListener as a fallback, until we receive the first event from
|
||||
// DisplayListener (which proves that it works).
|
||||
private boolean displayListenerWorks; // only accessed from the display listener thread
|
||||
private IRotationWatcher rotationWatcher;
|
||||
private IDisplayFoldListener displayFoldListener;
|
||||
private AffineMatrix transform;
|
||||
private OpenGLRunner glRunner;
|
||||
|
||||
public ScreenCapture(VirtualDisplayListener vdListener, Options options) {
|
||||
this.vdListener = vdListener;
|
||||
@ -56,62 +52,16 @@ public class ScreenCapture extends SurfaceCapture {
|
||||
assert displayId != Device.DISPLAY_ID_NONE;
|
||||
this.maxSize = options.getMaxSize();
|
||||
this.crop = options.getCrop();
|
||||
this.lockVideoOrientation = options.getLockVideoOrientation();
|
||||
this.captureOrientationLock = options.getCaptureOrientationLock();
|
||||
this.captureOrientation = options.getCaptureOrientation();
|
||||
assert captureOrientationLock != null;
|
||||
assert captureOrientation != null;
|
||||
this.angle = options.getAngle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) {
|
||||
registerDisplayListenerFallbacks();
|
||||
}
|
||||
|
||||
handlerThread = new HandlerThread("DisplayListener");
|
||||
handlerThread.start();
|
||||
Handler handler = new Handler(handlerThread.getLooper());
|
||||
displayListenerHandle = ServiceManager.getDisplayManager().registerDisplayListener(displayId -> {
|
||||
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
|
||||
Ln.v("ScreenCapture: onDisplayChanged(" + displayId + ")");
|
||||
}
|
||||
if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) {
|
||||
if (!displayListenerWorks) {
|
||||
// On the first display listener event, we know it works, we can unregister the fallbacks
|
||||
displayListenerWorks = true;
|
||||
unregisterDisplayListenerFallbacks();
|
||||
}
|
||||
}
|
||||
if (this.displayId == displayId) {
|
||||
DisplayInfo di = ServiceManager.getDisplayManager().getDisplayInfo(displayId);
|
||||
if (di == null) {
|
||||
Ln.w("DisplayInfo for " + displayId + " cannot be retrieved");
|
||||
// We can't compare with the current size, so reset unconditionally
|
||||
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
|
||||
Ln.v("ScreenCapture: requestReset(): " + getSessionDisplaySize() + " -> (unknown)");
|
||||
}
|
||||
setSessionDisplaySize(null);
|
||||
invalidate();
|
||||
} else {
|
||||
Size size = di.getSize();
|
||||
|
||||
// The field is hidden on purpose, to read it with synchronization
|
||||
@SuppressWarnings("checkstyle:HiddenField")
|
||||
Size sessionDisplaySize = getSessionDisplaySize(); // synchronized
|
||||
|
||||
// .equals() also works if sessionDisplaySize == null
|
||||
if (!size.equals(sessionDisplaySize)) {
|
||||
// Reset only if the size is different
|
||||
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
|
||||
Ln.v("ScreenCapture: requestReset(): " + sessionDisplaySize + " -> " + size);
|
||||
}
|
||||
// Set the new size immediately, so that a future onDisplayChanged() event called before the asynchronous prepare()
|
||||
// considers that the current size is the requested size (to avoid a duplicate requestReset())
|
||||
setSessionDisplaySize(size);
|
||||
invalidate();
|
||||
} else if (Ln.isEnabled(Ln.Level.VERBOSE)) {
|
||||
Ln.v("ScreenCapture: Size not changed (" + size + "): do not requestReset()");
|
||||
}
|
||||
}
|
||||
}
|
||||
}, handler);
|
||||
displaySizeMonitor.start(displayId, this::invalidate);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -126,12 +76,32 @@ public class ScreenCapture extends SurfaceCapture {
|
||||
Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted");
|
||||
}
|
||||
|
||||
setSessionDisplaySize(displayInfo.getSize());
|
||||
screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), displayInfo.getSize(), crop, maxSize, lockVideoOrientation);
|
||||
Size displaySize = displayInfo.getSize();
|
||||
displaySizeMonitor.setSessionDisplaySize(displaySize);
|
||||
|
||||
if (captureOrientationLock == Orientation.Lock.LockedInitial) {
|
||||
// The user requested to lock the video orientation to the current orientation
|
||||
captureOrientationLock = Orientation.Lock.LockedValue;
|
||||
captureOrientation = Orientation.fromRotation(displayInfo.getRotation());
|
||||
}
|
||||
|
||||
VideoFilter filter = new VideoFilter(displaySize);
|
||||
|
||||
if (crop != null) {
|
||||
boolean transposed = (displayInfo.getRotation() % 2) != 0;
|
||||
filter.addCrop(crop, transposed);
|
||||
}
|
||||
|
||||
boolean locked = captureOrientationLock != Orientation.Lock.Unlocked;
|
||||
filter.addOrientation(displayInfo.getRotation(), locked, captureOrientation);
|
||||
filter.addAngle(angle);
|
||||
|
||||
transform = filter.getInverseTransform();
|
||||
videoSize = filter.getOutputSize().limit(maxSize).round8();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Surface surface) {
|
||||
public void start(Surface surface) throws IOException {
|
||||
if (display != null) {
|
||||
SurfaceControl.destroyDisplay(display);
|
||||
display = null;
|
||||
@ -141,31 +111,40 @@ public class ScreenCapture extends SurfaceCapture {
|
||||
virtualDisplay = null;
|
||||
}
|
||||
|
||||
Size inputSize;
|
||||
if (transform != null) {
|
||||
// If there is a filter, it must receive the full display content
|
||||
inputSize = displayInfo.getSize();
|
||||
assert glRunner == null;
|
||||
OpenGLFilter glFilter = new AffineOpenGLFilter(transform);
|
||||
glRunner = new OpenGLRunner(glFilter);
|
||||
surface = glRunner.start(inputSize, videoSize, surface);
|
||||
} else {
|
||||
// If there is no filter, the display must be rendered at target video size directly
|
||||
inputSize = videoSize;
|
||||
}
|
||||
|
||||
int virtualDisplayId;
|
||||
PositionMapper positionMapper;
|
||||
try {
|
||||
Size videoSize = screenInfo.getVideoSize();
|
||||
virtualDisplay = ServiceManager.getDisplayManager()
|
||||
.createVirtualDisplay("scrcpy", videoSize.getWidth(), videoSize.getHeight(), displayId, surface);
|
||||
.createVirtualDisplay("scrcpy", inputSize.getWidth(), inputSize.getHeight(), displayId, surface);
|
||||
virtualDisplayId = virtualDisplay.getDisplay().getDisplayId();
|
||||
Rect contentRect = new Rect(0, 0, videoSize.getWidth(), videoSize.getHeight());
|
||||
// The position are relative to the virtual display, not the original display
|
||||
positionMapper = new PositionMapper(videoSize, contentRect, 0);
|
||||
|
||||
// The positions are relative to the virtual display, not the original display (so use inputSize, not deviceSize!)
|
||||
positionMapper = PositionMapper.create(videoSize, transform, inputSize);
|
||||
Ln.d("Display: using DisplayManager API");
|
||||
} catch (Exception displayManagerException) {
|
||||
try {
|
||||
display = createDisplay();
|
||||
|
||||
Rect contentRect = screenInfo.getContentRect();
|
||||
|
||||
// does not include the locked video orientation
|
||||
Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect();
|
||||
int videoRotation = screenInfo.getVideoRotation();
|
||||
Size deviceSize = displayInfo.getSize();
|
||||
int layerStack = displayInfo.getLayerStack();
|
||||
|
||||
setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack);
|
||||
setDisplaySurface(display, surface, deviceSize.toRect(), inputSize.toRect(), layerStack);
|
||||
virtualDisplayId = displayId;
|
||||
positionMapper = PositionMapper.from(screenInfo);
|
||||
|
||||
positionMapper = PositionMapper.create(videoSize, transform, deviceSize);
|
||||
Ln.d("Display: using SurfaceControl API");
|
||||
} catch (Exception surfaceControlException) {
|
||||
Ln.e("Could not create display using DisplayManager", displayManagerException);
|
||||
@ -179,20 +158,17 @@ public class ScreenCapture extends SurfaceCapture {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
if (glRunner != null) {
|
||||
glRunner.stopAndRelease();
|
||||
glRunner = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) {
|
||||
unregisterDisplayListenerFallbacks();
|
||||
}
|
||||
|
||||
handlerThread.quitSafely();
|
||||
handlerThread = null;
|
||||
|
||||
// displayListenerHandle may be null if registration failed
|
||||
if (displayListenerHandle != null) {
|
||||
ServiceManager.getDisplayManager().unregisterDisplayListener(displayListenerHandle);
|
||||
displayListenerHandle = null;
|
||||
}
|
||||
displaySizeMonitor.stopAndRelease();
|
||||
|
||||
if (display != null) {
|
||||
SurfaceControl.destroyDisplay(display);
|
||||
@ -206,7 +182,7 @@ public class ScreenCapture extends SurfaceCapture {
|
||||
|
||||
@Override
|
||||
public Size getSize() {
|
||||
return screenInfo.getVideoSize();
|
||||
return videoSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -223,78 +199,17 @@ public class ScreenCapture extends SurfaceCapture {
|
||||
return SurfaceControl.createDisplay("scrcpy", secure);
|
||||
}
|
||||
|
||||
private static void setDisplaySurface(IBinder display, Surface surface, int orientation, Rect deviceRect, Rect displayRect, int layerStack) {
|
||||
private static void setDisplaySurface(IBinder display, Surface surface, Rect deviceRect, Rect displayRect, int layerStack) {
|
||||
SurfaceControl.openTransaction();
|
||||
try {
|
||||
SurfaceControl.setDisplaySurface(display, surface);
|
||||
SurfaceControl.setDisplayProjection(display, orientation, deviceRect, displayRect);
|
||||
SurfaceControl.setDisplayProjection(display, 0, deviceRect, displayRect);
|
||||
SurfaceControl.setDisplayLayerStack(display, layerStack);
|
||||
} finally {
|
||||
SurfaceControl.closeTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized Size getSessionDisplaySize() {
|
||||
return sessionDisplaySize;
|
||||
}
|
||||
|
||||
private synchronized void setSessionDisplaySize(Size sessionDisplaySize) {
|
||||
this.sessionDisplaySize = sessionDisplaySize;
|
||||
}
|
||||
|
||||
private void registerDisplayListenerFallbacks() {
|
||||
rotationWatcher = new IRotationWatcher.Stub() {
|
||||
@Override
|
||||
public void onRotationChanged(int rotation) {
|
||||
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
|
||||
Ln.v("ScreenCapture: onRotationChanged(" + rotation + ")");
|
||||
}
|
||||
invalidate();
|
||||
}
|
||||
};
|
||||
ServiceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId);
|
||||
|
||||
// Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10 (but implied by == API_34_ANDROID 14)
|
||||
displayFoldListener = new IDisplayFoldListener.Stub() {
|
||||
|
||||
private boolean first = true;
|
||||
|
||||
@Override
|
||||
public void onDisplayFoldChanged(int displayId, boolean folded) {
|
||||
if (first) {
|
||||
// An event is posted on registration to signal the initial state. Ignore it to avoid restarting encoding.
|
||||
first = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
|
||||
Ln.v("ScreenCapture: onDisplayFoldChanged(" + displayId + ", " + folded + ")");
|
||||
}
|
||||
|
||||
if (ScreenCapture.this.displayId != displayId) {
|
||||
// Ignore events related to other display ids
|
||||
return;
|
||||
}
|
||||
invalidate();
|
||||
}
|
||||
};
|
||||
ServiceManager.getWindowManager().registerDisplayFoldListener(displayFoldListener);
|
||||
}
|
||||
|
||||
private void unregisterDisplayListenerFallbacks() {
|
||||
synchronized (this) {
|
||||
if (rotationWatcher != null) {
|
||||
ServiceManager.getWindowManager().unregisterRotationWatcher(rotationWatcher);
|
||||
rotationWatcher = null;
|
||||
}
|
||||
if (displayFoldListener != null) {
|
||||
// Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10 (but implied by == API_34_ANDROID 14)
|
||||
ServiceManager.getWindowManager().unregisterDisplayFoldListener(displayFoldListener);
|
||||
displayFoldListener = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestInvalidate() {
|
||||
invalidate();
|
||||
|
@ -1,149 +0,0 @@
|
||||
package com.genymobile.scrcpy.video;
|
||||
|
||||
import com.genymobile.scrcpy.BuildConfig;
|
||||
import com.genymobile.scrcpy.device.Device;
|
||||
import com.genymobile.scrcpy.device.Size;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
|
||||
import android.graphics.Rect;
|
||||
|
||||
public final class ScreenInfo {
|
||||
/**
|
||||
* Device (physical) size, possibly cropped
|
||||
*/
|
||||
private final Rect contentRect; // device size, possibly cropped
|
||||
|
||||
/**
|
||||
* Video size, possibly smaller than the device size, already taking the device rotation and crop into account.
|
||||
* <p>
|
||||
* However, it does not include the locked video orientation.
|
||||
*/
|
||||
private final Size unlockedVideoSize;
|
||||
|
||||
/**
|
||||
* Device rotation, related to the natural device orientation (0, 1, 2 or 3)
|
||||
*/
|
||||
private final int deviceRotation;
|
||||
|
||||
/**
|
||||
* The locked video orientation (-1: disabled, 0: normal, 1: 90° CCW, 2: 180°, 3: 90° CW)
|
||||
*/
|
||||
private final int lockedVideoOrientation;
|
||||
|
||||
public ScreenInfo(Rect contentRect, Size unlockedVideoSize, int deviceRotation, int lockedVideoOrientation) {
|
||||
this.contentRect = contentRect;
|
||||
this.unlockedVideoSize = unlockedVideoSize;
|
||||
this.deviceRotation = deviceRotation;
|
||||
this.lockedVideoOrientation = lockedVideoOrientation;
|
||||
}
|
||||
|
||||
public Rect getContentRect() {
|
||||
return contentRect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the video size as if locked video orientation was not set.
|
||||
*
|
||||
* @return the unlocked video size
|
||||
*/
|
||||
public Size getUnlockedVideoSize() {
|
||||
return unlockedVideoSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the actual video size if locked video orientation is set.
|
||||
*
|
||||
* @return the actual video size
|
||||
*/
|
||||
public Size getVideoSize() {
|
||||
if (getVideoRotation() % 2 == 0) {
|
||||
return unlockedVideoSize;
|
||||
}
|
||||
|
||||
return unlockedVideoSize.rotate();
|
||||
}
|
||||
|
||||
public static ScreenInfo computeScreenInfo(int rotation, Size deviceSize, Rect crop, int maxSize, int lockedVideoOrientation) {
|
||||
if (lockedVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL || lockedVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL_AUTO) {
|
||||
// The user requested to lock the video orientation to the current orientation
|
||||
lockedVideoOrientation = rotation;
|
||||
}
|
||||
|
||||
Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight());
|
||||
if (crop != null) {
|
||||
if (rotation % 2 != 0) { // 180s preserve dimensions
|
||||
// the crop (provided by the user) is expressed in the natural orientation
|
||||
crop = flipRect(crop);
|
||||
}
|
||||
if (!contentRect.intersect(crop)) {
|
||||
// intersect() changes contentRect so that it is intersected with crop
|
||||
Ln.w("Crop rectangle (" + formatCrop(crop) + ") does not intersect device screen (" + formatCrop(deviceSize.toRect()) + ")");
|
||||
contentRect = new Rect(); // empty
|
||||
}
|
||||
}
|
||||
|
||||
Size videoSize = computeVideoSize(contentRect.width(), contentRect.height(), maxSize);
|
||||
return new ScreenInfo(contentRect, videoSize, rotation, lockedVideoOrientation);
|
||||
}
|
||||
|
||||
private static String formatCrop(Rect rect) {
|
||||
return rect.width() + ":" + rect.height() + ":" + rect.left + ":" + rect.top;
|
||||
}
|
||||
|
||||
public static Size computeVideoSize(int w, int h, int maxSize) {
|
||||
// Compute the video size and the padding of the content inside this video.
|
||||
// Principle:
|
||||
// - scale down the great side of the screen to maxSize (if necessary);
|
||||
// - scale down the other side so that the aspect ratio is preserved;
|
||||
// - round this value to the nearest multiple of 8 (H.264 only accepts multiples of 8)
|
||||
w &= ~7; // in case it's not a multiple of 8
|
||||
h &= ~7;
|
||||
if (maxSize > 0) {
|
||||
if (BuildConfig.DEBUG && maxSize % 8 != 0) {
|
||||
throw new AssertionError("Max size must be a multiple of 8");
|
||||
}
|
||||
boolean portrait = h > w;
|
||||
int major = portrait ? h : w;
|
||||
int minor = portrait ? w : h;
|
||||
if (major > maxSize) {
|
||||
int minorExact = minor * maxSize / major;
|
||||
// +4 to round the value to the nearest multiple of 8
|
||||
minor = (minorExact + 4) & ~7;
|
||||
major = maxSize;
|
||||
}
|
||||
w = portrait ? minor : major;
|
||||
h = portrait ? major : minor;
|
||||
}
|
||||
return new Size(w, h);
|
||||
}
|
||||
|
||||
private static Rect flipRect(Rect crop) {
|
||||
return new Rect(crop.top, crop.left, crop.bottom, crop.right);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the rotation to apply to the device rotation to get the requested locked video orientation
|
||||
*
|
||||
* @return the rotation offset
|
||||
*/
|
||||
public int getVideoRotation() {
|
||||
if (lockedVideoOrientation == -1) {
|
||||
// no offset
|
||||
return 0;
|
||||
}
|
||||
return (deviceRotation + 4 - lockedVideoOrientation) % 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the rotation to apply to the requested locked video orientation to get the device rotation
|
||||
*
|
||||
* @return the (reverse) rotation offset
|
||||
*/
|
||||
public int getReverseVideoRotation() {
|
||||
if (lockedVideoOrientation == -1) {
|
||||
// no offset
|
||||
return 0;
|
||||
}
|
||||
return (lockedVideoOrientation + 4 - deviceRotation) % 4;
|
||||
}
|
||||
}
|
@ -57,6 +57,13 @@ public abstract class SurfaceCapture {
|
||||
*/
|
||||
public abstract void start(Surface surface) throws IOException;
|
||||
|
||||
/**
|
||||
* Stop the capture.
|
||||
*/
|
||||
public void stop() {
|
||||
// Do nothing by default
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the video size
|
||||
*
|
||||
|
@ -86,13 +86,17 @@ public class SurfaceEncoder implements AsyncProcessor {
|
||||
format.setInteger(MediaFormat.KEY_HEIGHT, size.getHeight());
|
||||
|
||||
Surface surface = null;
|
||||
boolean mediaCodecStarted = false;
|
||||
boolean captureStarted = false;
|
||||
try {
|
||||
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
||||
surface = mediaCodec.createInputSurface();
|
||||
|
||||
capture.start(surface);
|
||||
captureStarted = true;
|
||||
|
||||
mediaCodec.start();
|
||||
mediaCodecStarted = true;
|
||||
|
||||
// Set the MediaCodec instance to "interrupt" (by signaling an EOS) on reset
|
||||
reset.setRunningMediaCodec(mediaCodec);
|
||||
@ -108,18 +112,24 @@ public class SurfaceEncoder implements AsyncProcessor {
|
||||
// The capture might have been closed internally (for example if the camera is disconnected)
|
||||
alive = !stopped.get() && !capture.isClosed();
|
||||
}
|
||||
|
||||
// do not call stop() on exception, it would trigger an IllegalStateException
|
||||
mediaCodec.stop();
|
||||
} catch (IllegalStateException | IllegalArgumentException e) {
|
||||
Ln.e("Encoding error: " + e.getClass().getName() + ": " + e.getMessage());
|
||||
} catch (IllegalStateException | IllegalArgumentException | IOException e) {
|
||||
Ln.e("Capture/encoding error: " + e.getClass().getName() + ": " + e.getMessage());
|
||||
if (!prepareRetry(size)) {
|
||||
throw e;
|
||||
}
|
||||
Ln.i("Retrying...");
|
||||
alive = true;
|
||||
} finally {
|
||||
reset.setRunningMediaCodec(null);
|
||||
if (captureStarted) {
|
||||
capture.stop();
|
||||
}
|
||||
if (mediaCodecStarted) {
|
||||
try {
|
||||
mediaCodec.stop();
|
||||
} catch (IllegalStateException e) {
|
||||
// ignore (just in case)
|
||||
}
|
||||
}
|
||||
mediaCodec.reset();
|
||||
if (surface != null) {
|
||||
surface.release();
|
||||
|
@ -0,0 +1,119 @@
|
||||
package com.genymobile.scrcpy.video;
|
||||
|
||||
import com.genymobile.scrcpy.device.Orientation;
|
||||
import com.genymobile.scrcpy.device.Size;
|
||||
import com.genymobile.scrcpy.util.AffineMatrix;
|
||||
|
||||
import android.graphics.Rect;
|
||||
|
||||
public class VideoFilter {
|
||||
|
||||
private Size size;
|
||||
private AffineMatrix transform;
|
||||
|
||||
public VideoFilter(Size inputSize) {
|
||||
this.size = inputSize;
|
||||
}
|
||||
|
||||
public Size getOutputSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public AffineMatrix getTransform() {
|
||||
return transform;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the inverse transform.
|
||||
* <p/>
|
||||
* The direct affine transform describes how the input image is transformed.
|
||||
* <p/>
|
||||
* It is often useful to retrieve the inverse transform instead:
|
||||
* <ul>
|
||||
* <li>The OpenGL filter expects the matrix to transform the image <em>coordinates</em>, which is the inverse transform;</li>
|
||||
* <li>The click positions must be transformed back to the device positions, using the inverse transform too.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @return the inverse transform
|
||||
*/
|
||||
public AffineMatrix getInverseTransform() {
|
||||
if (transform == null) {
|
||||
return null;
|
||||
}
|
||||
return transform.invert();
|
||||
}
|
||||
|
||||
private static Rect transposeRect(Rect rect) {
|
||||
return new Rect(rect.top, rect.left, rect.bottom, rect.right);
|
||||
}
|
||||
|
||||
public void addCrop(Rect crop, boolean transposed) {
|
||||
if (transposed) {
|
||||
crop = transposeRect(crop);
|
||||
}
|
||||
|
||||
double inputWidth = size.getWidth();
|
||||
double inputHeight = size.getHeight();
|
||||
|
||||
if (crop.left < 0 || crop.top < 0 || crop.right > inputWidth || crop.bottom > inputHeight) {
|
||||
throw new IllegalArgumentException("Crop " + crop + " exceeds the input area (" + size + ")");
|
||||
}
|
||||
|
||||
double x = crop.left / inputWidth;
|
||||
double y = 1 - (crop.bottom / inputHeight); // OpenGL origin is bottom-left
|
||||
double w = crop.width() / inputWidth;
|
||||
double h = crop.height() / inputHeight;
|
||||
|
||||
transform = AffineMatrix.reframe(x, y, w, h).multiply(transform);
|
||||
size = new Size(crop.width(), crop.height());
|
||||
}
|
||||
|
||||
public void addRotation(int ccwRotation) {
|
||||
if (ccwRotation == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
transform = AffineMatrix.rotateOrtho(ccwRotation).multiply(transform);
|
||||
if (ccwRotation % 2 != 0) {
|
||||
size = size.rotate();
|
||||
}
|
||||
}
|
||||
|
||||
public void addOrientation(Orientation captureOrientation) {
|
||||
if (captureOrientation.isFlipped()) {
|
||||
transform = AffineMatrix.hflip().multiply(transform);
|
||||
}
|
||||
int ccwRotation = (4 - captureOrientation.getRotation()) % 4;
|
||||
addRotation(ccwRotation);
|
||||
}
|
||||
|
||||
public void addOrientation(int displayRotation, boolean locked, Orientation captureOrientation) {
|
||||
if (locked) {
|
||||
// flip/rotate the current display from the natural device orientation (i.e. where display rotation is 0)
|
||||
int reverseDisplayRotation = (4 - displayRotation) % 4;
|
||||
addRotation(reverseDisplayRotation);
|
||||
}
|
||||
addOrientation(captureOrientation);
|
||||
}
|
||||
|
||||
public void addAngle(double cwAngle) {
|
||||
if (cwAngle == 0) {
|
||||
return;
|
||||
}
|
||||
double ccwAngle = -cwAngle;
|
||||
transform = AffineMatrix.rotate(ccwAngle).withAspectRatio(size).fromCenter().multiply(transform);
|
||||
}
|
||||
|
||||
public void addResize(Size targetSize) {
|
||||
if (size.equals(targetSize)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (transform == null) {
|
||||
// The requested scaling is performed by the viewport (by changing the output size), but the OpenGL filter must still run, even if
|
||||
// resizing is not performed by the shader. So transform MUST NOT be null.
|
||||
transform = AffineMatrix.IDENTITY;
|
||||
}
|
||||
size = targetSize;
|
||||
}
|
||||
}
|
@ -192,6 +192,9 @@ public final class DisplayManager {
|
||||
if ("onDisplayChanged".equals(method.getName())) {
|
||||
listener.onDisplayChanged((int) args[0]);
|
||||
}
|
||||
if ("toString".equals(method.getName())) {
|
||||
return "DisplayListener";
|
||||
}
|
||||
return null;
|
||||
});
|
||||
try {
|
||||
|
@ -0,0 +1,39 @@
|
||||
package com.genymobile.scrcpy.wrappers;
|
||||
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Rect;
|
||||
import android.view.IDisplayWindowListener;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class DisplayWindowListener extends IDisplayWindowListener.Stub {
|
||||
@Override
|
||||
public void onDisplayAdded(int displayId) {
|
||||
// empty default implementation
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) {
|
||||
// empty default implementation
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisplayRemoved(int displayId) {
|
||||
// empty default implementation
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFixedRotationStarted(int displayId, int newRotation) {
|
||||
// empty default implementation
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFixedRotationFinished(int displayId) {
|
||||
// empty default implementation
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onKeepClearAreasChanged(int displayId, List<Rect> restricted, List<Rect> unrestricted) {
|
||||
// empty default implementation
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ import com.genymobile.scrcpy.util.Ln;
|
||||
import android.annotation.TargetApi;
|
||||
import android.os.IInterface;
|
||||
import android.view.IDisplayFoldListener;
|
||||
import android.view.IDisplayWindowListener;
|
||||
import android.view.IRotationWatcher;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
@ -226,4 +227,23 @@ public final class WindowManager {
|
||||
Ln.e("Could not unregister display fold listener", e);
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(AndroidVersions.API_30_ANDROID_11)
|
||||
public int[] registerDisplayWindowListener(IDisplayWindowListener listener) {
|
||||
try {
|
||||
return (int[]) manager.getClass().getMethod("registerDisplayWindowListener", IDisplayWindowListener.class).invoke(manager, listener);
|
||||
} catch (Exception e) {
|
||||
Ln.e("Could not register display window listener", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@TargetApi(AndroidVersions.API_30_ANDROID_11)
|
||||
public void unregisterDisplayWindowListener(IDisplayWindowListener listener) {
|
||||
try {
|
||||
manager.getClass().getMethod("unregisterDisplayWindowListener", IDisplayWindowListener.class).invoke(manager, listener);
|
||||
} catch (Exception e) {
|
||||
Ln.e("Could not unregister display window listener", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user