Compare commits
119 Commits
video_buff
...
gamepad_fi
Author | SHA1 | Date | |
---|---|---|---|
309d0371eb | |||
a582c0cc1b | |||
696295f66d | |||
347169aab6 | |||
75bdd0c831 | |||
baa10ed0a3 | |||
2ed2247e8f | |||
5febb1e9fb | |||
5c3626ed47 | |||
0e473eb005 | |||
b26b4fb745 | |||
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 | |||
bd9d93194b | |||
794595e3f0 | |||
5e10c37f02 | |||
0e399b65bd | |||
2337f524d1 | |||
df74cceb6f | |||
91373d906b | |||
04dd72b594 | |||
762816cac6 | |||
c0e2e27cf9 | |||
eff5b4b219 | |||
d3db9c4065 | |||
5936167ff7 | |||
e9dd0f68ad | |||
104195fc3b | |||
9958302e6f | |||
69b836930a | |||
790ea5e58c | |||
1270997f6b | |||
c905fbba8d | |||
f08a6d86c5 | |||
3ac4b64461 | |||
c7378f4dc8 | |||
e26bdb07a2 | |||
04a3e6fb06 | |||
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.2)
|
||||
|
||||
<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=
|
||||
@ -77,6 +78,7 @@ _scrcpy() {
|
||||
--rotation=
|
||||
-s --serial=
|
||||
-S --turn-screen-off
|
||||
--screen-off-timeout=
|
||||
--shortcut-mod=
|
||||
--start-app=
|
||||
-t --show-touches
|
||||
@ -137,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
|
||||
@ -145,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
|
||||
@ -193,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)'
|
||||
@ -80,6 +82,7 @@ arguments=(
|
||||
'--require-audio=[Make scrcpy fail if audio is enabled but does not work]'
|
||||
{-s,--serial=}'[The device serial number \(mandatory for multiple devices only\)]:serial:($("${ADB-adb}" devices | awk '\''$2 == "device" {print $1}'\''))'
|
||||
{-S,--turn-screen-off}'[Turn the device screen off immediately]'
|
||||
'--screen-off-timeout=[Set the screen off timeout in seconds]'
|
||||
'--shortcut-mod=[\[key1,key2+key3,...\] Specify the modifiers to use for scrcpy shortcuts]:shortcut mod:(lctrl rctrl lalt ralt lsuper rsuper)'
|
||||
'--start-app=[Start an Android app]'
|
||||
{-t,--show-touches}'[Show physical touches]'
|
||||
|
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'
|
||||
@ -167,9 +170,6 @@ conf.set('DEFAULT_LOCAL_PORT_RANGE_LAST', '27199')
|
||||
# run a server debugger and wait for a client to be attached
|
||||
conf.set('SERVER_DEBUGGER', get_option('server_debugger'))
|
||||
|
||||
# select the debugger method ('old' for Android < 9, 'new' for Android >= 9)
|
||||
conf.set('SERVER_DEBUGGER_METHOD_NEW', get_option('server_debugger_method') == 'new')
|
||||
|
||||
# enable V4L2 support (linux only)
|
||||
conf.set('HAVE_V4L2', v4l2_support)
|
||||
|
||||
|
@ -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.2"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
73
app/scrcpy.1
73
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.
|
||||
@ -511,13 +518,15 @@ Enable "show touches" on start, restore the initial value on exit.
|
||||
It only shows physical touches (not clicks from scrcpy).
|
||||
|
||||
.TP
|
||||
.BI "\-\-tcpip\fR[=\fIip\fR[:\fIport\fR]]
|
||||
Configure and reconnect the device over TCP/IP.
|
||||
.BI "\-\-tcpip\fR[=[+]\fIip\fR[:\fIport\fR]]
|
||||
Configure and connect the device over TCP/IP.
|
||||
|
||||
If a destination address is provided, then scrcpy connects to this address before starting. The device must listen on the given TCP port (default is 5555).
|
||||
|
||||
If no destination address is provided, then scrcpy attempts to find the IP address and adb port of the current device (typically connected over USB), enables TCP/IP mode if necessary, then connects to this address before starting.
|
||||
|
||||
Prefix the address with a '+' to force a reconnection.
|
||||
|
||||
.TP
|
||||
.BI "\-\-time\-limit " seconds
|
||||
Set the maximum mirroring time, in seconds.
|
||||
@ -548,8 +557,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.
|
||||
@ -671,6 +678,10 @@ Pause or re-pause display
|
||||
.B MOD+Shift+z
|
||||
Unpause display
|
||||
|
||||
.TP
|
||||
.B MOD+Shift+r
|
||||
Reset video capture/encoding
|
||||
|
||||
.TP
|
||||
.B MOD+g
|
||||
Resize window to 1:1 (pixel\-perfect)
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
@ -381,7 +412,7 @@ sc_adb_connect(struct sc_intr *intr, const char *ip_port, unsigned flags) {
|
||||
|
||||
// "adb connect" always returns successfully (with exit code 0), even in
|
||||
// case of failure. As a workaround, check if its output starts with
|
||||
// "connected".
|
||||
// "connected" or "already connected".
|
||||
char buf[128];
|
||||
ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf) - 1);
|
||||
sc_pipe_close(pout);
|
||||
@ -398,7 +429,8 @@ sc_adb_connect(struct sc_intr *intr, const char *ip_port, unsigned flags) {
|
||||
assert((size_t) r < sizeof(buf));
|
||||
buf[r] = '\0';
|
||||
|
||||
ok = !strncmp("connected", buf, sizeof("connected") - 1);
|
||||
ok = !strncmp("connected", buf, sizeof("connected") - 1)
|
||||
|| !strncmp("already connected", buf, sizeof("already connected") - 1);
|
||||
if (!ok && !(flags & SC_ADB_NO_STDERR)) {
|
||||
// "adb connect" also prints errors to stdout. Since we capture it,
|
||||
// re-print the error to stderr.
|
||||
@ -739,3 +771,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
|
||||
|
@ -288,7 +288,7 @@ sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) {
|
||||
|
||||
// Enable compensation when the difference exceeds +/- 4ms.
|
||||
// Disable compensation when the difference is lower than +/- 1ms.
|
||||
int threshold = ar->compensation != 0
|
||||
int threshold = ar->compensation_active
|
||||
? ar->sample_rate / 1000 /* 1ms */
|
||||
: ar->sample_rate * 4 / 1000; /* 4ms */
|
||||
|
||||
@ -309,14 +309,12 @@ sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) {
|
||||
LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32
|
||||
" compensation=%d", ar->target_buffering, avg, can_read, diff);
|
||||
|
||||
if (diff != ar->compensation) {
|
||||
int ret = swr_set_compensation(swr_ctx, diff, distance);
|
||||
if (ret < 0) {
|
||||
LOGW("Resampling compensation failed: %d", ret);
|
||||
// not fatal
|
||||
} else {
|
||||
ar->compensation = diff;
|
||||
}
|
||||
int ret = swr_set_compensation(swr_ctx, diff, distance);
|
||||
if (ret < 0) {
|
||||
LOGW("Resampling compensation failed: %d", ret);
|
||||
// not fatal
|
||||
} else {
|
||||
ar->compensation_active = diff != 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -392,7 +390,7 @@ sc_audio_regulator_init(struct sc_audio_regulator *ar, size_t sample_size,
|
||||
atomic_init(&ar->played, false);
|
||||
atomic_init(&ar->received, false);
|
||||
atomic_init(&ar->underflow, 0);
|
||||
ar->compensation = 0;
|
||||
ar->compensation_active = false;
|
||||
|
||||
return true;
|
||||
|
||||
|
@ -44,8 +44,8 @@ struct sc_audio_regulator {
|
||||
// Number of silence samples inserted since the last received packet
|
||||
atomic_uint_least32_t underflow;
|
||||
|
||||
// Current applied compensation value (only used by the receiver thread)
|
||||
int compensation;
|
||||
// Non-zero compensation applied (only used by the receiver thread)
|
||||
bool compensation_active;
|
||||
|
||||
// Set to true the first time a sample is received
|
||||
atomic_bool received;
|
||||
|
303
app/src/cli.c
303
app/src/cli.c
@ -106,6 +106,10 @@ enum {
|
||||
OPT_NEW_DISPLAY,
|
||||
OPT_LIST_APPS,
|
||||
OPT_START_APP,
|
||||
OPT_SCREEN_OFF_TIMEOUT,
|
||||
OPT_CAPTURE_ORIENTATION,
|
||||
OPT_ANGLE,
|
||||
OPT_NO_VD_SYSTEM_DECORATIONS,
|
||||
};
|
||||
|
||||
struct sc_option {
|
||||
@ -147,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",
|
||||
@ -244,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",
|
||||
@ -259,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",
|
||||
@ -266,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",
|
||||
@ -273,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
|
||||
@ -301,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',
|
||||
@ -470,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',
|
||||
@ -571,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",
|
||||
},
|
||||
{
|
||||
@ -641,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",
|
||||
@ -793,6 +816,13 @@ static const struct sc_option options[] = {
|
||||
.longopt = "turn-screen-off",
|
||||
.text = "Turn the device screen off immediately.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_SCREEN_OFF_TIMEOUT,
|
||||
.longopt = "screen-off-timeout",
|
||||
.argdesc = "seconds",
|
||||
.text = "Set the screen off timeout while scrcpy is running (restore "
|
||||
"the initial value on exit).",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_SHORTCUT_MOD,
|
||||
.longopt = "shortcut-mod",
|
||||
@ -830,16 +860,17 @@ static const struct sc_option options[] = {
|
||||
{
|
||||
.longopt_id = OPT_TCPIP,
|
||||
.longopt = "tcpip",
|
||||
.argdesc = "ip[:port]",
|
||||
.argdesc = "[+]ip[:port]",
|
||||
.optional_arg = true,
|
||||
.text = "Configure and reconnect the device over TCP/IP.\n"
|
||||
.text = "Configure and connect the device over TCP/IP.\n"
|
||||
"If a destination address is provided, then scrcpy connects to "
|
||||
"this address before starting. The device must listen on the "
|
||||
"given TCP port (default is 5555).\n"
|
||||
"If no destination address is provided, then scrcpy attempts "
|
||||
"to find the IP address of the current device (typically "
|
||||
"connected over USB), enables TCP/IP mode, then connects to "
|
||||
"this address before starting.",
|
||||
"this address before starting.\n"
|
||||
"Prefix the address with a '+' to force a reconnection.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_TIME_LIMIT,
|
||||
@ -887,8 +918,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.",
|
||||
},
|
||||
{
|
||||
@ -1022,6 +1051,10 @@ static const struct sc_shortcut shortcuts[] = {
|
||||
.shortcuts = { "MOD+Shift+z" },
|
||||
.text = "Unpause display",
|
||||
},
|
||||
{
|
||||
.shortcuts = { "MOD+Shift+r" },
|
||||
.text = "Reset video capture/encoding",
|
||||
},
|
||||
{
|
||||
.shortcuts = { "MOD+g" },
|
||||
.text = "Resize window to 1:1 (pixel-perfect)",
|
||||
@ -1570,78 +1603,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")) {
|
||||
@ -1681,6 +1642,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"
|
||||
@ -2151,6 +2138,20 @@ parse_time_limit(const char *s, sc_tick *tick) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
parse_screen_off_timeout(const char *s, sc_tick *tick) {
|
||||
long value;
|
||||
// value in seconds, but must fit in 31 bits in milliseconds
|
||||
bool ok = parse_integer_arg(s, &value, false, 0, 0x7FFFFFFF / 1000,
|
||||
"screen off timeout");
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*tick = SC_TICK_FROM_SEC(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
parse_pause_on_exit(const char *s, enum sc_pause_on_exit *pause_on_exit) {
|
||||
if (!s || !strcmp(s, "true")) {
|
||||
@ -2276,8 +2277,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;
|
||||
@ -2341,8 +2342,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;
|
||||
@ -2360,8 +2366,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;
|
||||
@ -2447,32 +2454,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;
|
||||
@ -2533,23 +2517,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;
|
||||
@ -2557,9 +2527,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;
|
||||
@ -2726,6 +2696,18 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
case OPT_START_APP:
|
||||
opts->start_app = optarg;
|
||||
break;
|
||||
case OPT_SCREEN_OFF_TIMEOUT:
|
||||
if (!parse_screen_off_timeout(optarg,
|
||||
&opts->screen_off_timeout)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case OPT_ANGLE:
|
||||
opts->angle = optarg;
|
||||
break;
|
||||
case OPT_NO_VD_SYSTEM_DECORATIONS:
|
||||
opts->vd_system_decorations = optarg;
|
||||
break;
|
||||
default:
|
||||
// getopt prints the error message on stderr
|
||||
return false;
|
||||
@ -2820,14 +2802,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.
|
||||
@ -2917,13 +2891,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) {
|
||||
|
@ -152,8 +152,10 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) {
|
||||
return 2;
|
||||
case SC_CONTROL_MSG_TYPE_UHID_CREATE:
|
||||
sc_write16be(&buf[1], msg->uhid_create.id);
|
||||
sc_write16be(&buf[3], msg->uhid_create.vendor_id);
|
||||
sc_write16be(&buf[5], msg->uhid_create.product_id);
|
||||
|
||||
size_t index = 3;
|
||||
size_t index = 7;
|
||||
index += write_string_tiny(&buf[index], msg->uhid_create.name, 127);
|
||||
|
||||
sc_write16be(&buf[index], msg->uhid_create.report_desc_size);
|
||||
@ -181,6 +183,7 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) {
|
||||
case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS:
|
||||
case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE:
|
||||
case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
|
||||
case SC_CONTROL_MSG_TYPE_RESET_VIDEO:
|
||||
// no additional data
|
||||
return 1;
|
||||
default:
|
||||
@ -277,9 +280,13 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
|
||||
// Quote only if name is not null
|
||||
const char *name = msg->uhid_create.name;
|
||||
const char *quote = name ? "\"" : "";
|
||||
LOG_CMSG("UHID create [%" PRIu16 "] name=%s%s%s "
|
||||
"report_desc_size=%" PRIu16, msg->uhid_create.id,
|
||||
quote, name, quote, msg->uhid_create.report_desc_size);
|
||||
LOG_CMSG("UHID create [%" PRIu16 "] %04" PRIx16 ":%04" PRIx16
|
||||
" name=%s%s%s report_desc_size=%" PRIu16,
|
||||
msg->uhid_create.id,
|
||||
msg->uhid_create.vendor_id,
|
||||
msg->uhid_create.product_id,
|
||||
quote, name, quote,
|
||||
msg->uhid_create.report_desc_size);
|
||||
break;
|
||||
}
|
||||
case SC_CONTROL_MSG_TYPE_UHID_INPUT: {
|
||||
@ -304,6 +311,9 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
|
||||
case SC_CONTROL_MSG_TYPE_START_APP:
|
||||
LOG_CMSG("start app \"%s\"", msg->start_app.name);
|
||||
break;
|
||||
case SC_CONTROL_MSG_TYPE_RESET_VIDEO:
|
||||
LOG_CMSG("reset video");
|
||||
break;
|
||||
default:
|
||||
LOG_CMSG("unknown type: %u", (unsigned) msg->type);
|
||||
break;
|
||||
|
@ -42,6 +42,7 @@ enum sc_control_msg_type {
|
||||
SC_CONTROL_MSG_TYPE_UHID_DESTROY,
|
||||
SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS,
|
||||
SC_CONTROL_MSG_TYPE_START_APP,
|
||||
SC_CONTROL_MSG_TYPE_RESET_VIDEO,
|
||||
};
|
||||
|
||||
enum sc_copy_key {
|
||||
@ -93,6 +94,8 @@ struct sc_control_msg {
|
||||
} set_display_power;
|
||||
struct {
|
||||
uint16_t id;
|
||||
uint16_t vendor_id;
|
||||
uint16_t product_id;
|
||||
const char *name; // pointer to static data
|
||||
uint16_t report_desc_size;
|
||||
const uint8_t *report_desc; // pointer to static data
|
||||
|
@ -15,6 +15,8 @@ struct sc_hid_input {
|
||||
|
||||
struct sc_hid_open {
|
||||
uint16_t hid_id;
|
||||
uint16_t vendor_id;
|
||||
uint16_t product_id;
|
||||
const char *name; // pointer to static memory
|
||||
const uint8_t *report_desc; // pointer to static memory
|
||||
size_t report_desc_size;
|
||||
|
@ -52,10 +52,10 @@ static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = {
|
||||
0x09, 0x30,
|
||||
// Usage (Y) Left stick y
|
||||
0x09, 0x31,
|
||||
// Usage (Z) Right stick x
|
||||
0x09, 0x32,
|
||||
// Usage (Rz) Right stick y
|
||||
0x09, 0x35,
|
||||
// Usage (Rx) Right stick x
|
||||
0x09, 0x33,
|
||||
// Usage (Ry) Right stick y
|
||||
0x09, 0x34,
|
||||
// Logical Minimum (0)
|
||||
0x15, 0x00,
|
||||
// Logical Maximum (65535)
|
||||
@ -65,15 +65,15 @@ static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = {
|
||||
0x75, 0x10,
|
||||
// Report Count (4)
|
||||
0x95, 0x04,
|
||||
// Input (Data, Variable, Absolute): 4 bytes (X, Y, Z, Rz)
|
||||
// Input (Data, Variable, Absolute): 4x2 bytes (X, Y, Z, Rz)
|
||||
0x81, 0x02,
|
||||
|
||||
// Usage Page (Simulation Controls)
|
||||
0x05, 0x02,
|
||||
// Usage (Brake)
|
||||
0x09, 0xC5,
|
||||
// Usage (Accelerator)
|
||||
0x09, 0xC4,
|
||||
// Usage Page (Generic Desktop)
|
||||
0x05, 0x01,
|
||||
// Usage (Z)
|
||||
0x09, 0x32,
|
||||
// Usage (Rz)
|
||||
0x09, 0x35,
|
||||
// Logical Minimum (0)
|
||||
0x15, 0x00,
|
||||
// Logical Maximum (32767)
|
||||
@ -82,7 +82,7 @@ static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = {
|
||||
0x75, 0x10,
|
||||
// Report Count (2)
|
||||
0x95, 0x02,
|
||||
// Input (Data, Variable, Absolute): 2 bytes (L2, R2)
|
||||
// Input (Data, Variable, Absolute): 2x2 bytes (L2, R2)
|
||||
0x81, 0x02,
|
||||
|
||||
// Usage Page (Buttons)
|
||||
@ -182,7 +182,7 @@ static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = {
|
||||
* `------------- SC_GAMEPAD_BUTTON_RIGHT_STICK
|
||||
*
|
||||
* +---------------+
|
||||
* byte 14: |0 0 0 . . . . .| hat switch (dpad) position (0-8)
|
||||
* byte 14: |0 0 0 0 . . . .| hat switch (dpad) position (0-8)
|
||||
* +---------------+
|
||||
* 9 possible positions and their values:
|
||||
* 8 1 2
|
||||
@ -191,16 +191,19 @@ static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = {
|
||||
* (8 is top-left, 1 is top, 2 is top-right, etc.)
|
||||
*/
|
||||
|
||||
// [-32768 to 32767] -> [0 to 65535]
|
||||
#define AXIS_RESCALE(V) (uint16_t) (((int32_t) V) + 0x8000)
|
||||
|
||||
static void
|
||||
sc_hid_gamepad_slot_init(struct sc_hid_gamepad_slot *slot,
|
||||
uint32_t gamepad_id) {
|
||||
assert(gamepad_id != SC_GAMEPAD_ID_INVALID);
|
||||
slot->gamepad_id = gamepad_id;
|
||||
slot->buttons = 0;
|
||||
slot->axis_left_x = 0;
|
||||
slot->axis_left_y = 0;
|
||||
slot->axis_right_x = 0;
|
||||
slot->axis_right_y = 0;
|
||||
slot->axis_left_x = AXIS_RESCALE(0);
|
||||
slot->axis_left_y = AXIS_RESCALE(0);
|
||||
slot->axis_right_x = AXIS_RESCALE(0);
|
||||
slot->axis_right_y = AXIS_RESCALE(0);
|
||||
slot->axis_left_trigger = 0;
|
||||
slot->axis_right_trigger = 0;
|
||||
}
|
||||
@ -233,7 +236,9 @@ sc_hid_gamepad_slot_get_id(size_t slot_idx) {
|
||||
bool
|
||||
sc_hid_gamepad_generate_open(struct sc_hid_gamepad *hid,
|
||||
struct sc_hid_open *hid_open,
|
||||
uint32_t gamepad_id) {
|
||||
uint32_t gamepad_id,
|
||||
uint16_t vendor_id,
|
||||
uint16_t product_id) {
|
||||
assert(gamepad_id != SC_GAMEPAD_ID_INVALID);
|
||||
ssize_t slot_idx = sc_hid_gamepad_slot_find(hid, SC_GAMEPAD_ID_INVALID);
|
||||
if (slot_idx == -1) {
|
||||
@ -250,6 +255,8 @@ sc_hid_gamepad_generate_open(struct sc_hid_gamepad *hid,
|
||||
|
||||
uint16_t hid_id = sc_hid_gamepad_slot_get_id(slot_idx);
|
||||
hid_open->hid_id = hid_id;
|
||||
hid_open->vendor_id = vendor_id;
|
||||
hid_open->product_id = product_id;
|
||||
hid_open->name = name;
|
||||
hid_open->report_desc = SC_HID_GAMEPAD_REPORT_DESC;
|
||||
hid_open->report_desc_size = sizeof(SC_HID_GAMEPAD_REPORT_DESC);
|
||||
@ -423,8 +430,6 @@ sc_hid_gamepad_generate_input_from_axis(struct sc_hid_gamepad *hid,
|
||||
|
||||
struct sc_hid_gamepad_slot *slot = &hid->slots[slot_idx];
|
||||
|
||||
// [-32768 to 32767] -> [0 to 65535]
|
||||
#define AXIS_RESCALE(V) (uint16_t) (((int32_t) V) + 0x8000)
|
||||
switch (event->axis) {
|
||||
case SC_GAMEPAD_AXIS_LEFTX:
|
||||
slot->axis_left_x = AXIS_RESCALE(event->value);
|
||||
|
@ -33,7 +33,9 @@ sc_hid_gamepad_init(struct sc_hid_gamepad *hid);
|
||||
bool
|
||||
sc_hid_gamepad_generate_open(struct sc_hid_gamepad *hid,
|
||||
struct sc_hid_open *hid_open,
|
||||
uint32_t gamepad_id);
|
||||
uint32_t gamepad_id,
|
||||
uint16_t vendor_id,
|
||||
uint16_t product_id);
|
||||
|
||||
bool
|
||||
sc_hid_gamepad_generate_close(struct sc_hid_gamepad *hid,
|
||||
|
@ -335,6 +335,8 @@ sc_hid_keyboard_generate_input_from_mods(struct sc_hid_input *hid_input,
|
||||
|
||||
void sc_hid_keyboard_generate_open(struct sc_hid_open *hid_open) {
|
||||
hid_open->hid_id = SC_HID_ID_KEYBOARD;
|
||||
hid_open->vendor_id = 0;
|
||||
hid_open->product_id = 0;
|
||||
hid_open->name = NULL; // No name specified after "scrcpy"
|
||||
hid_open->report_desc = SC_HID_KEYBOARD_REPORT_DESC;
|
||||
hid_open->report_desc_size = sizeof(SC_HID_KEYBOARD_REPORT_DESC);
|
||||
|
@ -190,6 +190,8 @@ sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input,
|
||||
|
||||
void sc_hid_mouse_generate_open(struct sc_hid_open *hid_open) {
|
||||
hid_open->hid_id = SC_HID_ID_MOUSE;
|
||||
hid_open->vendor_id = 0;
|
||||
hid_open->product_id = 0;
|
||||
hid_open->name = NULL; // No name specified after "scrcpy"
|
||||
hid_open->report_desc = SC_HID_MOUSE_REPORT_DESC;
|
||||
hid_open->report_desc_size = sizeof(SC_HID_MOUSE_REPORT_DESC);
|
||||
|
@ -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;
|
||||
|
@ -412,18 +412,16 @@ struct sc_touch_event {
|
||||
float pressure;
|
||||
};
|
||||
|
||||
enum sc_gamepad_device_event_type {
|
||||
SC_GAMEPAD_DEVICE_ADDED,
|
||||
SC_GAMEPAD_DEVICE_REMOVED,
|
||||
};
|
||||
|
||||
// As documented in <https://wiki.libsdl.org/SDL2/SDL_JoystickID>:
|
||||
// The ID value starts at 0 and increments from there. The value -1 is an
|
||||
// invalid ID.
|
||||
#define SC_GAMEPAD_ID_INVALID UINT32_C(-1)
|
||||
|
||||
struct sc_gamepad_device_event {
|
||||
enum sc_gamepad_device_event_type type;
|
||||
struct sc_gamepad_added_event {
|
||||
uint32_t gamepad_id;
|
||||
};
|
||||
|
||||
struct sc_gamepad_removed_event {
|
||||
uint32_t gamepad_id;
|
||||
};
|
||||
|
||||
@ -503,16 +501,6 @@ sc_mouse_buttons_state_from_sdl(uint32_t buttons_state) {
|
||||
return buttons_state;
|
||||
}
|
||||
|
||||
static inline enum sc_gamepad_device_event_type
|
||||
sc_gamepad_device_event_type_from_sdl_type(uint32_t type) {
|
||||
assert(type == SDL_CONTROLLERDEVICEADDED
|
||||
|| type == SDL_CONTROLLERDEVICEREMOVED);
|
||||
if (type == SDL_CONTROLLERDEVICEADDED) {
|
||||
return SC_GAMEPAD_DEVICE_ADDED;
|
||||
}
|
||||
return SC_GAMEPAD_DEVICE_REMOVED;
|
||||
}
|
||||
|
||||
static inline enum sc_gamepad_axis
|
||||
sc_gamepad_axis_from_sdl(uint8_t axis) {
|
||||
if (axis <= SDL_CONTROLLER_AXIS_TRIGGERRIGHT) {
|
||||
|
@ -284,6 +284,18 @@ open_hard_keyboard_settings(struct sc_input_manager *im) {
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
reset_video(struct sc_input_manager *im) {
|
||||
assert(im->controller);
|
||||
|
||||
struct sc_control_msg msg;
|
||||
msg.type = SC_CONTROL_MSG_TYPE_RESET_VIDEO;
|
||||
|
||||
if (!sc_controller_push_msg(im->controller, &msg)) {
|
||||
LOGW("Could not request reset video");
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
apply_orientation_transform(struct sc_input_manager *im,
|
||||
enum sc_orientation transform) {
|
||||
@ -521,8 +533,12 @@ sc_input_manager_process_key(struct sc_input_manager *im,
|
||||
}
|
||||
return;
|
||||
case SDLK_r:
|
||||
if (control && !shift && !repeat && down && !paused) {
|
||||
rotate_device(im);
|
||||
if (control && !repeat && down && !paused) {
|
||||
if (shift) {
|
||||
reset_video(im);
|
||||
} else {
|
||||
rotate_device(im);
|
||||
}
|
||||
}
|
||||
return;
|
||||
case SDLK_k:
|
||||
@ -892,7 +908,6 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im,
|
||||
static void
|
||||
sc_input_manager_process_gamepad_device(struct sc_input_manager *im,
|
||||
const SDL_ControllerDeviceEvent *event) {
|
||||
SDL_JoystickID id;
|
||||
if (event->type == SDL_CONTROLLERDEVICEADDED) {
|
||||
SDL_GameController *gc = SDL_GameControllerOpen(event->which);
|
||||
if (!gc) {
|
||||
@ -907,9 +922,12 @@ sc_input_manager_process_gamepad_device(struct sc_input_manager *im,
|
||||
return;
|
||||
}
|
||||
|
||||
id = SDL_JoystickInstanceID(joystick);
|
||||
struct sc_gamepad_added_event evt = {
|
||||
.gamepad_id = SDL_JoystickInstanceID(joystick),
|
||||
};
|
||||
im->gp->ops->process_gamepad_added(im->gp, &evt);
|
||||
} else if (event->type == SDL_CONTROLLERDEVICEREMOVED) {
|
||||
id = event->which;
|
||||
SDL_JoystickID id = event->which;
|
||||
|
||||
SDL_GameController *gc = SDL_GameControllerFromInstanceID(id);
|
||||
if (gc) {
|
||||
@ -917,16 +935,15 @@ sc_input_manager_process_gamepad_device(struct sc_input_manager *im,
|
||||
} else {
|
||||
LOGW("Unknown gamepad device removed");
|
||||
}
|
||||
|
||||
struct sc_gamepad_removed_event evt = {
|
||||
.gamepad_id = id,
|
||||
};
|
||||
im->gp->ops->process_gamepad_removed(im->gp, &evt);
|
||||
} else {
|
||||
// Nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
struct sc_gamepad_device_event evt = {
|
||||
.type = sc_gamepad_device_event_type_from_sdl_type(event->type),
|
||||
.gamepad_id = id,
|
||||
};
|
||||
im->gp->ops->process_gamepad_device(im->gp, &evt);
|
||||
}
|
||||
|
||||
static void
|
||||
|
@ -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,
|
||||
@ -62,6 +63,7 @@ const struct scrcpy_options scrcpy_options_default = {
|
||||
.audio_buffer = -1, // depends on the audio format,
|
||||
.audio_output_buffer = SC_TICK_FROM_MS(5),
|
||||
.time_limit = 0,
|
||||
.screen_off_timeout = -1,
|
||||
#ifdef HAVE_V4L2
|
||||
.v4l2_device = NULL,
|
||||
.v4l2_buffer = 0,
|
||||
@ -105,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"
|
||||
@ -265,6 +261,7 @@ struct scrcpy_options {
|
||||
sc_tick audio_buffer;
|
||||
sc_tick audio_output_buffer;
|
||||
sc_tick time_limit;
|
||||
sc_tick screen_off_timeout;
|
||||
#ifdef HAVE_V4L2
|
||||
const char *v4l2_device;
|
||||
sc_tick v4l2_buffer;
|
||||
@ -313,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,7 +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,
|
||||
.lock_video_orientation = options->lock_video_orientation,
|
||||
.angle = options->angle,
|
||||
.screen_off_timeout = options->screen_off_timeout,
|
||||
.capture_orientation = options->capture_orientation,
|
||||
.capture_orientation_lock = options->capture_orientation_lock,
|
||||
.control = options->control,
|
||||
.display_id = options->display_id,
|
||||
.new_display = options->new_display,
|
||||
@ -455,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,
|
||||
};
|
||||
|
||||
|
128
app/src/server.c
128
app/src/server.c
@ -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");
|
||||
@ -201,18 +189,31 @@ execute_server(struct sc_server *server,
|
||||
cmd[count++] = "app_process";
|
||||
|
||||
#ifdef SERVER_DEBUGGER
|
||||
uint16_t sdk_version = sc_adb_get_device_sdk_version(&server->intr, serial);
|
||||
if (!sdk_version) {
|
||||
LOGE("Could not determine SDK version");
|
||||
return 0;
|
||||
}
|
||||
|
||||
# define SERVER_DEBUGGER_PORT "5005"
|
||||
cmd[count++] =
|
||||
# ifdef SERVER_DEBUGGER_METHOD_NEW
|
||||
/* Android 9 and above */
|
||||
"-XjdwpProvider:internal -XjdwpOptions:transport=dt_socket,suspend=y,"
|
||||
"server=y,address="
|
||||
# else
|
||||
/* Android 8 and below */
|
||||
"-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address="
|
||||
# endif
|
||||
SERVER_DEBUGGER_PORT;
|
||||
const char *dbg;
|
||||
if (sdk_version < 28) {
|
||||
// Android < 9
|
||||
dbg = "-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address="
|
||||
SERVER_DEBUGGER_PORT;
|
||||
} else if (sdk_version < 30) {
|
||||
// Android >= 9 && Android < 11
|
||||
dbg = "-XjdwpProvider:internal -XjdwpOptions:transport=dt_socket,"
|
||||
"suspend=y,server=y,address=" SERVER_DEBUGGER_PORT;
|
||||
} else {
|
||||
// Android >= 11
|
||||
// Contrary to the other methods, this does not suspend on start.
|
||||
// <https://github.com/Genymobile/scrcpy/pull/5466>
|
||||
dbg = "-XjdwpProvider:adbconnection";
|
||||
}
|
||||
cmd[count++] = dbg;
|
||||
#endif
|
||||
|
||||
cmd[count++] = "/"; // unused
|
||||
cmd[count++] = "com.genymobile.scrcpy.Server";
|
||||
cmd[count++] = SCRCPY_VERSION;
|
||||
@ -274,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");
|
||||
@ -320,6 +333,11 @@ execute_server(struct sc_server *server,
|
||||
if (params->stay_awake) {
|
||||
ADD_PARAM("stay_awake=true");
|
||||
}
|
||||
if (params->screen_off_timeout != -1) {
|
||||
assert(params->screen_off_timeout >= 0);
|
||||
uint64_t ms = SC_TICK_TO_MS(params->screen_off_timeout);
|
||||
ADD_PARAM("screen_off_timeout=%" PRIu64, ms);
|
||||
}
|
||||
if (params->video_codec_options) {
|
||||
VALIDATE_STRING(params->video_codec_options);
|
||||
ADD_PARAM("video_codec_options=%s", params->video_codec_options);
|
||||
@ -359,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");
|
||||
}
|
||||
@ -380,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
|
||||
@ -460,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;
|
||||
}
|
||||
|
||||
@ -475,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;
|
||||
}
|
||||
|
||||
@ -796,11 +829,14 @@ sc_server_switch_to_tcpip(struct sc_server *server, const char *serial) {
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_server_connect_to_tcpip(struct sc_server *server, const char *ip_port) {
|
||||
sc_server_connect_to_tcpip(struct sc_server *server, const char *ip_port,
|
||||
bool disconnect) {
|
||||
struct sc_intr *intr = &server->intr;
|
||||
|
||||
// Error expected if not connected, do not report any error
|
||||
sc_adb_disconnect(intr, ip_port, SC_ADB_SILENT);
|
||||
if (disconnect) {
|
||||
// Error expected if not connected, do not report any error
|
||||
sc_adb_disconnect(intr, ip_port, SC_ADB_SILENT);
|
||||
}
|
||||
|
||||
LOGI("Connecting to %s...", ip_port);
|
||||
|
||||
@ -816,7 +852,7 @@ sc_server_connect_to_tcpip(struct sc_server *server, const char *ip_port) {
|
||||
|
||||
static bool
|
||||
sc_server_configure_tcpip_known_address(struct sc_server *server,
|
||||
const char *addr) {
|
||||
const char *addr, bool disconnect) {
|
||||
// Append ":5555" if no port is present
|
||||
bool contains_port = strchr(addr, ':');
|
||||
char *ip_port = contains_port ? strdup(addr)
|
||||
@ -827,7 +863,7 @@ sc_server_configure_tcpip_known_address(struct sc_server *server,
|
||||
}
|
||||
|
||||
server->serial = ip_port;
|
||||
return sc_server_connect_to_tcpip(server, ip_port);
|
||||
return sc_server_connect_to_tcpip(server, ip_port, disconnect);
|
||||
}
|
||||
|
||||
static bool
|
||||
@ -852,7 +888,7 @@ sc_server_configure_tcpip_unknown_address(struct sc_server *server,
|
||||
}
|
||||
|
||||
server->serial = ip_port;
|
||||
return sc_server_connect_to_tcpip(server, ip_port);
|
||||
return sc_server_connect_to_tcpip(server, ip_port, false);
|
||||
}
|
||||
|
||||
static void
|
||||
@ -939,7 +975,13 @@ run_server(void *data) {
|
||||
sc_adb_device_destroy(&device);
|
||||
}
|
||||
} else {
|
||||
ok = sc_server_configure_tcpip_known_address(server, params->tcpip_dst);
|
||||
// If the user passed a '+' (--tcpip=+ip), then disconnect first
|
||||
const char *tcpip_dst = params->tcpip_dst;
|
||||
bool plus = tcpip_dst[0] == '+';
|
||||
if (plus) {
|
||||
++tcpip_dst;
|
||||
}
|
||||
ok = sc_server_configure_tcpip_known_address(server, tcpip_dst, plus);
|
||||
if (!ok) {
|
||||
goto error_connection_failed;
|
||||
}
|
||||
@ -1116,4 +1158,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,7 +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
|
||||
int8_t lock_video_orientation;
|
||||
const char *angle; // float to be parsed by the server
|
||||
sc_tick screen_off_timeout;
|
||||
enum sc_orientation capture_orientation;
|
||||
enum sc_orientation_lock capture_orientation_lock;
|
||||
bool control;
|
||||
uint32_t display_id;
|
||||
const char *new_display;
|
||||
@ -66,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
|
||||
}
|
||||
|
||||
|
@ -20,13 +20,22 @@ struct sc_gamepad_processor {
|
||||
struct sc_gamepad_processor_ops {
|
||||
|
||||
/**
|
||||
* Process a gamepad device added or removed
|
||||
* Process a gamepad device added
|
||||
*
|
||||
* This function is mandatory.
|
||||
*/
|
||||
void
|
||||
(*process_gamepad_device)(struct sc_gamepad_processor *gp,
|
||||
const struct sc_gamepad_device_event *event);
|
||||
(*process_gamepad_added)(struct sc_gamepad_processor *gp,
|
||||
const struct sc_gamepad_added_event *event);
|
||||
|
||||
/**
|
||||
* Process a gamepad device removed
|
||||
*
|
||||
* This function is mandatory.
|
||||
*/
|
||||
void
|
||||
(*process_gamepad_removed)(struct sc_gamepad_processor *gp,
|
||||
const struct sc_gamepad_removed_event *event);
|
||||
|
||||
/**
|
||||
* Process a gamepad axis event
|
||||
|
@ -7,6 +7,9 @@
|
||||
/** Downcast gamepad processor to sc_gamepad_uhid */
|
||||
#define DOWNCAST(GP) container_of(GP, struct sc_gamepad_uhid, gamepad_processor)
|
||||
|
||||
#define SC_GAMEPAD_UHID_VENDOR_ID 0
|
||||
#define SC_GAMEPAD_UHID_PRODUCT_ID 0
|
||||
|
||||
static void
|
||||
sc_gamepad_uhid_send_input(struct sc_gamepad_uhid *gamepad,
|
||||
const struct sc_hid_input *hid_input,
|
||||
@ -30,6 +33,8 @@ sc_gamepad_uhid_send_open(struct sc_gamepad_uhid *gamepad,
|
||||
struct sc_control_msg msg;
|
||||
msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE;
|
||||
msg.uhid_create.id = hid_open->hid_id;
|
||||
msg.uhid_create.vendor_id = hid_open->vendor_id;
|
||||
msg.uhid_create.product_id = hid_open->product_id;
|
||||
msg.uhid_create.name = hid_open->name;
|
||||
msg.uhid_create.report_desc = hid_open->report_desc;
|
||||
msg.uhid_create.report_desc_size = hid_open->report_desc_size;
|
||||
@ -52,29 +57,33 @@ sc_gamepad_uhid_send_close(struct sc_gamepad_uhid *gamepad,
|
||||
}
|
||||
|
||||
static void
|
||||
sc_gamepad_processor_process_gamepad_device(struct sc_gamepad_processor *gp,
|
||||
const struct sc_gamepad_device_event *event) {
|
||||
sc_gamepad_processor_process_gamepad_added(struct sc_gamepad_processor *gp,
|
||||
const struct sc_gamepad_added_event *event) {
|
||||
struct sc_gamepad_uhid *gamepad = DOWNCAST(gp);
|
||||
|
||||
if (event->type == SC_GAMEPAD_DEVICE_ADDED) {
|
||||
struct sc_hid_open hid_open;
|
||||
if (!sc_hid_gamepad_generate_open(&gamepad->hid, &hid_open,
|
||||
event->gamepad_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
sc_gamepad_uhid_send_open(gamepad, &hid_open);
|
||||
} else {
|
||||
assert(event->type == SC_GAMEPAD_DEVICE_REMOVED);
|
||||
|
||||
struct sc_hid_close hid_close;
|
||||
if (!sc_hid_gamepad_generate_close(&gamepad->hid, &hid_close,
|
||||
event->gamepad_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
sc_gamepad_uhid_send_close(gamepad, &hid_close);
|
||||
struct sc_hid_open hid_open;
|
||||
if (!sc_hid_gamepad_generate_open(&gamepad->hid, &hid_open,
|
||||
event->gamepad_id,
|
||||
SC_GAMEPAD_UHID_VENDOR_ID,
|
||||
SC_GAMEPAD_UHID_PRODUCT_ID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
sc_gamepad_uhid_send_open(gamepad, &hid_open);
|
||||
}
|
||||
|
||||
static void
|
||||
sc_gamepad_processor_process_gamepad_removed(struct sc_gamepad_processor *gp,
|
||||
const struct sc_gamepad_removed_event *event) {
|
||||
struct sc_gamepad_uhid *gamepad = DOWNCAST(gp);
|
||||
|
||||
struct sc_hid_close hid_close;
|
||||
if (!sc_hid_gamepad_generate_close(&gamepad->hid, &hid_close,
|
||||
event->gamepad_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
sc_gamepad_uhid_send_close(gamepad, &hid_close);
|
||||
}
|
||||
|
||||
static void
|
||||
@ -114,7 +123,8 @@ sc_gamepad_uhid_init(struct sc_gamepad_uhid *gamepad,
|
||||
gamepad->controller = controller;
|
||||
|
||||
static const struct sc_gamepad_processor_ops ops = {
|
||||
.process_gamepad_device = sc_gamepad_processor_process_gamepad_device,
|
||||
.process_gamepad_added = sc_gamepad_processor_process_gamepad_added,
|
||||
.process_gamepad_removed = sc_gamepad_processor_process_gamepad_removed,
|
||||
.process_gamepad_axis = sc_gamepad_processor_process_gamepad_axis,
|
||||
.process_gamepad_button = sc_gamepad_processor_process_gamepad_button,
|
||||
};
|
||||
|
@ -141,6 +141,8 @@ sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb,
|
||||
struct sc_control_msg msg;
|
||||
msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE;
|
||||
msg.uhid_create.id = SC_HID_ID_KEYBOARD;
|
||||
msg.uhid_create.vendor_id = hid_open.vendor_id;
|
||||
msg.uhid_create.product_id = hid_open.product_id;
|
||||
msg.uhid_create.name = hid_open.name;
|
||||
msg.uhid_create.report_desc = hid_open.report_desc;
|
||||
msg.uhid_create.report_desc_size = hid_open.report_desc_size;
|
||||
|
@ -81,6 +81,8 @@ sc_mouse_uhid_init(struct sc_mouse_uhid *mouse,
|
||||
struct sc_control_msg msg;
|
||||
msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE;
|
||||
msg.uhid_create.id = SC_HID_ID_MOUSE;
|
||||
msg.uhid_create.vendor_id = hid_open.vendor_id;
|
||||
msg.uhid_create.product_id = hid_open.product_id;
|
||||
msg.uhid_create.name = hid_open.name;
|
||||
msg.uhid_create.report_desc = hid_open.report_desc;
|
||||
msg.uhid_create.report_desc_size = hid_open.report_desc_size;
|
||||
|
@ -7,33 +7,35 @@
|
||||
#define DOWNCAST(GP) container_of(GP, struct sc_gamepad_aoa, gamepad_processor)
|
||||
|
||||
static void
|
||||
sc_gamepad_processor_process_gamepad_device(struct sc_gamepad_processor *gp,
|
||||
const struct sc_gamepad_device_event *event) {
|
||||
sc_gamepad_processor_process_gamepad_added(struct sc_gamepad_processor *gp,
|
||||
const struct sc_gamepad_added_event *event) {
|
||||
struct sc_gamepad_aoa *gamepad = DOWNCAST(gp);
|
||||
|
||||
if (event->type == SC_GAMEPAD_DEVICE_ADDED) {
|
||||
struct sc_hid_open hid_open;
|
||||
if (!sc_hid_gamepad_generate_open(&gamepad->hid, &hid_open,
|
||||
event->gamepad_id)) {
|
||||
return;
|
||||
}
|
||||
struct sc_hid_open hid_open;
|
||||
if (!sc_hid_gamepad_generate_open(&gamepad->hid, &hid_open,
|
||||
event->gamepad_id, 0, 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// exit_on_error: false (a gamepad open failure should not exit scrcpy)
|
||||
if (!sc_aoa_push_open(gamepad->aoa, &hid_open, false)) {
|
||||
LOGW("Could not push AOA HID open (gamepad)");
|
||||
}
|
||||
} else {
|
||||
assert(event->type == SC_GAMEPAD_DEVICE_REMOVED);
|
||||
// exit_on_error: false (a gamepad open failure should not exit scrcpy)
|
||||
if (!sc_aoa_push_open(gamepad->aoa, &hid_open, false)) {
|
||||
LOGW("Could not push AOA HID open (gamepad)");
|
||||
}
|
||||
}
|
||||
|
||||
struct sc_hid_close hid_close;
|
||||
if (!sc_hid_gamepad_generate_close(&gamepad->hid, &hid_close,
|
||||
event->gamepad_id)) {
|
||||
return;
|
||||
}
|
||||
static void
|
||||
sc_gamepad_processor_process_gamepad_removed(struct sc_gamepad_processor *gp,
|
||||
const struct sc_gamepad_removed_event *event) {
|
||||
struct sc_gamepad_aoa *gamepad = DOWNCAST(gp);
|
||||
|
||||
if (!sc_aoa_push_close(gamepad->aoa, &hid_close)) {
|
||||
LOGW("Could not push AOA HID close (gamepad)");
|
||||
}
|
||||
struct sc_hid_close hid_close;
|
||||
if (!sc_hid_gamepad_generate_close(&gamepad->hid, &hid_close,
|
||||
event->gamepad_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sc_aoa_push_close(gamepad->aoa, &hid_close)) {
|
||||
LOGW("Could not push AOA HID close (gamepad)");
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,7 +78,8 @@ sc_gamepad_aoa_init(struct sc_gamepad_aoa *gamepad, struct sc_aoa *aoa) {
|
||||
sc_hid_gamepad_init(&gamepad->hid);
|
||||
|
||||
static const struct sc_gamepad_processor_ops ops = {
|
||||
.process_gamepad_device = sc_gamepad_processor_process_gamepad_device,
|
||||
.process_gamepad_added = sc_gamepad_processor_process_gamepad_added,
|
||||
.process_gamepad_removed = sc_gamepad_processor_process_gamepad_removed,
|
||||
.process_gamepad_axis = sc_gamepad_processor_process_gamepad_axis,
|
||||
.process_gamepad_button = sc_gamepad_processor_process_gamepad_button,
|
||||
};
|
||||
|
@ -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 = {
|
||||
|
@ -175,7 +175,6 @@ sc_screen_otg_process_gamepad_device(struct sc_screen_otg *screen,
|
||||
assert(screen->gamepad);
|
||||
struct sc_gamepad_processor *gp = &screen->gamepad->gamepad_processor;
|
||||
|
||||
SDL_JoystickID id;
|
||||
if (event->type == SDL_CONTROLLERDEVICEADDED) {
|
||||
SDL_GameController *gc = SDL_GameControllerOpen(event->which);
|
||||
if (!gc) {
|
||||
@ -190,9 +189,12 @@ sc_screen_otg_process_gamepad_device(struct sc_screen_otg *screen,
|
||||
return;
|
||||
}
|
||||
|
||||
id = SDL_JoystickInstanceID(joystick);
|
||||
struct sc_gamepad_added_event evt = {
|
||||
.gamepad_id = SDL_JoystickInstanceID(joystick),
|
||||
};
|
||||
gp->ops->process_gamepad_added(gp, &evt);
|
||||
} else if (event->type == SDL_CONTROLLERDEVICEREMOVED) {
|
||||
id = event->which;
|
||||
SDL_JoystickID id = event->which;
|
||||
|
||||
SDL_GameController *gc = SDL_GameControllerFromInstanceID(id);
|
||||
if (gc) {
|
||||
@ -200,16 +202,12 @@ sc_screen_otg_process_gamepad_device(struct sc_screen_otg *screen,
|
||||
} else {
|
||||
LOGW("Unknown gamepad device removed");
|
||||
}
|
||||
} else {
|
||||
// Nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
struct sc_gamepad_device_event evt = {
|
||||
.type = sc_gamepad_device_event_type_from_sdl_type(event->type),
|
||||
.gamepad_id = id,
|
||||
};
|
||||
gp->ops->process_gamepad_device(gp, &evt);
|
||||
struct sc_gamepad_removed_event evt = {
|
||||
.gamepad_id = id,
|
||||
};
|
||||
gp->ops->process_gamepad_removed(gp, &evt);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
|
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"));
|
||||
|
@ -329,6 +329,8 @@ static void test_serialize_uhid_create(void) {
|
||||
.type = SC_CONTROL_MSG_TYPE_UHID_CREATE,
|
||||
.uhid_create = {
|
||||
.id = 42,
|
||||
.vendor_id = 0x1234,
|
||||
.product_id = 0x5678,
|
||||
.name = "ABC",
|
||||
.report_desc_size = sizeof(report_desc),
|
||||
.report_desc = report_desc,
|
||||
@ -337,11 +339,13 @@ static void test_serialize_uhid_create(void) {
|
||||
|
||||
uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
|
||||
size_t size = sc_control_msg_serialize(&msg, buf);
|
||||
assert(size == 20);
|
||||
assert(size == 24);
|
||||
|
||||
const uint8_t expected[] = {
|
||||
SC_CONTROL_MSG_TYPE_UHID_CREATE,
|
||||
0, 42, // id
|
||||
0x12, 0x34, // vendor id
|
||||
0x56, 0x78, // product id
|
||||
3, // name size
|
||||
65, 66, 67, // "ABC"
|
||||
0, 11, // report desc size
|
||||
@ -407,6 +411,21 @@ static void test_serialize_open_hard_keyboard(void) {
|
||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||
}
|
||||
|
||||
static void test_serialize_reset_video(void) {
|
||||
struct sc_control_msg msg = {
|
||||
.type = SC_CONTROL_MSG_TYPE_RESET_VIDEO,
|
||||
};
|
||||
|
||||
uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
|
||||
size_t size = sc_control_msg_serialize(&msg, buf);
|
||||
assert(size == 1);
|
||||
|
||||
const uint8_t expected[] = {
|
||||
SC_CONTROL_MSG_TYPE_RESET_VIDEO,
|
||||
};
|
||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
(void) argc;
|
||||
(void) argv;
|
||||
@ -429,5 +448,6 @@ int main(int argc, char *argv[]) {
|
||||
test_serialize_uhid_input();
|
||||
test_serialize_uhid_destroy();
|
||||
test_serialize_open_hard_keyboard();
|
||||
test_serialize_reset_video();
|
||||
return 0;
|
||||
}
|
||||
|
@ -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.2`][direct-scrcpy-server]
|
||||
<sub>SHA-256: `e19fe024bfa3367809494407ad6ca809a6f6e77dac95e99f85ba75144e0ba35d`</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.2/scrcpy-server-v3.0.2
|
||||
|
||||
Download the prebuilt server somewhere, and specify its path during the Meson
|
||||
configuration:
|
||||
|
@ -85,6 +85,12 @@ scrcpy --tcpip=192.168.1.1 # default port is 5555
|
||||
scrcpy --tcpip=192.168.1.1:5555
|
||||
```
|
||||
|
||||
Prefix the address with a '+' to force a reconnection:
|
||||
|
||||
```bash
|
||||
scrcpy --tcpip=+192.168.1.1
|
||||
```
|
||||
|
||||
|
||||
### Manual
|
||||
|
||||
|
@ -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`
|
||||
|
@ -71,6 +71,31 @@ adb shell cmd display power-on 0
|
||||
```
|
||||
|
||||
|
||||
## Screen off timeout
|
||||
|
||||
The Android screen automatically turns off after some delay.
|
||||
|
||||
To change this delay while scrcpy is running:
|
||||
|
||||
```bash
|
||||
scrcpy --screen-off-timeout=300 # 300 seconds (5 minutes)
|
||||
```
|
||||
|
||||
The initial value is restored on exit.
|
||||
|
||||
It is possible to change this setting manually:
|
||||
|
||||
```bash
|
||||
# get the current screen_off_timeout value
|
||||
adb shell settings get system screen_off_timeout
|
||||
# set a new value (in milliseconds)
|
||||
adb shell settings put system screen_off_timeout 30000
|
||||
```
|
||||
|
||||
Note that the Android value is in milliseconds, but the scrcpy command line
|
||||
argument is in seconds.
|
||||
|
||||
|
||||
## Show touches
|
||||
|
||||
For presentations, it may be useful to show physical touches (on the physical
|
||||
|
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-x86_64-v3.0.2.tar.gz`][direct-linux-x86_64] (x86_64)
|
||||
<sub>SHA-256: `20b69dcd379bb7d7208bf1e4858cf04162fc856697be0e6c03863d7b3c1e734a`</sub>
|
||||
|
||||
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
|
||||
[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.2/scrcpy-linux-x86_64-v3.0.2.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:
|
||||
|
||||
|
23
doc/macos.md
23
doc/macos.md
@ -2,6 +2,27 @@
|
||||
|
||||
## Install
|
||||
|
||||
### From the official release
|
||||
|
||||
Download a static build of the [latest release]:
|
||||
|
||||
- [`scrcpy-macos-aarch64-v3.0.2.tar.gz`][direct-macos-aarch64] (aarch64)
|
||||
<sub>SHA-256: `811ba2f4e856146bdd161e24c3490d78efbec2339ca783fac791d041c0aecfb6`</sub>
|
||||
|
||||
- [`scrcpy-macos-x86_64-v3.0.2.tar.gz`][direct-macos-x86_64] (x86_64)
|
||||
<sub>SHA-256: `8effff54dca3a3e46eaaec242771a13a7f81af2e18670b3d0d8ed6b461bb4f79`</sub>
|
||||
|
||||
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
|
||||
[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.2/scrcpy-macos-aarch64-v3.0.2.tar.gz
|
||||
[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.2/scrcpy-macos-x86_64-v3.0.2.tar.gz
|
||||
|
||||
and extract it.
|
||||
|
||||
_Static builds of scrcpy for macOS are still experimental._
|
||||
|
||||
|
||||
### From a package manager
|
||||
|
||||
Scrcpy is available in [Homebrew]:
|
||||
|
||||
```bash
|
||||
@ -13,7 +34,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:
|
||||
|
@ -30,6 +30,7 @@ _<kbd>[Super]</kbd> is typically the <kbd>Windows</kbd> or <kbd>Cmd</kbd> key._
|
||||
| Flip display vertically | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>↑</kbd> _(up)_ \| <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>↓</kbd> _(down)_
|
||||
| Pause or re-pause display | <kbd>MOD</kbd>+<kbd>z</kbd>
|
||||
| Unpause display | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>z</kbd>
|
||||
| Reset video capture/encoding | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>r</kbd>
|
||||
| Resize window to 1:1 (pixel-perfect) | <kbd>MOD</kbd>+<kbd>g</kbd>
|
||||
| Resize window to remove black borders | <kbd>MOD</kbd>+<kbd>w</kbd> \| _Double-left-click¹_
|
||||
| Click on `HOME` | <kbd>MOD</kbd>+<kbd>h</kbd> \| _Middle-click_
|
||||
|
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.2.zip`][direct-win64] (64-bit)
|
||||
<sub>SHA-256: `f0de59f5d46127c87cd822d39d6665e016b86db4cd048101b262f6adb6766832`</sub>
|
||||
- [`scrcpy-win32-v3.0.2.zip`][direct-win32] (32-bit)
|
||||
<sub>SHA-256: `8db8d4984d642012c55802de71f507f8ff9f68a8cfed456d7a1982d47e065f64`</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.2/scrcpy-win64-v3.0.2.zip
|
||||
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.2/scrcpy-win32-v3.0.2.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.2/scrcpy-server-v3.0.2
|
||||
PREBUILT_SERVER_SHA256=e19fe024bfa3367809494407ad6ca809a6f6e77dac95e99f85ba75144e0ba35d
|
||||
|
||||
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.2',
|
||||
meson_version: '>= 0.48',
|
||||
default_options: [
|
||||
'c_std=c11',
|
||||
|
@ -2,7 +2,7 @@ option('compile_app', type: 'boolean', value: true, description: 'Build the clie
|
||||
option('compile_server', type: 'boolean', value: true, description: 'Build the server')
|
||||
option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server')
|
||||
option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server from the same directory as the scrcpy executable')
|
||||
option('static', type: 'boolean', value: false, description: 'Use static dependencies')
|
||||
option('server_debugger', type: 'boolean', value: false, description: 'Run a server debugger and wait for a client to be attached')
|
||||
option('server_debugger_method', type: 'combo', choices: ['old', 'new'], value: 'new', description: 'Select the debugger method (Android < 9: "old", Android >= 9: "new")')
|
||||
option('v4l2', type: 'boolean', value: true, description: 'Enable V4L2 feature when supported')
|
||||
option('usb', type: 'boolean', value: true, description: 'Enable HID/OTG features when supported')
|
||||
|
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 30002
|
||||
versionName "3.0.2"
|
||||
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.2
|
||||
|
||||
PLATFORM=${ANDROID_PLATFORM:-35}
|
||||
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.0.0}
|
||||
PLATFORM_TOOLS="$ANDROID_HOME/platforms/android-$PLATFORM"
|
||||
BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS"
|
||||
|
||||
BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})"
|
||||
@ -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,12 +51,20 @@ 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=( \
|
||||
android/content/*java \
|
||||
)
|
||||
|
||||
SRC=( \
|
||||
com/genymobile/scrcpy/*.java \
|
||||
com/genymobile/scrcpy/audio/*.java \
|
||||
com/genymobile/scrcpy/control/*.java \
|
||||
com/genymobile/scrcpy/device/*.java \
|
||||
com/genymobile/scrcpy/opengl/*.java \
|
||||
com/genymobile/scrcpy/util/*.java \
|
||||
com/genymobile/scrcpy/video/*.java \
|
||||
com/genymobile/scrcpy/wrappers/*.java \
|
||||
@ -68,10 +78,11 @@ done
|
||||
|
||||
echo "Compiling java sources..."
|
||||
cd ../java
|
||||
javac -bootclasspath "$ANDROID_JAR" \
|
||||
javac -encoding UTF-8 -bootclasspath "$ANDROID_JAR" \
|
||||
-cp "$LAMBDA_JAR:$GEN_DIR" \
|
||||
-d "$CLASSES_DIR" \
|
||||
-source 1.8 -target 1.8 \
|
||||
${FAKE_SRC[@]} \
|
||||
${SRC[@]}
|
||||
|
||||
echo "Dexing..."
|
||||
|
@ -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);
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package android.content;
|
||||
|
||||
public interface IContentProvider {
|
||||
// android.content.IContentProvider is hidden, this is a fake one to expose the type to the project
|
||||
}
|
@ -5,6 +5,8 @@ import com.genymobile.scrcpy.util.Ln;
|
||||
import com.genymobile.scrcpy.util.Settings;
|
||||
import com.genymobile.scrcpy.util.SettingsException;
|
||||
|
||||
import android.os.BatteryManager;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
@ -16,59 +18,132 @@ import java.io.OutputStream;
|
||||
*/
|
||||
public final class CleanUp {
|
||||
|
||||
private static final int MSG_TYPE_MASK = 0b11;
|
||||
private static final int MSG_TYPE_RESTORE_STAY_ON = 0;
|
||||
private static final int MSG_TYPE_DISABLE_SHOW_TOUCHES = 1;
|
||||
private static final int MSG_TYPE_RESTORE_DISPLAY_POWER = 2;
|
||||
private static final int MSG_TYPE_POWER_OFF_SCREEN = 3;
|
||||
// Dynamic options
|
||||
private static final int PENDING_CHANGE_DISPLAY_POWER = 1 << 0;
|
||||
private int pendingChanges;
|
||||
private boolean pendingRestoreDisplayPower;
|
||||
|
||||
private static final int MSG_PARAM_SHIFT = 2;
|
||||
private Thread thread;
|
||||
|
||||
private final OutputStream out;
|
||||
|
||||
public CleanUp(OutputStream out) {
|
||||
this.out = out;
|
||||
private CleanUp(Options options) {
|
||||
thread = new Thread(() -> runCleanUp(options), "cleanup");
|
||||
thread.start();
|
||||
}
|
||||
|
||||
public static CleanUp configure(int displayId) throws IOException {
|
||||
String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(displayId)};
|
||||
public static CleanUp start(Options options) {
|
||||
return new CleanUp(options);
|
||||
}
|
||||
|
||||
public void interrupt() {
|
||||
thread.interrupt();
|
||||
}
|
||||
|
||||
public void join() throws InterruptedException {
|
||||
thread.join();
|
||||
}
|
||||
|
||||
private void runCleanUp(Options options) {
|
||||
boolean disableShowTouches = false;
|
||||
if (options.getShowTouches()) {
|
||||
try {
|
||||
String oldValue = Settings.getAndPutValue(Settings.TABLE_SYSTEM, "show_touches", "1");
|
||||
// If "show touches" was disabled, it must be disabled back on clean up
|
||||
disableShowTouches = !"1".equals(oldValue);
|
||||
} catch (SettingsException e) {
|
||||
Ln.e("Could not change \"show_touches\"", e);
|
||||
}
|
||||
}
|
||||
|
||||
int restoreStayOn = -1;
|
||||
if (options.getStayAwake()) {
|
||||
int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS;
|
||||
try {
|
||||
String oldValue = Settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn));
|
||||
try {
|
||||
int currentStayOn = Integer.parseInt(oldValue);
|
||||
// Restore only if the current value is different
|
||||
if (currentStayOn != stayOn) {
|
||||
restoreStayOn = currentStayOn;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
// ignore
|
||||
}
|
||||
} catch (SettingsException e) {
|
||||
Ln.e("Could not change \"stay_on_while_plugged_in\"", e);
|
||||
}
|
||||
}
|
||||
|
||||
int restoreScreenOffTimeout = -1;
|
||||
int screenOffTimeout = options.getScreenOffTimeout();
|
||||
if (screenOffTimeout != -1) {
|
||||
try {
|
||||
String oldValue = Settings.getAndPutValue(Settings.TABLE_SYSTEM, "screen_off_timeout", String.valueOf(screenOffTimeout));
|
||||
try {
|
||||
int currentScreenOffTimeout = Integer.parseInt(oldValue);
|
||||
// Restore only if the current value is different
|
||||
if (currentScreenOffTimeout != screenOffTimeout) {
|
||||
restoreScreenOffTimeout = currentScreenOffTimeout;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
// ignore
|
||||
}
|
||||
} catch (SettingsException e) {
|
||||
Ln.e("Could not change \"screen_off_timeout\"", e);
|
||||
}
|
||||
}
|
||||
|
||||
boolean powerOffScreen = options.getPowerOffScreenOnClose();
|
||||
int displayId = options.getDisplayId();
|
||||
|
||||
try {
|
||||
run(displayId, restoreStayOn, disableShowTouches, powerOffScreen, restoreScreenOffTimeout);
|
||||
} catch (InterruptedException e) {
|
||||
// ignore
|
||||
} catch (IOException e) {
|
||||
Ln.e("Clean up I/O exception", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void run(int displayId, int restoreStayOn, boolean disableShowTouches, boolean powerOffScreen, int restoreScreenOffTimeout)
|
||||
throws IOException, InterruptedException {
|
||||
String[] cmd = {
|
||||
"app_process",
|
||||
"/",
|
||||
CleanUp.class.getName(),
|
||||
String.valueOf(displayId),
|
||||
String.valueOf(restoreStayOn),
|
||||
String.valueOf(disableShowTouches),
|
||||
String.valueOf(powerOffScreen),
|
||||
String.valueOf(restoreScreenOffTimeout),
|
||||
};
|
||||
|
||||
ProcessBuilder builder = new ProcessBuilder(cmd);
|
||||
builder.environment().put("CLASSPATH", Server.SERVER_PATH);
|
||||
Process process = builder.start();
|
||||
return new CleanUp(process.getOutputStream());
|
||||
}
|
||||
OutputStream out = process.getOutputStream();
|
||||
|
||||
private boolean sendMessage(int type, int param) {
|
||||
assert (type & ~MSG_TYPE_MASK) == 0;
|
||||
int msg = type | param << MSG_PARAM_SHIFT;
|
||||
try {
|
||||
out.write(msg);
|
||||
out.flush();
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Ln.w("Could not configure cleanup (type=" + type + ", param=" + param + ")", e);
|
||||
return false;
|
||||
while (true) {
|
||||
int localPendingChanges;
|
||||
boolean localPendingRestoreDisplayPower;
|
||||
synchronized (this) {
|
||||
while (pendingChanges == 0) {
|
||||
wait();
|
||||
}
|
||||
localPendingChanges = pendingChanges;
|
||||
localPendingRestoreDisplayPower = pendingRestoreDisplayPower;
|
||||
pendingChanges = 0;
|
||||
}
|
||||
if ((localPendingChanges & PENDING_CHANGE_DISPLAY_POWER) != 0) {
|
||||
out.write(localPendingRestoreDisplayPower ? 1 : 0);
|
||||
out.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean setRestoreStayOn(int restoreValue) {
|
||||
// Restore the value (between 0 and 7), -1 to not restore
|
||||
// <https://developer.android.com/reference/android/provider/Settings.Global#STAY_ON_WHILE_PLUGGED_IN>
|
||||
assert restoreValue >= -1 && restoreValue <= 7;
|
||||
return sendMessage(MSG_TYPE_RESTORE_STAY_ON, restoreValue & 0b1111);
|
||||
}
|
||||
|
||||
public boolean setDisableShowTouches(boolean disableOnExit) {
|
||||
return sendMessage(MSG_TYPE_DISABLE_SHOW_TOUCHES, disableOnExit ? 1 : 0);
|
||||
}
|
||||
|
||||
public boolean setRestoreDisplayPower(boolean restoreOnExit) {
|
||||
return sendMessage(MSG_TYPE_RESTORE_DISPLAY_POWER, restoreOnExit ? 1 : 0);
|
||||
}
|
||||
|
||||
public boolean setPowerOffScreen(boolean powerOffScreenOnExit) {
|
||||
return sendMessage(MSG_TYPE_POWER_OFF_SCREEN, powerOffScreenOnExit ? 1 : 0);
|
||||
public synchronized void setRestoreDisplayPower(boolean restoreDisplayPower) {
|
||||
pendingRestoreDisplayPower = restoreDisplayPower;
|
||||
pendingChanges |= PENDING_CHANGE_DISPLAY_POWER;
|
||||
notify();
|
||||
}
|
||||
|
||||
public static void unlinkSelf() {
|
||||
@ -83,35 +158,21 @@ public final class CleanUp {
|
||||
unlinkSelf();
|
||||
|
||||
int displayId = Integer.parseInt(args[0]);
|
||||
int restoreStayOn = Integer.parseInt(args[1]);
|
||||
boolean disableShowTouches = Boolean.parseBoolean(args[2]);
|
||||
boolean powerOffScreen = Boolean.parseBoolean(args[3]);
|
||||
int restoreScreenOffTimeout = Integer.parseInt(args[4]);
|
||||
|
||||
int restoreStayOn = -1;
|
||||
boolean disableShowTouches = false;
|
||||
// Dynamic option
|
||||
boolean restoreDisplayPower = false;
|
||||
boolean powerOffScreen = false;
|
||||
|
||||
try {
|
||||
// Wait for the server to die
|
||||
int msg;
|
||||
while ((msg = System.in.read()) != -1) {
|
||||
int type = msg & MSG_TYPE_MASK;
|
||||
int param = msg >> MSG_PARAM_SHIFT;
|
||||
switch (type) {
|
||||
case MSG_TYPE_RESTORE_STAY_ON:
|
||||
restoreStayOn = param > 7 ? -1 : param;
|
||||
break;
|
||||
case MSG_TYPE_DISABLE_SHOW_TOUCHES:
|
||||
disableShowTouches = param != 0;
|
||||
break;
|
||||
case MSG_TYPE_RESTORE_DISPLAY_POWER:
|
||||
restoreDisplayPower = param != 0;
|
||||
break;
|
||||
case MSG_TYPE_POWER_OFF_SCREEN:
|
||||
powerOffScreen = param != 0;
|
||||
break;
|
||||
default:
|
||||
Ln.w("Unexpected msg type: " + type);
|
||||
break;
|
||||
}
|
||||
// Only restore display power
|
||||
assert msg == 0 || msg == 1;
|
||||
restoreDisplayPower = msg != 0;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Expected when the server is dead
|
||||
@ -137,13 +198,24 @@ public final class CleanUp {
|
||||
}
|
||||
}
|
||||
|
||||
if (Device.isScreenOn() && displayId != Device.DISPLAY_ID_NONE) {
|
||||
if (restoreScreenOffTimeout != -1) {
|
||||
Ln.i("Restoring \"screen off timeout\"");
|
||||
try {
|
||||
Settings.putValue(Settings.TABLE_SYSTEM, "screen_off_timeout", String.valueOf(restoreScreenOffTimeout));
|
||||
} catch (SettingsException e) {
|
||||
Ln.e("Could not restore \"screen_off_timeout\"", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,14 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.AttributionSource;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.ContextWrapper;
|
||||
import android.content.IContentProvider;
|
||||
import android.os.Binder;
|
||||
import android.os.Process;
|
||||
|
||||
public final class FakeContext extends ContextWrapper {
|
||||
@ -17,6 +22,38 @@ public final class FakeContext extends ContextWrapper {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
private final ContentResolver contentResolver = new ContentResolver(this) {
|
||||
@SuppressWarnings({"unused", "ProtectedMemberInFinalClass"})
|
||||
// @Override (but super-class method not visible)
|
||||
protected IContentProvider acquireProvider(Context c, String name) {
|
||||
return ServiceManager.getActivityManager().getContentProviderExternal(name, new Binder());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
// @Override (but super-class method not visible)
|
||||
public boolean releaseProvider(IContentProvider icp) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unused", "ProtectedMemberInFinalClass"})
|
||||
// @Override (but super-class method not visible)
|
||||
protected IContentProvider acquireUnstableProvider(Context c, String name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
// @Override (but super-class method not visible)
|
||||
public boolean releaseUnstableProvider(IContentProvider icp) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
// @Override (but super-class method not visible)
|
||||
public void unstableProviderDied(IContentProvider icp) {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
private FakeContext() {
|
||||
super(Workarounds.getSystemContext());
|
||||
}
|
||||
@ -49,4 +86,9 @@ public final class FakeContext extends ContextWrapper {
|
||||
public Context getApplicationContext() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContentResolver getContentResolver() {
|
||||
return contentResolver;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
@ -45,6 +47,7 @@ public class Options {
|
||||
private boolean cameraHighSpeed;
|
||||
private boolean showTouches;
|
||||
private boolean stayAwake;
|
||||
private int screenOffTimeout = -1;
|
||||
private List<CodecOption> videoCodecOptions;
|
||||
private List<CodecOption> audioCodecOptions;
|
||||
|
||||
@ -57,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;
|
||||
@ -122,8 +129,8 @@ public class Options {
|
||||
return maxFps;
|
||||
}
|
||||
|
||||
public int getLockVideoOrientation() {
|
||||
return lockVideoOrientation;
|
||||
public float getAngle() {
|
||||
return angle;
|
||||
}
|
||||
|
||||
public boolean isTunnelForward() {
|
||||
@ -174,6 +181,10 @@ public class Options {
|
||||
return stayAwake;
|
||||
}
|
||||
|
||||
public int getScreenOffTimeout() {
|
||||
return screenOffTimeout;
|
||||
}
|
||||
|
||||
public List<CodecOption> getVideoCodecOptions() {
|
||||
return videoCodecOptions;
|
||||
}
|
||||
@ -214,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;
|
||||
}
|
||||
@ -254,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) {
|
||||
@ -340,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);
|
||||
@ -363,6 +382,12 @@ public class Options {
|
||||
case "stay_awake":
|
||||
options.stayAwake = Boolean.parseBoolean(value);
|
||||
break;
|
||||
case "screen_off_timeout":
|
||||
options.screenOffTimeout = Integer.parseInt(value);
|
||||
if (options.screenOffTimeout < -1) {
|
||||
throw new IllegalArgumentException("Invalid screen off timeout: " + options.screenOffTimeout);
|
||||
}
|
||||
break;
|
||||
case "video_codec_options":
|
||||
options.videoCodecOptions = CodecOption.parse(value);
|
||||
break;
|
||||
@ -441,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;
|
||||
@ -468,6 +501,11 @@ public class Options {
|
||||
}
|
||||
}
|
||||
|
||||
if (options.newDisplay != null) {
|
||||
assert options.displayId == 0 : "Must not set both displayId and newDisplay";
|
||||
options.displayId = Device.DISPLAY_ID_NONE;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
@ -559,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,10 +14,9 @@ import com.genymobile.scrcpy.device.DesktopConnection;
|
||||
import com.genymobile.scrcpy.device.Device;
|
||||
import com.genymobile.scrcpy.device.NewDisplay;
|
||||
import com.genymobile.scrcpy.device.Streamer;
|
||||
import com.genymobile.scrcpy.opengl.OpenGLRunner;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
import com.genymobile.scrcpy.util.LogUtils;
|
||||
import com.genymobile.scrcpy.util.Settings;
|
||||
import com.genymobile.scrcpy.util.SettingsException;
|
||||
import com.genymobile.scrcpy.video.CameraCapture;
|
||||
import com.genymobile.scrcpy.video.NewDisplayCapture;
|
||||
import com.genymobile.scrcpy.video.ScreenCapture;
|
||||
@ -25,7 +24,6 @@ import com.genymobile.scrcpy.video.SurfaceCapture;
|
||||
import com.genymobile.scrcpy.video.SurfaceEncoder;
|
||||
import com.genymobile.scrcpy.video.VideoSource;
|
||||
|
||||
import android.os.BatteryManager;
|
||||
import android.os.Build;
|
||||
|
||||
import java.io.File;
|
||||
@ -76,51 +74,6 @@ public final class Server {
|
||||
// not instantiable
|
||||
}
|
||||
|
||||
private static void initAndCleanUp(Options options, CleanUp cleanUp) {
|
||||
// This method is called from its own thread, so it may only configure cleanup actions which are NOT dynamic (i.e. they are configured once
|
||||
// and for all, they cannot be changed from another thread)
|
||||
|
||||
if (options.getShowTouches()) {
|
||||
try {
|
||||
String oldValue = Settings.getAndPutValue(Settings.TABLE_SYSTEM, "show_touches", "1");
|
||||
// If "show touches" was disabled, it must be disabled back on clean up
|
||||
if (!"1".equals(oldValue)) {
|
||||
if (!cleanUp.setDisableShowTouches(true)) {
|
||||
Ln.e("Could not disable show touch on exit");
|
||||
}
|
||||
}
|
||||
} catch (SettingsException e) {
|
||||
Ln.e("Could not change \"show_touches\"", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.getStayAwake()) {
|
||||
int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS;
|
||||
try {
|
||||
String oldValue = Settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn));
|
||||
try {
|
||||
int restoreStayOn = Integer.parseInt(oldValue);
|
||||
if (restoreStayOn != stayOn) {
|
||||
// Restore only if the current value is different
|
||||
if (!cleanUp.setRestoreStayOn(restoreStayOn)) {
|
||||
Ln.e("Could not restore stay on on exit");
|
||||
}
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
// ignore
|
||||
}
|
||||
} catch (SettingsException e) {
|
||||
Ln.e("Could not change \"stay_on_while_plugged_in\"", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.getPowerOffScreenOnClose()) {
|
||||
if (!cleanUp.setPowerOffScreen(true)) {
|
||||
Ln.e("Could not power off screen on exit");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void scrcpy(Options options) throws IOException, ConfigurationException {
|
||||
if (Build.VERSION.SDK_INT < AndroidVersions.API_31_ANDROID_12 && options.getVideoSource() == VideoSource.CAMERA) {
|
||||
Ln.e("Camera mirroring is not supported before Android 12");
|
||||
@ -132,32 +85,10 @@ 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;
|
||||
Thread initThread = null;
|
||||
|
||||
NewDisplay newDisplay = options.getNewDisplay();
|
||||
int displayId = newDisplay == null ? options.getDisplayId() : Device.DISPLAY_ID_NONE;
|
||||
|
||||
if (options.getCleanup()) {
|
||||
cleanUp = CleanUp.configure(displayId);
|
||||
initThread = startInitThread(options, cleanUp);
|
||||
cleanUp = CleanUp.start(options);
|
||||
}
|
||||
|
||||
int scid = options.getScid();
|
||||
@ -181,7 +112,7 @@ public final class Server {
|
||||
|
||||
if (control) {
|
||||
ControlChannel controlChannel = connection.getControlChannel();
|
||||
controller = new Controller(displayId, controlChannel, cleanUp, options.getClipboardAutosync(), options.getPowerOn());
|
||||
controller = new Controller(controlChannel, cleanUp, options);
|
||||
asyncProcessors.add(controller);
|
||||
}
|
||||
|
||||
@ -200,8 +131,7 @@ public final class Server {
|
||||
if (audioCodec == AudioCodec.RAW) {
|
||||
audioRecorder = new AudioRawRecorder(audioCapture, audioStreamer);
|
||||
} else {
|
||||
audioRecorder = new AudioEncoder(audioCapture, audioStreamer, options.getAudioBitRate(), options.getAudioCodecOptions(),
|
||||
options.getAudioEncoder());
|
||||
audioRecorder = new AudioEncoder(audioCapture, audioStreamer, options);
|
||||
}
|
||||
asyncProcessors.add(audioRecorder);
|
||||
}
|
||||
@ -211,20 +141,22 @@ public final class Server {
|
||||
options.getSendFrameMeta());
|
||||
SurfaceCapture surfaceCapture;
|
||||
if (options.getVideoSource() == VideoSource.DISPLAY) {
|
||||
NewDisplay newDisplay = options.getNewDisplay();
|
||||
if (newDisplay != null) {
|
||||
surfaceCapture = new NewDisplayCapture(controller, newDisplay, options.getMaxSize());
|
||||
surfaceCapture = new NewDisplayCapture(controller, options);
|
||||
} else {
|
||||
assert displayId != Device.DISPLAY_ID_NONE;
|
||||
surfaceCapture = new ScreenCapture(controller, displayId, options.getMaxSize(), options.getCrop(),
|
||||
options.getLockVideoOrientation());
|
||||
assert options.getDisplayId() != Device.DISPLAY_ID_NONE;
|
||||
surfaceCapture = new ScreenCapture(controller, options);
|
||||
}
|
||||
} else {
|
||||
surfaceCapture = new CameraCapture(options.getCameraId(), options.getCameraFacing(), options.getCameraSize(),
|
||||
options.getMaxSize(), options.getCameraAspectRatio(), options.getCameraFps(), options.getCameraHighSpeed());
|
||||
surfaceCapture = new CameraCapture(options);
|
||||
}
|
||||
SurfaceEncoder surfaceEncoder = new SurfaceEncoder(surfaceCapture, videoStreamer, options.getVideoBitRate(), options.getMaxFps(),
|
||||
options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError());
|
||||
SurfaceEncoder surfaceEncoder = new SurfaceEncoder(surfaceCapture, videoStreamer, options);
|
||||
asyncProcessors.add(surfaceEncoder);
|
||||
|
||||
if (controller != null) {
|
||||
controller.setSurfaceCapture(surfaceCapture);
|
||||
}
|
||||
}
|
||||
|
||||
Completion completion = new Completion(asyncProcessors.size());
|
||||
@ -236,22 +168,25 @@ public final class Server {
|
||||
|
||||
completion.await();
|
||||
} finally {
|
||||
if (initThread != null) {
|
||||
initThread.interrupt();
|
||||
if (cleanUp != null) {
|
||||
cleanUp.interrupt();
|
||||
}
|
||||
for (AsyncProcessor asyncProcessor : asyncProcessors) {
|
||||
asyncProcessor.stop();
|
||||
}
|
||||
|
||||
OpenGLRunner.quit(); // quit the OpenGL thread, if any
|
||||
|
||||
connection.shutdown();
|
||||
|
||||
try {
|
||||
if (initThread != null) {
|
||||
initThread.join();
|
||||
if (cleanUp != null) {
|
||||
cleanUp.join();
|
||||
}
|
||||
for (AsyncProcessor asyncProcessor : asyncProcessors) {
|
||||
asyncProcessor.join();
|
||||
}
|
||||
OpenGLRunner.join();
|
||||
} catch (InterruptedException e) {
|
||||
// ignore
|
||||
}
|
||||
@ -260,12 +195,6 @@ public final class Server {
|
||||
}
|
||||
}
|
||||
|
||||
private static Thread startInitThread(final Options options, final CleanUp cleanUp) {
|
||||
Thread thread = new Thread(() -> initAndCleanUp(options, cleanUp), "init-cleanup");
|
||||
thread.start();
|
||||
return thread;
|
||||
}
|
||||
|
||||
public static void main(String... args) {
|
||||
int status = 0;
|
||||
try {
|
||||
|
@ -2,6 +2,7 @@ package com.genymobile.scrcpy.audio;
|
||||
|
||||
import com.genymobile.scrcpy.AndroidVersions;
|
||||
import com.genymobile.scrcpy.AsyncProcessor;
|
||||
import com.genymobile.scrcpy.Options;
|
||||
import com.genymobile.scrcpy.device.ConfigurationException;
|
||||
import com.genymobile.scrcpy.device.Streamer;
|
||||
import com.genymobile.scrcpy.util.Codec;
|
||||
@ -67,12 +68,12 @@ public final class AudioEncoder implements AsyncProcessor {
|
||||
|
||||
private boolean ended;
|
||||
|
||||
public AudioEncoder(AudioCapture capture, Streamer streamer, int bitRate, List<CodecOption> codecOptions, String encoderName) {
|
||||
public AudioEncoder(AudioCapture capture, Streamer streamer, Options options) {
|
||||
this.capture = capture;
|
||||
this.streamer = streamer;
|
||||
this.bitRate = bitRate;
|
||||
this.codecOptions = codecOptions;
|
||||
this.encoderName = encoderName;
|
||||
this.bitRate = options.getAudioBitRate();
|
||||
this.codecOptions = options.getAudioCodecOptions();
|
||||
this.encoderName = options.getAudioEncoder();
|
||||
}
|
||||
|
||||
private static MediaFormat createFormat(String mimeType, int bitRate, List<CodecOption> codecOptions) {
|
||||
|
@ -24,6 +24,7 @@ public final class ControlMessage {
|
||||
public static final int TYPE_UHID_DESTROY = 14;
|
||||
public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 15;
|
||||
public static final int TYPE_START_APP = 16;
|
||||
public static final int TYPE_RESET_VIDEO = 17;
|
||||
|
||||
public static final long SEQUENCE_INVALID = 0;
|
||||
|
||||
@ -50,6 +51,8 @@ public final class ControlMessage {
|
||||
private int id;
|
||||
private byte[] data;
|
||||
private boolean on;
|
||||
private int vendorId;
|
||||
private int productId;
|
||||
|
||||
private ControlMessage() {
|
||||
}
|
||||
@ -130,10 +133,12 @@ public final class ControlMessage {
|
||||
return msg;
|
||||
}
|
||||
|
||||
public static ControlMessage createUhidCreate(int id, String name, byte[] reportDesc) {
|
||||
public static ControlMessage createUhidCreate(int id, int vendorId, int productId, String name, byte[] reportDesc) {
|
||||
ControlMessage msg = new ControlMessage();
|
||||
msg.type = TYPE_UHID_CREATE;
|
||||
msg.id = id;
|
||||
msg.vendorId = vendorId;
|
||||
msg.productId = productId;
|
||||
msg.text = name;
|
||||
msg.data = reportDesc;
|
||||
return msg;
|
||||
@ -236,4 +241,12 @@ public final class ControlMessage {
|
||||
public boolean getOn() {
|
||||
return on;
|
||||
}
|
||||
|
||||
public int getVendorId() {
|
||||
return vendorId;
|
||||
}
|
||||
|
||||
public int getProductId() {
|
||||
return productId;
|
||||
}
|
||||
}
|
||||
|
@ -46,6 +46,7 @@ public class ControlMessageReader {
|
||||
case ControlMessage.TYPE_COLLAPSE_PANELS:
|
||||
case ControlMessage.TYPE_ROTATE_DEVICE:
|
||||
case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
|
||||
case ControlMessage.TYPE_RESET_VIDEO:
|
||||
return ControlMessage.createEmpty(type);
|
||||
case ControlMessage.TYPE_UHID_CREATE:
|
||||
return parseUhidCreate();
|
||||
@ -141,9 +142,11 @@ public class ControlMessageReader {
|
||||
|
||||
private ControlMessage parseUhidCreate() throws IOException {
|
||||
int id = dis.readUnsignedShort();
|
||||
int vendorId = dis.readUnsignedShort();
|
||||
int productId = dis.readUnsignedShort();
|
||||
String name = parseString(1);
|
||||
byte[] data = parseByteArray(2);
|
||||
return ControlMessage.createUhidCreate(id, name, data);
|
||||
return ControlMessage.createUhidCreate(id, vendorId, productId, name, data);
|
||||
}
|
||||
|
||||
private ControlMessage parseUhidInput() throws IOException {
|
||||
|
@ -3,12 +3,15 @@ package com.genymobile.scrcpy.control;
|
||||
import com.genymobile.scrcpy.AndroidVersions;
|
||||
import com.genymobile.scrcpy.AsyncProcessor;
|
||||
import com.genymobile.scrcpy.CleanUp;
|
||||
import com.genymobile.scrcpy.Options;
|
||||
import com.genymobile.scrcpy.device.Device;
|
||||
import com.genymobile.scrcpy.device.DeviceApp;
|
||||
import com.genymobile.scrcpy.device.Point;
|
||||
import com.genymobile.scrcpy.device.Position;
|
||||
import com.genymobile.scrcpy.device.Size;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
import com.genymobile.scrcpy.util.LogUtils;
|
||||
import com.genymobile.scrcpy.video.SurfaceCapture;
|
||||
import com.genymobile.scrcpy.video.VirtualDisplayListener;
|
||||
import com.genymobile.scrcpy.wrappers.ClipboardManager;
|
||||
import com.genymobile.scrcpy.wrappers.InputManager;
|
||||
@ -18,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;
|
||||
@ -93,12 +97,15 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
|
||||
private boolean keepDisplayPowerOff;
|
||||
|
||||
public Controller(int displayId, ControlChannel controlChannel, CleanUp cleanUp, boolean clipboardAutosync, boolean powerOn) {
|
||||
this.displayId = displayId;
|
||||
// Used for resetting video encoding on RESET_VIDEO message
|
||||
private SurfaceCapture surfaceCapture;
|
||||
|
||||
public Controller(ControlChannel controlChannel, CleanUp cleanUp, Options options) {
|
||||
this.displayId = options.getDisplayId();
|
||||
this.controlChannel = controlChannel;
|
||||
this.cleanUp = cleanUp;
|
||||
this.clipboardAutosync = clipboardAutosync;
|
||||
this.powerOn = powerOn;
|
||||
this.clipboardAutosync = options.getClipboardAutosync();
|
||||
this.powerOn = options.getPowerOn();
|
||||
initPointers();
|
||||
sender = new DeviceMessageSender(controlChannel);
|
||||
|
||||
@ -143,6 +150,10 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
}
|
||||
}
|
||||
|
||||
public void setSurfaceCapture(SurfaceCapture surfaceCapture) {
|
||||
this.surfaceCapture = surfaceCapture;
|
||||
}
|
||||
|
||||
private UhidManager getUhidManager() {
|
||||
if (uhidManager == null) {
|
||||
uhidManager = new UhidManager(sender);
|
||||
@ -166,7 +177,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
|
||||
private void control() throws IOException {
|
||||
// on start, power on the device
|
||||
if (powerOn && displayId != Device.DISPLAY_ID_NONE && !Device.isScreenOn()) {
|
||||
if (powerOn && displayId == 0 && !Device.isScreenOn(displayId)) {
|
||||
Device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC);
|
||||
|
||||
// dirty hack
|
||||
@ -271,24 +282,15 @@ 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) {
|
||||
boolean on = msg.getOn();
|
||||
boolean setDisplayPowerOk = Device.setDisplayPower(displayId, on);
|
||||
if (setDisplayPowerOk) {
|
||||
keepDisplayPowerOff = !on;
|
||||
Ln.i("Device display turned " + (on ? "on" : "off"));
|
||||
if (cleanUp != null) {
|
||||
boolean mustRestoreOnExit = !on;
|
||||
cleanUp.setRestoreDisplayPower(mustRestoreOnExit);
|
||||
}
|
||||
}
|
||||
if (supportsInputEvents) {
|
||||
setDisplayPower(msg.getOn());
|
||||
}
|
||||
break;
|
||||
case ControlMessage.TYPE_ROTATE_DEVICE:
|
||||
Device.rotateDevice(getActionDisplayId());
|
||||
break;
|
||||
case ControlMessage.TYPE_UHID_CREATE:
|
||||
getUhidManager().open(msg.getId(), msg.getText(), msg.getData());
|
||||
getUhidManager().open(msg.getId(), msg.getVendorId(), msg.getProductId(), msg.getText(), msg.getData());
|
||||
break;
|
||||
case ControlMessage.TYPE_UHID_INPUT:
|
||||
getUhidManager().writeInput(msg.getId(), msg.getData());
|
||||
@ -302,6 +304,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
case ControlMessage.TYPE_START_APP:
|
||||
startAppAsync(msg.getText());
|
||||
break;
|
||||
case ControlMessage.TYPE_RESET_VIDEO:
|
||||
resetVideo();
|
||||
break;
|
||||
default:
|
||||
// do nothing
|
||||
}
|
||||
@ -346,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");
|
||||
@ -413,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;
|
||||
}
|
||||
}
|
||||
@ -424,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;
|
||||
}
|
||||
|
||||
@ -438,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;
|
||||
}
|
||||
|
||||
@ -446,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;
|
||||
}
|
||||
}
|
||||
@ -457,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;
|
||||
|
||||
@ -485,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -499,7 +528,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
}
|
||||
|
||||
private boolean pressBackOrTurnScreenOn(int action) {
|
||||
if (Device.isScreenOn()) {
|
||||
if (displayId == Device.DISPLAY_ID_NONE || Device.isScreenOn(displayId)) {
|
||||
return injectKeyEvent(action, KeyEvent.KEYCODE_BACK, 0, 0, Device.INJECT_MODE_ASYNC);
|
||||
}
|
||||
|
||||
@ -677,4 +706,26 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
private void setDisplayPower(boolean on) {
|
||||
// Change the power of the main display when mirroring a virtual display
|
||||
int targetDisplayId = displayId != Device.DISPLAY_ID_NONE ? displayId : 0;
|
||||
boolean setDisplayPowerOk = Device.setDisplayPower(targetDisplayId, on);
|
||||
if (setDisplayPowerOk) {
|
||||
// Do not keep display power off for virtual displays: MOD+p must wake up the physical device
|
||||
keepDisplayPowerOff = displayId != Device.DISPLAY_ID_NONE && !on;
|
||||
Ln.i("Device display turned " + (on ? "on" : "off"));
|
||||
if (cleanUp != null) {
|
||||
boolean mustRestoreOnExit = !on;
|
||||
cleanUp.setRestoreDisplayPower(mustRestoreOnExit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void resetVideo() {
|
||||
if (surfaceCapture != null) {
|
||||
Ln.i("Video capture reset");
|
||||
surfaceCapture.requestInvalidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ public final class UhidManager {
|
||||
}
|
||||
}
|
||||
|
||||
public void open(int id, String name, byte[] reportDesc) throws IOException {
|
||||
public void open(int id, int vendorId, int productId, String name, byte[] reportDesc) throws IOException {
|
||||
try {
|
||||
FileDescriptor fd = Os.open("/dev/uhid", OsConstants.O_RDWR, 0);
|
||||
try {
|
||||
@ -58,7 +58,7 @@ public final class UhidManager {
|
||||
close(old);
|
||||
}
|
||||
|
||||
byte[] req = buildUhidCreate2Req(name, reportDesc);
|
||||
byte[] req = buildUhidCreate2Req(vendorId, productId, name, reportDesc);
|
||||
Os.write(fd, req, 0, req.length);
|
||||
|
||||
registerUhidListener(id, fd);
|
||||
@ -148,7 +148,7 @@ public final class UhidManager {
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] buildUhidCreate2Req(String name, byte[] reportDesc) {
|
||||
private static byte[] buildUhidCreate2Req(int vendorId, int productId, String name, byte[] reportDesc) {
|
||||
/*
|
||||
* struct uhid_event {
|
||||
* uint32_t type;
|
||||
@ -183,8 +183,8 @@ public final class UhidManager {
|
||||
|
||||
buf.putShort((short) reportDesc.length);
|
||||
buf.putShort(BUS_VIRTUAL);
|
||||
buf.putInt(0); // vendor id
|
||||
buf.putInt(0); // product id
|
||||
buf.putInt(vendorId);
|
||||
buf.putInt(productId);
|
||||
buf.putInt(0); // version
|
||||
buf.putInt(0); // country;
|
||||
buf.put(reportDesc);
|
||||
|
@ -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
|
||||
@ -82,8 +81,9 @@ public final class Device {
|
||||
&& injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0, displayId, injectMode);
|
||||
}
|
||||
|
||||
public static boolean isScreenOn() {
|
||||
return ServiceManager.getPowerManager().isScreenOn();
|
||||
public static boolean isScreenOn(int displayId) {
|
||||
assert displayId != DISPLAY_ID_NONE;
|
||||
return ServiceManager.getPowerManager().isScreenOn(displayId);
|
||||
}
|
||||
|
||||
public static void expandNotificationPanel() {
|
||||
@ -131,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);
|
||||
}
|
||||
|
||||
@ -181,7 +181,7 @@ public final class Device {
|
||||
public static boolean powerOffScreen(int displayId) {
|
||||
assert displayId != DISPLAY_ID_NONE;
|
||||
|
||||
if (!isScreenOn()) {
|
||||
if (!isScreenOn(displayId)) {
|
||||
return true;
|
||||
}
|
||||
return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC);
|
||||
|
@ -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=" + width + ", height=" + 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);
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user