Compare commits
152 Commits
virtual_di
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
cac8e9c821 | ||
|
1c7680f689 | ||
|
5ae01749bf | ||
|
1fd57ede1f | ||
|
48fc18e380 | ||
|
ea6a94d355 | ||
|
0e2d084751 | ||
|
754f4fc6fe | ||
|
aca6d30af5 | ||
|
f2018e026c | ||
|
a507b4f559 | ||
|
a9aadc95df | ||
|
28b5bfb90e | ||
|
65256d7cc7 | ||
|
328bb74f80 | ||
|
7418fd0662 | ||
|
0a09518a49 | ||
|
27a5934a1d | ||
|
86a68fac6c | ||
|
1786f28e6f | ||
|
9cf4d52721 | ||
|
4bd1c5981d | ||
|
c59a3c3169 | ||
|
2780e0bd7b | ||
|
6c6607d404 | ||
|
988174805c | ||
|
f90dc216d1 | ||
|
97fa77c76c | ||
|
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 | ||
|
c29ecd0314 | ||
|
d62fa8880e | ||
|
1f6634ea87 | ||
|
58ba00fa06 | ||
|
569c37cec1 | ||
|
58a0fbbf2e | ||
|
acff5b005c | ||
|
5474ae6bd6 | ||
|
2c25fd7a80 | ||
|
ce21f515e3 | ||
|
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
LICENSE
2
LICENSE
@ -188,7 +188,7 @@
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright (C) 2018 Genymobile
|
||||
Copyright (C) 2018-2024 Romain Vimont
|
||||
Copyright (C) 2018-2025 Romain Vimont
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
19
README.md
19
README.md
@ -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.1)
|
||||
|
||||
<img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />
|
||||
|
||||
@ -74,10 +74,20 @@ 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)
|
||||
|
||||
|
||||
## Must-know tips
|
||||
|
||||
- [Reducing resolution](doc/video.md#size) may greatly improve performance
|
||||
(`scrcpy -m1024`)
|
||||
- [_Right-click_](doc/mouse.md#mouse-bindings) triggers `BACK`
|
||||
- [_Middle-click_](doc/mouse.md#mouse-bindings) triggers `HOME`
|
||||
- <kbd>Alt</kbd>+<kbd>f</kbd> toggles [fullscreen](doc/window.md#fullscreen)
|
||||
- There are many other [shortcuts](doc/shortcuts.md)
|
||||
|
||||
|
||||
## Usage examples
|
||||
|
||||
There are a lot of options, [documented](#user-documentation) in separate pages.
|
||||
@ -141,7 +151,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 +191,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)
|
||||
|
||||
|
||||
@ -199,7 +210,7 @@ work][donate]:
|
||||
## Licence
|
||||
|
||||
Copyright (C) 2018 Genymobile
|
||||
Copyright (C) 2018-2024 Romain Vimont
|
||||
Copyright (C) 2018-2025 Romain Vimont
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -2,6 +2,7 @@ _scrcpy() {
|
||||
local cur prev words cword
|
||||
local opts="
|
||||
--always-on-top
|
||||
--angle
|
||||
--audio-bit-rate=
|
||||
--audio-buffer=
|
||||
--audio-codec=
|
||||
@ -17,10 +18,10 @@ _scrcpy() {
|
||||
--camera-fps=
|
||||
--camera-high-speed
|
||||
--camera-size=
|
||||
--capture-orientation=
|
||||
--crop=
|
||||
-d --select-usb
|
||||
--disable-screensaver
|
||||
--display-buffer=
|
||||
--display-id=
|
||||
--display-orientation=
|
||||
-e --select-tcpip
|
||||
@ -38,8 +39,6 @@ _scrcpy() {
|
||||
--list-cameras
|
||||
--list-displays
|
||||
--list-encoders
|
||||
--lock-video-orientation
|
||||
--lock-video-orientation=
|
||||
-m --max-size=
|
||||
-M
|
||||
--max-fps=
|
||||
@ -58,6 +57,8 @@ _scrcpy() {
|
||||
--no-mipmaps
|
||||
--no-mouse-hover
|
||||
--no-power-on
|
||||
--no-vd-destroy-content
|
||||
--no-vd-system-decorations
|
||||
--no-video
|
||||
--no-video-playback
|
||||
--orientation=
|
||||
@ -78,6 +79,7 @@ _scrcpy() {
|
||||
--rotation=
|
||||
-s --serial=
|
||||
-S --turn-screen-off
|
||||
--screen-off-timeout=
|
||||
--shortcut-mod=
|
||||
--start-app=
|
||||
-t --show-touches
|
||||
@ -90,6 +92,7 @@ _scrcpy() {
|
||||
--v4l2-sink=
|
||||
-v --version
|
||||
-V --verbosity=
|
||||
--video-buffer=
|
||||
--video-codec=
|
||||
--video-codec-options=
|
||||
--video-encoder=
|
||||
@ -137,6 +140,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 +152,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
|
||||
@ -191,9 +194,9 @@ _scrcpy() {
|
||||
|--camera-size \
|
||||
|--crop \
|
||||
|--display-id \
|
||||
|--display-buffer \
|
||||
|--max-fps \
|
||||
|-m|--max-size \
|
||||
|--new-display \
|
||||
|-p|--port \
|
||||
|--push-target \
|
||||
|--rotation \
|
||||
@ -201,6 +204,7 @@ _scrcpy() {
|
||||
|--tunnel-port \
|
||||
|--v4l2-buffer \
|
||||
|--v4l2-sink \
|
||||
|--video-buffer \
|
||||
|--video-codec-options \
|
||||
|--video-encoder \
|
||||
|--tcpip \
|
||||
|
@ -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,10 +25,10 @@ arguments=(
|
||||
'--camera-facing=[Select the device camera by its facing direction]:facing:(front back external)'
|
||||
'--camera-fps=[Specify the camera capture frame rate]'
|
||||
'--camera-size=[Specify an explicit camera capture size]'
|
||||
'--capture-orientation=[Set the capture video orientation]:orientation:(0 90 180 270 flip0 flip90 flip180 flip270 @0 @90 @180 @270 @flip0 @flip90 @flip180 @flip270)'
|
||||
'--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]'
|
||||
{-d,--select-usb}'[Use USB device]'
|
||||
'--disable-screensaver[Disable screensaver while scrcpy is running]'
|
||||
'--display-buffer=[Add a buffering delay \(in milliseconds\) before displaying]'
|
||||
'--display-id=[Specify the display id to mirror]'
|
||||
'--display-orientation=[Set the initial display orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)'
|
||||
{-e,--select-tcpip}'[Use TCP/IP device]'
|
||||
@ -45,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]'
|
||||
@ -63,6 +63,8 @@ arguments=(
|
||||
'--no-mipmaps[Disable the generation of mipmaps]'
|
||||
'--no-mouse-hover[Do not forward mouse hover events]'
|
||||
'--no-power-on[Do not power on the device on start]'
|
||||
'--no-vd-destroy-content[Disable virtual display "destroy content on removal" flag]'
|
||||
'--no-vd-system-decorations[Disable virtual display system decorations flag]'
|
||||
'--no-video[Disable video forwarding]'
|
||||
'--no-video-playback[Disable video playback]'
|
||||
'--orientation=[Set the video orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)'
|
||||
@ -81,6 +83,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]'
|
||||
@ -92,6 +95,7 @@ arguments=(
|
||||
'--v4l2-sink=[\[\/dev\/videoN\] Output to v4l2loopback device]'
|
||||
{-v,--version}'[Print the version of scrcpy]'
|
||||
{-V,--verbosity=}'[Set the log level]:verbosity:(verbose debug info warn error)'
|
||||
'--video-buffer=[Add a buffering delay \(in milliseconds\) before displaying video frames]'
|
||||
'--video-codec=[Select the video codec]:codec:(h264 h265 av1)'
|
||||
'--video-codec-options=[Set a list of comma-separated key\:type=value options for the device video encoder]'
|
||||
'--video-encoder=[Use a specific MediaCodec video encoder]'
|
||||
|
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() {
|
||||
|
68
app/deps/dav1d.sh
Executable file
68
app/deps/dav1d.sh
Executable file
@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bash
|
||||
set -ex
|
||||
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
|
||||
cd "$DEPS_DIR"
|
||||
. common
|
||||
process_args "$@"
|
||||
|
||||
VERSION=1.5.0
|
||||
FILENAME=dav1d-$VERSION.tar.gz
|
||||
PROJECT_DIR=dav1d-$VERSION
|
||||
SHA256SUM=78b15d9954b513ea92d27f39362535ded2243e1b0924fde39f37a31ebed5f76b
|
||||
|
||||
cd "$SOURCES_DIR"
|
||||
|
||||
if [[ -d "$PROJECT_DIR" ]]
|
||||
then
|
||||
echo "$PWD/$PROJECT_DIR" found
|
||||
else
|
||||
get_file "https://code.videolan.org/videolan/dav1d/-/archive/$VERSION/$FILENAME" "$FILENAME" "$SHA256SUM"
|
||||
tar xf "$FILENAME" # First level directory is "$PROJECT_DIR"
|
||||
fi
|
||||
|
||||
mkdir -p "$BUILD_DIR/$PROJECT_DIR"
|
||||
cd "$BUILD_DIR/$PROJECT_DIR"
|
||||
|
||||
if [[ -d "$DIRNAME" ]]
|
||||
then
|
||||
echo "'$PWD/$DIRNAME' already exists, not reconfigured"
|
||||
cd "$DIRNAME"
|
||||
else
|
||||
mkdir "$DIRNAME"
|
||||
cd "$DIRNAME"
|
||||
|
||||
conf=(
|
||||
--prefix="$INSTALL_DIR/$DIRNAME"
|
||||
--libdir=lib
|
||||
-Denable_tests=false
|
||||
-Denable_tools=false
|
||||
# Always build dav1d statically
|
||||
--default-library=static
|
||||
)
|
||||
|
||||
if [[ "$BUILD_TYPE" == cross ]]
|
||||
then
|
||||
case "$HOST" in
|
||||
win32)
|
||||
conf+=(
|
||||
--cross-file="$SOURCES_DIR/$PROJECT_DIR/package/crossfiles/i686-w64-mingw32.meson"
|
||||
)
|
||||
;;
|
||||
|
||||
win64)
|
||||
conf+=(
|
||||
--cross-file="$SOURCES_DIR/$PROJECT_DIR/package/crossfiles/x86_64-w64-mingw32.meson"
|
||||
)
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Unsupported host: $HOST" >&2
|
||||
exit 1
|
||||
esac
|
||||
fi
|
||||
|
||||
meson setup . "$SOURCES_DIR/$PROJECT_DIR" "${conf[@]}"
|
||||
fi
|
||||
|
||||
ninja
|
||||
ninja install
|
@ -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 PKG_CONFIG_PATH="/opt/homebrew/opt/zlib/lib/pkgconfig"
|
||||
fi
|
||||
|
||||
if [[ -d "$HOST" ]]
|
||||
then
|
||||
echo "'$PWD/$HOST' already exists, not reconfigured"
|
||||
cd "$HOST"
|
||||
else
|
||||
mkdir "$HOST"
|
||||
cd "$HOST"
|
||||
export PKG_CONFIG_PATH="$INSTALL_DIR/$DIRNAME/lib/pkgconfig:$PKG_CONFIG_PATH"
|
||||
|
||||
"$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"
|
||||
--pkg-config-flags="--static"
|
||||
--extra-cflags="-O2 -fPIC"
|
||||
--disable-programs
|
||||
--disable-doc
|
||||
--disable-swscale
|
||||
--disable-postproc
|
||||
--disable-avfilter
|
||||
--disable-network
|
||||
--disable-everything
|
||||
--disable-vulkan
|
||||
--disable-vaapi
|
||||
--disable-vdpau
|
||||
--enable-swresample
|
||||
--enable-libdav1d
|
||||
--enable-decoder=h264
|
||||
--enable-decoder=hevc
|
||||
--enable-decoder=av1
|
||||
--enable-decoder=libdav1d
|
||||
--enable-decoder=pcm_s16le
|
||||
--enable-decoder=opus
|
||||
--enable-decoder=aac
|
||||
--enable-decoder=flac
|
||||
--enable-decoder=png
|
||||
--enable-protocol=file
|
||||
--enable-demuxer=image2
|
||||
--enable-parser=png
|
||||
--enable-zlib
|
||||
--enable-muxer=matroska
|
||||
--enable-muxer=mp4
|
||||
--enable-muxer=opus
|
||||
--enable-muxer=flac
|
||||
--enable-muxer=wav
|
||||
)
|
||||
|
||||
if [[ "$HOST" == linux ]]
|
||||
then
|
||||
conf+=(
|
||||
--enable-libv4l2
|
||||
--enable-outdev=v4l2
|
||||
--enable-encoder=rawvideo
|
||||
)
|
||||
else
|
||||
# libavdevice is only used for V4L2 on Linux
|
||||
conf+=(
|
||||
--disable-avdevice
|
||||
)
|
||||
fi
|
||||
|
||||
if [[ "$LINK_TYPE" == static ]]
|
||||
then
|
||||
conf+=(
|
||||
--enable-static
|
||||
--disable-shared
|
||||
)
|
||||
else
|
||||
conf+=(
|
||||
--disable-static
|
||||
--enable-shared
|
||||
)
|
||||
fi
|
||||
|
||||
if [[ "$BUILD_TYPE" == cross ]]
|
||||
then
|
||||
conf+=(
|
||||
--enable-cross-compile
|
||||
--cross-prefix="${HOST_TRIPLET}-"
|
||||
--cc="${HOST_TRIPLET}-gcc"
|
||||
)
|
||||
|
||||
case "$HOST" in
|
||||
win32)
|
||||
conf+=(
|
||||
--target-os=mingw32
|
||||
--arch=x86
|
||||
)
|
||||
;;
|
||||
|
||||
win64)
|
||||
conf+=(
|
||||
--target-os=mingw32
|
||||
--arch=x86_64
|
||||
)
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Unsupported host: $HOST" >&2
|
||||
exit 1
|
||||
esac
|
||||
fi
|
||||
|
||||
"$SOURCES_DIR/$PROJECT_DIR"/configure "${conf[@]}"
|
||||
fi
|
||||
|
||||
make -j
|
||||
|
@ -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.10
|
||||
FILENAME=SDL-$VERSION.tar.gz
|
||||
PROJECT_DIR=SDL-release-$VERSION
|
||||
SHA256SUM=1578c96f62c9ae36b64e431b2aa0e0b0fd07c275dedbc694afc38e19056688f5
|
||||
SHA256SUM=35a8b9c4f3635d85762b904ac60ca4e0806bff89faeb269caafbe80860d67168
|
||||
|
||||
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.1"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
97
app/scrcpy.1
97
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
|
||||
@ -139,12 +153,6 @@ Also see \fB\-e\fR (\fB\-\-select\-tcpip\fR).
|
||||
.BI "\-\-disable\-screensaver"
|
||||
Disable screensaver while scrcpy is running.
|
||||
|
||||
.TP
|
||||
.BI "\-\-display\-buffer " ms
|
||||
Add a buffering delay (in milliseconds) before displaying. This increases latency to compensate for jitter.
|
||||
|
||||
Default is 0 (no buffering).
|
||||
|
||||
.TP
|
||||
.BI "\-\-display\-id " id
|
||||
Specify the device display id to mirror.
|
||||
@ -247,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.
|
||||
@ -320,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
|
||||
@ -372,6 +369,16 @@ Do not forward mouse hover (mouse motion without any clicks) events.
|
||||
.B \-\-no\-power\-on
|
||||
Do not power on the device on start.
|
||||
|
||||
.TP
|
||||
.B \-\-no\-vd\-destroy\-content
|
||||
Disable virtual display "destroy content on removal" flag.
|
||||
|
||||
With this option, when the virtual display is closed, the running apps are moved to the main display rather than being destroyed.
|
||||
|
||||
.TP
|
||||
.B \-\-no\-vd\-system\-decorations
|
||||
Disable virtual display system decorations flag.
|
||||
|
||||
.TP
|
||||
.B \-\-no\-video
|
||||
Disable video forwarding.
|
||||
@ -517,13 +524,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.
|
||||
@ -554,13 +563,19 @@ Default is "info" for release builds, "debug" for debug builds.
|
||||
.BI "\-\-v4l2-sink " /dev/videoN
|
||||
Output to v4l2loopback device.
|
||||
|
||||
It requires to lock the video orientation (see \fB\-\-lock\-video\-orientation\fR).
|
||||
|
||||
.TP
|
||||
.BI "\-\-v4l2-buffer " ms
|
||||
Add a buffering delay (in milliseconds) before pushing frames. This increases latency to compensate for jitter.
|
||||
|
||||
This option is similar to \fB\-\-display\-buffer\fR, but specific to V4L2 sink.
|
||||
This option is similar to \fB\-\-video\-buffer\fR, but specific to V4L2 sink.
|
||||
|
||||
Default is 0 (no buffering).
|
||||
|
||||
.TP
|
||||
.BI "\-\-video\-buffer " ms
|
||||
Add a buffering delay (in milliseconds) before displaying video frames.
|
||||
|
||||
This increases latency to compensate for jitter.
|
||||
|
||||
Default is 0 (no buffering).
|
||||
|
||||
@ -669,6 +684,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)
|
||||
@ -810,7 +829,7 @@ Report bugs to <https://github.com/Genymobile/scrcpy/issues>.
|
||||
.SH COPYRIGHT
|
||||
Copyright \(co 2018 Genymobile <https://www.genymobile.com>
|
||||
|
||||
Copyright \(co 2018\-2024 Romain Vimont <rom@rom1v.com>
|
||||
Copyright \(co 2018\-2025 Romain Vimont <rom@rom1v.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0.
|
||||
|
||||
|
@ -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;
|
||||
|
339
app/src/cli.c
339
app/src/cli.c
@ -50,6 +50,7 @@ enum {
|
||||
OPT_POWER_OFF_ON_CLOSE,
|
||||
OPT_V4L2_SINK,
|
||||
OPT_DISPLAY_BUFFER,
|
||||
OPT_VIDEO_BUFFER,
|
||||
OPT_V4L2_BUFFER,
|
||||
OPT_TUNNEL_HOST,
|
||||
OPT_TUNNEL_PORT,
|
||||
@ -105,6 +106,11 @@ enum {
|
||||
OPT_NEW_DISPLAY,
|
||||
OPT_LIST_APPS,
|
||||
OPT_START_APP,
|
||||
OPT_SCREEN_OFF_TIMEOUT,
|
||||
OPT_CAPTURE_ORIENTATION,
|
||||
OPT_ANGLE,
|
||||
OPT_NO_VD_SYSTEM_DECORATIONS,
|
||||
OPT_NO_VD_DESTROY_CONTENT,
|
||||
};
|
||||
|
||||
struct sc_option {
|
||||
@ -146,6 +152,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",
|
||||
@ -243,14 +256,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",
|
||||
@ -258,6 +263,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",
|
||||
@ -265,6 +278,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",
|
||||
@ -272,12 +293,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
|
||||
@ -300,8 +330,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',
|
||||
@ -321,12 +350,10 @@ static const struct sc_option options[] = {
|
||||
.argdesc = "id",
|
||||
},
|
||||
{
|
||||
// deprecated
|
||||
.longopt_id = OPT_DISPLAY_BUFFER,
|
||||
.longopt = "display-buffer",
|
||||
.argdesc = "ms",
|
||||
.text = "Add a buffering delay (in milliseconds) before displaying. "
|
||||
"This increases latency to compensate for jitter.\n"
|
||||
"Default is 0 (no buffering).",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_DISPLAY_ID,
|
||||
@ -471,18 +498,10 @@ static const struct sc_option options[] = {
|
||||
.text = "List video and audio encoders available on the device.",
|
||||
},
|
||||
{
|
||||
// deprecated
|
||||
.longopt_id = OPT_LOCK_VIDEO_ORIENTATION,
|
||||
.longopt = "lock-video-orientation",
|
||||
.argdesc = "value",
|
||||
.optional_arg = true,
|
||||
.text = "Lock capture video orientation to value.\n"
|
||||
"Possible values are \"unlocked\", \"initial\" (locked to the "
|
||||
"initial orientation), 0, 90, 180 and 270. The values "
|
||||
"represent the clockwise rotation from the natural device "
|
||||
"orientation, in degrees.\n"
|
||||
"Default is \"unlocked\".\n"
|
||||
"Passing the option without argument is equivalent to passing "
|
||||
"\"initial\".",
|
||||
},
|
||||
{
|
||||
.shortopt = 'm',
|
||||
@ -572,12 +591,11 @@ static const struct sc_option options[] = {
|
||||
.optional_arg = true,
|
||||
.text = "Create a new display with the specified resolution and "
|
||||
"density. If not provided, they default to the main display "
|
||||
"dimensions and DPI, and --max-size is considered.\n"
|
||||
"dimensions and DPI.\n"
|
||||
"Examples:\n"
|
||||
" --new-display=1920x1080\n"
|
||||
" --new-display=1920x1080/420 # force 420 dpi\n"
|
||||
" --new-display # main display size and density\n"
|
||||
" --new-display -m1920 # scaled to fit a max size of 1920\n"
|
||||
" --new-display=/240 # main display size and 240 dpi",
|
||||
},
|
||||
{
|
||||
@ -642,6 +660,20 @@ static const struct sc_option options[] = {
|
||||
.longopt = "no-power-on",
|
||||
.text = "Do not power on the device on start.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_NO_VD_DESTROY_CONTENT,
|
||||
.longopt = "no-vd-destroy-content",
|
||||
.text = "Disable virtual display \"destroy content on removal\" "
|
||||
"flag.\n"
|
||||
"With this option, when the virtual display is closed, the "
|
||||
"running apps are moved to the main display rather than being "
|
||||
"destroyed.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_NO_VD_SYSTEM_DECORATIONS,
|
||||
.longopt = "no-vd-system-decorations",
|
||||
.text = "Disable virtual display system decorations flag.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_NO_VIDEO,
|
||||
.longopt = "no-video",
|
||||
@ -794,6 +826,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",
|
||||
@ -831,16 +870,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,
|
||||
@ -888,8 +928,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.",
|
||||
},
|
||||
{
|
||||
@ -898,11 +936,20 @@ static const struct sc_option options[] = {
|
||||
.argdesc = "ms",
|
||||
.text = "Add a buffering delay (in milliseconds) before pushing "
|
||||
"frames. This increases latency to compensate for jitter.\n"
|
||||
"This option is similar to --display-buffer, but specific to "
|
||||
"This option is similar to --video-buffer, but specific to "
|
||||
"V4L2 sink.\n"
|
||||
"Default is 0 (no buffering).\n"
|
||||
"This option is only available on Linux.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_VIDEO_BUFFER,
|
||||
.longopt = "video-buffer",
|
||||
.argdesc = "ms",
|
||||
.text = "Add a buffering delay (in milliseconds) before displaying "
|
||||
"video frames.\n"
|
||||
"This increases latency to compensate for jitter.\n"
|
||||
"Default is 0 (no buffering).",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_VIDEO_CODEC,
|
||||
.longopt = "video-codec",
|
||||
@ -1014,6 +1061,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)",
|
||||
@ -1562,78 +1613,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")) {
|
||||
@ -1673,6 +1652,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"
|
||||
@ -2143,6 +2148,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")) {
|
||||
@ -2268,8 +2287,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;
|
||||
@ -2333,8 +2352,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;
|
||||
@ -2352,8 +2376,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;
|
||||
@ -2439,32 +2464,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;
|
||||
@ -2525,23 +2527,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;
|
||||
@ -2549,7 +2537,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
opts->power_off_on_close = true;
|
||||
break;
|
||||
case OPT_DISPLAY_BUFFER:
|
||||
if (!parse_buffering_time(optarg, &opts->display_buffer)) {
|
||||
LOGE("--display-buffer has been removed, use --video-buffer "
|
||||
"instead.");
|
||||
return false;
|
||||
case OPT_VIDEO_BUFFER:
|
||||
if (!parse_buffering_time(optarg, &opts->video_buffer)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
@ -2714,6 +2706,21 @@ 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_DESTROY_CONTENT:
|
||||
opts->vd_destroy_content = false;
|
||||
break;
|
||||
case OPT_NO_VD_SYSTEM_DECORATIONS:
|
||||
opts->vd_system_decorations = false;
|
||||
break;
|
||||
default:
|
||||
// getopt prints the error message on stderr
|
||||
return false;
|
||||
@ -2808,13 +2815,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;
|
||||
}
|
||||
|
||||
// V4L2 could not handle size change.
|
||||
// Do not log because downsizing on error is the default behavior,
|
||||
// not an explicit request from the user.
|
||||
@ -2822,7 +2822,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
}
|
||||
|
||||
if (opts->v4l2_buffer && !opts->v4l2_device) {
|
||||
LOGE("V4L2 buffer value without V4L2 sink\n");
|
||||
LOGE("V4L2 buffer value without V4L2 sink");
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
@ -2841,8 +2841,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
if (otg) {
|
||||
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_AOA;
|
||||
} else if (!opts->video_playback) {
|
||||
LOGI("No video mirroring, mouse mode switched to UHID");
|
||||
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_UHID;
|
||||
LOGI("No video mirroring, SDK mouse disabled");
|
||||
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_DISABLED;
|
||||
} else {
|
||||
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_SDK;
|
||||
}
|
||||
@ -2904,13 +2904,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) {
|
||||
|
@ -22,9 +22,6 @@
|
||||
#define MOTIONEVENT_ACTION_LABEL(value) \
|
||||
ENUM_TO_LABEL(android_motionevent_action_labels, value)
|
||||
|
||||
#define SCREEN_POWER_MODE_LABEL(value) \
|
||||
ENUM_TO_LABEL(screen_power_mode_labels, value)
|
||||
|
||||
static const char *const android_keyevent_action_labels[] = {
|
||||
"down",
|
||||
"up",
|
||||
@ -47,14 +44,6 @@ static const char *const android_motionevent_action_labels[] = {
|
||||
"btn-release",
|
||||
};
|
||||
|
||||
static const char *const screen_power_mode_labels[] = {
|
||||
"off",
|
||||
"doze",
|
||||
"normal",
|
||||
"doze-suspend",
|
||||
"suspend",
|
||||
};
|
||||
|
||||
static const char *const copy_key_labels[] = {
|
||||
"none",
|
||||
"copy",
|
||||
@ -158,13 +147,15 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) {
|
||||
size_t len = write_string(&buf[10], msg->set_clipboard.text,
|
||||
SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH);
|
||||
return 10 + len;
|
||||
case SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE:
|
||||
buf[1] = msg->set_screen_power_mode.mode;
|
||||
case SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER:
|
||||
buf[1] = msg->set_display_power.on;
|
||||
return 2;
|
||||
case SC_CONTROL_MSG_TYPE_UHID_CREATE:
|
||||
sc_write16be(&buf[1], msg->uhid_create.id);
|
||||
sc_write16be(&buf[3], msg->uhid_create.vendor_id);
|
||||
sc_write16be(&buf[5], msg->uhid_create.product_id);
|
||||
|
||||
size_t index = 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);
|
||||
@ -192,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:
|
||||
@ -268,9 +260,9 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
|
||||
msg->set_clipboard.paste ? "paste" : "nopaste",
|
||||
msg->set_clipboard.text);
|
||||
break;
|
||||
case SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE:
|
||||
LOG_CMSG("power mode %s",
|
||||
SCREEN_POWER_MODE_LABEL(msg->set_screen_power_mode.mode));
|
||||
case SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER:
|
||||
LOG_CMSG("display power %s",
|
||||
msg->set_display_power.on ? "on" : "off");
|
||||
break;
|
||||
case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL:
|
||||
LOG_CMSG("expand notification panel");
|
||||
@ -288,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: {
|
||||
@ -315,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;
|
||||
|
@ -35,19 +35,14 @@ enum sc_control_msg_type {
|
||||
SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS,
|
||||
SC_CONTROL_MSG_TYPE_GET_CLIPBOARD,
|
||||
SC_CONTROL_MSG_TYPE_SET_CLIPBOARD,
|
||||
SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE,
|
||||
SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER,
|
||||
SC_CONTROL_MSG_TYPE_ROTATE_DEVICE,
|
||||
SC_CONTROL_MSG_TYPE_UHID_CREATE,
|
||||
SC_CONTROL_MSG_TYPE_UHID_INPUT,
|
||||
SC_CONTROL_MSG_TYPE_UHID_DESTROY,
|
||||
SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS,
|
||||
SC_CONTROL_MSG_TYPE_START_APP,
|
||||
};
|
||||
|
||||
enum sc_screen_power_mode {
|
||||
// see <https://android.googlesource.com/platform/frameworks/base.git/+/pie-release-2/core/java/android/view/SurfaceControl.java#305>
|
||||
SC_SCREEN_POWER_MODE_OFF = 0,
|
||||
SC_SCREEN_POWER_MODE_NORMAL = 2,
|
||||
SC_CONTROL_MSG_TYPE_RESET_VIDEO,
|
||||
};
|
||||
|
||||
enum sc_copy_key {
|
||||
@ -95,10 +90,12 @@ struct sc_control_msg {
|
||||
bool paste;
|
||||
} set_clipboard;
|
||||
struct {
|
||||
enum sc_screen_power_mode mode;
|
||||
} set_screen_power_mode;
|
||||
bool on;
|
||||
} set_display_power;
|
||||
struct {
|
||||
uint16_t id;
|
||||
uint16_t vendor_id;
|
||||
uint16_t product_id;
|
||||
const char *name; // pointer to static data
|
||||
uint16_t report_desc_size;
|
||||
const uint8_t *report_desc; // pointer to static data
|
||||
|
@ -15,7 +15,6 @@ struct sc_hid_input {
|
||||
|
||||
struct sc_hid_open {
|
||||
uint16_t hid_id;
|
||||
const char *name; // pointer to static memory
|
||||
const uint8_t *report_desc; // pointer to static memory
|
||||
size_t report_desc_size;
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
@ -243,14 +246,8 @@ sc_hid_gamepad_generate_open(struct sc_hid_gamepad *hid,
|
||||
|
||||
sc_hid_gamepad_slot_init(&hid->slots[slot_idx], gamepad_id);
|
||||
|
||||
SDL_GameController* game_controller =
|
||||
SDL_GameControllerFromInstanceID(gamepad_id);
|
||||
assert(game_controller);
|
||||
const char *name = SDL_GameControllerName(game_controller);
|
||||
|
||||
uint16_t hid_id = sc_hid_gamepad_slot_get_id(slot_idx);
|
||||
hid_open->hid_id = hid_id;
|
||||
hid_open->name = name;
|
||||
hid_open->report_desc = SC_HID_GAMEPAD_REPORT_DESC;
|
||||
hid_open->report_desc_size = sizeof(SC_HID_GAMEPAD_REPORT_DESC);
|
||||
|
||||
@ -423,8 +420,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);
|
||||
|
@ -335,7 +335,6 @@ sc_hid_keyboard_generate_input_from_mods(struct sc_hid_input *hid_input,
|
||||
|
||||
void sc_hid_keyboard_generate_open(struct sc_hid_open *hid_open) {
|
||||
hid_open->hid_id = SC_HID_ID_KEYBOARD;
|
||||
hid_open->name = NULL; // No name specified after "scrcpy"
|
||||
hid_open->report_desc = SC_HID_KEYBOARD_REPORT_DESC;
|
||||
hid_open->report_desc_size = sizeof(SC_HID_KEYBOARD_REPORT_DESC);
|
||||
}
|
||||
|
@ -190,7 +190,6 @@ 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->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,12 @@ 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;
|
||||
uint32_t gamepad_id;
|
||||
};
|
||||
|
||||
@ -503,16 +497,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) {
|
||||
|
@ -203,13 +203,12 @@ set_device_clipboard(struct sc_input_manager *im, bool paste,
|
||||
}
|
||||
|
||||
static void
|
||||
set_screen_power_mode(struct sc_input_manager *im,
|
||||
enum sc_screen_power_mode mode) {
|
||||
set_display_power(struct sc_input_manager *im, bool on) {
|
||||
assert(im->controller);
|
||||
|
||||
struct sc_control_msg msg;
|
||||
msg.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE;
|
||||
msg.set_screen_power_mode.mode = mode;
|
||||
msg.type = SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER;
|
||||
msg.set_display_power.on = on;
|
||||
|
||||
if (!sc_controller_push_msg(im->controller, &msg)) {
|
||||
LOGW("Could not request 'set screen power mode'");
|
||||
@ -285,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) {
|
||||
@ -415,10 +426,8 @@ sc_input_manager_process_key(struct sc_input_manager *im,
|
||||
return;
|
||||
case SDLK_o:
|
||||
if (control && !repeat && down && !paused) {
|
||||
enum sc_screen_power_mode mode = shift
|
||||
? SC_SCREEN_POWER_MODE_NORMAL
|
||||
: SC_SCREEN_POWER_MODE_OFF;
|
||||
set_screen_power_mode(im, mode);
|
||||
bool on = shift;
|
||||
set_display_power(im, on);
|
||||
}
|
||||
return;
|
||||
case SDLK_z:
|
||||
@ -524,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:
|
||||
@ -895,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) {
|
||||
@ -910,9 +922,12 @@ sc_input_manager_process_gamepad_device(struct sc_input_manager *im,
|
||||
return;
|
||||
}
|
||||
|
||||
id = SDL_JoystickInstanceID(joystick);
|
||||
struct sc_gamepad_device_event evt = {
|
||||
.gamepad_id = SDL_JoystickInstanceID(joystick),
|
||||
};
|
||||
im->gp->ops->process_gamepad_added(im->gp, &evt);
|
||||
} else if (event->type == SDL_CONTROLLERDEVICEREMOVED) {
|
||||
id = event->which;
|
||||
SDL_JoystickID id = event->which;
|
||||
|
||||
SDL_GameController *gc = SDL_GameControllerFromInstanceID(id);
|
||||
if (gc) {
|
||||
@ -920,16 +935,15 @@ sc_input_manager_process_gamepad_device(struct sc_input_manager *im,
|
||||
} else {
|
||||
LOGW("Unknown gamepad device removed");
|
||||
}
|
||||
|
||||
struct sc_gamepad_device_event evt = {
|
||||
.gamepad_id = id,
|
||||
};
|
||||
im->gp->ops->process_gamepad_removed(im->gp, &evt);
|
||||
} else {
|
||||
// Nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
struct sc_gamepad_device_event evt = {
|
||||
.type = sc_gamepad_device_event_type_from_sdl_type(event->type),
|
||||
.gamepad_id = id,
|
||||
};
|
||||
im->gp->ops->process_gamepad_device(im->gp, &evt);
|
||||
}
|
||||
|
||||
static void
|
||||
|
@ -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,
|
||||
@ -58,10 +59,11 @@ const struct scrcpy_options scrcpy_options_default = {
|
||||
.window_width = 0,
|
||||
.window_height = 0,
|
||||
.display_id = 0,
|
||||
.display_buffer = 0,
|
||||
.video_buffer = 0,
|
||||
.audio_buffer = -1, // depends on the audio format,
|
||||
.audio_output_buffer = SC_TICK_FROM_MS(5),
|
||||
.time_limit = 0,
|
||||
.screen_off_timeout = -1,
|
||||
#ifdef HAVE_V4L2
|
||||
.v4l2_device = NULL,
|
||||
.v4l2_buffer = 0,
|
||||
@ -105,6 +107,9 @@ const struct scrcpy_options scrcpy_options_default = {
|
||||
.audio_dup = false,
|
||||
.new_display = NULL,
|
||||
.start_app = NULL,
|
||||
.angle = NULL,
|
||||
.vd_destroy_content = true,
|
||||
.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,16 +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,
|
||||
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
|
||||
@ -251,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"
|
||||
@ -259,10 +257,11 @@ struct scrcpy_options {
|
||||
uint16_t window_width;
|
||||
uint16_t window_height;
|
||||
uint32_t display_id;
|
||||
sc_tick display_buffer;
|
||||
sc_tick video_buffer;
|
||||
sc_tick audio_buffer;
|
||||
sc_tick audio_output_buffer;
|
||||
sc_tick time_limit;
|
||||
sc_tick screen_off_timeout;
|
||||
#ifdef HAVE_V4L2
|
||||
const char *v4l2_device;
|
||||
sc_tick v4l2_buffer;
|
||||
@ -311,6 +310,8 @@ struct scrcpy_options {
|
||||
bool audio_dup;
|
||||
const char *new_display; // [<width>x<height>][/<dpi>] parsed by the server
|
||||
const char *start_app;
|
||||
bool vd_destroy_content;
|
||||
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);
|
||||
|
@ -53,7 +53,7 @@ struct scrcpy {
|
||||
struct sc_decoder video_decoder;
|
||||
struct sc_decoder audio_decoder;
|
||||
struct sc_recorder recorder;
|
||||
struct sc_delay_buffer display_buffer;
|
||||
struct sc_delay_buffer video_buffer;
|
||||
#ifdef HAVE_V4L2
|
||||
struct sc_v4l2_sink v4l2_sink;
|
||||
struct sc_delay_buffer v4l2_buffer;
|
||||
@ -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,8 @@ scrcpy(struct scrcpy_options *options) {
|
||||
.power_on = options->power_on,
|
||||
.kill_adb_on_close = options->kill_adb_on_close,
|
||||
.camera_high_speed = options->camera_high_speed,
|
||||
.vd_destroy_content = options->vd_destroy_content,
|
||||
.vd_system_decorations = options->vd_system_decorations,
|
||||
.list = options->list,
|
||||
};
|
||||
|
||||
@ -815,11 +820,11 @@ aoa_complete:
|
||||
|
||||
if (options->video_playback) {
|
||||
struct sc_frame_source *src = &s->video_decoder.frame_source;
|
||||
if (options->display_buffer) {
|
||||
sc_delay_buffer_init(&s->display_buffer,
|
||||
options->display_buffer, true);
|
||||
sc_frame_source_add_sink(src, &s->display_buffer.frame_sink);
|
||||
src = &s->display_buffer.frame_source;
|
||||
if (options->video_buffer) {
|
||||
sc_delay_buffer_init(&s->video_buffer,
|
||||
options->video_buffer, true);
|
||||
sc_frame_source_add_sink(src, &s->video_buffer.frame_sink);
|
||||
src = &s->video_buffer.frame_source;
|
||||
}
|
||||
|
||||
sc_frame_source_add_sink(src, &s->screen.frame_sink);
|
||||
@ -873,11 +878,11 @@ aoa_complete:
|
||||
// everything is set up
|
||||
if (options->control && options->turn_screen_off) {
|
||||
struct sc_control_msg msg;
|
||||
msg.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE;
|
||||
msg.set_screen_power_mode.mode = SC_SCREEN_POWER_MODE_OFF;
|
||||
msg.type = SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER;
|
||||
msg.set_display_power.on = false;
|
||||
|
||||
if (!sc_controller_push_msg(&s->controller, &msg)) {
|
||||
LOGW("Could not request 'set screen power mode'");
|
||||
LOGW("Could not request 'set display power'");
|
||||
}
|
||||
}
|
||||
|
||||
|
131
app/src/server.c
131
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,12 @@ execute_server(struct sc_server *server,
|
||||
VALIDATE_STRING(params->new_display);
|
||||
ADD_PARAM("new_display=%s", params->new_display);
|
||||
}
|
||||
if (!params->vd_destroy_content) {
|
||||
ADD_PARAM("vd_destroy_content=false");
|
||||
}
|
||||
if (!params->vd_system_decorations) {
|
||||
ADD_PARAM("vd_system_decorations=false");
|
||||
}
|
||||
if (params->list & SC_OPTION_LIST_ENCODERS) {
|
||||
ADD_PARAM("list_encoders=true");
|
||||
}
|
||||
@ -380,10 +404,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 +488,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 +510,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 +832,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 +855,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 +866,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 +891,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 +978,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 +1161,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,8 @@ struct sc_server_params {
|
||||
bool power_on;
|
||||
bool kill_adb_on_close;
|
||||
bool camera_high_speed;
|
||||
bool vd_destroy_content;
|
||||
bool vd_system_decorations;
|
||||
uint8_t list;
|
||||
};
|
||||
|
||||
|
@ -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 event
|
||||
*
|
||||
* 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_device_event *event);
|
||||
|
||||
/**
|
||||
* Process a gamepad device removed event
|
||||
*
|
||||
* This function is mandatory.
|
||||
*/
|
||||
void
|
||||
(*process_gamepad_removed)(struct sc_gamepad_processor *gp,
|
||||
const struct sc_gamepad_device_event *event);
|
||||
|
||||
/**
|
||||
* Process a gamepad axis event
|
||||
|
@ -7,6 +7,11 @@
|
||||
/** Downcast gamepad processor to sc_gamepad_uhid */
|
||||
#define DOWNCAST(GP) container_of(GP, struct sc_gamepad_uhid, gamepad_processor)
|
||||
|
||||
// Xbox 360
|
||||
#define SC_GAMEPAD_UHID_VENDOR_ID UINT16_C(0x045e)
|
||||
#define SC_GAMEPAD_UHID_PRODUCT_ID UINT16_C(0x028e)
|
||||
#define SC_GAMEPAD_UHID_NAME "Microsoft X-Box 360 Pad"
|
||||
|
||||
static void
|
||||
sc_gamepad_uhid_send_input(struct sc_gamepad_uhid *gamepad,
|
||||
const struct sc_hid_input *hid_input,
|
||||
@ -30,7 +35,9 @@ 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.name = hid_open->name;
|
||||
msg.uhid_create.vendor_id = SC_GAMEPAD_UHID_VENDOR_ID;
|
||||
msg.uhid_create.product_id = SC_GAMEPAD_UHID_PRODUCT_ID;
|
||||
msg.uhid_create.name = SC_GAMEPAD_UHID_NAME;
|
||||
msg.uhid_create.report_desc = hid_open->report_desc;
|
||||
msg.uhid_create.report_desc_size = hid_open->report_desc_size;
|
||||
|
||||
@ -52,29 +59,39 @@ sc_gamepad_uhid_send_close(struct sc_gamepad_uhid *gamepad,
|
||||
}
|
||||
|
||||
static void
|
||||
sc_gamepad_processor_process_gamepad_device(struct sc_gamepad_processor *gp,
|
||||
sc_gamepad_processor_process_gamepad_added(struct sc_gamepad_processor *gp,
|
||||
const struct sc_gamepad_device_event *event) {
|
||||
struct sc_gamepad_uhid *gamepad = DOWNCAST(gp);
|
||||
|
||||
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)) {
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_GameController* game_controller =
|
||||
SDL_GameControllerFromInstanceID(event->gamepad_id);
|
||||
assert(game_controller);
|
||||
const char *name = SDL_GameControllerName(game_controller);
|
||||
LOGI("Gamepad added: [%" PRIu32 "] %s", event->gamepad_id, name);
|
||||
|
||||
sc_gamepad_uhid_send_open(gamepad, &hid_open);
|
||||
}
|
||||
|
||||
static void
|
||||
sc_gamepad_processor_process_gamepad_removed(struct sc_gamepad_processor *gp,
|
||||
const struct sc_gamepad_device_event *event) {
|
||||
struct sc_gamepad_uhid *gamepad = DOWNCAST(gp);
|
||||
|
||||
struct sc_hid_close hid_close;
|
||||
if (!sc_hid_gamepad_generate_close(&gamepad->hid, &hid_close,
|
||||
event->gamepad_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOGI("Gamepad removed: [%" PRIu32 "]", event->gamepad_id);
|
||||
|
||||
sc_gamepad_uhid_send_close(gamepad, &hid_close);
|
||||
}
|
||||
|
||||
static void
|
||||
@ -114,7 +131,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,7 +141,9 @@ 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.name = hid_open.name;
|
||||
msg.uhid_create.vendor_id = 0;
|
||||
msg.uhid_create.product_id = 0;
|
||||
msg.uhid_create.name = NULL;
|
||||
msg.uhid_create.report_desc = hid_open.report_desc;
|
||||
msg.uhid_create.report_desc_size = hid_open.report_desc_size;
|
||||
if (!sc_controller_push_msg(controller, &msg)) {
|
||||
|
@ -81,7 +81,9 @@ 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.name = hid_open.name;
|
||||
msg.uhid_create.vendor_id = 0;
|
||||
msg.uhid_create.product_id = 0;
|
||||
msg.uhid_create.name = NULL;
|
||||
msg.uhid_create.report_desc = hid_open.report_desc;
|
||||
msg.uhid_create.report_desc_size = hid_open.report_desc_size;
|
||||
if (!sc_controller_push_msg(controller, &msg)) {
|
||||
|
@ -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,
|
||||
sc_gamepad_processor_process_gamepad_added(struct sc_gamepad_processor *gp,
|
||||
const struct sc_gamepad_device_event *event) {
|
||||
struct sc_gamepad_aoa *gamepad = DOWNCAST(gp);
|
||||
|
||||
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)) {
|
||||
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_device_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_device_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_device_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"));
|
||||
|
@ -289,11 +289,11 @@ static void test_serialize_set_clipboard_long(void) {
|
||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||
}
|
||||
|
||||
static void test_serialize_set_screen_power_mode(void) {
|
||||
static void test_serialize_set_display_power(void) {
|
||||
struct sc_control_msg msg = {
|
||||
.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE,
|
||||
.set_screen_power_mode = {
|
||||
.mode = SC_SCREEN_POWER_MODE_NORMAL,
|
||||
.type = SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER,
|
||||
.set_display_power = {
|
||||
.on = true,
|
||||
},
|
||||
};
|
||||
|
||||
@ -302,8 +302,8 @@ static void test_serialize_set_screen_power_mode(void) {
|
||||
assert(size == 2);
|
||||
|
||||
const uint8_t expected[] = {
|
||||
SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE,
|
||||
0x02, // SC_SCREEN_POWER_MODE_NORMAL
|
||||
SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER,
|
||||
0x01, // true
|
||||
};
|
||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||
}
|
||||
@ -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;
|
||||
@ -423,11 +442,12 @@ int main(int argc, char *argv[]) {
|
||||
test_serialize_get_clipboard();
|
||||
test_serialize_set_clipboard();
|
||||
test_serialize_set_clipboard_long();
|
||||
test_serialize_set_screen_power_mode();
|
||||
test_serialize_set_display_power();
|
||||
test_serialize_rotate_device();
|
||||
test_serialize_uhid_create();
|
||||
test_serialize_uhid_input();
|
||||
test_serialize_uhid_destroy();
|
||||
test_serialize_open_hard_keyboard();
|
||||
test_serialize_reset_video();
|
||||
return 0;
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -170,7 +170,7 @@ latency (for both [video](video.md#buffering) and audio) might be preferable to
|
||||
avoid glitches and smooth the playback:
|
||||
|
||||
```
|
||||
scrcpy --display-buffer=200 --audio-buffer=200
|
||||
scrcpy --video-buffer=200 --audio-buffer=200
|
||||
```
|
||||
|
||||
It is also possible to configure another audio buffer (the audio output buffer),
|
||||
|
@ -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.1`][direct-scrcpy-server]
|
||||
<sub>SHA-256: `958f0944a62f23b1f33a16e9eb14844c1a04b882ca175a738c16d23cb22b86c0`</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.1/scrcpy-server-v3.1
|
||||
|
||||
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
|
||||
|
||||
@ -107,16 +113,17 @@ with the device IP address you found)_.
|
||||
7. Run `scrcpy` as usual.
|
||||
8. Run `adb disconnect` once you're done.
|
||||
|
||||
Since Android 11, a [wireless debugging option][adb-wireless] allows to bypass
|
||||
having to physically connect your device directly to your computer.
|
||||
Since Android 11, a [wireless debugging option][adb-wireless] allows you to
|
||||
bypass having to physically connect your device to your computer.
|
||||
|
||||
[adb-wireless]: https://developer.android.com/studio/command-line/adb#wireless-android11-command-line
|
||||
|
||||
|
||||
## Autostart
|
||||
|
||||
A small tool (by the scrcpy author) allows to run arbitrary commands whenever a
|
||||
new Android device is connected: [AutoAdb]. It can be used to start scrcpy:
|
||||
A small tool (by the scrcpy author) allows you to run arbitrary commands
|
||||
whenever a new Android device is connected: [AutoAdb]. It can be used to start
|
||||
scrcpy:
|
||||
|
||||
```bash
|
||||
autoadb scrcpy -s '{}'
|
||||
|
@ -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):
|
||||
|
@ -21,9 +21,9 @@ the client and on the server.
|
||||
If video is enabled, then the server sends a raw video stream (H.264 by default)
|
||||
of the device screen, with some additional headers for each packet. The client
|
||||
decodes the video frames, and displays them as soon as possible, without
|
||||
buffering (unless `--display-buffer=delay` is specified) to minimize latency.
|
||||
The client is not aware of the device rotation (which is handled by the server),
|
||||
it just knows the dimensions of the video frames it receives.
|
||||
buffering (unless `--video-buffer=delay` is specified) to minimize latency. The
|
||||
client is not aware of the device rotation (which is handled by the server), it
|
||||
just knows the dimensions of the video frames it receives.
|
||||
|
||||
Similarly, if audio is enabled, then the server sends a raw audio stream (OPUS
|
||||
by default) of the device audio output (or the microphone if
|
||||
@ -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`
|
||||
|
@ -18,6 +18,46 @@ The initial state is restored when _scrcpy_ is closed.
|
||||
If the device is not plugged in (i.e. only connected over TCP/IP),
|
||||
`--stay-awake` has no effect (this is the Android behavior).
|
||||
|
||||
This changes the value of [`stay_on_while_plugged_in`], setting which can be
|
||||
changed manually:
|
||||
|
||||
[`stay_on_while_plugged_in`]: https://developer.android.com/reference/android/provider/Settings.Global#STAY_ON_WHILE_PLUGGED_IN
|
||||
|
||||
|
||||
```bash
|
||||
# get the current show_touches value
|
||||
adb shell settings get global stay_on_while_plugged_in
|
||||
# enable for AC/USB/wireless chargers
|
||||
adb shell settings put global stay_on_while_plugged_in 7
|
||||
# disable
|
||||
adb shell settings put global stay_on_while_plugged_in 0
|
||||
```
|
||||
|
||||
|
||||
## Screen off timeout
|
||||
|
||||
The Android screen automatically turns off after some delay.
|
||||
|
||||
To change this delay while scrcpy is running:
|
||||
|
||||
```bash
|
||||
scrcpy --screen-off-timeout=300 # 300 seconds (5 minutes)
|
||||
```
|
||||
|
||||
The initial value is restored on exit.
|
||||
|
||||
It is possible to change this setting manually:
|
||||
|
||||
```bash
|
||||
# get the current screen_off_timeout value
|
||||
adb shell settings get system screen_off_timeout
|
||||
# set a new value (in milliseconds)
|
||||
adb shell settings put system screen_off_timeout 30000
|
||||
```
|
||||
|
||||
Note that the Android value is in milliseconds, but the scrcpy command line
|
||||
argument is in seconds.
|
||||
|
||||
|
||||
## Turn screen off
|
||||
|
||||
@ -46,6 +86,15 @@ scrcpy --turn-screen-off --stay-awake
|
||||
scrcpy -Sw # short version
|
||||
```
|
||||
|
||||
Since Android 15, it is possible to change this setting manually:
|
||||
|
||||
```
|
||||
# turn screen off (0 for main display)
|
||||
adb shell cmd display power-off 0
|
||||
# turn screen on
|
||||
adb shell cmd display power-on 0
|
||||
```
|
||||
|
||||
|
||||
## Show touches
|
||||
|
||||
@ -62,6 +111,16 @@ scrcpy -t # short version
|
||||
|
||||
Note that it only shows _physical_ touches (by a finger on the device).
|
||||
|
||||
It is possible to change this setting manually:
|
||||
|
||||
```bash
|
||||
# get the current show_touches value
|
||||
adb shell settings get system show_touches
|
||||
# enable show_touches
|
||||
adb shell settings put system show_touches 1
|
||||
# disable show_touches
|
||||
adb shell settings put system show_touches 0
|
||||
```
|
||||
|
||||
## Power off on close
|
||||
|
||||
|
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.1.tar.gz`][direct-linux-x86_64] (x86_64)
|
||||
<sub>SHA-256: `37dba54092ed9ec6b2f8f95432f61b8ea124aec9f1e9f2b3d22d4b10bb04c59a`</sub>
|
||||
|
||||
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
|
||||
[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-linux-x86_64-v3.1.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.1.tar.gz`][direct-macos-aarch64] (aarch64)
|
||||
<sub>SHA-256: `478618d940421e5f57942f5479d493ecbb38210682937a200f712aee5f235daf`</sub>
|
||||
|
||||
- [`scrcpy-macos-x86_64-v3.1.tar.gz`][direct-macos-x86_64] (x86_64)
|
||||
<sub>SHA-256: `acde98e29c273710ffa469371dbca4a728a44c41c380381f8a54e5b5301b9e87`</sub>
|
||||
|
||||
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
|
||||
[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-macos-aarch64-v3.1.tar.gz
|
||||
[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-macos-x86_64-v3.1.tar.gz
|
||||
|
||||
and extract it.
|
||||
|
||||
_Static builds of scrcpy for macOS are still experimental._
|
||||
|
||||
|
||||
### From a package manager
|
||||
|
||||
Scrcpy is available in [Homebrew]:
|
||||
|
||||
```bash
|
||||
@ -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:
|
||||
|
12
doc/mouse.md
12
doc/mouse.md
@ -83,9 +83,9 @@ process like the _adb daemon_).
|
||||
## Mouse bindings
|
||||
|
||||
By default, with SDK mouse:
|
||||
- right-click triggers BACK (or POWER on)
|
||||
- middle-click triggers HOME
|
||||
- the 4th click triggers APP_SWITCH
|
||||
- right-click triggers `BACK` (or `POWER` on)
|
||||
- middle-click triggers `HOME`
|
||||
- the 4th click triggers `APP_SWITCH`
|
||||
- the 5th click expands the notification panel
|
||||
|
||||
The secondary clicks may be forwarded to the device instead by pressing the
|
||||
@ -121,9 +121,9 @@ Each character must be one of the following:
|
||||
|
||||
- `+`: forward the click to the device
|
||||
- `-`: ignore the click
|
||||
- `b`: trigger shortcut BACK (or turn screen on if off)
|
||||
- `h`: trigger shortcut HOME
|
||||
- `s`: trigger shortcut APP_SWITCH
|
||||
- `b`: trigger shortcut `BACK` (or turn screen on if off)
|
||||
- `h`: trigger shortcut `HOME`
|
||||
- `s`: trigger shortcut `APP_SWITCH`
|
||||
- `n`: trigger shortcut "expand notification panel"
|
||||
|
||||
For example:
|
||||
|
@ -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_
|
||||
|
69
doc/video.md
69
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
|
||||
|
||||
@ -189,15 +232,15 @@ The configuration is available independently for the display,
|
||||
[v4l2 sinks](video.md#video4linux) and [audio](audio.md#buffering) playback.
|
||||
|
||||
```bash
|
||||
scrcpy --display-buffer=50 # add 50ms buffering for display
|
||||
scrcpy --v4l2-buffer=300 # add 300ms buffering for v4l2 sink
|
||||
scrcpy --video-buffer=50 # add 50ms buffering for video playback
|
||||
scrcpy --audio-buffer=200 # set 200ms buffering for audio playback
|
||||
scrcpy --v4l2-buffer=300 # add 300ms buffering for v4l2 sink
|
||||
```
|
||||
|
||||
They can be applied simultaneously:
|
||||
|
||||
```bash
|
||||
scrcpy --display-buffer=50 --v4l2-buffer=300
|
||||
scrcpy --video-buffer=50 --v4l2-buffer=300
|
||||
```
|
||||
|
||||
|
||||
|
@ -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,49 @@ 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.
|
||||
|
||||
|
||||
## Destroy on close
|
||||
|
||||
By default, when the virtual display is closed, the running apps are destroyed.
|
||||
|
||||
To move them to the main display instead, use:
|
||||
|
||||
```
|
||||
scrcpy --new-display --no-vd-destroy-content
|
||||
```
|
||||
|
@ -2,35 +2,45 @@
|
||||
|
||||
## 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.1.zip`][direct-win64] (64-bit)
|
||||
<sub>SHA-256: `0c05ea395d95cfe36bee974eeb435a3db87ea5594ff738370d5dc3068a9538ca`</sub>
|
||||
- [`scrcpy-win32-v3.1.zip`][direct-win32] (32-bit)
|
||||
<sub>SHA-256: `2b4674ef76719680ac5a9b482d1943bdde3fa25821ad2e98f3c40c347d00d560`</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.1/scrcpy-win64-v3.1.zip
|
||||
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-win32-v3.1.zip
|
||||
|
||||
and extract it.
|
||||
|
||||
Alternatively, you could install it from packages manager, like [Chocolatey]:
|
||||
|
||||
### From a package manager
|
||||
|
||||
From [WinGet] (ADB and other dependencies will be installed alongside scrcpy):
|
||||
|
||||
```bash
|
||||
winget install --exact Genymobile.scrcpy
|
||||
```
|
||||
|
||||
From [Chocolatey]:
|
||||
|
||||
```bash
|
||||
choco install scrcpy
|
||||
choco install adb # if you don't have it yet
|
||||
```
|
||||
|
||||
or [Scoop]:
|
||||
|
||||
From [Scoop]:
|
||||
|
||||
```bash
|
||||
scoop install scrcpy
|
||||
scoop install adb # if you don't have it yet
|
||||
```
|
||||
|
||||
[Winget]: https://github.com/microsoft/winget-cli
|
||||
[WinGet]: https://github.com/microsoft/winget-cli
|
||||
[Chocolatey]: https://chocolatey.org/
|
||||
[Scoop]: https://scoop.sh
|
||||
|
||||
|
@ -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.1/scrcpy-server-v3.1
|
||||
PREBUILT_SERVER_SHA256=958f0944a62f23b1f33a16e9eb14844c1a04b882ca175a738c16d23cb22b86c0
|
||||
|
||||
echo "[scrcpy] Downloading prebuilt server..."
|
||||
wget "$PREBUILT_SERVER_URL" -O scrcpy-server
|
||||
|
@ -1,5 +1,5 @@
|
||||
project('scrcpy', 'c',
|
||||
version: '2.7',
|
||||
version: '3.1',
|
||||
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)}"
|
43
release/build_linux.sh
Executable file
43
release/build_linux.sh
Executable file
@ -0,0 +1,43 @@
|
||||
#!/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/dav1d.sh linux native static
|
||||
app/deps/ffmpeg.sh linux native static
|
||||
app/deps/libusb.sh linux native static
|
||||
|
||||
DEPS_INSTALL_DIR="$PWD/app/deps/work/install/linux-native-static"
|
||||
ADB_INSTALL_DIR="$PWD/app/deps/work/install/adb-linux"
|
||||
|
||||
rm -rf "$LINUX_BUILD_DIR"
|
||||
meson setup "$LINUX_BUILD_DIR" \
|
||||
--pkg-config-path="$DEPS_INSTALL_DIR/lib/pkgconfig" \
|
||||
-Dc_args="-I$DEPS_INSTALL_DIR/include" \
|
||||
-Dc_link_args="-L$DEPS_INSTALL_DIR/lib" \
|
||||
--buildtype=release \
|
||||
--strip \
|
||||
-Db_lto=true \
|
||||
-Dcompile_server=false \
|
||||
-Dportable=true \
|
||||
-Dstatic=true
|
||||
ninja -C "$LINUX_BUILD_DIR"
|
||||
|
||||
# Group intermediate outputs into a 'dist' directory
|
||||
mkdir -p "$LINUX_BUILD_DIR/dist"
|
||||
cp "$LINUX_BUILD_DIR"/app/scrcpy "$LINUX_BUILD_DIR/dist/"
|
||||
cp app/data/icon.png "$LINUX_BUILD_DIR/dist/"
|
||||
cp app/scrcpy.1 "$LINUX_BUILD_DIR/dist/"
|
||||
cp -r "$ADB_INSTALL_DIR"/. "$LINUX_BUILD_DIR/dist/"
|
43
release/build_macos.sh
Executable file
43
release/build_macos.sh
Executable file
@ -0,0 +1,43 @@
|
||||
#!/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/dav1d.sh macos native static
|
||||
app/deps/ffmpeg.sh macos native static
|
||||
app/deps/libusb.sh macos native static
|
||||
|
||||
DEPS_INSTALL_DIR="$PWD/app/deps/work/install/macos-native-static"
|
||||
ADB_INSTALL_DIR="$PWD/app/deps/work/install/adb-macos"
|
||||
|
||||
rm -rf "$MACOS_BUILD_DIR"
|
||||
meson setup "$MACOS_BUILD_DIR" \
|
||||
--pkg-config-path="$DEPS_INSTALL_DIR/lib/pkgconfig" \
|
||||
-Dc_args="-I$DEPS_INSTALL_DIR/include" \
|
||||
-Dc_link_args="-L$DEPS_INSTALL_DIR/lib" \
|
||||
--buildtype=release \
|
||||
--strip \
|
||||
-Db_lto=true \
|
||||
-Dcompile_server=false \
|
||||
-Dportable=true \
|
||||
-Dstatic=true
|
||||
ninja -C "$MACOS_BUILD_DIR"
|
||||
|
||||
# Group intermediate outputs into a 'dist' directory
|
||||
mkdir -p "$MACOS_BUILD_DIR/dist"
|
||||
cp "$MACOS_BUILD_DIR"/app/scrcpy "$MACOS_BUILD_DIR/dist/"
|
||||
cp app/data/icon.png "$MACOS_BUILD_DIR/dist/"
|
||||
cp app/scrcpy.1 "$MACOS_BUILD_DIR/dist/"
|
||||
cp -r "$ADB_INSTALL_DIR"/. "$MACOS_BUILD_DIR/dist/"
|
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"
|
53
release/build_windows.sh
Executable file
53
release/build_windows.sh
Executable file
@ -0,0 +1,53 @@
|
||||
#!/bin/bash
|
||||
set -ex
|
||||
|
||||
case "$1" in
|
||||
32)
|
||||
WINXX=win32
|
||||
;;
|
||||
64)
|
||||
WINXX=win64
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: $0 must be called with one argument: 32 or 64" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
cd "$(dirname ${BASH_SOURCE[0]})"
|
||||
. build_common
|
||||
cd .. # root project dir
|
||||
|
||||
WINXX_BUILD_DIR="$WORK_DIR/build-$WINXX"
|
||||
|
||||
app/deps/adb_windows.sh
|
||||
app/deps/sdl.sh $WINXX cross shared
|
||||
app/deps/dav1d.sh $WINXX cross shared
|
||||
app/deps/ffmpeg.sh $WINXX cross shared
|
||||
app/deps/libusb.sh $WINXX cross shared
|
||||
|
||||
DEPS_INSTALL_DIR="$PWD/app/deps/work/install/$WINXX-cross-shared"
|
||||
ADB_INSTALL_DIR="$PWD/app/deps/work/install/adb-windows"
|
||||
|
||||
rm -rf "$WINXX_BUILD_DIR"
|
||||
meson setup "$WINXX_BUILD_DIR" \
|
||||
--pkg-config-path="$DEPS_INSTALL_DIR/lib/pkgconfig" \
|
||||
-Dc_args="-I$DEPS_INSTALL_DIR/include" \
|
||||
-Dc_link_args="-L$DEPS_INSTALL_DIR/lib" \
|
||||
--cross-file=cross_$WINXX.txt \
|
||||
--buildtype=release \
|
||||
--strip \
|
||||
-Db_lto=true \
|
||||
-Dcompile_server=false \
|
||||
-Dportable=true
|
||||
ninja -C "$WINXX_BUILD_DIR"
|
||||
|
||||
# Group intermediate outputs into a 'dist' directory
|
||||
mkdir -p "$WINXX_BUILD_DIR/dist"
|
||||
cp "$WINXX_BUILD_DIR"/app/scrcpy.exe "$WINXX_BUILD_DIR/dist/"
|
||||
cp app/data/scrcpy-console.bat "$WINXX_BUILD_DIR/dist/"
|
||||
cp app/data/scrcpy-noconsole.vbs "$WINXX_BUILD_DIR/dist/"
|
||||
cp app/data/icon.png "$WINXX_BUILD_DIR/dist/"
|
||||
cp app/data/open_a_terminal_here.bat "$WINXX_BUILD_DIR/dist/"
|
||||
cp "$DEPS_INSTALL_DIR"/bin/*.dll "$WINXX_BUILD_DIR/dist/"
|
||||
cp -r "$ADB_INSTALL_DIR"/. "$WINXX_BUILD_DIR/dist/"
|
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 30100
|
||||
versionName "3.1"
|
||||
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.1
|
||||
|
||||
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,10 @@ import com.genymobile.scrcpy.util.Ln;
|
||||
import com.genymobile.scrcpy.util.Settings;
|
||||
import com.genymobile.scrcpy.util.SettingsException;
|
||||
|
||||
import android.os.BatteryManager;
|
||||
import android.system.ErrnoException;
|
||||
import android.system.Os;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
@ -16,59 +20,140 @@ 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_NORMAL_POWER_MODE = 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 boolean interrupted;
|
||||
|
||||
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 synchronized void interrupt() {
|
||||
// Do not use thread.interrupt() because only the wait() call must be interrupted, not Command.exec()
|
||||
interrupted = true;
|
||||
notify();
|
||||
}
|
||||
|
||||
public void join() throws InterruptedException {
|
||||
thread.join();
|
||||
}
|
||||
|
||||
private void runCleanUp(Options options) {
|
||||
boolean disableShowTouches = false;
|
||||
if (options.getShowTouches()) {
|
||||
try {
|
||||
String oldValue = Settings.getAndPutValue(Settings.TABLE_SYSTEM, "show_touches", "1");
|
||||
// If "show touches" was disabled, it must be disabled back on clean up
|
||||
disableShowTouches = !"1".equals(oldValue);
|
||||
} catch (SettingsException e) {
|
||||
Ln.e("Could not change \"show_touches\"", e);
|
||||
}
|
||||
}
|
||||
|
||||
int restoreStayOn = -1;
|
||||
if (options.getStayAwake()) {
|
||||
int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS;
|
||||
try {
|
||||
String oldValue = Settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn));
|
||||
try {
|
||||
int currentStayOn = Integer.parseInt(oldValue);
|
||||
// Restore only if the current value is different
|
||||
if (currentStayOn != stayOn) {
|
||||
restoreStayOn = currentStayOn;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
// ignore
|
||||
}
|
||||
} catch (SettingsException e) {
|
||||
Ln.e("Could not change \"stay_on_while_plugged_in\"", e);
|
||||
}
|
||||
}
|
||||
|
||||
int restoreScreenOffTimeout = -1;
|
||||
int screenOffTimeout = options.getScreenOffTimeout();
|
||||
if (screenOffTimeout != -1) {
|
||||
try {
|
||||
String oldValue = Settings.getAndPutValue(Settings.TABLE_SYSTEM, "screen_off_timeout", String.valueOf(screenOffTimeout));
|
||||
try {
|
||||
int currentScreenOffTimeout = Integer.parseInt(oldValue);
|
||||
// Restore only if the current value is different
|
||||
if (currentScreenOffTimeout != screenOffTimeout) {
|
||||
restoreScreenOffTimeout = currentScreenOffTimeout;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
// ignore
|
||||
}
|
||||
} catch (SettingsException e) {
|
||||
Ln.e("Could not change \"screen_off_timeout\"", e);
|
||||
}
|
||||
}
|
||||
|
||||
boolean powerOffScreen = options.getPowerOffScreenOnClose();
|
||||
int displayId = options.getDisplayId();
|
||||
|
||||
try {
|
||||
run(displayId, restoreStayOn, disableShowTouches, powerOffScreen, restoreScreenOffTimeout);
|
||||
} 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 {
|
||||
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 (!interrupted && pendingChanges == 0) {
|
||||
try {
|
||||
wait();
|
||||
} catch (InterruptedException e) {
|
||||
throw new AssertionError("Clean up thread MUST NOT be interrupted");
|
||||
}
|
||||
}
|
||||
if (interrupted) {
|
||||
break;
|
||||
}
|
||||
localPendingChanges = pendingChanges;
|
||||
localPendingRestoreDisplayPower = pendingRestoreDisplayPower;
|
||||
pendingChanges = 0;
|
||||
}
|
||||
if ((localPendingChanges & PENDING_CHANGE_DISPLAY_POWER) != 0) {
|
||||
out.write(localPendingRestoreDisplayPower ? 1 : 0);
|
||||
out.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public 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 setRestoreNormalPowerMode(boolean restoreOnExit) {
|
||||
return sendMessage(MSG_TYPE_RESTORE_NORMAL_POWER_MODE, restoreOnExit ? 1 : 0);
|
||||
}
|
||||
|
||||
public boolean setPowerOffScreen(boolean powerOffScreenOnExit) {
|
||||
return sendMessage(MSG_TYPE_POWER_OFF_SCREEN, powerOffScreenOnExit ? 1 : 0);
|
||||
public synchronized void setRestoreDisplayPower(boolean restoreDisplayPower) {
|
||||
pendingRestoreDisplayPower = restoreDisplayPower;
|
||||
pendingChanges |= PENDING_CHANGE_DISPLAY_POWER;
|
||||
notify();
|
||||
}
|
||||
|
||||
public static void unlinkSelf() {
|
||||
@ -80,38 +165,30 @@ public final class CleanUp {
|
||||
}
|
||||
|
||||
public static void main(String... args) {
|
||||
try {
|
||||
// Start a new session to avoid being terminated along with the server process on some devices
|
||||
Os.setsid();
|
||||
} catch (ErrnoException e) {
|
||||
Ln.e("setsid() failed", e);
|
||||
}
|
||||
unlinkSelf();
|
||||
|
||||
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;
|
||||
boolean restoreNormalPowerMode = false;
|
||||
boolean powerOffScreen = false;
|
||||
// Dynamic option
|
||||
boolean restoreDisplayPower = 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_NORMAL_POWER_MODE:
|
||||
restoreNormalPowerMode = param != 0;
|
||||
break;
|
||||
case MSG_TYPE_POWER_OFF_SCREEN:
|
||||
powerOffScreen = param != 0;
|
||||
break;
|
||||
default:
|
||||
Ln.w("Unexpected msg type: " + type);
|
||||
break;
|
||||
}
|
||||
// Only restore display power
|
||||
assert msg == 0 || msg == 1;
|
||||
restoreDisplayPower = msg != 0;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Expected when the server is dead
|
||||
@ -137,15 +214,24 @@ public final class CleanUp {
|
||||
}
|
||||
}
|
||||
|
||||
if (Device.isScreenOn()) {
|
||||
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) {
|
||||
if (displayId != Device.DISPLAY_ID_NONE) {
|
||||
Ln.i("Power off screen");
|
||||
Device.powerOffScreen(displayId);
|
||||
}
|
||||
} else if (restoreNormalPowerMode) {
|
||||
Ln.i("Restoring normal power mode");
|
||||
Device.setScreenPowerMode(Device.POWER_MODE_NORMAL);
|
||||
Ln.i("Power off screen");
|
||||
Device.powerOffScreen(targetDisplayId);
|
||||
} else if (restoreDisplayPower) {
|
||||
Ln.i("Restoring display power");
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,9 @@ package com.genymobile.scrcpy;
|
||||
|
||||
import com.genymobile.scrcpy.audio.AudioCodec;
|
||||
import com.genymobile.scrcpy.audio.AudioSource;
|
||||
import com.genymobile.scrcpy.device.Device;
|
||||
import com.genymobile.scrcpy.device.NewDisplay;
|
||||
import com.genymobile.scrcpy.device.Orientation;
|
||||
import com.genymobile.scrcpy.device.Size;
|
||||
import com.genymobile.scrcpy.util.CodecOption;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
@ -12,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;
|
||||
@ -31,7 +34,7 @@ public class Options {
|
||||
private int videoBitRate = 8000000;
|
||||
private int audioBitRate = 128000;
|
||||
private float maxFps;
|
||||
private int lockVideoOrientation = -1;
|
||||
private float angle;
|
||||
private boolean tunnelForward;
|
||||
private Rect crop;
|
||||
private boolean control = true;
|
||||
@ -44,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;
|
||||
|
||||
@ -56,6 +60,11 @@ public class Options {
|
||||
private boolean powerOn = true;
|
||||
|
||||
private NewDisplay newDisplay;
|
||||
private boolean vdDestroyContent = true;
|
||||
private boolean vdSystemDecorations = true;
|
||||
|
||||
private Orientation.Lock captureOrientationLock = Orientation.Lock.Unlocked;
|
||||
private Orientation captureOrientation = Orientation.Orient0;
|
||||
|
||||
private boolean listEncoders;
|
||||
private boolean listDisplays;
|
||||
@ -121,8 +130,8 @@ public class Options {
|
||||
return maxFps;
|
||||
}
|
||||
|
||||
public int getLockVideoOrientation() {
|
||||
return lockVideoOrientation;
|
||||
public float getAngle() {
|
||||
return angle;
|
||||
}
|
||||
|
||||
public boolean isTunnelForward() {
|
||||
@ -173,6 +182,10 @@ public class Options {
|
||||
return stayAwake;
|
||||
}
|
||||
|
||||
public int getScreenOffTimeout() {
|
||||
return screenOffTimeout;
|
||||
}
|
||||
|
||||
public List<CodecOption> getVideoCodecOptions() {
|
||||
return videoCodecOptions;
|
||||
}
|
||||
@ -213,6 +226,22 @@ public class Options {
|
||||
return newDisplay;
|
||||
}
|
||||
|
||||
public Orientation getCaptureOrientation() {
|
||||
return captureOrientation;
|
||||
}
|
||||
|
||||
public Orientation.Lock getCaptureOrientationLock() {
|
||||
return captureOrientationLock;
|
||||
}
|
||||
|
||||
public boolean getVDDestroyContent() {
|
||||
return vdDestroyContent;
|
||||
}
|
||||
|
||||
public boolean getVDSystemDecorations() {
|
||||
return vdSystemDecorations;
|
||||
}
|
||||
|
||||
public boolean getList() {
|
||||
return listEncoders || listDisplays || listCameras || listCameraSizes || listApps;
|
||||
}
|
||||
@ -335,8 +364,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);
|
||||
@ -358,6 +387,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;
|
||||
@ -436,6 +471,17 @@ public class Options {
|
||||
case "new_display":
|
||||
options.newDisplay = parseNewDisplay(value);
|
||||
break;
|
||||
case "vd_destroy_content":
|
||||
options.vdDestroyContent = Boolean.parseBoolean(value);
|
||||
break;
|
||||
case "vd_system_decorations":
|
||||
options.vdSystemDecorations = Boolean.parseBoolean(value);
|
||||
break;
|
||||
case "capture_orientation":
|
||||
Pair<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;
|
||||
@ -463,6 +509,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;
|
||||
}
|
||||
|
||||
@ -554,4 +605,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");
|
||||
@ -133,14 +86,9 @@ public final class Server {
|
||||
}
|
||||
|
||||
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();
|
||||
@ -164,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);
|
||||
}
|
||||
|
||||
@ -183,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);
|
||||
}
|
||||
@ -194,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());
|
||||
@ -219,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
|
||||
}
|
||||
@ -243,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) {
|
||||
|
@ -17,13 +17,14 @@ public final class ControlMessage {
|
||||
public static final int TYPE_COLLAPSE_PANELS = 7;
|
||||
public static final int TYPE_GET_CLIPBOARD = 8;
|
||||
public static final int TYPE_SET_CLIPBOARD = 9;
|
||||
public static final int TYPE_SET_SCREEN_POWER_MODE = 10;
|
||||
public static final int TYPE_SET_DISPLAY_POWER = 10;
|
||||
public static final int TYPE_ROTATE_DEVICE = 11;
|
||||
public static final int TYPE_UHID_CREATE = 12;
|
||||
public static final int TYPE_UHID_INPUT = 13;
|
||||
public static final int TYPE_UHID_DESTROY = 14;
|
||||
public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 15;
|
||||
public static final int TYPE_START_APP = 16;
|
||||
public static final int TYPE_RESET_VIDEO = 17;
|
||||
|
||||
public static final long SEQUENCE_INVALID = 0;
|
||||
|
||||
@ -34,7 +35,7 @@ public final class ControlMessage {
|
||||
private int type;
|
||||
private String text;
|
||||
private int metaState; // KeyEvent.META_*
|
||||
private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or POWER_MODE_*
|
||||
private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_*
|
||||
private int keycode; // KeyEvent.KEYCODE_*
|
||||
private int actionButton; // MotionEvent.BUTTON_*
|
||||
private int buttons; // MotionEvent.BUTTON_*
|
||||
@ -49,6 +50,9 @@ public final class ControlMessage {
|
||||
private long sequence;
|
||||
private int id;
|
||||
private byte[] data;
|
||||
private boolean on;
|
||||
private int vendorId;
|
||||
private int productId;
|
||||
|
||||
private ControlMessage() {
|
||||
}
|
||||
@ -116,13 +120,10 @@ public final class ControlMessage {
|
||||
return msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mode one of the {@code Device.SCREEN_POWER_MODE_*} constants
|
||||
*/
|
||||
public static ControlMessage createSetScreenPowerMode(int mode) {
|
||||
public static ControlMessage createSetDisplayPower(boolean on) {
|
||||
ControlMessage msg = new ControlMessage();
|
||||
msg.type = TYPE_SET_SCREEN_POWER_MODE;
|
||||
msg.action = mode;
|
||||
msg.type = TYPE_SET_DISPLAY_POWER;
|
||||
msg.on = on;
|
||||
return msg;
|
||||
}
|
||||
|
||||
@ -132,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;
|
||||
@ -234,4 +237,16 @@ public final class ControlMessage {
|
||||
public byte[] getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public boolean getOn() {
|
||||
return on;
|
||||
}
|
||||
|
||||
public int getVendorId() {
|
||||
return vendorId;
|
||||
}
|
||||
|
||||
public int getProductId() {
|
||||
return productId;
|
||||
}
|
||||
}
|
||||
|
@ -39,13 +39,14 @@ public class ControlMessageReader {
|
||||
return parseGetClipboard();
|
||||
case ControlMessage.TYPE_SET_CLIPBOARD:
|
||||
return parseSetClipboard();
|
||||
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
|
||||
return parseSetScreenPowerMode();
|
||||
case ControlMessage.TYPE_SET_DISPLAY_POWER:
|
||||
return parseSetDisplayPower();
|
||||
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
|
||||
case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL:
|
||||
case ControlMessage.TYPE_COLLAPSE_PANELS:
|
||||
case ControlMessage.TYPE_ROTATE_DEVICE:
|
||||
case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
|
||||
case ControlMessage.TYPE_RESET_VIDEO:
|
||||
return ControlMessage.createEmpty(type);
|
||||
case ControlMessage.TYPE_UHID_CREATE:
|
||||
return parseUhidCreate();
|
||||
@ -134,16 +135,18 @@ public class ControlMessageReader {
|
||||
return ControlMessage.createSetClipboard(sequence, text, paste);
|
||||
}
|
||||
|
||||
private ControlMessage parseSetScreenPowerMode() throws IOException {
|
||||
int mode = dis.readUnsignedByte();
|
||||
return ControlMessage.createSetScreenPowerMode(mode);
|
||||
private ControlMessage parseSetDisplayPower() throws IOException {
|
||||
boolean on = dis.readBoolean();
|
||||
return ControlMessage.createSetDisplayPower(on);
|
||||
}
|
||||
|
||||
private ControlMessage parseUhidCreate() throws IOException {
|
||||
int id = dis.readUnsignedShort();
|
||||
int vendorId = dis.readUnsignedShort();
|
||||
int productId = dis.readUnsignedShort();
|
||||
String name = parseString(1);
|
||||
byte[] data = parseByteArray(2);
|
||||
return ControlMessage.createUhidCreate(id, 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;
|
||||
@ -91,14 +95,17 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS];
|
||||
private final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[PointersState.MAX_POINTERS];
|
||||
|
||||
private boolean keepPowerModeOff;
|
||||
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
|
||||
@ -270,25 +281,16 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
case ControlMessage.TYPE_SET_CLIPBOARD:
|
||||
setClipboard(msg.getText(), msg.getPaste(), msg.getSequence());
|
||||
break;
|
||||
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
|
||||
case ControlMessage.TYPE_SET_DISPLAY_POWER:
|
||||
if (supportsInputEvents) {
|
||||
int mode = msg.getAction();
|
||||
boolean setPowerModeOk = Device.setScreenPowerMode(mode);
|
||||
if (setPowerModeOk) {
|
||||
keepPowerModeOff = mode == Device.POWER_MODE_OFF;
|
||||
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
|
||||
if (cleanUp != null) {
|
||||
boolean mustRestoreOnExit = mode != Device.POWER_MODE_NORMAL;
|
||||
cleanUp.setRestoreNormalPowerMode(mustRestoreOnExit);
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
@ -310,8 +315,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
}
|
||||
|
||||
private boolean injectKeycode(int action, int keycode, int repeat, int metaState) {
|
||||
if (keepPowerModeOff && action == KeyEvent.ACTION_UP && (keycode == KeyEvent.KEYCODE_POWER || keycode == KeyEvent.KEYCODE_WAKEUP)) {
|
||||
schedulePowerModeOff();
|
||||
if (keepDisplayPowerOff && action == KeyEvent.ACTION_UP && (keycode == KeyEvent.KEYCODE_POWER || keycode == KeyEvent.KEYCODE_WAKEUP)) {
|
||||
assert displayId != Device.DISPLAY_ID_NONE;
|
||||
scheduleDisplayPowerOff(displayId);
|
||||
}
|
||||
return injectKeyEvent(action, keycode, repeat, metaState, Device.INJECT_MODE_ASYNC);
|
||||
}
|
||||
@ -345,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");
|
||||
@ -412,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;
|
||||
}
|
||||
}
|
||||
@ -423,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;
|
||||
}
|
||||
|
||||
@ -437,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;
|
||||
}
|
||||
|
||||
@ -445,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;
|
||||
}
|
||||
}
|
||||
@ -456,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;
|
||||
|
||||
@ -484,21 +514,21 @@ 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a call to set power mode to off after a small delay.
|
||||
* Schedule a call to set display power to off after a small delay.
|
||||
*/
|
||||
private static void schedulePowerModeOff() {
|
||||
private static void scheduleDisplayPowerOff(int displayId) {
|
||||
EXECUTOR.schedule(() -> {
|
||||
Ln.i("Forcing screen off");
|
||||
Device.setScreenPowerMode(Device.POWER_MODE_OFF);
|
||||
Ln.i("Forcing display off");
|
||||
Device.setDisplayPower(displayId, false);
|
||||
}, 200, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -509,8 +539,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (keepPowerModeOff) {
|
||||
schedulePowerModeOff();
|
||||
if (keepDisplayPowerOff) {
|
||||
assert displayId != Device.DISPLAY_ID_NONE;
|
||||
scheduleDisplayPowerOff(displayId);
|
||||
}
|
||||
return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC);
|
||||
}
|
||||
@ -675,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;
|
||||
@ -174,7 +174,7 @@ public final class UhidManager {
|
||||
ByteBuffer buf = ByteBuffer.allocate(280 + reportDesc.length).order(ByteOrder.nativeOrder());
|
||||
buf.putInt(UHID_CREATE2);
|
||||
|
||||
String actualName = name.isEmpty() ? "scrcpy" : "scrcpy: " + name;
|
||||
String actualName = name.isEmpty() ? "scrcpy" : name;
|
||||
byte[] utf8Name = actualName.getBytes(StandardCharsets.UTF_8);
|
||||
int len = StringUtils.getUtf8TruncationIndex(utf8Name, 127);
|
||||
assert len <= 127;
|
||||
@ -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,8 +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;
|
||||
// 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
|
||||
@ -80,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() {
|
||||
@ -126,10 +128,13 @@ public final class Device {
|
||||
return clipboardManager.setText(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mode one of the {@code POWER_MODE_*} constants
|
||||
*/
|
||||
public static boolean setScreenPowerMode(int mode) {
|
||||
public static boolean setDisplayPower(int displayId, boolean on) {
|
||||
assert displayId != Device.DISPLAY_ID_NONE;
|
||||
|
||||
if (USE_ANDROID_15_DISPLAY_POWER && Build.VERSION.SDK_INT >= AndroidVersions.API_35_ANDROID_15) {
|
||||
return ServiceManager.getDisplayManager().requestDisplayPower(displayId, on);
|
||||
}
|
||||
|
||||
boolean applyToMultiPhysicalDisplays = Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10;
|
||||
|
||||
if (applyToMultiPhysicalDisplays
|
||||
@ -142,6 +147,7 @@ public final class Device {
|
||||
applyToMultiPhysicalDisplays = false;
|
||||
}
|
||||
|
||||
int mode = on ? POWER_MODE_NORMAL : POWER_MODE_OFF;
|
||||
if (applyToMultiPhysicalDisplays) {
|
||||
// On Android 14, these internal methods have been moved to DisplayControl
|
||||
boolean useDisplayControl =
|
||||
@ -175,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;
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user