Compare commits

..

No commits in common. "master" and "virtual_display" have entirely different histories.

125 changed files with 1585 additions and 4703 deletions

View File

@ -6,15 +6,11 @@ on:
name: name:
description: 'Version name (default is ref name)' description: 'Version name (default is ref name)'
env:
# $VERSION is used by release scripts
VERSION: ${{ github.event.inputs.name || github.ref_name }}
jobs: jobs:
test-scrcpy-server: build-scrcpy-server:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
GRADLE: gradle # use native gradle instead of ./gradlew in scripts GRADLE: gradle # use native gradle instead of ./gradlew in release.mk
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -26,45 +22,16 @@ jobs:
java-version: '17' java-version: '17'
- name: Test scrcpy-server - name: Test scrcpy-server
run: release/test_server.sh run: make -f release.mk test-server
build-scrcpy-server: - name: Build scrcpy-server
runs-on: ubuntu-latest run: make -f release.mk build-server
env:
GRADLE: gradle # use native gradle instead of ./gradlew in scripts
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup JDK - name: Upload scrcpy-server artifact
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 uses: actions/upload-artifact@v4
with: with:
name: scrcpy-server name: scrcpy-server
path: release/work/build-server/server/scrcpy-server path: 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: test-client:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -74,52 +41,18 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
sudo apt update
sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \
libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \
libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev
libv4l-dev
- name: Test
run: release/test_client.sh
build-linux-x86_64:
runs-on: ubuntu-latest
steps:
- name: Check architecture
run: |
arch=$(uname -m)
if [[ "$arch" != x86_64 ]]
then
echo "Unexpected architecture: $arch" >&2
exit 1
fi
- name: Checkout code
uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt 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 - name: Build
run: release/build_linux.sh x86_64
# upload-artifact does not preserve permissions
- name: Tar
run: | run: |
cd release/work/build-linux-x86_64 meson setup d -Db_sanitize=address,undefined
mkdir dist-tar
cd dist-tar
tar -C .. -cvf dist.tar.gz dist/
- name: Upload artifact - name: Test
uses: actions/upload-artifact@v4 run: |
with: meson test -Cd
name: build-linux-x86_64-intermediate
path: release/work/build-linux-x86_64/dist-tar/
build-win32: build-win32:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -129,6 +62,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
sudo apt update
sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \
libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \
libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \
@ -137,22 +71,14 @@ jobs:
- name: Workaround for old meson version run by Github Actions - name: Workaround for old meson version run by Github Actions
run: sed -i 's/^pkg-config/pkgconfig/' cross_win32.txt run: sed -i 's/^pkg-config/pkgconfig/' cross_win32.txt
- name: Build - name: Build scrcpy win32
run: release/build_windows.sh 32 run: make -f release.mk build-win32
# upload-artifact does not preserve permissions - name: Upload build-win32 artifact
- 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 uses: actions/upload-artifact@v4
with: with:
name: build-win32-intermediate name: build-win32-intermediate
path: release/work/build-win32/dist-tar/ path: build-win32/dist/
build-win64: build-win64:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -162,6 +88,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
sudo apt update
sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \
libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \
libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \
@ -170,137 +97,24 @@ jobs:
- name: Workaround for old meson version run by Github Actions - name: Workaround for old meson version run by Github Actions
run: sed -i 's/^pkg-config/pkgconfig/' cross_win64.txt run: sed -i 's/^pkg-config/pkgconfig/' cross_win64.txt
- name: Build - name: Build scrcpy win64
run: release/build_windows.sh 64 run: make -f release.mk build-win64
# upload-artifact does not preserve permissions - name: Upload build-win64 artifact
- 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 uses: actions/upload-artifact@v4
with: with:
name: build-win64-intermediate name: build-win64-intermediate
path: release/work/build-win64/dist-tar/ path: build-win64/dist/
build-macos-aarch64: package:
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-linux-x86_64
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download scrcpy-server
uses: actions/download-artifact@v4
with:
name: scrcpy-server
path: release/work/build-server/server/
- name: Download build-linux-x86_64
uses: actions/download-artifact@v4
with:
name: build-linux-x86_64-intermediate
path: release/work/build-linux-x86_64/dist-tar/
# upload-artifact does not preserve permissions
- name: Detar
run: |
cd release/work/build-linux-x86_64
tar xf dist-tar/dist.tar.gz
- name: Package
run: release/package_client.sh linux-x86_64 tar.gz
- name: Upload release
uses: actions/upload-artifact@v4
with:
name: release-linux-x86_64
path: release/output/
package-win32:
needs: needs:
- build-scrcpy-server - build-scrcpy-server
- build-win32 - build-win32
- build-win64
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
# $VERSION is used by release.mk
VERSION: ${{ github.event.inputs.name || github.ref_name }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -309,194 +123,25 @@ jobs:
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
name: scrcpy-server name: scrcpy-server
path: release/work/build-server/server/ path: build-server/server/
- name: Download build-win32 - name: Download build-win32
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
name: build-win32-intermediate name: build-win32-intermediate
path: release/work/build-win32/dist-tar/ path: build-win32/dist/
# 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 - name: Download build-win64
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
name: build-win64-intermediate name: build-win64-intermediate
path: release/work/build-win64/dist-tar/ path: build-win64/dist/
# upload-artifact does not preserve permissions
- name: Detar
run: |
cd release/work/build-win64
tar xf dist-tar/dist.tar.gz
- name: Package - name: Package
run: release/package_client.sh win64 zip run: make -f release.mk package
- 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 - name: Upload release artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: scrcpy-release-${{ env.VERSION }} name: scrcpy-release-${{ env.VERSION }}
path: release/output path: release-${{ env.VERSION }}

View File

@ -188,7 +188,7 @@
identification within third-party archives. identification within third-party archives.
Copyright (C) 2018 Genymobile Copyright (C) 2018 Genymobile
Copyright (C) 2018-2025 Romain Vimont Copyright (C) 2018-2024 Romain Vimont
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

@ -2,7 +2,7 @@
source for the project. Do not download releases from random websites, even if source for the project. Do not download releases from random websites, even if
their name contains `scrcpy`.** their name contains `scrcpy`.**
# scrcpy (v3.1) # scrcpy (v2.7)
<img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" /> <img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />
@ -74,20 +74,10 @@ Note that USB debugging is not required to run scrcpy in [OTG mode](doc/otg.md).
## Get the app ## Get the app
- [Linux](doc/linux.md) - [Linux](doc/linux.md)
- [Windows](doc/windows.md) (read [how to run](doc/windows.md#run)) - [Windows](doc/windows.md)
- [macOS](doc/macos.md) - [macOS](doc/macos.md)
## 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 ## Usage examples
There are a lot of options, [documented](#user-documentation) in separate pages. There are a lot of options, [documented](#user-documentation) in separate pages.
@ -151,7 +141,7 @@ documented in the following pages:
- [Device](doc/device.md) - [Device](doc/device.md)
- [Window](doc/window.md) - [Window](doc/window.md)
- [Recording](doc/recording.md) - [Recording](doc/recording.md)
- [Virtual display](doc/virtual_display.md) - [Virtual display](doc/virtual_displays.md)
- [Tunnels](doc/tunnels.md) - [Tunnels](doc/tunnels.md)
- [OTG](doc/otg.md) - [OTG](doc/otg.md)
- [Camera](doc/camera.md) - [Camera](doc/camera.md)
@ -191,7 +181,6 @@ to your problem immediately.
You can also use: You can also use:
- Reddit: [`r/scrcpy`](https://www.reddit.com/r/scrcpy) - Reddit: [`r/scrcpy`](https://www.reddit.com/r/scrcpy)
- BlueSky: [`@scrcpy.bsky.social`](https://bsky.app/profile/scrcpy.bsky.social)
- Twitter: [`@scrcpy_app`](https://twitter.com/scrcpy_app) - Twitter: [`@scrcpy_app`](https://twitter.com/scrcpy_app)
@ -210,7 +199,7 @@ work][donate]:
## Licence ## Licence
Copyright (C) 2018 Genymobile Copyright (C) 2018 Genymobile
Copyright (C) 2018-2025 Romain Vimont Copyright (C) 2018-2024 Romain Vimont
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

@ -2,7 +2,6 @@ _scrcpy() {
local cur prev words cword local cur prev words cword
local opts=" local opts="
--always-on-top --always-on-top
--angle
--audio-bit-rate= --audio-bit-rate=
--audio-buffer= --audio-buffer=
--audio-codec= --audio-codec=
@ -18,10 +17,10 @@ _scrcpy() {
--camera-fps= --camera-fps=
--camera-high-speed --camera-high-speed
--camera-size= --camera-size=
--capture-orientation=
--crop= --crop=
-d --select-usb -d --select-usb
--disable-screensaver --disable-screensaver
--display-buffer=
--display-id= --display-id=
--display-orientation= --display-orientation=
-e --select-tcpip -e --select-tcpip
@ -39,6 +38,8 @@ _scrcpy() {
--list-cameras --list-cameras
--list-displays --list-displays
--list-encoders --list-encoders
--lock-video-orientation
--lock-video-orientation=
-m --max-size= -m --max-size=
-M -M
--max-fps= --max-fps=
@ -57,8 +58,6 @@ _scrcpy() {
--no-mipmaps --no-mipmaps
--no-mouse-hover --no-mouse-hover
--no-power-on --no-power-on
--no-vd-destroy-content
--no-vd-system-decorations
--no-video --no-video
--no-video-playback --no-video-playback
--orientation= --orientation=
@ -79,7 +78,6 @@ _scrcpy() {
--rotation= --rotation=
-s --serial= -s --serial=
-S --turn-screen-off -S --turn-screen-off
--screen-off-timeout=
--shortcut-mod= --shortcut-mod=
--start-app= --start-app=
-t --show-touches -t --show-touches
@ -92,7 +90,6 @@ _scrcpy() {
--v4l2-sink= --v4l2-sink=
-v --version -v --version
-V --verbosity= -V --verbosity=
--video-buffer=
--video-codec= --video-codec=
--video-codec-options= --video-codec-options=
--video-encoder= --video-encoder=
@ -140,10 +137,6 @@ _scrcpy() {
COMPREPLY=($(compgen -W 'disabled uhid aoa' -- "$cur")) COMPREPLY=($(compgen -W 'disabled uhid aoa' -- "$cur"))
return return
;; ;;
--capture-orientation)
COMPREPLY=($(compgen -W '0 90 180 270 flip0 flip90 flip180 flip270 @0 @90 @180 @270 @flip0 @flip90 @flip180 @flip270' -- "$cur"))
return
;;
--orientation|--display-orientation) --orientation|--display-orientation)
COMPREPLY=($(compgen -W '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur")) COMPREPLY=($(compgen -W '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur"))
return return
@ -152,6 +145,10 @@ _scrcpy() {
COMPREPLY=($(compgen -W '0 90 180 270' -- "$cur")) COMPREPLY=($(compgen -W '0 90 180 270' -- "$cur"))
return return
;; ;;
--lock-video-orientation)
COMPREPLY=($(compgen -W 'unlocked initial 0 90 180 270' -- "$cur"))
return
;;
--pause-on-exit) --pause-on-exit)
COMPREPLY=($(compgen -W 'true false if-error' -- "$cur")) COMPREPLY=($(compgen -W 'true false if-error' -- "$cur"))
return return
@ -194,9 +191,9 @@ _scrcpy() {
|--camera-size \ |--camera-size \
|--crop \ |--crop \
|--display-id \ |--display-id \
|--display-buffer \
|--max-fps \ |--max-fps \
|-m|--max-size \ |-m|--max-size \
|--new-display \
|-p|--port \ |-p|--port \
|--push-target \ |--push-target \
|--rotation \ |--rotation \
@ -204,7 +201,6 @@ _scrcpy() {
|--tunnel-port \ |--tunnel-port \
|--v4l2-buffer \ |--v4l2-buffer \
|--v4l2-sink \ |--v4l2-sink \
|--video-buffer \
|--video-codec-options \ |--video-codec-options \
|--video-encoder \ |--video-encoder \
|--tcpip \ |--tcpip \

View File

@ -9,7 +9,6 @@ local arguments
arguments=( arguments=(
'--always-on-top[Make scrcpy window always on top \(above other windows\)]' '--always-on-top[Make scrcpy window always on top \(above other windows\)]'
'--angle=[Rotate the video content by a custom angle, in degrees]'
'--audio-bit-rate=[Encode the audio at the given bit-rate]' '--audio-bit-rate=[Encode the audio at the given bit-rate]'
'--audio-buffer=[Configure the audio buffering delay (in milliseconds)]' '--audio-buffer=[Configure the audio buffering delay (in milliseconds)]'
'--audio-codec=[Select the audio codec]:codec:(opus aac flac raw)' '--audio-codec=[Select the audio codec]:codec:(opus aac flac raw)'
@ -25,10 +24,10 @@ arguments=(
'--camera-facing=[Select the device camera by its facing direction]:facing:(front back external)' '--camera-facing=[Select the device camera by its facing direction]:facing:(front back external)'
'--camera-fps=[Specify the camera capture frame rate]' '--camera-fps=[Specify the camera capture frame rate]'
'--camera-size=[Specify an explicit camera capture size]' '--camera-size=[Specify an explicit camera capture size]'
'--capture-orientation=[Set the capture video orientation]:orientation:(0 90 180 270 flip0 flip90 flip180 flip270 @0 @90 @180 @270 @flip0 @flip90 @flip180 @flip270)'
'--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]' '--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]'
{-d,--select-usb}'[Use USB device]' {-d,--select-usb}'[Use USB device]'
'--disable-screensaver[Disable screensaver while scrcpy is running]' '--disable-screensaver[Disable screensaver while scrcpy is running]'
'--display-buffer=[Add a buffering delay \(in milliseconds\) before displaying]'
'--display-id=[Specify the display id to mirror]' '--display-id=[Specify the display id to mirror]'
'--display-orientation=[Set the initial display orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)' '--display-orientation=[Set the initial display orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)'
{-e,--select-tcpip}'[Use TCP/IP device]' {-e,--select-tcpip}'[Use TCP/IP device]'
@ -46,6 +45,7 @@ arguments=(
'--list-cameras[List cameras available on the device]' '--list-cameras[List cameras available on the device]'
'--list-displays[List displays available on the device]' '--list-displays[List displays available on the device]'
'--list-encoders[List video and audio encoders available on the device]' '--list-encoders[List video and audio encoders available on the device]'
'--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 90 180 270)'
{-m,--max-size=}'[Limit both the width and height of the video to value]' {-m,--max-size=}'[Limit both the width and height of the video to value]'
'-M[Use UHID/AOA mouse (same as --mouse=uhid or --mouse=aoa, depending on OTG mode)]' '-M[Use UHID/AOA mouse (same as --mouse=uhid or --mouse=aoa, depending on OTG mode)]'
'--max-fps=[Limit the frame rate of screen capture]' '--max-fps=[Limit the frame rate of screen capture]'
@ -63,8 +63,6 @@ arguments=(
'--no-mipmaps[Disable the generation of mipmaps]' '--no-mipmaps[Disable the generation of mipmaps]'
'--no-mouse-hover[Do not forward mouse hover events]' '--no-mouse-hover[Do not forward mouse hover events]'
'--no-power-on[Do not power on the device on start]' '--no-power-on[Do not power on the device on start]'
'--no-vd-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[Disable video forwarding]'
'--no-video-playback[Disable video playback]' '--no-video-playback[Disable video playback]'
'--orientation=[Set the video orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)' '--orientation=[Set the video orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)'
@ -83,7 +81,6 @@ arguments=(
'--require-audio=[Make scrcpy fail if audio is enabled but does not work]' '--require-audio=[Make scrcpy fail if audio is enabled but does not work]'
{-s,--serial=}'[The device serial number \(mandatory for multiple devices only\)]:serial:($("${ADB-adb}" devices | awk '\''$2 == "device" {print $1}'\''))' {-s,--serial=}'[The device serial number \(mandatory for multiple devices only\)]:serial:($("${ADB-adb}" devices | awk '\''$2 == "device" {print $1}'\''))'
{-S,--turn-screen-off}'[Turn the device screen off immediately]' {-S,--turn-screen-off}'[Turn the device screen off immediately]'
'--screen-off-timeout=[Set the screen off timeout in seconds]'
'--shortcut-mod=[\[key1,key2+key3,...\] Specify the modifiers to use for scrcpy shortcuts]:shortcut mod:(lctrl rctrl lalt ralt lsuper rsuper)' '--shortcut-mod=[\[key1,key2+key3,...\] Specify the modifiers to use for scrcpy shortcuts]:shortcut mod:(lctrl rctrl lalt ralt lsuper rsuper)'
'--start-app=[Start an Android app]' '--start-app=[Start an Android app]'
{-t,--show-touches}'[Show physical touches]' {-t,--show-touches}'[Show physical touches]'
@ -95,7 +92,6 @@ arguments=(
'--v4l2-sink=[\[\/dev\/videoN\] Output to v4l2loopback device]' '--v4l2-sink=[\[\/dev\/videoN\] Output to v4l2loopback device]'
{-v,--version}'[Print the version of scrcpy]' {-v,--version}'[Print the version of scrcpy]'
{-V,--verbosity=}'[Set the log level]:verbosity:(verbose debug info warn error)' {-V,--verbosity=}'[Set the log level]:verbosity:(verbose debug info warn error)'
'--video-buffer=[Add a buffering delay \(in milliseconds\) before displaying video frames]'
'--video-codec=[Select the video codec]:codec:(h264 h265 av1)' '--video-codec=[Select the video codec]:codec:(h264 h265 av1)'
'--video-codec-options=[Set a list of comma-separated key\:type=value options for the device video encoder]' '--video-codec-options=[Set a list of comma-separated key\:type=value options for the device video encoder]'
'--video-encoder=[Use a specific MediaCodec video encoder]' '--video-encoder=[Use a specific MediaCodec video encoder]'

View File

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

View File

@ -1,29 +0,0 @@
#!/usr/bin/env bash
set -ex
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
VERSION=35.0.2
FILENAME=platform-tools_r$VERSION-linux.zip
PROJECT_DIR=platform-tools-$VERSION-linux
SHA256SUM=acfdcccb123a8718c46c46c059b2f621140194e5ec1ac9d81715be3d6ab6cd0a
cd "$SOURCES_DIR"
if [[ -d "$PROJECT_DIR" ]]
then
echo "$PWD/$PROJECT_DIR" found
else
get_file "https://dl.google.com/android/repository/$FILENAME" "$FILENAME" "$SHA256SUM"
mkdir -p "$PROJECT_DIR"
cd "$PROJECT_DIR"
ZIP_PREFIX=platform-tools
unzip "../$FILENAME" "$ZIP_PREFIX"/adb
mv "$ZIP_PREFIX"/* .
rmdir "$ZIP_PREFIX"
fi
mkdir -p "$INSTALL_DIR/adb-linux"
cd "$INSTALL_DIR/adb-linux"
cp -r "$SOURCES_DIR/$PROJECT_DIR"/. "$INSTALL_DIR/adb-linux/"

View File

@ -1,29 +0,0 @@
#!/usr/bin/env bash
set -ex
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
VERSION=35.0.2
FILENAME=platform-tools_r$VERSION-darwin.zip
PROJECT_DIR=platform-tools-$VERSION-darwin
SHA256SUM=1820078db90bf21628d257ff052528af1c61bb48f754b3555648f5652fa35d78
cd "$SOURCES_DIR"
if [[ -d "$PROJECT_DIR" ]]
then
echo "$PWD/$PROJECT_DIR" found
else
get_file "https://dl.google.com/android/repository/$FILENAME" "$FILENAME" "$SHA256SUM"
mkdir -p "$PROJECT_DIR"
cd "$PROJECT_DIR"
ZIP_PREFIX=platform-tools
unzip "../$FILENAME" "$ZIP_PREFIX"/adb
mv "$ZIP_PREFIX"/* .
rmdir "$ZIP_PREFIX"
fi
mkdir -p "$INSTALL_DIR/adb-macos"
cd "$INSTALL_DIR/adb-macos"
cp -r "$SOURCES_DIR/$PROJECT_DIR"/. "$INSTALL_DIR/adb-macos/"

View File

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

View File

@ -1,68 +0,0 @@
#!/usr/bin/env bash
set -ex
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
process_args "$@"
VERSION=1.5.0
FILENAME=dav1d-$VERSION.tar.gz
PROJECT_DIR=dav1d-$VERSION
SHA256SUM=78b15d9954b513ea92d27f39362535ded2243e1b0924fde39f37a31ebed5f76b
cd "$SOURCES_DIR"
if [[ -d "$PROJECT_DIR" ]]
then
echo "$PWD/$PROJECT_DIR" found
else
get_file "https://code.videolan.org/videolan/dav1d/-/archive/$VERSION/$FILENAME" "$FILENAME" "$SHA256SUM"
tar xf "$FILENAME" # First level directory is "$PROJECT_DIR"
fi
mkdir -p "$BUILD_DIR/$PROJECT_DIR"
cd "$BUILD_DIR/$PROJECT_DIR"
if [[ -d "$DIRNAME" ]]
then
echo "'$PWD/$DIRNAME' already exists, not reconfigured"
cd "$DIRNAME"
else
mkdir "$DIRNAME"
cd "$DIRNAME"
conf=(
--prefix="$INSTALL_DIR/$DIRNAME"
--libdir=lib
-Denable_tests=false
-Denable_tools=false
# Always build dav1d statically
--default-library=static
)
if [[ "$BUILD_TYPE" == cross ]]
then
case "$HOST" in
win32)
conf+=(
--cross-file="$SOURCES_DIR/$PROJECT_DIR/package/crossfiles/i686-w64-mingw32.meson"
)
;;
win64)
conf+=(
--cross-file="$SOURCES_DIR/$PROJECT_DIR/package/crossfiles/x86_64-w64-mingw32.meson"
)
;;
*)
echo "Unsupported host: $HOST" >&2
exit 1
esac
fi
meson setup . "$SOURCES_DIR/$PROJECT_DIR" "${conf[@]}"
fi
ninja
ninja install

View File

@ -3,12 +3,11 @@ set -ex
DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR" cd "$DEPS_DIR"
. common . common
process_args "$@"
VERSION=7.1 VERSION=7.0.2
FILENAME=ffmpeg-$VERSION.tar.xz FILENAME=ffmpeg-$VERSION.tar.xz
PROJECT_DIR=ffmpeg-$VERSION PROJECT_DIR=ffmpeg-$VERSION
SHA256SUM=40973D44970DBC83EF302B0609F2E74982BE2D85916DD2EE7472D30678A7ABE6 SHA256SUM=8646515b638a3ad303e23af6a3587734447cb8fc0a0c064ecdb8e95c4fd8b389
cd "$SOURCES_DIR" cd "$SOURCES_DIR"
@ -23,121 +22,68 @@ fi
mkdir -p "$BUILD_DIR/$PROJECT_DIR" mkdir -p "$BUILD_DIR/$PROJECT_DIR"
cd "$BUILD_DIR/$PROJECT_DIR" cd "$BUILD_DIR/$PROJECT_DIR"
if [[ -d "$DIRNAME" ]] if [[ "$HOST" = win32 ]]
then then
echo "'$PWD/$DIRNAME' already exists, not reconfigured" ARCH=x86
cd "$DIRNAME" elif [[ "$HOST" = win64 ]]
then
ARCH=x86_64
else else
mkdir "$DIRNAME"
cd "$DIRNAME"
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
export PKG_CONFIG_PATH="$INSTALL_DIR/$DIRNAME/lib/pkgconfig:$PKG_CONFIG_PATH"
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 echo "Unsupported host: $HOST" >&2
exit 1 exit 1
esac fi
fi
"$SOURCES_DIR/$PROJECT_DIR"/configure "${conf[@]}" # -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 [[ -d "$HOST" ]]
then
echo "'$PWD/$HOST' already exists, not reconfigured"
cd "$HOST"
else
mkdir "$HOST"
cd "$HOST"
"$SOURCES_DIR/$PROJECT_DIR"/configure \
--prefix="$INSTALL_DIR/$HOST" \
--enable-cross-compile \
--target-os=mingw32 \
--arch="$ARCH" \
--cross-prefix="${HOST_TRIPLET}-" \
--cc="${HOST_TRIPLET}-gcc" \
--extra-cflags="-O2 -fPIC" \
--enable-shared \
--disable-static \
--disable-programs \
--disable-doc \
--disable-swscale \
--disable-postproc \
--disable-avfilter \
--disable-avdevice \
--disable-network \
--disable-everything \
--enable-swresample \
--enable-decoder=h264 \
--enable-decoder=hevc \
--enable-decoder=av1 \
--enable-decoder=pcm_s16le \
--enable-decoder=opus \
--enable-decoder=aac \
--enable-decoder=flac \
--enable-decoder=png \
--enable-protocol=file \
--enable-demuxer=image2 \
--enable-parser=png \
--enable-zlib \
--enable-muxer=matroska \
--enable-muxer=mp4 \
--enable-muxer=opus \
--enable-muxer=flac \
--enable-muxer=wav \
--disable-vulkan
fi fi
make -j make -j

View File

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

View File

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

View File

@ -46,7 +46,6 @@ src = [
'src/util/acksync.c', 'src/util/acksync.c',
'src/util/audiobuf.c', 'src/util/audiobuf.c',
'src/util/average.c', 'src/util/average.c',
'src/util/env.c',
'src/util/file.c', 'src/util/file.c',
'src/util/intmap.c', 'src/util/intmap.c',
'src/util/intr.c', 'src/util/intr.c',
@ -110,22 +109,20 @@ endif
cc = meson.get_compiler('c') cc = meson.get_compiler('c')
static = get_option('static')
dependencies = [ dependencies = [
dependency('libavformat', version: '>= 57.33', static: static), dependency('libavformat', version: '>= 57.33'),
dependency('libavcodec', version: '>= 57.37', static: static), dependency('libavcodec', version: '>= 57.37'),
dependency('libavutil', static: static), dependency('libavutil'),
dependency('libswresample', static: static), dependency('libswresample'),
dependency('sdl2', version: '>= 2.0.5', static: static), dependency('sdl2', version: '>= 2.0.5'),
] ]
if v4l2_support if v4l2_support
dependencies += dependency('libavdevice', static: static) dependencies += dependency('libavdevice')
endif endif
if usb_support if usb_support
dependencies += dependency('libusb-1.0', static: static) dependencies += dependency('libusb-1.0')
endif endif
if host_machine.system() == 'windows' if host_machine.system() == 'windows'
@ -170,6 +167,9 @@ conf.set('DEFAULT_LOCAL_PORT_RANGE_LAST', '27199')
# run a server debugger and wait for a client to be attached # run a server debugger and wait for a client to be attached
conf.set('SERVER_DEBUGGER', get_option('server_debugger')) conf.set('SERVER_DEBUGGER', get_option('server_debugger'))
# select the debugger method ('old' for Android < 9, 'new' for Android >= 9)
conf.set('SERVER_DEBUGGER_METHOD_NEW', get_option('server_debugger_method') == 'new')
# enable V4L2 support (linux only) # enable V4L2 support (linux only)
conf.set('HAVE_V4L2', v4l2_support) conf.set('HAVE_V4L2', v4l2_support)

View File

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

View File

@ -19,10 +19,6 @@ provides display and control of Android devices connected on USB (or over TCP/IP
.B \-\-always\-on\-top .B \-\-always\-on\-top
Make scrcpy window always on top (above other windows). Make scrcpy window always on top (above other windows).
.TP
.BI "\-\-angle " degrees
Rotate the video content by a custom angle, in degrees (clockwise).
.TP .TP
.BI "\-\-audio\-bit\-rate " value .BI "\-\-audio\-bit\-rate " value
Encode the audio at the given bit rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000). Encode the audio at the given bit rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000).
@ -97,18 +93,6 @@ Select the camera size by its aspect ratio (+/- 10%).
Possible values are "sensor" (use the camera sensor aspect ratio), "\fInum\fR:\fIden\fR" (e.g. "4:3") and "\fIvalue\fR" (e.g. "1.6"). Possible values are "sensor" (use the camera sensor aspect ratio), "\fInum\fR:\fIden\fR" (e.g. "4:3") and "\fIvalue\fR" (e.g. "1.6").
.TP
.BI "\-\-camera\-facing " facing
Select the device camera by its facing direction.
Possible values are "front", "back" and "external".
.TP
.BI "\-\-camera\-fps " fps
Specify the camera capture frame rate.
If not specified, Android's default frame rate (30 fps) is used.
.TP .TP
.B \-\-camera\-high\-speed .B \-\-camera\-high\-speed
Enable high-speed camera capture mode. Enable high-speed camera capture mode.
@ -122,26 +106,28 @@ Specify the device camera id to mirror.
The available camera ids can be listed by \fB\-\-list\-cameras\fR. The available camera ids can be listed by \fB\-\-list\-cameras\fR.
.TP .TP
.BI "\-\-camera\-size " width\fRx\fIheight .BI "\-\-camera\-facing " facing
Specify an explicit camera capture size. Select the device camera by its facing direction.
Possible values are "front", "back" and "external".
.TP .TP
.BI "\-\-capture\-orientation " value .BI "\-\-camera\-fps " fps
Possible values are 0, 90, 180, 270, flip0, flip90, flip180 and flip270, possibly prefixed by '@'. Specify the camera capture frame rate.
The number represents the clockwise rotation in degrees; the "flip" keyword applies a horizontal flip before the rotation. If not specified, Android's default frame rate (30 fps) is used.
If a leading '@' is passed (@90) for display capture, then the rotation is locked, and is relative to the natural device orientation. .TP
.BI "\-\-camera\-size " width\fRx\fIheight
If '@' is passed alone, then the rotation is locked to the initial device orientation. Specify an explicit camera capture size.
Default is 0.
.TP .TP
.BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy .BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy
Crop the device screen on the server. Crop the device screen on the server.
The values are expressed in the device natural orientation (typically, portrait for a phone, landscape for a tablet). The values are expressed in the device natural orientation (typically, portrait for a phone, landscape for a tablet). Any
.B \-\-max\-size
value is computed on the cropped size.
.TP .TP
.B \-d, \-\-select\-usb .B \-d, \-\-select\-usb
@ -153,6 +139,12 @@ Also see \fB\-e\fR (\fB\-\-select\-tcpip\fR).
.BI "\-\-disable\-screensaver" .BI "\-\-disable\-screensaver"
Disable screensaver while scrcpy is running. Disable screensaver while scrcpy is running.
.TP
.BI "\-\-display\-buffer " ms
Add a buffering delay (in milliseconds) before displaying. This increases latency to compensate for jitter.
Default is 0 (no buffering).
.TP .TP
.BI "\-\-display\-id " id .BI "\-\-display\-id " id
Specify the device display id to mirror. Specify the device display id to mirror.
@ -255,6 +247,16 @@ List video and audio encoders available on the device.
.B \-\-list\-displays .B \-\-list\-displays
List displays available on the device. List displays available on the device.
.TP
\fB\-\-lock\-video\-orientation\fR[=\fIvalue\fR]
Lock capture video orientation to \fIvalue\fR.
Possible values are "unlocked", "initial" (locked to the initial orientation), 0, 90, 180, and 270. The values represent the clockwise rotation from the natural device orientation, in degrees.
Default is "unlocked".
Passing the option without argument is equivalent to passing "initial".
.TP .TP
.BI "\-m, \-\-max\-size " value .BI "\-m, \-\-max\-size " value
Limit both the width and height of the video to \fIvalue\fR. The other dimension is computed so that the device aspect\-ratio is preserved. Limit both the width and height of the video to \fIvalue\fR. The other dimension is computed so that the device aspect\-ratio is preserved.
@ -318,13 +320,14 @@ Disable video and audio playback on the computer (equivalent to \fB\-\-no\-video
.TP .TP
\fB\-\-new\-display\fR[=[\fIwidth\fRx\fIheight\fR][/\fIdpi\fR]] \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. 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.
Examples: Examples:
\-\-new\-display=1920x1080 \-\-new\-display=1920x1080
\-\-new\-display=1920x1080/420 \-\-new\-display=1920x1080/420
\-\-new\-display # main display size and density \-\-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 \-\-new\-display=/240 # main display size and 240 dpi
.TP .TP
@ -369,16 +372,6 @@ Do not forward mouse hover (mouse motion without any clicks) events.
.B \-\-no\-power\-on .B \-\-no\-power\-on
Do not power on the device on start. Do not power on the device on start.
.TP
.B \-\-no\-vd\-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 .TP
.B \-\-no\-video .B \-\-no\-video
Disable video forwarding. Disable video forwarding.
@ -524,15 +517,13 @@ Enable "show touches" on start, restore the initial value on exit.
It only shows physical touches (not clicks from scrcpy). It only shows physical touches (not clicks from scrcpy).
.TP .TP
.BI "\-\-tcpip\fR[=[+]\fIip\fR[:\fIport\fR]] .BI "\-\-tcpip\fR[=\fIip\fR[:\fIport\fR]]
Configure and connect the device over TCP/IP. Configure and reconnect the device over TCP/IP.
If a destination address is provided, then scrcpy connects to this address before starting. The device must listen on the given TCP port (default is 5555). If 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. 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 .TP
.BI "\-\-time\-limit " seconds .BI "\-\-time\-limit " seconds
Set the maximum mirroring time, in seconds. Set the maximum mirroring time, in seconds.
@ -563,19 +554,13 @@ Default is "info" for release builds, "debug" for debug builds.
.BI "\-\-v4l2-sink " /dev/videoN .BI "\-\-v4l2-sink " /dev/videoN
Output to v4l2loopback device. Output to v4l2loopback device.
It requires to lock the video orientation (see \fB\-\-lock\-video\-orientation\fR).
.TP .TP
.BI "\-\-v4l2-buffer " ms .BI "\-\-v4l2-buffer " ms
Add a buffering delay (in milliseconds) before pushing frames. This increases latency to compensate for jitter. Add a buffering delay (in milliseconds) before pushing frames. This increases latency to compensate for jitter.
This option is similar to \fB\-\-video\-buffer\fR, but specific to V4L2 sink. This option is similar to \fB\-\-display\-buffer\fR, but specific to V4L2 sink.
Default is 0 (no buffering).
.TP
.BI "\-\-video\-buffer " ms
Add a buffering delay (in milliseconds) before displaying video frames.
This increases latency to compensate for jitter.
Default is 0 (no buffering). Default is 0 (no buffering).
@ -684,10 +669,6 @@ Pause or re-pause display
.B MOD+Shift+z .B MOD+Shift+z
Unpause display Unpause display
.TP
.B MOD+Shift+r
Reset video capture/encoding
.TP .TP
.B MOD+g .B MOD+g
Resize window to 1:1 (pixel\-perfect) Resize window to 1:1 (pixel\-perfect)
@ -829,7 +810,7 @@ Report bugs to <https://github.com/Genymobile/scrcpy/issues>.
.SH COPYRIGHT .SH COPYRIGHT
Copyright \(co 2018 Genymobile <https://www.genymobile.com> Copyright \(co 2018 Genymobile <https://www.genymobile.com>
Copyright \(co 2018\-2025 Romain Vimont <rom@rom1v.com> Copyright \(co 2018\-2024 Romain Vimont <rom@rom1v.com>
Licensed under the Apache License, Version 2.0. Licensed under the Apache License, Version 2.0.

View File

@ -7,7 +7,6 @@
#include "adb_device.h" #include "adb_device.h"
#include "adb_parser.h" #include "adb_parser.h"
#include "util/env.h"
#include "util/file.h" #include "util/file.h"
#include "util/log.h" #include "util/log.h"
#include "util/process_intr.h" #include "util/process_intr.h"
@ -25,45 +24,15 @@
*/ */
#define SC_ADB_COMMAND(...) { sc_adb_get_executable(), __VA_ARGS__, NULL } #define SC_ADB_COMMAND(...) { sc_adb_get_executable(), __VA_ARGS__, NULL }
static char *adb_executable; static const 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 * const char *
sc_adb_get_executable(void) { sc_adb_get_executable(void) {
if (!adb_executable) {
adb_executable = getenv("ADB");
if (!adb_executable)
adb_executable = "adb";
}
return adb_executable; return adb_executable;
} }
@ -412,7 +381,7 @@ sc_adb_connect(struct sc_intr *intr, const char *ip_port, unsigned flags) {
// "adb connect" always returns successfully (with exit code 0), even in // "adb connect" always returns successfully (with exit code 0), even in
// case of failure. As a workaround, check if its output starts with // case of failure. As a workaround, check if its output starts with
// "connected" or "already connected". // "connected".
char buf[128]; char buf[128];
ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf) - 1); ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf) - 1);
sc_pipe_close(pout); sc_pipe_close(pout);
@ -429,8 +398,7 @@ sc_adb_connect(struct sc_intr *intr, const char *ip_port, unsigned flags) {
assert((size_t) r < sizeof(buf)); assert((size_t) r < sizeof(buf));
buf[r] = '\0'; 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)) { if (!ok && !(flags & SC_ADB_NO_STDERR)) {
// "adb connect" also prints errors to stdout. Since we capture it, // "adb connect" also prints errors to stdout. Since we capture it,
// re-print the error to stderr. // re-print the error to stderr.
@ -771,21 +739,3 @@ sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags) {
return sc_adb_parse_device_ip(buf); return sc_adb_parse_device_ip(buf);
} }
uint16_t
sc_adb_get_device_sdk_version(struct sc_intr *intr, const char *serial) {
char *sdk_version =
sc_adb_getprop(intr, serial, "ro.build.version.sdk", SC_ADB_SILENT);
if (!sdk_version) {
return 0;
}
long value;
bool ok = sc_str_parse_integer(sdk_version, &value);
free(sdk_version);
if (!ok || value < 0 || value > 0xFFFF) {
return 0;
}
return value;
}

View File

@ -15,12 +15,6 @@
#define SC_ADB_SILENT (SC_ADB_NO_STDOUT | SC_ADB_NO_STDERR | SC_ADB_NO_LOGERR) #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 * const char *
sc_adb_get_executable(void); sc_adb_get_executable(void);
@ -120,10 +114,4 @@ sc_adb_getprop(struct sc_intr *intr, const char *serial, const char *prop,
char * char *
sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags); sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags);
/**
* Return the device SDK version.
*/
uint16_t
sc_adb_get_device_sdk_version(struct sc_intr *intr, const char *serial);
#endif #endif

View File

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

View File

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

View File

@ -50,7 +50,6 @@ enum {
OPT_POWER_OFF_ON_CLOSE, OPT_POWER_OFF_ON_CLOSE,
OPT_V4L2_SINK, OPT_V4L2_SINK,
OPT_DISPLAY_BUFFER, OPT_DISPLAY_BUFFER,
OPT_VIDEO_BUFFER,
OPT_V4L2_BUFFER, OPT_V4L2_BUFFER,
OPT_TUNNEL_HOST, OPT_TUNNEL_HOST,
OPT_TUNNEL_PORT, OPT_TUNNEL_PORT,
@ -106,11 +105,6 @@ enum {
OPT_NEW_DISPLAY, OPT_NEW_DISPLAY,
OPT_LIST_APPS, OPT_LIST_APPS,
OPT_START_APP, 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 { struct sc_option {
@ -152,13 +146,6 @@ static const struct sc_option options[] = {
.longopt = "always-on-top", .longopt = "always-on-top",
.text = "Make scrcpy window always on top (above other windows).", .text = "Make scrcpy window always on top (above other windows).",
}, },
{
.longopt_id = OPT_ANGLE,
.longopt = "angle",
.argdesc = "degrees",
.text = "Rotate the video content by a custom angle, in degrees "
"(clockwise).",
},
{ {
.longopt_id = OPT_AUDIO_BIT_RATE, .longopt_id = OPT_AUDIO_BIT_RATE,
.longopt = "audio-bit-rate", .longopt = "audio-bit-rate",
@ -256,6 +243,14 @@ static const struct sc_option options[] = {
"ratio), \"<num>:<den>\" (e.g. \"4:3\") or \"<value>\" (e.g. " "ratio), \"<num>:<den>\" (e.g. \"4:3\") or \"<value>\" (e.g. "
"\"1.6\")." "\"1.6\")."
}, },
{
.longopt_id = OPT_CAMERA_ID,
.longopt = "camera-id",
.argdesc = "id",
.text = "Specify the device camera id to mirror.\n"
"The available camera ids can be listed by:\n"
" scrcpy --list-cameras",
},
{ {
.longopt_id = OPT_CAMERA_FACING, .longopt_id = OPT_CAMERA_FACING,
.longopt = "camera-facing", .longopt = "camera-facing",
@ -263,14 +258,6 @@ static const struct sc_option options[] = {
.text = "Select the device camera by its facing direction.\n" .text = "Select the device camera by its facing direction.\n"
"Possible values are \"front\", \"back\" and \"external\".", "Possible values are \"front\", \"back\" and \"external\".",
}, },
{
.longopt_id = OPT_CAMERA_FPS,
.longopt = "camera-fps",
.argdesc = "value",
.text = "Specify the camera capture frame rate.\n"
"If not specified, Android's default frame rate (30 fps) is "
"used.",
},
{ {
.longopt_id = OPT_CAMERA_HIGH_SPEED, .longopt_id = OPT_CAMERA_HIGH_SPEED,
.longopt = "camera-high-speed", .longopt = "camera-high-speed",
@ -278,14 +265,6 @@ static const struct sc_option options[] = {
"This mode is restricted to specific resolutions and frame " "This mode is restricted to specific resolutions and frame "
"rates, listed by --list-camera-sizes.", "rates, listed by --list-camera-sizes.",
}, },
{
.longopt_id = OPT_CAMERA_ID,
.longopt = "camera-id",
.argdesc = "id",
.text = "Specify the device camera id to mirror.\n"
"The available camera ids can be listed by:\n"
" scrcpy --list-cameras",
},
{ {
.longopt_id = OPT_CAMERA_SIZE, .longopt_id = OPT_CAMERA_SIZE,
.longopt = "camera-size", .longopt = "camera-size",
@ -293,21 +272,12 @@ static const struct sc_option options[] = {
.text = "Specify an explicit camera capture size.", .text = "Specify an explicit camera capture size.",
}, },
{ {
.longopt_id = OPT_CAPTURE_ORIENTATION, .longopt_id = OPT_CAMERA_FPS,
.longopt = "capture-orientation", .longopt = "camera-fps",
.argdesc = "value", .argdesc = "value",
.text = "Set the capture video orientation.\n" .text = "Specify the camera capture frame rate.\n"
"Possible values are 0, 90, 180, 270, flip0, flip90, flip180 " "If not specified, Android's default frame rate (30 fps) is "
"and flip270, possibly prefixed by '@'.\n" "used.",
"The number represents the clockwise rotation in degrees; the "
"flip\" keyword applies a horizontal flip before the "
"rotation.\n"
"If a leading '@' is passed (@90) for display capture, then "
"the rotation is locked, and is relative to the natural device "
"orientation.\n"
"If '@' is passed alone, then the rotation is locked to the "
"initial device orientation.\n"
"Default is 0.",
}, },
{ {
// Not really deprecated (--codec has never been released), but without // Not really deprecated (--codec has never been released), but without
@ -330,7 +300,8 @@ static const struct sc_option options[] = {
.argdesc = "width:height:x:y", .argdesc = "width:height:x:y",
.text = "Crop the device screen on the server.\n" .text = "Crop the device screen on the server.\n"
"The values are expressed in the device natural orientation " "The values are expressed in the device natural orientation "
"(typically, portrait for a phone, landscape for a tablet).", "(typically, portrait for a phone, landscape for a tablet). "
"Any --max-size value is computed on the cropped size.",
}, },
{ {
.shortopt = 'd', .shortopt = 'd',
@ -350,10 +321,12 @@ static const struct sc_option options[] = {
.argdesc = "id", .argdesc = "id",
}, },
{ {
// deprecated
.longopt_id = OPT_DISPLAY_BUFFER, .longopt_id = OPT_DISPLAY_BUFFER,
.longopt = "display-buffer", .longopt = "display-buffer",
.argdesc = "ms", .argdesc = "ms",
.text = "Add a buffering delay (in milliseconds) before displaying. "
"This increases latency to compensate for jitter.\n"
"Default is 0 (no buffering).",
}, },
{ {
.longopt_id = OPT_DISPLAY_ID, .longopt_id = OPT_DISPLAY_ID,
@ -498,10 +471,18 @@ static const struct sc_option options[] = {
.text = "List video and audio encoders available on the device.", .text = "List video and audio encoders available on the device.",
}, },
{ {
// deprecated
.longopt_id = OPT_LOCK_VIDEO_ORIENTATION, .longopt_id = OPT_LOCK_VIDEO_ORIENTATION,
.longopt = "lock-video-orientation", .longopt = "lock-video-orientation",
.argdesc = "value", .argdesc = "value",
.optional_arg = true,
.text = "Lock capture video orientation to value.\n"
"Possible values are \"unlocked\", \"initial\" (locked to the "
"initial orientation), 0, 90, 180 and 270. The values "
"represent the clockwise rotation from the natural device "
"orientation, in degrees.\n"
"Default is \"unlocked\".\n"
"Passing the option without argument is equivalent to passing "
"\"initial\".",
}, },
{ {
.shortopt = 'm', .shortopt = 'm',
@ -591,11 +572,12 @@ static const struct sc_option options[] = {
.optional_arg = true, .optional_arg = true,
.text = "Create a new display with the specified resolution and " .text = "Create a new display with the specified resolution and "
"density. If not provided, they default to the main display " "density. If not provided, they default to the main display "
"dimensions and DPI.\n" "dimensions and DPI, and --max-size is considered.\n"
"Examples:\n" "Examples:\n"
" --new-display=1920x1080\n" " --new-display=1920x1080\n"
" --new-display=1920x1080/420 # force 420 dpi\n" " --new-display=1920x1080/420 # force 420 dpi\n"
" --new-display # main display size and density\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", " --new-display=/240 # main display size and 240 dpi",
}, },
{ {
@ -660,20 +642,6 @@ static const struct sc_option options[] = {
.longopt = "no-power-on", .longopt = "no-power-on",
.text = "Do not power on the device on start.", .text = "Do not power on the device on start.",
}, },
{
.longopt_id = OPT_NO_VD_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_id = OPT_NO_VIDEO,
.longopt = "no-video", .longopt = "no-video",
@ -826,13 +794,6 @@ static const struct sc_option options[] = {
.longopt = "turn-screen-off", .longopt = "turn-screen-off",
.text = "Turn the device screen off immediately.", .text = "Turn the device screen off immediately.",
}, },
{
.longopt_id = OPT_SCREEN_OFF_TIMEOUT,
.longopt = "screen-off-timeout",
.argdesc = "seconds",
.text = "Set the screen off timeout while scrcpy is running (restore "
"the initial value on exit).",
},
{ {
.longopt_id = OPT_SHORTCUT_MOD, .longopt_id = OPT_SHORTCUT_MOD,
.longopt = "shortcut-mod", .longopt = "shortcut-mod",
@ -870,17 +831,16 @@ static const struct sc_option options[] = {
{ {
.longopt_id = OPT_TCPIP, .longopt_id = OPT_TCPIP,
.longopt = "tcpip", .longopt = "tcpip",
.argdesc = "[+]ip[:port]", .argdesc = "ip[:port]",
.optional_arg = true, .optional_arg = true,
.text = "Configure and connect the device over TCP/IP.\n" .text = "Configure and reconnect the device over TCP/IP.\n"
"If a destination address is provided, then scrcpy connects to " "If a destination address is provided, then scrcpy connects to "
"this address before starting. The device must listen on the " "this address before starting. The device must listen on the "
"given TCP port (default is 5555).\n" "given TCP port (default is 5555).\n"
"If no destination address is provided, then scrcpy attempts " "If no destination address is provided, then scrcpy attempts "
"to find the IP address of the current device (typically " "to find the IP address of the current device (typically "
"connected over USB), enables TCP/IP mode, then connects to " "connected over USB), enables TCP/IP mode, then connects to "
"this address before starting.\n" "this address before starting.",
"Prefix the address with a '+' to force a reconnection.",
}, },
{ {
.longopt_id = OPT_TIME_LIMIT, .longopt_id = OPT_TIME_LIMIT,
@ -928,6 +888,8 @@ static const struct sc_option options[] = {
.longopt = "v4l2-sink", .longopt = "v4l2-sink",
.argdesc = "/dev/videoN", .argdesc = "/dev/videoN",
.text = "Output to v4l2loopback device.\n" .text = "Output to v4l2loopback device.\n"
"It requires to lock the video orientation (see "
"--lock-video-orientation).\n"
"This feature is only available on Linux.", "This feature is only available on Linux.",
}, },
{ {
@ -936,20 +898,11 @@ static const struct sc_option options[] = {
.argdesc = "ms", .argdesc = "ms",
.text = "Add a buffering delay (in milliseconds) before pushing " .text = "Add a buffering delay (in milliseconds) before pushing "
"frames. This increases latency to compensate for jitter.\n" "frames. This increases latency to compensate for jitter.\n"
"This option is similar to --video-buffer, but specific to " "This option is similar to --display-buffer, but specific to "
"V4L2 sink.\n" "V4L2 sink.\n"
"Default is 0 (no buffering).\n" "Default is 0 (no buffering).\n"
"This option is only available on Linux.", "This option is only available on Linux.",
}, },
{
.longopt_id = OPT_VIDEO_BUFFER,
.longopt = "video-buffer",
.argdesc = "ms",
.text = "Add a buffering delay (in milliseconds) before displaying "
"video frames.\n"
"This increases latency to compensate for jitter.\n"
"Default is 0 (no buffering).",
},
{ {
.longopt_id = OPT_VIDEO_CODEC, .longopt_id = OPT_VIDEO_CODEC,
.longopt = "video-codec", .longopt = "video-codec",
@ -1061,10 +1014,6 @@ static const struct sc_shortcut shortcuts[] = {
.shortcuts = { "MOD+Shift+z" }, .shortcuts = { "MOD+Shift+z" },
.text = "Unpause display", .text = "Unpause display",
}, },
{
.shortcuts = { "MOD+Shift+r" },
.text = "Reset video capture/encoding",
},
{ {
.shortcuts = { "MOD+g" }, .shortcuts = { "MOD+g" },
.text = "Resize window to 1:1 (pixel-perfect)", .text = "Resize window to 1:1 (pixel-perfect)",
@ -1613,6 +1562,78 @@ parse_audio_output_buffer(const char *s, sc_tick *tick) {
return true; return true;
} }
static bool
parse_lock_video_orientation(const char *s,
enum sc_lock_video_orientation *lock_mode) {
if (!s || !strcmp(s, "initial")) {
// Without argument, lock the initial orientation
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_INITIAL;
return true;
}
if (!strcmp(s, "unlocked")) {
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED;
return true;
}
if (!strcmp(s, "0")) {
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_0;
return true;
}
if (!strcmp(s, "90")) {
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_90;
return true;
}
if (!strcmp(s, "180")) {
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_180;
return true;
}
if (!strcmp(s, "270")) {
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_270;
return true;
}
if (!strcmp(s, "1")) {
LOGW("--lock-video-orientation=1 is deprecated, use "
"--lock-video-orientation=270 instead.");
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_270;
return true;
}
if (!strcmp(s, "2")) {
LOGW("--lock-video-orientation=2 is deprecated, use "
"--lock-video-orientation=180 instead.");
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_180;
return true;
}
if (!strcmp(s, "3")) {
LOGW("--lock-video-orientation=3 is deprecated, use "
"--lock-video-orientation=90 instead.");
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_90;
return true;
}
LOGE("Unsupported --lock-video-orientation value: %s (expected initial, "
"unlocked, 0, 90, 180 or 270).", s);
return false;
}
static bool
parse_rotation(const char *s, uint8_t *rotation) {
long value;
bool ok = parse_integer_arg(s, &value, false, 0, 3, "rotation");
if (!ok) {
return false;
}
*rotation = (uint8_t) value;
return true;
}
static bool static bool
parse_orientation(const char *s, enum sc_orientation *orientation) { parse_orientation(const char *s, enum sc_orientation *orientation) {
if (!strcmp(s, "0")) { if (!strcmp(s, "0")) {
@ -1652,32 +1673,6 @@ parse_orientation(const char *s, enum sc_orientation *orientation) {
return false; return false;
} }
static bool
parse_capture_orientation(const char *s, enum sc_orientation *orientation,
enum sc_orientation_lock *lock) {
if (*s == '\0') {
LOGE("Capture orientation may not be empty (expected 0, 90, 180, 270, "
"flip0, flip90, flip180 or flip270, possibly prefixed by '@')");
return false;
}
// Lock the orientation by a leading '@'
if (s[0] == '@') {
// Consume '@'
++s;
if (*s == '\0') {
// Only '@': lock to the initial orientation (orientation is unused)
*lock = SC_ORIENTATION_LOCKED_INITIAL;
return true;
}
*lock = SC_ORIENTATION_LOCKED_VALUE;
} else {
*lock = SC_ORIENTATION_UNLOCKED;
}
return parse_orientation(s, orientation);
}
static bool static bool
parse_window_position(const char *s, int16_t *position) { parse_window_position(const char *s, int16_t *position) {
// special value for "auto" // special value for "auto"
@ -2148,20 +2143,6 @@ parse_time_limit(const char *s, sc_tick *tick) {
return true; return true;
} }
static bool
parse_screen_off_timeout(const char *s, sc_tick *tick) {
long value;
// value in seconds, but must fit in 31 bits in milliseconds
bool ok = parse_integer_arg(s, &value, false, 0, 0x7FFFFFFF / 1000,
"screen off timeout");
if (!ok) {
return false;
}
*tick = SC_TICK_FROM_SEC(value);
return true;
}
static bool static bool
parse_pause_on_exit(const char *s, enum sc_pause_on_exit *pause_on_exit) { parse_pause_on_exit(const char *s, enum sc_pause_on_exit *pause_on_exit) {
if (!s || !strcmp(s, "true")) { if (!s || !strcmp(s, "true")) {
@ -2287,8 +2268,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
opts->crop = optarg; opts->crop = optarg;
break; break;
case OPT_DISPLAY: case OPT_DISPLAY:
LOGE("--display has been removed, use --display-id instead."); LOGW("--display is deprecated, use --display-id instead.");
return false; // fall through
case OPT_DISPLAY_ID: case OPT_DISPLAY_ID:
if (!parse_display_id(optarg, &opts->display_id)) { if (!parse_display_id(optarg, &opts->display_id)) {
return false; return false;
@ -2352,13 +2333,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
"--mouse=uhid instead."); "--mouse=uhid instead.");
return false; return false;
case OPT_LOCK_VIDEO_ORIENTATION: case OPT_LOCK_VIDEO_ORIENTATION:
LOGE("--lock-video-orientation has been removed, use " if (!parse_lock_video_orientation(optarg,
"--capture-orientation instead."); &opts->lock_video_orientation)) {
return false;
case OPT_CAPTURE_ORIENTATION:
if (!parse_capture_orientation(optarg,
&opts->capture_orientation,
&opts->capture_orientation_lock)) {
return false; return false;
} }
break; break;
@ -2376,9 +2352,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
opts->control = false; opts->control = false;
break; break;
case OPT_NO_DISPLAY: case OPT_NO_DISPLAY:
LOGE("--no-display has been removed, use --no-playback " LOGW("--no-display is deprecated, use --no-playback instead.");
"instead."); // fall through
return false;
case 'N': case 'N':
opts->video_playback = false; opts->video_playback = false;
opts->audio_playback = false; opts->audio_playback = false;
@ -2464,9 +2439,32 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
opts->key_inject_mode = SC_KEY_INJECT_MODE_RAW; opts->key_inject_mode = SC_KEY_INJECT_MODE_RAW;
break; break;
case OPT_ROTATION: case OPT_ROTATION:
LOGE("--rotation has been removed, use --orientation or " LOGW("--rotation is deprecated, use --display-orientation "
"--capture-orientation instead."); "instead.");
uint8_t rotation;
if (!parse_rotation(optarg, &rotation)) {
return false; return false;
}
assert(rotation <= 3);
switch (rotation) {
case 0:
opts->display_orientation = SC_ORIENTATION_0;
break;
case 1:
// rotation 1 was 90° counterclockwise, but orientation
// is expressed clockwise
opts->display_orientation = SC_ORIENTATION_270;
break;
case 2:
opts->display_orientation = SC_ORIENTATION_180;
break;
case 3:
// rotation 3 was 270° counterclockwise, but orientation
// is expressed clockwise
opts->display_orientation = SC_ORIENTATION_90;
break;
}
break;
case OPT_DISPLAY_ORIENTATION: case OPT_DISPLAY_ORIENTATION:
if (!parse_orientation(optarg, &opts->display_orientation)) { if (!parse_orientation(optarg, &opts->display_orientation)) {
return false; return false;
@ -2527,9 +2525,23 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
} }
break; break;
case OPT_FORWARD_ALL_CLICKS: case OPT_FORWARD_ALL_CLICKS:
LOGE("--forward-all-clicks has been removed, " LOGW("--forward-all-clicks is deprecated, "
"use --mouse-bind=++++ instead."); "use --mouse-bind=++++ instead.");
return false; opts->mouse_bindings = (struct sc_mouse_bindings) {
.pri = {
.right_click = SC_MOUSE_BINDING_CLICK,
.middle_click = SC_MOUSE_BINDING_CLICK,
.click4 = SC_MOUSE_BINDING_CLICK,
.click5 = SC_MOUSE_BINDING_CLICK,
},
.sec = {
.right_click = SC_MOUSE_BINDING_CLICK,
.middle_click = SC_MOUSE_BINDING_CLICK,
.click4 = SC_MOUSE_BINDING_CLICK,
.click5 = SC_MOUSE_BINDING_CLICK,
},
};
break;
case OPT_LEGACY_PASTE: case OPT_LEGACY_PASTE:
opts->legacy_paste = true; opts->legacy_paste = true;
break; break;
@ -2537,11 +2549,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
opts->power_off_on_close = true; opts->power_off_on_close = true;
break; break;
case OPT_DISPLAY_BUFFER: case OPT_DISPLAY_BUFFER:
LOGE("--display-buffer has been removed, use --video-buffer " if (!parse_buffering_time(optarg, &opts->display_buffer)) {
"instead.");
return false;
case OPT_VIDEO_BUFFER:
if (!parse_buffering_time(optarg, &opts->video_buffer)) {
return false; return false;
} }
break; break;
@ -2706,21 +2714,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
case OPT_START_APP: case OPT_START_APP:
opts->start_app = optarg; opts->start_app = optarg;
break; 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: default:
// getopt prints the error message on stderr // getopt prints the error message on stderr
return false; return false;
@ -2815,6 +2808,13 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
return false; return false;
} }
if (opts->lock_video_orientation ==
SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) {
LOGI("Video orientation is locked for v4l2 sink. "
"See --lock-video-orientation.");
opts->lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_INITIAL;
}
// V4L2 could not handle size change. // V4L2 could not handle size change.
// Do not log because downsizing on error is the default behavior, // Do not log because downsizing on error is the default behavior,
// not an explicit request from the user. // not an explicit request from the user.
@ -2822,7 +2822,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
} }
if (opts->v4l2_buffer && !opts->v4l2_device) { if (opts->v4l2_buffer && !opts->v4l2_device) {
LOGE("V4L2 buffer value without V4L2 sink"); LOGE("V4L2 buffer value without V4L2 sink\n");
return false; return false;
} }
#endif #endif
@ -2841,8 +2841,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
if (otg) { if (otg) {
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_AOA; opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_AOA;
} else if (!opts->video_playback) { } else if (!opts->video_playback) {
LOGI("No video mirroring, SDK mouse disabled"); LOGI("No video mirroring, mouse mode switched to UHID");
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_DISABLED; opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_UHID;
} else { } else {
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_SDK; opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_SDK;
} }
@ -2904,6 +2904,13 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
LOGE("--new-display is incompatible with --no-video"); LOGE("--new-display is incompatible with --no-video");
return false; 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) { if (otg) {

View File

@ -22,6 +22,9 @@
#define MOTIONEVENT_ACTION_LABEL(value) \ #define MOTIONEVENT_ACTION_LABEL(value) \
ENUM_TO_LABEL(android_motionevent_action_labels, value) ENUM_TO_LABEL(android_motionevent_action_labels, value)
#define SCREEN_POWER_MODE_LABEL(value) \
ENUM_TO_LABEL(screen_power_mode_labels, value)
static const char *const android_keyevent_action_labels[] = { static const char *const android_keyevent_action_labels[] = {
"down", "down",
"up", "up",
@ -44,6 +47,14 @@ static const char *const android_motionevent_action_labels[] = {
"btn-release", "btn-release",
}; };
static const char *const screen_power_mode_labels[] = {
"off",
"doze",
"normal",
"doze-suspend",
"suspend",
};
static const char *const copy_key_labels[] = { static const char *const copy_key_labels[] = {
"none", "none",
"copy", "copy",
@ -147,15 +158,13 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) {
size_t len = write_string(&buf[10], msg->set_clipboard.text, size_t len = write_string(&buf[10], msg->set_clipboard.text,
SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH); SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH);
return 10 + len; return 10 + len;
case SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER: case SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE:
buf[1] = msg->set_display_power.on; buf[1] = msg->set_screen_power_mode.mode;
return 2; return 2;
case SC_CONTROL_MSG_TYPE_UHID_CREATE: case SC_CONTROL_MSG_TYPE_UHID_CREATE:
sc_write16be(&buf[1], msg->uhid_create.id); sc_write16be(&buf[1], msg->uhid_create.id);
sc_write16be(&buf[3], msg->uhid_create.vendor_id);
sc_write16be(&buf[5], msg->uhid_create.product_id);
size_t index = 7; size_t index = 3;
index += write_string_tiny(&buf[index], msg->uhid_create.name, 127); index += write_string_tiny(&buf[index], msg->uhid_create.name, 127);
sc_write16be(&buf[index], msg->uhid_create.report_desc_size); sc_write16be(&buf[index], msg->uhid_create.report_desc_size);
@ -183,7 +192,6 @@ 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_COLLAPSE_PANELS:
case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE: case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE:
case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS: case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
case SC_CONTROL_MSG_TYPE_RESET_VIDEO:
// no additional data // no additional data
return 1; return 1;
default: default:
@ -260,9 +268,9 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
msg->set_clipboard.paste ? "paste" : "nopaste", msg->set_clipboard.paste ? "paste" : "nopaste",
msg->set_clipboard.text); msg->set_clipboard.text);
break; break;
case SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER: case SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE:
LOG_CMSG("display power %s", LOG_CMSG("power mode %s",
msg->set_display_power.on ? "on" : "off"); SCREEN_POWER_MODE_LABEL(msg->set_screen_power_mode.mode));
break; break;
case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL:
LOG_CMSG("expand notification panel"); LOG_CMSG("expand notification panel");
@ -280,13 +288,9 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
// Quote only if name is not null // Quote only if name is not null
const char *name = msg->uhid_create.name; const char *name = msg->uhid_create.name;
const char *quote = name ? "\"" : ""; const char *quote = name ? "\"" : "";
LOG_CMSG("UHID create [%" PRIu16 "] %04" PRIx16 ":%04" PRIx16 LOG_CMSG("UHID create [%" PRIu16 "] name=%s%s%s "
" name=%s%s%s report_desc_size=%" PRIu16, "report_desc_size=%" PRIu16, msg->uhid_create.id,
msg->uhid_create.id, quote, name, quote, msg->uhid_create.report_desc_size);
msg->uhid_create.vendor_id,
msg->uhid_create.product_id,
quote, name, quote,
msg->uhid_create.report_desc_size);
break; break;
} }
case SC_CONTROL_MSG_TYPE_UHID_INPUT: { case SC_CONTROL_MSG_TYPE_UHID_INPUT: {
@ -311,9 +315,6 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
case SC_CONTROL_MSG_TYPE_START_APP: case SC_CONTROL_MSG_TYPE_START_APP:
LOG_CMSG("start app \"%s\"", msg->start_app.name); LOG_CMSG("start app \"%s\"", msg->start_app.name);
break; break;
case SC_CONTROL_MSG_TYPE_RESET_VIDEO:
LOG_CMSG("reset video");
break;
default: default:
LOG_CMSG("unknown type: %u", (unsigned) msg->type); LOG_CMSG("unknown type: %u", (unsigned) msg->type);
break; break;

View File

@ -35,14 +35,19 @@ enum sc_control_msg_type {
SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS, SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS,
SC_CONTROL_MSG_TYPE_GET_CLIPBOARD, SC_CONTROL_MSG_TYPE_GET_CLIPBOARD,
SC_CONTROL_MSG_TYPE_SET_CLIPBOARD, SC_CONTROL_MSG_TYPE_SET_CLIPBOARD,
SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER, SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE,
SC_CONTROL_MSG_TYPE_ROTATE_DEVICE, SC_CONTROL_MSG_TYPE_ROTATE_DEVICE,
SC_CONTROL_MSG_TYPE_UHID_CREATE, SC_CONTROL_MSG_TYPE_UHID_CREATE,
SC_CONTROL_MSG_TYPE_UHID_INPUT, SC_CONTROL_MSG_TYPE_UHID_INPUT,
SC_CONTROL_MSG_TYPE_UHID_DESTROY, SC_CONTROL_MSG_TYPE_UHID_DESTROY,
SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS, SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS,
SC_CONTROL_MSG_TYPE_START_APP, SC_CONTROL_MSG_TYPE_START_APP,
SC_CONTROL_MSG_TYPE_RESET_VIDEO, };
enum sc_screen_power_mode {
// see <https://android.googlesource.com/platform/frameworks/base.git/+/pie-release-2/core/java/android/view/SurfaceControl.java#305>
SC_SCREEN_POWER_MODE_OFF = 0,
SC_SCREEN_POWER_MODE_NORMAL = 2,
}; };
enum sc_copy_key { enum sc_copy_key {
@ -90,12 +95,10 @@ struct sc_control_msg {
bool paste; bool paste;
} set_clipboard; } set_clipboard;
struct { struct {
bool on; enum sc_screen_power_mode mode;
} set_display_power; } set_screen_power_mode;
struct { struct {
uint16_t id; uint16_t id;
uint16_t vendor_id;
uint16_t product_id;
const char *name; // pointer to static data const char *name; // pointer to static data
uint16_t report_desc_size; uint16_t report_desc_size;
const uint8_t *report_desc; // pointer to static data const uint8_t *report_desc; // pointer to static data

View File

@ -15,6 +15,7 @@ struct sc_hid_input {
struct sc_hid_open { struct sc_hid_open {
uint16_t hid_id; uint16_t hid_id;
const char *name; // pointer to static memory
const uint8_t *report_desc; // pointer to static memory const uint8_t *report_desc; // pointer to static memory
size_t report_desc_size; size_t report_desc_size;
}; };

View File

@ -52,10 +52,10 @@ static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = {
0x09, 0x30, 0x09, 0x30,
// Usage (Y) Left stick y // Usage (Y) Left stick y
0x09, 0x31, 0x09, 0x31,
// Usage (Rx) Right stick x // Usage (Z) Right stick x
0x09, 0x33, 0x09, 0x32,
// Usage (Ry) Right stick y // Usage (Rz) Right stick y
0x09, 0x34, 0x09, 0x35,
// Logical Minimum (0) // Logical Minimum (0)
0x15, 0x00, 0x15, 0x00,
// Logical Maximum (65535) // Logical Maximum (65535)
@ -65,15 +65,15 @@ static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = {
0x75, 0x10, 0x75, 0x10,
// Report Count (4) // Report Count (4)
0x95, 0x04, 0x95, 0x04,
// Input (Data, Variable, Absolute): 4x2 bytes (X, Y, Z, Rz) // Input (Data, Variable, Absolute): 4 bytes (X, Y, Z, Rz)
0x81, 0x02, 0x81, 0x02,
// Usage Page (Generic Desktop) // Usage Page (Simulation Controls)
0x05, 0x01, 0x05, 0x02,
// Usage (Z) // Usage (Brake)
0x09, 0x32, 0x09, 0xC5,
// Usage (Rz) // Usage (Accelerator)
0x09, 0x35, 0x09, 0xC4,
// Logical Minimum (0) // Logical Minimum (0)
0x15, 0x00, 0x15, 0x00,
// Logical Maximum (32767) // Logical Maximum (32767)
@ -82,7 +82,7 @@ static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = {
0x75, 0x10, 0x75, 0x10,
// Report Count (2) // Report Count (2)
0x95, 0x02, 0x95, 0x02,
// Input (Data, Variable, Absolute): 2x2 bytes (L2, R2) // Input (Data, Variable, Absolute): 2 bytes (L2, R2)
0x81, 0x02, 0x81, 0x02,
// Usage Page (Buttons) // Usage Page (Buttons)
@ -182,7 +182,7 @@ static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = {
* `------------- SC_GAMEPAD_BUTTON_RIGHT_STICK * `------------- SC_GAMEPAD_BUTTON_RIGHT_STICK
* *
* +---------------+ * +---------------+
* byte 14: |0 0 0 0 . . . .| hat switch (dpad) position (0-8) * byte 14: |0 0 0 . . . . .| hat switch (dpad) position (0-8)
* +---------------+ * +---------------+
* 9 possible positions and their values: * 9 possible positions and their values:
* 8 1 2 * 8 1 2
@ -191,19 +191,16 @@ static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = {
* (8 is top-left, 1 is top, 2 is top-right, etc.) * (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 static void
sc_hid_gamepad_slot_init(struct sc_hid_gamepad_slot *slot, sc_hid_gamepad_slot_init(struct sc_hid_gamepad_slot *slot,
uint32_t gamepad_id) { uint32_t gamepad_id) {
assert(gamepad_id != SC_GAMEPAD_ID_INVALID); assert(gamepad_id != SC_GAMEPAD_ID_INVALID);
slot->gamepad_id = gamepad_id; slot->gamepad_id = gamepad_id;
slot->buttons = 0; slot->buttons = 0;
slot->axis_left_x = AXIS_RESCALE(0); slot->axis_left_x = 0;
slot->axis_left_y = AXIS_RESCALE(0); slot->axis_left_y = 0;
slot->axis_right_x = AXIS_RESCALE(0); slot->axis_right_x = 0;
slot->axis_right_y = AXIS_RESCALE(0); slot->axis_right_y = 0;
slot->axis_left_trigger = 0; slot->axis_left_trigger = 0;
slot->axis_right_trigger = 0; slot->axis_right_trigger = 0;
} }
@ -246,8 +243,14 @@ sc_hid_gamepad_generate_open(struct sc_hid_gamepad *hid,
sc_hid_gamepad_slot_init(&hid->slots[slot_idx], gamepad_id); 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); uint16_t hid_id = sc_hid_gamepad_slot_get_id(slot_idx);
hid_open->hid_id = hid_id; hid_open->hid_id = hid_id;
hid_open->name = name;
hid_open->report_desc = SC_HID_GAMEPAD_REPORT_DESC; hid_open->report_desc = SC_HID_GAMEPAD_REPORT_DESC;
hid_open->report_desc_size = sizeof(SC_HID_GAMEPAD_REPORT_DESC); hid_open->report_desc_size = sizeof(SC_HID_GAMEPAD_REPORT_DESC);
@ -420,6 +423,8 @@ sc_hid_gamepad_generate_input_from_axis(struct sc_hid_gamepad *hid,
struct sc_hid_gamepad_slot *slot = &hid->slots[slot_idx]; 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) { switch (event->axis) {
case SC_GAMEPAD_AXIS_LEFTX: case SC_GAMEPAD_AXIS_LEFTX:
slot->axis_left_x = AXIS_RESCALE(event->value); slot->axis_left_x = AXIS_RESCALE(event->value);

View File

@ -335,6 +335,7 @@ sc_hid_keyboard_generate_input_from_mods(struct sc_hid_input *hid_input,
void sc_hid_keyboard_generate_open(struct sc_hid_open *hid_open) { void sc_hid_keyboard_generate_open(struct sc_hid_open *hid_open) {
hid_open->hid_id = SC_HID_ID_KEYBOARD; 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 = SC_HID_KEYBOARD_REPORT_DESC;
hid_open->report_desc_size = sizeof(SC_HID_KEYBOARD_REPORT_DESC); hid_open->report_desc_size = sizeof(SC_HID_KEYBOARD_REPORT_DESC);
} }

View File

@ -190,6 +190,7 @@ 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) { void sc_hid_mouse_generate_open(struct sc_hid_open *hid_open) {
hid_open->hid_id = SC_HID_ID_MOUSE; 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 = SC_HID_MOUSE_REPORT_DESC;
hid_open->report_desc_size = sizeof(SC_HID_MOUSE_REPORT_DESC); hid_open->report_desc_size = sizeof(SC_HID_MOUSE_REPORT_DESC);
} }

View File

@ -9,7 +9,6 @@
#include "config.h" #include "config.h"
#include "compat.h" #include "compat.h"
#include "util/env.h"
#include "util/file.h" #include "util/file.h"
#include "util/log.h" #include "util/log.h"
#include "util/str.h" #include "util/str.h"
@ -20,22 +19,35 @@
static char * static char *
get_icon_path(void) { get_icon_path(void) {
char *icon_path = sc_get_env("SCRCPY_ICON_PATH"); #ifdef __WINDOWS__
if (icon_path) { const wchar_t *icon_path_env = _wgetenv(L"SCRCPY_ICON_PATH");
#else
const char *icon_path_env = getenv("SCRCPY_ICON_PATH");
#endif
if (icon_path_env) {
// if the envvar is set, use it // 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); LOGD("Using SCRCPY_ICON_PATH: %s", icon_path);
return icon_path; return icon_path;
} }
#ifndef PORTABLE #ifndef PORTABLE
LOGD("Using icon: " SCRCPY_DEFAULT_ICON_PATH); LOGD("Using icon: " SCRCPY_DEFAULT_ICON_PATH);
icon_path = strdup(SCRCPY_DEFAULT_ICON_PATH); char *icon_path = strdup(SCRCPY_DEFAULT_ICON_PATH);
if (!icon_path) { if (!icon_path) {
LOG_OOM(); LOG_OOM();
return NULL; return NULL;
} }
#else #else
icon_path = sc_file_get_local_path(SCRCPY_PORTABLE_ICON_FILENAME); char *icon_path = sc_file_get_local_path(SCRCPY_PORTABLE_ICON_FILENAME);
if (!icon_path) { if (!icon_path) {
LOGE("Could not get icon path"); LOGE("Could not get icon path");
return NULL; return NULL;

View File

@ -412,12 +412,18 @@ struct sc_touch_event {
float pressure; 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>: // 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 // The ID value starts at 0 and increments from there. The value -1 is an
// invalid ID. // invalid ID.
#define SC_GAMEPAD_ID_INVALID UINT32_C(-1) #define SC_GAMEPAD_ID_INVALID UINT32_C(-1)
struct sc_gamepad_device_event { struct sc_gamepad_device_event {
enum sc_gamepad_device_event_type type;
uint32_t gamepad_id; uint32_t gamepad_id;
}; };
@ -497,6 +503,16 @@ sc_mouse_buttons_state_from_sdl(uint32_t buttons_state) {
return 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 static inline enum sc_gamepad_axis
sc_gamepad_axis_from_sdl(uint8_t axis) { sc_gamepad_axis_from_sdl(uint8_t axis) {
if (axis <= SDL_CONTROLLER_AXIS_TRIGGERRIGHT) { if (axis <= SDL_CONTROLLER_AXIS_TRIGGERRIGHT) {

View File

@ -203,12 +203,13 @@ set_device_clipboard(struct sc_input_manager *im, bool paste,
} }
static void static void
set_display_power(struct sc_input_manager *im, bool on) { set_screen_power_mode(struct sc_input_manager *im,
enum sc_screen_power_mode mode) {
assert(im->controller); assert(im->controller);
struct sc_control_msg msg; struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER; msg.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE;
msg.set_display_power.on = on; msg.set_screen_power_mode.mode = mode;
if (!sc_controller_push_msg(im->controller, &msg)) { if (!sc_controller_push_msg(im->controller, &msg)) {
LOGW("Could not request 'set screen power mode'"); LOGW("Could not request 'set screen power mode'");
@ -284,18 +285,6 @@ open_hard_keyboard_settings(struct sc_input_manager *im) {
} }
} }
static void
reset_video(struct sc_input_manager *im) {
assert(im->controller);
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_RESET_VIDEO;
if (!sc_controller_push_msg(im->controller, &msg)) {
LOGW("Could not request reset video");
}
}
static void static void
apply_orientation_transform(struct sc_input_manager *im, apply_orientation_transform(struct sc_input_manager *im,
enum sc_orientation transform) { enum sc_orientation transform) {
@ -426,8 +415,10 @@ sc_input_manager_process_key(struct sc_input_manager *im,
return; return;
case SDLK_o: case SDLK_o:
if (control && !repeat && down && !paused) { if (control && !repeat && down && !paused) {
bool on = shift; enum sc_screen_power_mode mode = shift
set_display_power(im, on); ? SC_SCREEN_POWER_MODE_NORMAL
: SC_SCREEN_POWER_MODE_OFF;
set_screen_power_mode(im, mode);
} }
return; return;
case SDLK_z: case SDLK_z:
@ -533,13 +524,9 @@ sc_input_manager_process_key(struct sc_input_manager *im,
} }
return; return;
case SDLK_r: case SDLK_r:
if (control && !repeat && down && !paused) { if (control && !shift && !repeat && down && !paused) {
if (shift) {
reset_video(im);
} else {
rotate_device(im); rotate_device(im);
} }
}
return; return;
case SDLK_k: case SDLK_k:
if (control && !shift && !repeat && down && !paused if (control && !shift && !repeat && down && !paused
@ -908,6 +895,7 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im,
static void static void
sc_input_manager_process_gamepad_device(struct sc_input_manager *im, sc_input_manager_process_gamepad_device(struct sc_input_manager *im,
const SDL_ControllerDeviceEvent *event) { const SDL_ControllerDeviceEvent *event) {
SDL_JoystickID id;
if (event->type == SDL_CONTROLLERDEVICEADDED) { if (event->type == SDL_CONTROLLERDEVICEADDED) {
SDL_GameController *gc = SDL_GameControllerOpen(event->which); SDL_GameController *gc = SDL_GameControllerOpen(event->which);
if (!gc) { if (!gc) {
@ -922,12 +910,9 @@ sc_input_manager_process_gamepad_device(struct sc_input_manager *im,
return; return;
} }
struct sc_gamepad_device_event evt = { id = SDL_JoystickInstanceID(joystick);
.gamepad_id = SDL_JoystickInstanceID(joystick),
};
im->gp->ops->process_gamepad_added(im->gp, &evt);
} else if (event->type == SDL_CONTROLLERDEVICEREMOVED) { } else if (event->type == SDL_CONTROLLERDEVICEREMOVED) {
SDL_JoystickID id = event->which; id = event->which;
SDL_GameController *gc = SDL_GameControllerFromInstanceID(id); SDL_GameController *gc = SDL_GameControllerFromInstanceID(id);
if (gc) { if (gc) {
@ -935,15 +920,16 @@ sc_input_manager_process_gamepad_device(struct sc_input_manager *im,
} else { } else {
LOGW("Unknown gamepad device removed"); LOGW("Unknown gamepad device removed");
} }
struct sc_gamepad_device_event evt = {
.gamepad_id = id,
};
im->gp->ops->process_gamepad_removed(im->gp, &evt);
} else { } else {
// Nothing to do // Nothing to do
return; 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 static void

View File

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

View File

@ -84,12 +84,6 @@ enum sc_orientation { // v v v
SC_ORIENTATION_FLIP_270, // 1 1 1 SC_ORIENTATION_FLIP_270, // 1 1 1
}; };
enum sc_orientation_lock {
SC_ORIENTATION_UNLOCKED,
SC_ORIENTATION_LOCKED_VALUE, // lock to specified orientation
SC_ORIENTATION_LOCKED_INITIAL, // lock to initial device orientation
};
static inline bool static inline bool
sc_orientation_is_mirror(enum sc_orientation orientation) { sc_orientation_is_mirror(enum sc_orientation orientation) {
assert(!(orientation & ~7)); assert(!(orientation & ~7));
@ -136,6 +130,16 @@ sc_orientation_get_name(enum sc_orientation orientation) {
} }
} }
enum sc_lock_video_orientation {
SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1,
// lock the current orientation when scrcpy starts
SC_LOCK_VIDEO_ORIENTATION_INITIAL = -2,
SC_LOCK_VIDEO_ORIENTATION_0 = 0,
SC_LOCK_VIDEO_ORIENTATION_90 = 3,
SC_LOCK_VIDEO_ORIENTATION_180 = 2,
SC_LOCK_VIDEO_ORIENTATION_270 = 1,
};
enum sc_keyboard_input_mode { enum sc_keyboard_input_mode {
SC_KEYBOARD_INPUT_MODE_AUTO, SC_KEYBOARD_INPUT_MODE_AUTO,
SC_KEYBOARD_INPUT_MODE_UHID_OR_AOA, // normal vs otg mode SC_KEYBOARD_INPUT_MODE_UHID_OR_AOA, // normal vs otg mode
@ -247,9 +251,7 @@ struct scrcpy_options {
uint32_t video_bit_rate; uint32_t video_bit_rate;
uint32_t audio_bit_rate; uint32_t audio_bit_rate;
const char *max_fps; // float to be parsed by the server const char *max_fps; // float to be parsed by the server
const char *angle; // float to be parsed by the server enum sc_lock_video_orientation lock_video_orientation;
enum sc_orientation capture_orientation;
enum sc_orientation_lock capture_orientation_lock;
enum sc_orientation display_orientation; enum sc_orientation display_orientation;
enum sc_orientation record_orientation; enum sc_orientation record_orientation;
int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto" int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto"
@ -257,11 +259,10 @@ struct scrcpy_options {
uint16_t window_width; uint16_t window_width;
uint16_t window_height; uint16_t window_height;
uint32_t display_id; uint32_t display_id;
sc_tick video_buffer; sc_tick display_buffer;
sc_tick audio_buffer; sc_tick audio_buffer;
sc_tick audio_output_buffer; sc_tick audio_output_buffer;
sc_tick time_limit; sc_tick time_limit;
sc_tick screen_off_timeout;
#ifdef HAVE_V4L2 #ifdef HAVE_V4L2
const char *v4l2_device; const char *v4l2_device;
sc_tick v4l2_buffer; sc_tick v4l2_buffer;
@ -310,8 +311,6 @@ struct scrcpy_options {
bool audio_dup; bool audio_dup;
const char *new_display; // [<width>x<height>][/<dpi>] parsed by the server const char *new_display; // [<width>x<height>][/<dpi>] parsed by the server
const char *start_app; const char *start_app;
bool vd_destroy_content;
bool vd_system_decorations;
}; };
extern const struct scrcpy_options scrcpy_options_default; extern const struct scrcpy_options scrcpy_options_default;

View File

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

View File

@ -53,7 +53,7 @@ struct scrcpy {
struct sc_decoder video_decoder; struct sc_decoder video_decoder;
struct sc_decoder audio_decoder; struct sc_decoder audio_decoder;
struct sc_recorder recorder; struct sc_recorder recorder;
struct sc_delay_buffer video_buffer; struct sc_delay_buffer display_buffer;
#ifdef HAVE_V4L2 #ifdef HAVE_V4L2
struct sc_v4l2_sink v4l2_sink; struct sc_v4l2_sink v4l2_sink;
struct sc_delay_buffer v4l2_buffer; struct sc_delay_buffer v4l2_buffer;
@ -428,10 +428,7 @@ scrcpy(struct scrcpy_options *options) {
.video_bit_rate = options->video_bit_rate, .video_bit_rate = options->video_bit_rate,
.audio_bit_rate = options->audio_bit_rate, .audio_bit_rate = options->audio_bit_rate,
.max_fps = options->max_fps, .max_fps = options->max_fps,
.angle = options->angle, .lock_video_orientation = options->lock_video_orientation,
.screen_off_timeout = options->screen_off_timeout,
.capture_orientation = options->capture_orientation,
.capture_orientation_lock = options->capture_orientation_lock,
.control = options->control, .control = options->control,
.display_id = options->display_id, .display_id = options->display_id,
.new_display = options->new_display, .new_display = options->new_display,
@ -458,8 +455,6 @@ scrcpy(struct scrcpy_options *options) {
.power_on = options->power_on, .power_on = options->power_on,
.kill_adb_on_close = options->kill_adb_on_close, .kill_adb_on_close = options->kill_adb_on_close,
.camera_high_speed = options->camera_high_speed, .camera_high_speed = options->camera_high_speed,
.vd_destroy_content = options->vd_destroy_content,
.vd_system_decorations = options->vd_system_decorations,
.list = options->list, .list = options->list,
}; };
@ -820,11 +815,11 @@ aoa_complete:
if (options->video_playback) { if (options->video_playback) {
struct sc_frame_source *src = &s->video_decoder.frame_source; struct sc_frame_source *src = &s->video_decoder.frame_source;
if (options->video_buffer) { if (options->display_buffer) {
sc_delay_buffer_init(&s->video_buffer, sc_delay_buffer_init(&s->display_buffer,
options->video_buffer, true); options->display_buffer, true);
sc_frame_source_add_sink(src, &s->video_buffer.frame_sink); sc_frame_source_add_sink(src, &s->display_buffer.frame_sink);
src = &s->video_buffer.frame_source; src = &s->display_buffer.frame_source;
} }
sc_frame_source_add_sink(src, &s->screen.frame_sink); sc_frame_source_add_sink(src, &s->screen.frame_sink);
@ -878,11 +873,11 @@ aoa_complete:
// everything is set up // everything is set up
if (options->control && options->turn_screen_off) { if (options->control && options->turn_screen_off) {
struct sc_control_msg msg; struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER; msg.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE;
msg.set_display_power.on = false; msg.set_screen_power_mode.mode = SC_SCREEN_POWER_MODE_OFF;
if (!sc_controller_push_msg(&s->controller, &msg)) { if (!sc_controller_push_msg(&s->controller, &msg)) {
LOGW("Could not request 'set display power'"); LOGW("Could not request 'set screen power mode'");
} }
} }

View File

@ -9,7 +9,6 @@
#include "adb/adb.h" #include "adb/adb.h"
#include "util/binary.h" #include "util/binary.h"
#include "util/env.h"
#include "util/file.h" #include "util/file.h"
#include "util/log.h" #include "util/log.h"
#include "util/net_intr.h" #include "util/net_intr.h"
@ -26,22 +25,35 @@
static char * static char *
get_server_path(void) { get_server_path(void) {
char *server_path = sc_get_env("SCRCPY_SERVER_PATH"); #ifdef __WINDOWS__
if (server_path) { const wchar_t *server_path_env = _wgetenv(L"SCRCPY_SERVER_PATH");
#else
const char *server_path_env = getenv("SCRCPY_SERVER_PATH");
#endif
if (server_path_env) {
// if the envvar is set, use it // 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); LOGD("Using SCRCPY_SERVER_PATH: %s", server_path);
return server_path; return server_path;
} }
#ifndef PORTABLE #ifndef PORTABLE
LOGD("Using server: " SC_SERVER_PATH_DEFAULT); LOGD("Using server: " SC_SERVER_PATH_DEFAULT);
server_path = strdup(SC_SERVER_PATH_DEFAULT); char *server_path = strdup(SC_SERVER_PATH_DEFAULT);
if (!server_path) { if (!server_path) {
LOG_OOM(); LOG_OOM();
return NULL; return NULL;
} }
#else #else
server_path = sc_file_get_local_path(SC_SERVER_FILENAME); char *server_path = sc_file_get_local_path(SC_SERVER_FILENAME);
if (!server_path) { if (!server_path) {
LOGE("Could not get local file path, " LOGE("Could not get local file path, "
"using " SC_SERVER_FILENAME " from current directory"); "using " SC_SERVER_FILENAME " from current directory");
@ -189,31 +201,18 @@ execute_server(struct sc_server *server,
cmd[count++] = "app_process"; cmd[count++] = "app_process";
#ifdef SERVER_DEBUGGER #ifdef SERVER_DEBUGGER
uint16_t sdk_version = sc_adb_get_device_sdk_version(&server->intr, serial);
if (!sdk_version) {
LOGE("Could not determine SDK version");
return 0;
}
# define SERVER_DEBUGGER_PORT "5005" # define SERVER_DEBUGGER_PORT "5005"
const char *dbg; cmd[count++] =
if (sdk_version < 28) { # ifdef SERVER_DEBUGGER_METHOD_NEW
// Android < 9 /* Android 9 and above */
dbg = "-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address=" "-XjdwpProvider:internal -XjdwpOptions:transport=dt_socket,suspend=y,"
"server=y,address="
# else
/* Android 8 and below */
"-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address="
# endif
SERVER_DEBUGGER_PORT; 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 #endif
cmd[count++] = "/"; // unused cmd[count++] = "/"; // unused
cmd[count++] = "com.genymobile.scrcpy.Server"; cmd[count++] = "com.genymobile.scrcpy.Server";
cmd[count++] = SCRCPY_VERSION; cmd[count++] = SCRCPY_VERSION;
@ -275,21 +274,9 @@ execute_server(struct sc_server *server,
VALIDATE_STRING(params->max_fps); VALIDATE_STRING(params->max_fps);
ADD_PARAM("max_fps=%s", params->max_fps); ADD_PARAM("max_fps=%s", params->max_fps);
} }
if (params->angle) { if (params->lock_video_orientation != SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) {
VALIDATE_STRING(params->angle); ADD_PARAM("lock_video_orientation=%" PRIi8,
ADD_PARAM("angle=%s", params->angle); params->lock_video_orientation);
}
if (params->capture_orientation_lock != SC_ORIENTATION_UNLOCKED
|| params->capture_orientation != SC_ORIENTATION_0) {
if (params->capture_orientation_lock == SC_ORIENTATION_LOCKED_INITIAL) {
ADD_PARAM("capture_orientation=@");
} else {
const char *orient =
sc_orientation_get_name(params->capture_orientation);
bool locked =
params->capture_orientation_lock != SC_ORIENTATION_UNLOCKED;
ADD_PARAM("capture_orientation=%s%s", locked ? "@" : "", orient);
}
} }
if (server->tunnel.forward) { if (server->tunnel.forward) {
ADD_PARAM("tunnel_forward=true"); ADD_PARAM("tunnel_forward=true");
@ -333,11 +320,6 @@ execute_server(struct sc_server *server,
if (params->stay_awake) { if (params->stay_awake) {
ADD_PARAM("stay_awake=true"); ADD_PARAM("stay_awake=true");
} }
if (params->screen_off_timeout != -1) {
assert(params->screen_off_timeout >= 0);
uint64_t ms = SC_TICK_TO_MS(params->screen_off_timeout);
ADD_PARAM("screen_off_timeout=%" PRIu64, ms);
}
if (params->video_codec_options) { if (params->video_codec_options) {
VALIDATE_STRING(params->video_codec_options); VALIDATE_STRING(params->video_codec_options);
ADD_PARAM("video_codec_options=%s", params->video_codec_options); ADD_PARAM("video_codec_options=%s", params->video_codec_options);
@ -377,12 +359,6 @@ execute_server(struct sc_server *server,
VALIDATE_STRING(params->new_display); VALIDATE_STRING(params->new_display);
ADD_PARAM("new_display=%s", 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) { if (params->list & SC_OPTION_LIST_ENCODERS) {
ADD_PARAM("list_encoders=true"); ADD_PARAM("list_encoders=true");
} }
@ -404,14 +380,10 @@ execute_server(struct sc_server *server,
cmd[count++] = NULL; cmd[count++] = NULL;
#ifdef SERVER_DEBUGGER #ifdef SERVER_DEBUGGER
LOGI("Server debugger listening%s...", LOGI("Server debugger waiting for a client on device port "
sdk_version < 30 ? " on port " SERVER_DEBUGGER_PORT : ""); SERVER_DEBUGGER_PORT "...");
// For Android < 11, from the computer: // From the computer, run
// - run `adb forward tcp:5005 tcp:5005` // adb forward tcp:5005 tcp:5005
// For Android >= 11:
// - execute `adb jdwp` to get the jdwp port
// - run `adb forward tcp:5005 jdwp:XXXX` (replace XXXX)
//
// Then, from Android Studio: Run > Debug > Edit configurations... // Then, from Android Studio: Run > Debug > Edit configurations...
// On the left, click on '+', "Remote", with: // On the left, click on '+', "Remote", with:
// Host: localhost // Host: localhost
@ -488,21 +460,14 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params,
// end of the program // end of the program
server->params = *params; server->params = *params;
bool ok = sc_adb_init(); bool ok = sc_mutex_init(&server->mutex);
if (!ok) { if (!ok) {
return false; return false;
} }
ok = sc_mutex_init(&server->mutex);
if (!ok) {
sc_adb_destroy();
return false;
}
ok = sc_cond_init(&server->cond_stopped); ok = sc_cond_init(&server->cond_stopped);
if (!ok) { if (!ok) {
sc_mutex_destroy(&server->mutex); sc_mutex_destroy(&server->mutex);
sc_adb_destroy();
return false; return false;
} }
@ -510,7 +475,6 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params,
if (!ok) { if (!ok) {
sc_cond_destroy(&server->cond_stopped); sc_cond_destroy(&server->cond_stopped);
sc_mutex_destroy(&server->mutex); sc_mutex_destroy(&server->mutex);
sc_adb_destroy();
return false; return false;
} }
@ -832,14 +796,11 @@ sc_server_switch_to_tcpip(struct sc_server *server, const char *serial) {
} }
static bool 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; struct sc_intr *intr = &server->intr;
if (disconnect) {
// Error expected if not connected, do not report any error // Error expected if not connected, do not report any error
sc_adb_disconnect(intr, ip_port, SC_ADB_SILENT); sc_adb_disconnect(intr, ip_port, SC_ADB_SILENT);
}
LOGI("Connecting to %s...", ip_port); LOGI("Connecting to %s...", ip_port);
@ -855,7 +816,7 @@ sc_server_connect_to_tcpip(struct sc_server *server, const char *ip_port,
static bool static bool
sc_server_configure_tcpip_known_address(struct sc_server *server, sc_server_configure_tcpip_known_address(struct sc_server *server,
const char *addr, bool disconnect) { const char *addr) {
// Append ":5555" if no port is present // Append ":5555" if no port is present
bool contains_port = strchr(addr, ':'); bool contains_port = strchr(addr, ':');
char *ip_port = contains_port ? strdup(addr) char *ip_port = contains_port ? strdup(addr)
@ -866,7 +827,7 @@ sc_server_configure_tcpip_known_address(struct sc_server *server,
} }
server->serial = ip_port; server->serial = ip_port;
return sc_server_connect_to_tcpip(server, ip_port, disconnect); return sc_server_connect_to_tcpip(server, ip_port);
} }
static bool static bool
@ -891,7 +852,7 @@ sc_server_configure_tcpip_unknown_address(struct sc_server *server,
} }
server->serial = ip_port; server->serial = ip_port;
return sc_server_connect_to_tcpip(server, ip_port, false); return sc_server_connect_to_tcpip(server, ip_port);
} }
static void static void
@ -978,13 +939,7 @@ run_server(void *data) {
sc_adb_device_destroy(&device); sc_adb_device_destroy(&device);
} }
} else { } else {
// If the user passed a '+' (--tcpip=+ip), then disconnect first ok = sc_server_configure_tcpip_known_address(server, params->tcpip_dst);
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) { if (!ok) {
goto error_connection_failed; goto error_connection_failed;
} }
@ -1161,6 +1116,4 @@ sc_server_destroy(struct sc_server *server) {
sc_intr_destroy(&server->intr); sc_intr_destroy(&server->intr);
sc_cond_destroy(&server->cond_stopped); sc_cond_destroy(&server->cond_stopped);
sc_mutex_destroy(&server->mutex); sc_mutex_destroy(&server->mutex);
sc_adb_destroy();
} }

View File

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

View File

@ -6,9 +6,6 @@
#include <string.h> #include <string.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <unistd.h> #include <unistd.h>
#ifdef __APPLE__
# include <mach-o/dyld.h> // for _NSGetExecutablePath()
#endif
#include "util/log.h" #include "util/log.h"
@ -63,22 +60,11 @@ sc_file_get_executable_path(void) {
} }
buf[len] = '\0'; buf[len] = '\0';
return strdup(buf); 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 #else
// "_" is often used to store the full path of the command being executed // in practice, we only need this feature for portable builds, only used on
char *path = getenv("_"); // Windows, so we don't care implementing it for every platform
if (!path) { // (it's useful to have a working version on Linux for debugging though)
LOGE("Could not determine executable path");
return NULL; return NULL;
}
return strdup(path);
#endif #endif
} }

View File

@ -20,21 +20,12 @@ struct sc_gamepad_processor {
struct sc_gamepad_processor_ops { struct sc_gamepad_processor_ops {
/** /**
* Process a gamepad device added event * Process a gamepad device added or removed
* *
* This function is mandatory. * This function is mandatory.
*/ */
void void
(*process_gamepad_added)(struct sc_gamepad_processor *gp, (*process_gamepad_device)(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); const struct sc_gamepad_device_event *event);
/** /**

View File

@ -7,11 +7,6 @@
/** Downcast gamepad processor to sc_gamepad_uhid */ /** Downcast gamepad processor to sc_gamepad_uhid */
#define DOWNCAST(GP) container_of(GP, struct sc_gamepad_uhid, gamepad_processor) #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 static void
sc_gamepad_uhid_send_input(struct sc_gamepad_uhid *gamepad, sc_gamepad_uhid_send_input(struct sc_gamepad_uhid *gamepad,
const struct sc_hid_input *hid_input, const struct sc_hid_input *hid_input,
@ -35,9 +30,7 @@ sc_gamepad_uhid_send_open(struct sc_gamepad_uhid *gamepad,
struct sc_control_msg msg; struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE; msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE;
msg.uhid_create.id = hid_open->hid_id; msg.uhid_create.id = hid_open->hid_id;
msg.uhid_create.vendor_id = SC_GAMEPAD_UHID_VENDOR_ID; msg.uhid_create.name = hid_open->name;
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 = hid_open->report_desc;
msg.uhid_create.report_desc_size = hid_open->report_desc_size; msg.uhid_create.report_desc_size = hid_open->report_desc_size;
@ -59,29 +52,20 @@ sc_gamepad_uhid_send_close(struct sc_gamepad_uhid *gamepad,
} }
static void static void
sc_gamepad_processor_process_gamepad_added(struct sc_gamepad_processor *gp, sc_gamepad_processor_process_gamepad_device(struct sc_gamepad_processor *gp,
const struct sc_gamepad_device_event *event) { const struct sc_gamepad_device_event *event) {
struct sc_gamepad_uhid *gamepad = DOWNCAST(gp); struct sc_gamepad_uhid *gamepad = DOWNCAST(gp);
if (event->type == SC_GAMEPAD_DEVICE_ADDED) {
struct sc_hid_open hid_open; struct sc_hid_open hid_open;
if (!sc_hid_gamepad_generate_open(&gamepad->hid, &hid_open, if (!sc_hid_gamepad_generate_open(&gamepad->hid, &hid_open,
event->gamepad_id)) { event->gamepad_id)) {
return; 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); sc_gamepad_uhid_send_open(gamepad, &hid_open);
} } else {
assert(event->type == SC_GAMEPAD_DEVICE_REMOVED);
static void
sc_gamepad_processor_process_gamepad_removed(struct sc_gamepad_processor *gp,
const struct sc_gamepad_device_event *event) {
struct sc_gamepad_uhid *gamepad = DOWNCAST(gp);
struct sc_hid_close hid_close; struct sc_hid_close hid_close;
if (!sc_hid_gamepad_generate_close(&gamepad->hid, &hid_close, if (!sc_hid_gamepad_generate_close(&gamepad->hid, &hid_close,
@ -89,9 +73,8 @@ sc_gamepad_processor_process_gamepad_removed(struct sc_gamepad_processor *gp,
return; return;
} }
LOGI("Gamepad removed: [%" PRIu32 "]", event->gamepad_id);
sc_gamepad_uhid_send_close(gamepad, &hid_close); sc_gamepad_uhid_send_close(gamepad, &hid_close);
}
} }
static void static void
@ -131,8 +114,7 @@ sc_gamepad_uhid_init(struct sc_gamepad_uhid *gamepad,
gamepad->controller = controller; gamepad->controller = controller;
static const struct sc_gamepad_processor_ops ops = { static const struct sc_gamepad_processor_ops ops = {
.process_gamepad_added = sc_gamepad_processor_process_gamepad_added, .process_gamepad_device = sc_gamepad_processor_process_gamepad_device,
.process_gamepad_removed = sc_gamepad_processor_process_gamepad_removed,
.process_gamepad_axis = sc_gamepad_processor_process_gamepad_axis, .process_gamepad_axis = sc_gamepad_processor_process_gamepad_axis,
.process_gamepad_button = sc_gamepad_processor_process_gamepad_button, .process_gamepad_button = sc_gamepad_processor_process_gamepad_button,
}; };

View File

@ -141,9 +141,7 @@ sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb,
struct sc_control_msg msg; struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE; msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE;
msg.uhid_create.id = SC_HID_ID_KEYBOARD; msg.uhid_create.id = SC_HID_ID_KEYBOARD;
msg.uhid_create.vendor_id = 0; msg.uhid_create.name = hid_open.name;
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 = hid_open.report_desc;
msg.uhid_create.report_desc_size = hid_open.report_desc_size; msg.uhid_create.report_desc_size = hid_open.report_desc_size;
if (!sc_controller_push_msg(controller, &msg)) { if (!sc_controller_push_msg(controller, &msg)) {

View File

@ -81,9 +81,7 @@ sc_mouse_uhid_init(struct sc_mouse_uhid *mouse,
struct sc_control_msg msg; struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE; msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE;
msg.uhid_create.id = SC_HID_ID_MOUSE; msg.uhid_create.id = SC_HID_ID_MOUSE;
msg.uhid_create.vendor_id = 0; msg.uhid_create.name = hid_open.name;
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 = hid_open.report_desc;
msg.uhid_create.report_desc_size = hid_open.report_desc_size; msg.uhid_create.report_desc_size = hid_open.report_desc_size;
if (!sc_controller_push_msg(controller, &msg)) { if (!sc_controller_push_msg(controller, &msg)) {

View File

@ -7,10 +7,11 @@
#define DOWNCAST(GP) container_of(GP, struct sc_gamepad_aoa, gamepad_processor) #define DOWNCAST(GP) container_of(GP, struct sc_gamepad_aoa, gamepad_processor)
static void static void
sc_gamepad_processor_process_gamepad_added(struct sc_gamepad_processor *gp, sc_gamepad_processor_process_gamepad_device(struct sc_gamepad_processor *gp,
const struct sc_gamepad_device_event *event) { const struct sc_gamepad_device_event *event) {
struct sc_gamepad_aoa *gamepad = DOWNCAST(gp); struct sc_gamepad_aoa *gamepad = DOWNCAST(gp);
if (event->type == SC_GAMEPAD_DEVICE_ADDED) {
struct sc_hid_open hid_open; struct sc_hid_open hid_open;
if (!sc_hid_gamepad_generate_open(&gamepad->hid, &hid_open, if (!sc_hid_gamepad_generate_open(&gamepad->hid, &hid_open,
event->gamepad_id)) { event->gamepad_id)) {
@ -21,12 +22,8 @@ sc_gamepad_processor_process_gamepad_added(struct sc_gamepad_processor *gp,
if (!sc_aoa_push_open(gamepad->aoa, &hid_open, false)) { if (!sc_aoa_push_open(gamepad->aoa, &hid_open, false)) {
LOGW("Could not push AOA HID open (gamepad)"); LOGW("Could not push AOA HID open (gamepad)");
} }
} } else {
assert(event->type == SC_GAMEPAD_DEVICE_REMOVED);
static void
sc_gamepad_processor_process_gamepad_removed(struct sc_gamepad_processor *gp,
const struct sc_gamepad_device_event *event) {
struct sc_gamepad_aoa *gamepad = DOWNCAST(gp);
struct sc_hid_close hid_close; struct sc_hid_close hid_close;
if (!sc_hid_gamepad_generate_close(&gamepad->hid, &hid_close, if (!sc_hid_gamepad_generate_close(&gamepad->hid, &hid_close,
@ -37,6 +34,7 @@ sc_gamepad_processor_process_gamepad_removed(struct sc_gamepad_processor *gp,
if (!sc_aoa_push_close(gamepad->aoa, &hid_close)) { if (!sc_aoa_push_close(gamepad->aoa, &hid_close)) {
LOGW("Could not push AOA HID close (gamepad)"); LOGW("Could not push AOA HID close (gamepad)");
} }
}
} }
static void static void
@ -78,8 +76,7 @@ sc_gamepad_aoa_init(struct sc_gamepad_aoa *gamepad, struct sc_aoa *aoa) {
sc_hid_gamepad_init(&gamepad->hid); sc_hid_gamepad_init(&gamepad->hid);
static const struct sc_gamepad_processor_ops ops = { static const struct sc_gamepad_processor_ops ops = {
.process_gamepad_added = sc_gamepad_processor_process_gamepad_added, .process_gamepad_device = sc_gamepad_processor_process_gamepad_device,
.process_gamepad_removed = sc_gamepad_processor_process_gamepad_removed,
.process_gamepad_axis = sc_gamepad_processor_process_gamepad_axis, .process_gamepad_axis = sc_gamepad_processor_process_gamepad_axis,
.process_gamepad_button = sc_gamepad_processor_process_gamepad_button, .process_gamepad_button = sc_gamepad_processor_process_gamepad_button,
}; };

View File

@ -95,14 +95,9 @@ scrcpy_otg(struct scrcpy_options *options) {
// On Windows, only one process could open a USB device // On Windows, only one process could open a USB device
// <https://github.com/Genymobile/scrcpy/issues/2773> // <https://github.com/Genymobile/scrcpy/issues/2773>
LOGI("Killing adb server (if any)..."); LOGI("Killing adb server (if any)...");
if (sc_adb_init()) {
unsigned flags = SC_ADB_NO_STDOUT | SC_ADB_NO_STDERR | SC_ADB_NO_LOGERR; unsigned flags = SC_ADB_NO_STDOUT | SC_ADB_NO_STDERR | SC_ADB_NO_LOGERR;
// uninterruptible (intr == NULL), but in practice it's very quick // uninterruptible (intr == NULL), but in practice it's very quick
sc_adb_kill_server(NULL, flags); sc_adb_kill_server(NULL, flags);
sc_adb_destroy();
} else {
LOGW("Could not call adb executable, adb server not killed");
}
#endif #endif
static const struct sc_usb_callbacks cbs = { static const struct sc_usb_callbacks cbs = {

View File

@ -175,6 +175,7 @@ sc_screen_otg_process_gamepad_device(struct sc_screen_otg *screen,
assert(screen->gamepad); assert(screen->gamepad);
struct sc_gamepad_processor *gp = &screen->gamepad->gamepad_processor; struct sc_gamepad_processor *gp = &screen->gamepad->gamepad_processor;
SDL_JoystickID id;
if (event->type == SDL_CONTROLLERDEVICEADDED) { if (event->type == SDL_CONTROLLERDEVICEADDED) {
SDL_GameController *gc = SDL_GameControllerOpen(event->which); SDL_GameController *gc = SDL_GameControllerOpen(event->which);
if (!gc) { if (!gc) {
@ -189,12 +190,9 @@ sc_screen_otg_process_gamepad_device(struct sc_screen_otg *screen,
return; return;
} }
struct sc_gamepad_device_event evt = { id = SDL_JoystickInstanceID(joystick);
.gamepad_id = SDL_JoystickInstanceID(joystick),
};
gp->ops->process_gamepad_added(gp, &evt);
} else if (event->type == SDL_CONTROLLERDEVICEREMOVED) { } else if (event->type == SDL_CONTROLLERDEVICEREMOVED) {
SDL_JoystickID id = event->which; id = event->which;
SDL_GameController *gc = SDL_GameControllerFromInstanceID(id); SDL_GameController *gc = SDL_GameControllerFromInstanceID(id);
if (gc) { if (gc) {
@ -202,12 +200,16 @@ sc_screen_otg_process_gamepad_device(struct sc_screen_otg *screen,
} else { } else {
LOGW("Unknown gamepad device removed"); LOGW("Unknown gamepad device removed");
} }
} else {
// Nothing to do
return;
}
struct sc_gamepad_device_event evt = { struct sc_gamepad_device_event evt = {
.type = sc_gamepad_device_event_type_from_sdl_type(event->type),
.gamepad_id = id, .gamepad_id = id,
}; };
gp->ops->process_gamepad_removed(gp, &evt); gp->ops->process_gamepad_device(gp, &evt);
}
} }
static void static void

View File

@ -1,29 +0,0 @@
#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
}

View File

@ -1,12 +0,0 @@
#ifndef SC_ENV_H
#define SC_ENV_H
#include "common.h"
// Return the value of the environment variable (may be NULL).
//
// The returned value must be freed by the caller.
char *
sc_get_env(const char *varname);
#endif

View File

@ -9,6 +9,8 @@
#ifdef _WIN32 #ifdef _WIN32
# include <ws2tcpip.h> # include <ws2tcpip.h>
typedef int socklen_t; typedef int socklen_t;
typedef SOCKET sc_raw_socket;
# define SC_RAW_SOCKET_NONE INVALID_SOCKET
#else #else
# include <sys/types.h> # include <sys/types.h>
# include <sys/socket.h> # include <sys/socket.h>
@ -21,6 +23,8 @@
typedef struct sockaddr_in SOCKADDR_IN; typedef struct sockaddr_in SOCKADDR_IN;
typedef struct sockaddr SOCKADDR; typedef struct sockaddr SOCKADDR;
typedef struct in_addr IN_ADDR; typedef struct in_addr IN_ADDR;
typedef int sc_raw_socket;
# define SC_RAW_SOCKET_NONE -1
#endif #endif
bool bool
@ -43,26 +47,17 @@ net_cleanup(void) {
#endif #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 static inline sc_socket
wrap(sc_raw_socket sock) { wrap(sc_raw_socket sock) {
#ifdef SC_SOCKET_CLOSE_ON_INTERRUPT #ifdef _WIN32
if (sock == SC_RAW_SOCKET_NONE) { if (sock == INVALID_SOCKET) {
return SC_SOCKET_NONE; return SC_SOCKET_NONE;
} }
struct sc_socket_wrapper *socket = malloc(sizeof(*socket)); struct sc_socket_windows *socket = malloc(sizeof(*socket));
if (!socket) { if (!socket) {
LOG_OOM(); LOG_OOM();
sc_raw_socket_close(sock); closesocket(sock);
return SC_SOCKET_NONE; return SC_SOCKET_NONE;
} }
@ -77,9 +72,9 @@ wrap(sc_raw_socket sock) {
static inline sc_raw_socket static inline sc_raw_socket
unwrap(sc_socket socket) { unwrap(sc_socket socket) {
#ifdef SC_SOCKET_CLOSE_ON_INTERRUPT #ifdef _WIN32
if (socket == SC_SOCKET_NONE) { if (socket == SC_SOCKET_NONE) {
return SC_RAW_SOCKET_NONE; return INVALID_SOCKET;
} }
return socket->socket; return socket->socket;
@ -88,6 +83,17 @@ unwrap(sc_socket socket) {
#endif #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 #ifndef HAVE_SOCK_CLOEXEC
// If SOCK_CLOEXEC does not exist, the flag must be set manually once the // If SOCK_CLOEXEC does not exist, the flag must be set manually once the
// socket is created // socket is created
@ -242,9 +248,9 @@ net_interrupt(sc_socket socket) {
sc_raw_socket raw_sock = unwrap(socket); sc_raw_socket raw_sock = unwrap(socket);
#ifdef SC_SOCKET_CLOSE_ON_INTERRUPT #ifdef _WIN32
if (!atomic_flag_test_and_set(&socket->closed)) { if (!atomic_flag_test_and_set(&socket->closed)) {
return sc_raw_socket_close(raw_sock); return !closesocket(raw_sock);
} }
return true; return true;
#else #else
@ -256,15 +262,15 @@ bool
net_close(sc_socket socket) { net_close(sc_socket socket) {
sc_raw_socket raw_sock = unwrap(socket); sc_raw_socket raw_sock = unwrap(socket);
#ifdef SC_SOCKET_CLOSE_ON_INTERRUPT #ifdef _WIN32
bool ret = true; bool ret = true;
if (!atomic_flag_test_and_set(&socket->closed)) { if (!atomic_flag_test_and_set(&socket->closed)) {
ret = sc_raw_socket_close(raw_sock); ret = !closesocket(raw_sock);
} }
free(socket); free(socket);
return ret; return ret;
#else #else
return sc_raw_socket_close(raw_sock); return !close(raw_sock);
#endif #endif
} }

View File

@ -7,36 +7,21 @@
#include <stdint.h> #include <stdint.h>
#ifdef _WIN32 #ifdef _WIN32
# include <winsock2.h> # 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> # include <stdatomic.h>
# define SC_SOCKET_NONE NULL # define SC_SOCKET_NONE NULL
typedef struct sc_socket_wrapper { typedef struct sc_socket_windows {
sc_raw_socket socket; SOCKET socket;
atomic_flag closed; atomic_flag closed;
} *sc_socket; } *sc_socket;
#else
#else // not _WIN32
# include <sys/socket.h>
# define SC_SOCKET_NONE -1 # define SC_SOCKET_NONE -1
typedef sc_raw_socket sc_socket; typedef int sc_socket;
#endif #endif
#define IPV4_LOCALHOST 0x7F000001 #define IPV4_LOCALHOST 0x7F000001

View File

@ -64,26 +64,6 @@ sc_str_quote(const char *src) {
return quoted; return quoted;
} }
char *
sc_str_concat(const char *start, const char *end) {
assert(start);
assert(end);
size_t start_len = strlen(start);
size_t end_len = strlen(end);
char *result = malloc(start_len + end_len + 1);
if (!result) {
LOG_OOM();
return NULL;
}
memcpy(result, start, start_len);
memcpy(result + start_len, end, end_len + 1);
return result;
}
bool bool
sc_str_parse_integer(const char *s, long *out) { sc_str_parse_integer(const char *s, long *out) {
char *endptr; char *endptr;

View File

@ -38,15 +38,6 @@ sc_str_join(char *dst, const char *const tokens[], char sep, size_t n);
char * char *
sc_str_quote(const char *src); sc_str_quote(const char *src);
/**
* Concat two strings
*
* Return a new allocated string, contanining the concatenation of the two
* input strings.
*/
char *
sc_str_concat(const char *start, const char *end);
/** /**
* Parse `s` as an integer into `out` * Parse `s` as an integer into `out`
* *

View File

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

View File

@ -289,11 +289,11 @@ static void test_serialize_set_clipboard_long(void) {
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
} }
static void test_serialize_set_display_power(void) { static void test_serialize_set_screen_power_mode(void) {
struct sc_control_msg msg = { struct sc_control_msg msg = {
.type = SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER, .type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE,
.set_display_power = { .set_screen_power_mode = {
.on = true, .mode = SC_SCREEN_POWER_MODE_NORMAL,
}, },
}; };
@ -302,8 +302,8 @@ static void test_serialize_set_display_power(void) {
assert(size == 2); assert(size == 2);
const uint8_t expected[] = { const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER, SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE,
0x01, // true 0x02, // SC_SCREEN_POWER_MODE_NORMAL
}; };
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
} }
@ -329,8 +329,6 @@ static void test_serialize_uhid_create(void) {
.type = SC_CONTROL_MSG_TYPE_UHID_CREATE, .type = SC_CONTROL_MSG_TYPE_UHID_CREATE,
.uhid_create = { .uhid_create = {
.id = 42, .id = 42,
.vendor_id = 0x1234,
.product_id = 0x5678,
.name = "ABC", .name = "ABC",
.report_desc_size = sizeof(report_desc), .report_desc_size = sizeof(report_desc),
.report_desc = report_desc, .report_desc = report_desc,
@ -339,13 +337,11 @@ static void test_serialize_uhid_create(void) {
uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf); size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 24); assert(size == 20);
const uint8_t expected[] = { const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_UHID_CREATE, SC_CONTROL_MSG_TYPE_UHID_CREATE,
0, 42, // id 0, 42, // id
0x12, 0x34, // vendor id
0x56, 0x78, // product id
3, // name size 3, // name size
65, 66, 67, // "ABC" 65, 66, 67, // "ABC"
0, 11, // report desc size 0, 11, // report desc size
@ -411,21 +407,6 @@ static void test_serialize_open_hard_keyboard(void) {
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
} }
static void test_serialize_reset_video(void) {
struct sc_control_msg msg = {
.type = SC_CONTROL_MSG_TYPE_RESET_VIDEO,
};
uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 1);
const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_RESET_VIDEO,
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
(void) argc; (void) argc;
(void) argv; (void) argv;
@ -442,12 +423,11 @@ int main(int argc, char *argv[]) {
test_serialize_get_clipboard(); test_serialize_get_clipboard();
test_serialize_set_clipboard(); test_serialize_set_clipboard();
test_serialize_set_clipboard_long(); test_serialize_set_clipboard_long();
test_serialize_set_display_power(); test_serialize_set_screen_power_mode();
test_serialize_rotate_device(); test_serialize_rotate_device();
test_serialize_uhid_create(); test_serialize_uhid_create();
test_serialize_uhid_input(); test_serialize_uhid_input();
test_serialize_uhid_destroy(); test_serialize_uhid_destroy();
test_serialize_open_hard_keyboard(); test_serialize_open_hard_keyboard();
test_serialize_reset_video();
return 0; return 0;
} }

View File

@ -141,16 +141,6 @@ static void test_quote(void) {
free(out); free(out);
} }
static void test_concat(void) {
const char *s = "2024:11";
char *out = sc_str_concat("my-prefix:", s);
// contains the concat
assert(!strcmp("my-prefix:2024:11", out));
free(out);
}
static void test_utf8_truncate(void) { static void test_utf8_truncate(void) {
const char *s = "aÉbÔc"; const char *s = "aÉbÔc";
assert(strlen(s) == 7); // É and Ô are 2 bytes-wide assert(strlen(s) == 7); // É and Ô are 2 bytes-wide
@ -399,7 +389,6 @@ int main(int argc, char *argv[]) {
test_join_truncated_before_sep(); test_join_truncated_before_sep();
test_join_truncated_after_sep(); test_join_truncated_after_sep();
test_quote(); test_quote();
test_concat();
test_utf8_truncate(); test_utf8_truncate();
test_parse_integer(); test_parse_integer();
test_parse_integers(); test_parse_integers();

View File

@ -170,7 +170,7 @@ latency (for both [video](video.md#buffering) and audio) might be preferable to
avoid glitches and smooth the playback: avoid glitches and smooth the playback:
``` ```
scrcpy --video-buffer=200 --audio-buffer=200 scrcpy --display-buffer=200 --audio-buffer=200
``` ```
It is also possible to configure another audio buffer (the audio output buffer), It is also possible to configure another audio buffer (the audio output buffer),

View File

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

View File

@ -85,12 +85,6 @@ scrcpy --tcpip=192.168.1.1 # default port is 5555
scrcpy --tcpip=192.168.1.1: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 ### Manual
@ -113,17 +107,16 @@ with the device IP address you found)_.
7. Run `scrcpy` as usual. 7. Run `scrcpy` as usual.
8. Run `adb disconnect` once you're done. 8. Run `adb disconnect` once you're done.
Since Android 11, a [wireless debugging option][adb-wireless] allows you to Since Android 11, a [wireless debugging option][adb-wireless] allows to bypass
bypass having to physically connect your device to your computer. having to physically connect your device directly to your computer.
[adb-wireless]: https://developer.android.com/studio/command-line/adb#wireless-android11-command-line [adb-wireless]: https://developer.android.com/studio/command-line/adb#wireless-android11-command-line
## Autostart ## Autostart
A small tool (by the scrcpy author) allows you to run arbitrary commands A small tool (by the scrcpy author) allows to run arbitrary commands whenever a
whenever a new Android device is connected: [AutoAdb]. It can be used to start new Android device is connected: [AutoAdb]. It can be used to start scrcpy:
scrcpy:
```bash ```bash
autoadb scrcpy -s '{}' autoadb scrcpy -s '{}'

View File

@ -23,20 +23,14 @@ To control the device without mirroring:
scrcpy --no-video --no-audio scrcpy --no-video --no-audio
``` ```
By default, the mouse is disabled when video playback is turned off. By default, mouse mode is switched to UHID if video mirroring is disabled (a
relative mouse mode is required).
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: To also use a UHID keyboard, set it explicitly:
```bash ```bash
scrcpy --no-video --no-audio --mouse=uhid --keyboard=uhid scrcpy --no-video --no-audio --keyboard=uhid
scrcpy --no-video --no-audio -MK # short version scrcpy --no-video --no-audio -K # short version
``` ```
To use AOA instead (over USB only): To use AOA instead (over USB only):

View File

@ -21,9 +21,9 @@ the client and on the server.
If video is enabled, then the server sends a raw video stream (H.264 by default) If video is enabled, then the server sends a raw video stream (H.264 by default)
of the device screen, with some additional headers for each packet. The client of the device screen, with some additional headers for each packet. The client
decodes the video frames, and displays them as soon as possible, without decodes the video frames, and displays them as soon as possible, without
buffering (unless `--video-buffer=delay` is specified) to minimize latency. The buffering (unless `--display-buffer=delay` is specified) to minimize latency.
client is not aware of the device rotation (which is handled by the server), it The client is not aware of the device rotation (which is handled by the server),
just knows the dimensions of the video frames it receives. it just knows the dimensions of the video frames it receives.
Similarly, if audio is enabled, then the server sends a raw audio stream (OPUS Similarly, if audio is enabled, then the server sends a raw audio stream (OPUS
by default) of the device audio output (or the microphone if by default) of the device audio output (or the microphone if
@ -461,30 +461,26 @@ meson setup x -Dserver_debugger=true
meson configure x -Dserver_debugger=true meson configure x -Dserver_debugger=true
``` ```
Then recompile, and run scrcpy. If your device runs Android 8 or below, set the `server_debugger_method` to
`old` in addition:
For Android < 11, it will start a debugger on port 5005 on the device and wait: ```bash
meson setup x -Dserver_debugger=true -Dserver_debugger_method=old
# or, if x is already configured
meson configure x -Dserver_debugger=true -Dserver_debugger_method=old
```
Then recompile.
When you start scrcpy, it will start a debugger on port 5005 on the device.
Redirect that port to the computer: Redirect that port to the computer:
```bash ```bash
adb forward tcp:5005 tcp:5005 adb forward tcp:5005 tcp:5005
``` ```
For Android >= 11, first find the listening port: In Android Studio, _Run_ > _Debug_ > _Edit configurations..._ On the left, click on
`+`, _Remote_, and fill the form:
```bash
adb jdwp
# press Ctrl+C to interrupt
```
Then redirect the resulting PID:
```bash
adb forward tcp:5005 jdwp:XXXX # replace XXXX
```
In Android Studio, _Run_ > _Debug_ > _Edit configurations..._ On the left, click
on `+`, _Remote_, and fill the form:
- Host: `localhost` - Host: `localhost`
- Port: `5005` - Port: `5005`

View File

@ -18,46 +18,6 @@ The initial state is restored when _scrcpy_ is closed.
If the device is not plugged in (i.e. only connected over TCP/IP), If the device is not plugged in (i.e. only connected over TCP/IP),
`--stay-awake` has no effect (this is the Android behavior). `--stay-awake` has no effect (this is the Android behavior).
This changes the value of [`stay_on_while_plugged_in`], setting which can be
changed manually:
[`stay_on_while_plugged_in`]: https://developer.android.com/reference/android/provider/Settings.Global#STAY_ON_WHILE_PLUGGED_IN
```bash
# get the current show_touches value
adb shell settings get global stay_on_while_plugged_in
# enable for AC/USB/wireless chargers
adb shell settings put global stay_on_while_plugged_in 7
# disable
adb shell settings put global stay_on_while_plugged_in 0
```
## 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 ## Turn screen off
@ -86,15 +46,6 @@ scrcpy --turn-screen-off --stay-awake
scrcpy -Sw # short version scrcpy -Sw # short version
``` ```
Since Android 15, it is possible to change this setting manually:
```
# turn screen off (0 for main display)
adb shell cmd display power-off 0
# turn screen on
adb shell cmd display power-on 0
```
## Show touches ## Show touches
@ -111,16 +62,6 @@ scrcpy -t # short version
Note that it only shows _physical_ touches (by a finger on the device). Note that it only shows _physical_ touches (by a finger on the device).
It is possible to change this setting manually:
```bash
# get the current show_touches value
adb shell settings get system show_touches
# enable show_touches
adb shell settings put system show_touches 1
# disable show_touches
adb shell settings put system show_touches 0
```
## Power off on close ## Power off on close

View File

@ -2,23 +2,6 @@
## Install ## 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> <a href="https://repology.org/project/scrcpy/versions"><img src="https://repology.org/badge/vertical-allrepos/scrcpy.svg" alt="Packaging status" align="right"></a>
Scrcpy is packaged in several distributions and package managers: Scrcpy is packaged in several distributions and package managers:
@ -30,10 +13,10 @@ Scrcpy is packaged in several distributions and package managers:
- Snap: `snap install scrcpy` - Snap: `snap install scrcpy`
- … (see [repology](https://repology.org/project/scrcpy/versions)) - … (see [repology](https://repology.org/project/scrcpy/versions))
### Latest version
### From an install script However, the packaged version is not always the latest release. To install the
latest release from `master`, follow this simplified process.
To install the latest release from `master`, follow this simplified process.
First, you need to install the required packages: First, you need to install the required packages:

View File

@ -2,27 +2,6 @@
## Install ## 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]: Scrcpy is available in [Homebrew]:
```bash ```bash
@ -34,7 +13,7 @@ brew install scrcpy
You need `adb`, accessible from your `PATH`. If you don't have it yet: You need `adb`, accessible from your `PATH`. If you don't have it yet:
```bash ```bash
brew install --cask android-platform-tools brew install android-platform-tools
``` ```
Alternatively, Scrcpy is also available in [MacPorts], which sets up `adb` for you: Alternatively, Scrcpy is also available in [MacPorts], which sets up `adb` for you:

View File

@ -83,9 +83,9 @@ process like the _adb daemon_).
## Mouse bindings ## Mouse bindings
By default, with SDK mouse: By default, with SDK mouse:
- right-click triggers `BACK` (or `POWER` on) - right-click triggers BACK (or POWER on)
- middle-click triggers `HOME` - middle-click triggers HOME
- the 4th click triggers `APP_SWITCH` - the 4th click triggers APP_SWITCH
- the 5th click expands the notification panel - the 5th click expands the notification panel
The secondary clicks may be forwarded to the device instead by pressing the 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 - `+`: forward the click to the device
- `-`: ignore the click - `-`: ignore the click
- `b`: trigger shortcut `BACK` (or turn screen on if off) - `b`: trigger shortcut BACK (or turn screen on if off)
- `h`: trigger shortcut `HOME` - `h`: trigger shortcut HOME
- `s`: trigger shortcut `APP_SWITCH` - `s`: trigger shortcut APP_SWITCH
- `n`: trigger shortcut "expand notification panel" - `n`: trigger shortcut "expand notification panel"
For example: For example:

View File

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

View File

@ -27,9 +27,6 @@ preserved. That way, a device in 1920×1080 will be mirrored at 1024×576.
If encoding fails, scrcpy automatically tries again with a lower definition If encoding fails, scrcpy automatically tries again with a lower definition
(unless `--no-downsize-on-error` is enabled). (unless `--no-downsize-on-error` is enabled).
For camera mirroring, the `--max-size` value is used to select the camera source
size instead (among the available resolutions).
## Bit rate ## Bit rate
@ -96,7 +93,7 @@ Sometimes, the default encoder may have issues or even crash, so it is useful to
try another one: try another one:
```bash ```bash
scrcpy --video-codec=h264 --video-encoder=OMX.qcom.video.encoder.avc scrcpy --video-codec=h264 --video-encoder='OMX.qcom.video.encoder.avc'
``` ```
@ -106,45 +103,24 @@ The orientation may be applied at 3 different levels:
- The [shortcut](shortcuts.md) <kbd>MOD</kbd>+<kbd>r</kbd> requests the - The [shortcut](shortcuts.md) <kbd>MOD</kbd>+<kbd>r</kbd> requests the
device to switch between portrait and landscape (the current running app may device to switch between portrait and landscape (the current running app may
refuse, if it does not support the requested orientation). refuse, if it does not support the requested orientation).
- `--capture-orientation` changes the mirroring orientation (the orientation - `--lock-video-orientation` changes the mirroring orientation (the orientation
of the video sent from the device to the computer). This affects the of the video sent from the device to the computer). This affects the
recording. recording.
- `--orientation` is applied on the client side, and affects display and - `--orientation` is applied on the client side, and affects display and
recording. For the display, it can be changed dynamically using recording. For the display, it can be changed dynamically using
[shortcuts](shortcuts.md). [shortcuts](shortcuts.md).
To capture the video with a specific orientation: To lock the mirroring orientation (on the capture side):
```bash ```bash
scrcpy --capture-orientation=0 scrcpy --lock-video-orientation # initial (current) orientation
scrcpy --capture-orientation=90 # 90° clockwise scrcpy --lock-video-orientation=0 # natural orientation
scrcpy --capture-orientation=180 # 180° scrcpy --lock-video-orientation=90 # 90° clockwise
scrcpy --capture-orientation=270 # 270° clockwise scrcpy --lock-video-orientation=180 # 180°
scrcpy --capture-orientation=flip0 # hflip scrcpy --lock-video-orientation=270 # 270° clockwise
scrcpy --capture-orientation=flip90 # hflip + 90° clockwise
scrcpy --capture-orientation=flip180 # hflip + 180°
scrcpy --capture-orientation=flip270 # hflip + 270° clockwise
``` ```
The capture orientation can be locked by using `@`, so that a physical device To orient the video (on the rendering side):
rotation does not change the captured video orientation:
```bash
scrcpy --capture-orientation=@ # locked to the initial orientation
scrcpy --capture-orientation=@0 # locked to 0°
scrcpy --capture-orientation=@90 # locked to 90° clockwise
scrcpy --capture-orientation=@180 # locked to 180°
scrcpy --capture-orientation=@270 # locked to 270° clockwise
scrcpy --capture-orientation=@flip0 # locked to hflip
scrcpy --capture-orientation=@flip90 # locked to hflip + 90° clockwise
scrcpy --capture-orientation=@flip180 # locked to hflip + 180°
scrcpy --capture-orientation=@flip270 # locked to hflip + 270° clockwise
```
The capture orientation transform is applied after `--crop`, but before
`--angle`.
To orient the video (on the client side):
```bash ```bash
scrcpy --orientation=0 scrcpy --orientation=0
@ -165,19 +141,6 @@ to the MP4 or MKV target file. Flipping is not supported, so only the 4 first
values are allowed when recording. values are allowed when recording.
## Angle
To rotate the video content by a custom angle (in degrees, clockwise):
```
scrcpy --angle=23
```
The center of rotation is the center of the visible area.
This transformation is applied after `--crop` and `--capture-orientation`.
## Crop ## Crop
The device screen may be cropped to mirror only part of the screen. The device screen may be cropped to mirror only part of the screen.
@ -191,11 +154,7 @@ scrcpy --crop=1224:1440:0:0 # 1224x1440 at offset (0,0)
The values are expressed in the device natural orientation (portrait for a The values are expressed in the device natural orientation (portrait for a
phone, landscape for a tablet). phone, landscape for a tablet).
Cropping is performed before `--capture-orientation` and `--angle`. If `--max-size` is also specified, resizing is applied after cropping.
For display mirroring, `--max-size` is applied after cropping. For camera,
`--max-size` is applied first (because it selects the source size rather than
resizing the content).
## Display ## Display
@ -216,8 +175,6 @@ scrcpy --list-displays
A secondary display may only be controlled if the device runs at least Android A secondary display may only be controlled if the device runs at least Android
10 (otherwise it is mirrored as read-only). 10 (otherwise it is mirrored as read-only).
It is also possible to create a [virtual display](virtual_display.md).
## Buffering ## Buffering
@ -232,15 +189,15 @@ The configuration is available independently for the display,
[v4l2 sinks](video.md#video4linux) and [audio](audio.md#buffering) playback. [v4l2 sinks](video.md#video4linux) and [audio](audio.md#buffering) playback.
```bash ```bash
scrcpy --video-buffer=50 # add 50ms buffering for video playback scrcpy --display-buffer=50 # add 50ms buffering for display
scrcpy --audio-buffer=200 # set 200ms buffering for audio playback
scrcpy --v4l2-buffer=300 # add 300ms buffering for v4l2 sink scrcpy --v4l2-buffer=300 # add 300ms buffering for v4l2 sink
scrcpy --audio-buffer=200 # set 200ms buffering for audio playback
``` ```
They can be applied simultaneously: They can be applied simultaneously:
```bash ```bash
scrcpy --video-buffer=50 --v4l2-buffer=300 scrcpy --display-buffer=50 --v4l2-buffer=300
``` ```

View File

@ -8,6 +8,7 @@ To mirror a new virtual display instead of the device screen:
scrcpy --new-display=1920x1080 scrcpy --new-display=1920x1080
scrcpy --new-display=1920x1080/420 # force 420 dpi scrcpy --new-display=1920x1080/420 # force 420 dpi
scrcpy --new-display # use the main display size and density 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 scrcpy --new-display=/240 # use the main display size and 240 dpi
``` ```
@ -15,49 +16,11 @@ scrcpy --new-display=/240 # use the main display size and 240 dpi
On some devices, a launcher is available in the virtual display. On some devices, a launcher is available in the virtual display.
When no launcher is available (or if is explicitly disabled by When no launcher is available, the virtual display is empty. In that case, you
[`--no-vd-system-decorations`](#system-decorations)), the virtual display is must [start an Android app](device.md#start-android-app).
empty. In that case, you must [start an Android
app](device.md#start-android-app).
For example: For example:
```bash ```bash
scrcpy --new-display=1920x1080 --start-app=org.videolan.vlc 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
```

View File

@ -2,45 +2,35 @@
## Install ## Install
### From the official release
Download the [latest release]: Download the [latest release]:
- [`scrcpy-win64-v3.1.zip`][direct-win64] (64-bit) - [`scrcpy-win64-v2.7.zip`][direct-win64] (64-bit)
<sub>SHA-256: `0c05ea395d95cfe36bee974eeb435a3db87ea5594ff738370d5dc3068a9538ca`</sub> <sub>SHA-256: `5910bc18d5a16f42d84185ddc7e16a4cee6a6f5f33451559c1a1d6d0099bd5f5`</sub>
- [`scrcpy-win32-v3.1.zip`][direct-win32] (32-bit) - [`scrcpy-win32-v2.7.zip`][direct-win32] (32-bit)
<sub>SHA-256: `2b4674ef76719680ac5a9b482d1943bdde3fa25821ad2e98f3c40c347d00d560`</sub> <sub>SHA-256: `ef4daf89d500f33d78b830625536ecb18481429dd94433e7634c824292059d06`</sub>
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest [latest release]: https://github.com/Genymobile/scrcpy/releases/latest
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-win64-v3.1.zip [direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-win64-v2.7.zip
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-win32-v3.1.zip [direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-win32-v2.7.zip
and extract it. and extract it.
Alternatively, you could install it from packages manager, like [Chocolatey]:
### From a package manager
From [WinGet] (ADB and other dependencies will be installed alongside scrcpy):
```bash
winget install --exact Genymobile.scrcpy
```
From [Chocolatey]:
```bash ```bash
choco install scrcpy choco install scrcpy
choco install adb # if you don't have it yet choco install adb # if you don't have it yet
``` ```
From [Scoop]: or [Scoop]:
```bash ```bash
scoop install scrcpy scoop install scrcpy
scoop install adb # if you don't have it yet scoop install adb # if you don't have it yet
``` ```
[WinGet]: https://github.com/microsoft/winget-cli [Winget]: https://github.com/microsoft/winget-cli
[Chocolatey]: https://chocolatey.org/ [Chocolatey]: https://chocolatey.org/
[Scoop]: https://scoop.sh [Scoop]: https://scoop.sh

View File

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

View File

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

View File

@ -2,7 +2,7 @@ option('compile_app', type: 'boolean', value: true, description: 'Build the clie
option('compile_server', type: 'boolean', value: true, description: 'Build the server') option('compile_server', type: 'boolean', value: true, description: 'Build the server')
option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server') option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server')
option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server from the same directory as the scrcpy executable') option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server from the same directory as the scrcpy executable')
option('static', type: 'boolean', value: false, description: 'Use static dependencies')
option('server_debugger', type: 'boolean', value: false, description: 'Run a server debugger and wait for a client to be attached') option('server_debugger', type: 'boolean', value: false, description: 'Run a server debugger and wait for a client to be attached')
option('server_debugger_method', type: 'combo', choices: ['old', 'new'], value: 'new', description: 'Select the debugger method (Android < 9: "old", Android >= 9: "new")')
option('v4l2', type: 'boolean', value: true, description: 'Enable V4L2 feature when supported') option('v4l2', type: 'boolean', value: true, description: 'Enable V4L2 feature when supported')
option('usb', type: 'boolean', value: true, description: 'Enable HID/OTG features when supported') option('usb', type: 'boolean', value: true, description: 'Enable HID/OTG features when supported')

141
release.mk Normal file
View File

@ -0,0 +1,141 @@
# 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)/"

2
release.sh Executable file
View File

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

2
release/.gitignore vendored
View File

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

View File

@ -1,5 +0,0 @@
# This file must be sourced from the release scripts directory
WORK_DIR="$PWD/work"
OUTPUT_DIR="$PWD/output"
VERSION="${VERSION:-$(git describe --tags --always)}"

View File

@ -1,43 +0,0 @@
#!/bin/bash
set -ex
cd "$(dirname ${BASH_SOURCE[0]})"
. build_common
cd .. # root project dir
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/"

View File

@ -1,43 +0,0 @@
#!/bin/bash
set -ex
cd "$(dirname ${BASH_SOURCE[0]})"
. build_common
cd .. # root project dir
if [[ $# != 1 ]]
then
echo "Syntax: $0 <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/"

View File

@ -1,14 +0,0 @@
#!/bin/bash
set -ex
cd "$(dirname ${BASH_SOURCE[0]})"
. build_common
cd .. # root project dir
GRADLE="${GRADLE:-./gradlew}"
SERVER_BUILD_DIR="$WORK_DIR/build-server"
rm -rf "$SERVER_BUILD_DIR"
"$GRADLE" -p server assembleRelease
mkdir -p "$SERVER_BUILD_DIR/server"
cp server/build/outputs/apk/release/server-release-unsigned.apk \
"$SERVER_BUILD_DIR/server/scrcpy-server"

View File

@ -1,53 +0,0 @@
#!/bin/bash
set -ex
case "$1" in
32)
WINXX=win32
;;
64)
WINXX=win64
;;
*)
echo "ERROR: $0 must be called with one argument: 32 or 64" >&2
exit 1
;;
esac
cd "$(dirname ${BASH_SOURCE[0]})"
. build_common
cd .. # root project dir
WINXX_BUILD_DIR="$WORK_DIR/build-$WINXX"
app/deps/adb_windows.sh
app/deps/sdl.sh $WINXX cross shared
app/deps/dav1d.sh $WINXX cross shared
app/deps/ffmpeg.sh $WINXX cross shared
app/deps/libusb.sh $WINXX cross shared
DEPS_INSTALL_DIR="$PWD/app/deps/work/install/$WINXX-cross-shared"
ADB_INSTALL_DIR="$PWD/app/deps/work/install/adb-windows"
rm -rf "$WINXX_BUILD_DIR"
meson setup "$WINXX_BUILD_DIR" \
--pkg-config-path="$DEPS_INSTALL_DIR/lib/pkgconfig" \
-Dc_args="-I$DEPS_INSTALL_DIR/include" \
-Dc_link_args="-L$DEPS_INSTALL_DIR/lib" \
--cross-file=cross_$WINXX.txt \
--buildtype=release \
--strip \
-Db_lto=true \
-Dcompile_server=false \
-Dportable=true
ninja -C "$WINXX_BUILD_DIR"
# Group intermediate outputs into a 'dist' directory
mkdir -p "$WINXX_BUILD_DIR/dist"
cp "$WINXX_BUILD_DIR"/app/scrcpy.exe "$WINXX_BUILD_DIR/dist/"
cp app/data/scrcpy-console.bat "$WINXX_BUILD_DIR/dist/"
cp app/data/scrcpy-noconsole.vbs "$WINXX_BUILD_DIR/dist/"
cp app/data/icon.png "$WINXX_BUILD_DIR/dist/"
cp app/data/open_a_terminal_here.bat "$WINXX_BUILD_DIR/dist/"
cp "$DEPS_INSTALL_DIR"/bin/*.dll "$WINXX_BUILD_DIR/dist/"
cp -r "$ADB_INSTALL_DIR"/. "$WINXX_BUILD_DIR/dist/"

View File

@ -1,14 +0,0 @@
#!/bin/bash
set -ex
cd "$(dirname ${BASH_SOURCE[0]})"
. build_common
cd "$OUTPUT_DIR"
sha256sum "scrcpy-server-$VERSION" \
"scrcpy-linux-x86_64-$VERSION.tar.gz" \
"scrcpy-win32-$VERSION.zip" \
"scrcpy-win64-$VERSION.zip" \
"scrcpy-macos-aarch64-$VERSION.tar.gz" \
"scrcpy-macos-x86_64-$VERSION.tar.gz" \
| tee SHA256SUMS.txt
echo "Release checksums generated in $PWD/SHA256SUMS.txt"

View File

@ -1,52 +0,0 @@
#!/bin/bash
set -ex
cd "$(dirname ${BASH_SOURCE[0]})"
. build_common
cd .. # root project dir
if [[ $# != 2 ]]
then
# <target_name>: for example win64
# <format>: zip or tar.gz
echo "Syntax: $0 <target> <format>" >&2
exit 1
fi
FORMAT=$2
if [[ "$FORMAT" != zip && "$FORMAT" != tar.gz ]]
then
echo "Invalid format (expected zip or tar.gz): $FORMAT" >&2
exit 1
fi
BUILD_DIR="$WORK_DIR/build-$1"
ARCHIVE_DIR="$BUILD_DIR/release-archive"
TARGET_DIRNAME="scrcpy-$1-$VERSION"
rm -rf "$ARCHIVE_DIR/$TARGET_DIRNAME"
mkdir -p "$ARCHIVE_DIR/$TARGET_DIRNAME"
cp -r "$BUILD_DIR/dist/." "$ARCHIVE_DIR/$TARGET_DIRNAME/"
cp "$WORK_DIR/build-server/server/scrcpy-server" "$ARCHIVE_DIR/$TARGET_DIRNAME/"
mkdir -p "$OUTPUT_DIR"
cd "$ARCHIVE_DIR"
rm -f "$OUTPUT_DIR/$TARGET_DIRNAME.$FORMAT"
case "$FORMAT" in
zip)
zip -r "$OUTPUT_DIR/$TARGET_DIRNAME.zip" "$TARGET_DIRNAME"
;;
tar.gz)
tar 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'"

View File

@ -1,10 +0,0 @@
#!/bin/bash
set -ex
cd "$(dirname ${BASH_SOURCE[0]})"
OUTPUT_DIR="$PWD/output"
. build_common
cd .. # root project dir
mkdir -p "$OUTPUT_DIR"
cp "$WORK_DIR/build-server/server/scrcpy-server" "$OUTPUT_DIR/scrcpy-server-$VERSION"
echo "Generated '$OUTPUT_DIR/scrcpy-server-$VERSION'"

View File

@ -1,24 +0,0 @@
#!/bin/bash
# To customize the version name:
# VERSION=myversion ./release.sh
set -e
cd "$(dirname ${BASH_SOURCE[0]})"
rm -rf output
./test_server.sh
./test_client.sh
./build_server.sh
./build_windows.sh 32
./build_windows.sh 64
./build_linux.sh x86_64
./package_server.sh
./package_client.sh win32 zip
./package_client.sh win64 zip
./package_client.sh linux-x86_64 tar.gz
./generate_checksums.sh
echo "Release generated in $PWD/output"

View File

@ -1,12 +0,0 @@
#!/bin/bash
set -ex
cd "$(dirname ${BASH_SOURCE[0]})"
. build_common
cd .. # root project dir
TEST_BUILD_DIR="$WORK_DIR/build-test"
rm -rf "$TEST_BUILD_DIR"
meson setup "$TEST_BUILD_DIR" -Dcompile_server=false \
-Db_sanitize=address,undefined
ninja -C "$TEST_BUILD_DIR" test

View File

@ -1,9 +0,0 @@
#!/bin/bash
set -ex
cd "$(dirname ${BASH_SOURCE[0]})"
. build_common
cd .. # root project dir
GRADLE="${GRADLE:-./gradlew}"
"$GRADLE" -p server check

View File

@ -7,8 +7,8 @@ android {
applicationId "com.genymobile.scrcpy" applicationId "com.genymobile.scrcpy"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 35 targetSdkVersion 35
versionCode 30100 versionCode 20700
versionName "3.1" versionName "2.7"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
} }
buildTypes { buildTypes {

View File

@ -12,11 +12,10 @@
set -e set -e
SCRCPY_DEBUG=false SCRCPY_DEBUG=false
SCRCPY_VERSION_NAME=3.1 SCRCPY_VERSION_NAME=2.7
PLATFORM=${ANDROID_PLATFORM:-35} PLATFORM=${ANDROID_PLATFORM:-35}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.0.0} 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_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS"
BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})" BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})"
@ -24,8 +23,7 @@ CLASSES_DIR="$BUILD_DIR/classes"
GEN_DIR="$BUILD_DIR/gen" GEN_DIR="$BUILD_DIR/gen"
SERVER_DIR=$(dirname "$0") SERVER_DIR=$(dirname "$0")
SERVER_BINARY=scrcpy-server SERVER_BINARY=scrcpy-server
ANDROID_JAR="$PLATFORM_TOOLS/android.jar" ANDROID_JAR="$ANDROID_HOME/platforms/android-$PLATFORM/android.jar"
ANDROID_AIDL="$PLATFORM_TOOLS/framework.aidl"
LAMBDA_JAR="$BUILD_TOOLS_DIR/core-lambda-stubs.jar" LAMBDA_JAR="$BUILD_TOOLS_DIR/core-lambda-stubs.jar"
echo "Platform: android-$PLATFORM" echo "Platform: android-$PLATFORM"
@ -51,20 +49,12 @@ cd "$SERVER_DIR/src/main/aidl"
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. \ "$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. \
android/content/IOnPrimaryClipChangedListener.aidl android/content/IOnPrimaryClipChangedListener.aidl
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. android/view/IDisplayFoldListener.aidl "$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -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=( \ SRC=( \
com/genymobile/scrcpy/*.java \ com/genymobile/scrcpy/*.java \
com/genymobile/scrcpy/audio/*.java \ com/genymobile/scrcpy/audio/*.java \
com/genymobile/scrcpy/control/*.java \ com/genymobile/scrcpy/control/*.java \
com/genymobile/scrcpy/device/*.java \ com/genymobile/scrcpy/device/*.java \
com/genymobile/scrcpy/opengl/*.java \
com/genymobile/scrcpy/util/*.java \ com/genymobile/scrcpy/util/*.java \
com/genymobile/scrcpy/video/*.java \ com/genymobile/scrcpy/video/*.java \
com/genymobile/scrcpy/wrappers/*.java \ com/genymobile/scrcpy/wrappers/*.java \
@ -78,11 +68,10 @@ done
echo "Compiling java sources..." echo "Compiling java sources..."
cd ../java cd ../java
javac -encoding UTF-8 -bootclasspath "$ANDROID_JAR" \ javac -bootclasspath "$ANDROID_JAR" \
-cp "$LAMBDA_JAR:$GEN_DIR" \ -cp "$LAMBDA_JAR:$GEN_DIR" \
-d "$CLASSES_DIR" \ -d "$CLASSES_DIR" \
-source 1.8 -target 1.8 \ -source 1.8 -target 1.8 \
${FAKE_SRC[@]} \
${SRC[@]} ${SRC[@]}
echo "Dexing..." echo "Dexing..."

View File

@ -1,66 +0,0 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.view;
import android.graphics.Rect;
import android.content.res.Configuration;
import java.util.List;
/**
* Interface to listen for changes to display window-containers.
*
* This differs from DisplayManager's DisplayListener in a couple ways:
* - onDisplayAdded is always called after the display is actually added to the WM hierarchy.
* This corresponds to the DisplayContent and not the raw Dislay from DisplayManager.
* - onDisplayConfigurationChanged is called for all configuration changes, not just changes
* to displayinfo (eg. windowing-mode).
*
*/
oneway interface IDisplayWindowListener {
/**
* Called when a new display is added to the WM hierarchy. The existing display ids are returned
* when this listener is registered with WM via {@link #registerDisplayWindowListener}.
*/
void onDisplayAdded(int displayId);
/**
* Called when a display's window-container configuration has changed.
*/
void onDisplayConfigurationChanged(int displayId, in Configuration newConfig);
/**
* Called when a display is removed from the hierarchy.
*/
void onDisplayRemoved(int displayId);
/**
* Called when fixed rotation is started on a display.
*/
void onFixedRotationStarted(int displayId, int newRotation);
/**
* Called when the previous fixed rotation on a display is finished.
*/
void onFixedRotationFinished(int displayId);
/**
* Called when the keep clear ares on a display have changed.
*/
void onKeepClearAreasChanged(int displayId, in List<Rect> restricted, in List<Rect> unrestricted);
}

View File

@ -1,5 +0,0 @@
package android.content;
public interface IContentProvider {
// android.content.IContentProvider is hidden, this is a fake one to expose the type to the project
}

View File

@ -5,10 +5,6 @@ import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.Settings; import com.genymobile.scrcpy.util.Settings;
import com.genymobile.scrcpy.util.SettingsException; import com.genymobile.scrcpy.util.SettingsException;
import android.os.BatteryManager;
import android.system.ErrnoException;
import android.system.Os;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
@ -20,140 +16,59 @@ import java.io.OutputStream;
*/ */
public final class CleanUp { public final class CleanUp {
// Dynamic options private static final int MSG_TYPE_MASK = 0b11;
private static final int PENDING_CHANGE_DISPLAY_POWER = 1 << 0; private static final int MSG_TYPE_RESTORE_STAY_ON = 0;
private int pendingChanges; private static final int MSG_TYPE_DISABLE_SHOW_TOUCHES = 1;
private boolean pendingRestoreDisplayPower; private static final int MSG_TYPE_RESTORE_NORMAL_POWER_MODE = 2;
private static final int MSG_TYPE_POWER_OFF_SCREEN = 3;
private Thread thread; private static final int MSG_PARAM_SHIFT = 2;
private boolean interrupted;
private CleanUp(Options options) { private final OutputStream out;
thread = new Thread(() -> runCleanUp(options), "cleanup");
thread.start(); public CleanUp(OutputStream out) {
this.out = out;
} }
public static CleanUp start(Options options) { public static CleanUp configure(int displayId) throws IOException {
return new CleanUp(options); String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(displayId)};
}
public 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); ProcessBuilder builder = new ProcessBuilder(cmd);
builder.environment().put("CLASSPATH", Server.SERVER_PATH); builder.environment().put("CLASSPATH", Server.SERVER_PATH);
Process process = builder.start(); Process process = builder.start();
OutputStream out = process.getOutputStream(); return new CleanUp(process.getOutputStream());
}
while (true) { private boolean sendMessage(int type, int param) {
int localPendingChanges; assert (type & ~MSG_TYPE_MASK) == 0;
boolean localPendingRestoreDisplayPower; int msg = type | param << MSG_PARAM_SHIFT;
synchronized (this) {
while (!interrupted && pendingChanges == 0) {
try { try {
wait(); out.write(msg);
} 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(); out.flush();
} return true;
} catch (IOException e) {
Ln.w("Could not configure cleanup (type=" + type + ", param=" + param + ")", e);
return false;
} }
} }
public synchronized void setRestoreDisplayPower(boolean restoreDisplayPower) { public boolean setRestoreStayOn(int restoreValue) {
pendingRestoreDisplayPower = restoreDisplayPower; // Restore the value (between 0 and 7), -1 to not restore
pendingChanges |= PENDING_CHANGE_DISPLAY_POWER; // <https://developer.android.com/reference/android/provider/Settings.Global#STAY_ON_WHILE_PLUGGED_IN>
notify(); assert restoreValue >= -1 && restoreValue <= 7;
return sendMessage(MSG_TYPE_RESTORE_STAY_ON, restoreValue & 0b1111);
}
public boolean setDisableShowTouches(boolean disableOnExit) {
return sendMessage(MSG_TYPE_DISABLE_SHOW_TOUCHES, disableOnExit ? 1 : 0);
}
public boolean setRestoreNormalPowerMode(boolean restoreOnExit) {
return sendMessage(MSG_TYPE_RESTORE_NORMAL_POWER_MODE, restoreOnExit ? 1 : 0);
}
public boolean setPowerOffScreen(boolean powerOffScreenOnExit) {
return sendMessage(MSG_TYPE_POWER_OFF_SCREEN, powerOffScreenOnExit ? 1 : 0);
} }
public static void unlinkSelf() { public static void unlinkSelf() {
@ -165,30 +80,38 @@ public final class CleanUp {
} }
public static void main(String... args) { 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(); unlinkSelf();
int displayId = Integer.parseInt(args[0]); int displayId = Integer.parseInt(args[0]);
int restoreStayOn = Integer.parseInt(args[1]);
boolean disableShowTouches = Boolean.parseBoolean(args[2]);
boolean powerOffScreen = Boolean.parseBoolean(args[3]);
int restoreScreenOffTimeout = Integer.parseInt(args[4]);
// Dynamic option int restoreStayOn = -1;
boolean restoreDisplayPower = false; boolean disableShowTouches = false;
boolean restoreNormalPowerMode = false;
boolean powerOffScreen = false;
try { try {
// Wait for the server to die // Wait for the server to die
int msg; int msg;
while ((msg = System.in.read()) != -1) { while ((msg = System.in.read()) != -1) {
// Only restore display power int type = msg & MSG_TYPE_MASK;
assert msg == 0 || msg == 1; int param = msg >> MSG_PARAM_SHIFT;
restoreDisplayPower = msg != 0; switch (type) {
case MSG_TYPE_RESTORE_STAY_ON:
restoreStayOn = param > 7 ? -1 : param;
break;
case MSG_TYPE_DISABLE_SHOW_TOUCHES:
disableShowTouches = param != 0;
break;
case MSG_TYPE_RESTORE_NORMAL_POWER_MODE:
restoreNormalPowerMode = param != 0;
break;
case MSG_TYPE_POWER_OFF_SCREEN:
powerOffScreen = param != 0;
break;
default:
Ln.w("Unexpected msg type: " + type);
break;
}
} }
} catch (IOException e) { } catch (IOException e) {
// Expected when the server is dead // Expected when the server is dead
@ -214,24 +137,15 @@ public final class CleanUp {
} }
} }
if (restoreScreenOffTimeout != -1) { if (Device.isScreenOn()) {
Ln.i("Restoring \"screen off timeout\"");
try {
Settings.putValue(Settings.TABLE_SYSTEM, "screen_off_timeout", String.valueOf(restoreScreenOffTimeout));
} catch (SettingsException e) {
Ln.e("Could not restore \"screen_off_timeout\"", e);
}
}
// 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 (powerOffScreen) {
if (displayId != Device.DISPLAY_ID_NONE) {
Ln.i("Power off screen"); Ln.i("Power off screen");
Device.powerOffScreen(targetDisplayId); Device.powerOffScreen(displayId);
} else if (restoreDisplayPower) { }
Ln.i("Restoring display power"); } else if (restoreNormalPowerMode) {
Device.setDisplayPower(targetDisplayId, true); Ln.i("Restoring normal power mode");
Device.setScreenPowerMode(Device.POWER_MODE_NORMAL);
} }
} }

View File

@ -1,14 +1,9 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.AttributionSource; import android.content.AttributionSource;
import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.ContextWrapper; import android.content.ContextWrapper;
import android.content.IContentProvider;
import android.os.Binder;
import android.os.Process; import android.os.Process;
public final class FakeContext extends ContextWrapper { public final class FakeContext extends ContextWrapper {
@ -22,38 +17,6 @@ public final class FakeContext extends ContextWrapper {
return INSTANCE; return INSTANCE;
} }
private final ContentResolver contentResolver = new ContentResolver(this) {
@SuppressWarnings({"unused", "ProtectedMemberInFinalClass"})
// @Override (but super-class method not visible)
protected IContentProvider acquireProvider(Context c, String name) {
return ServiceManager.getActivityManager().getContentProviderExternal(name, new Binder());
}
@SuppressWarnings("unused")
// @Override (but super-class method not visible)
public boolean releaseProvider(IContentProvider icp) {
return false;
}
@SuppressWarnings({"unused", "ProtectedMemberInFinalClass"})
// @Override (but super-class method not visible)
protected IContentProvider acquireUnstableProvider(Context c, String name) {
return null;
}
@SuppressWarnings("unused")
// @Override (but super-class method not visible)
public boolean releaseUnstableProvider(IContentProvider icp) {
return false;
}
@SuppressWarnings("unused")
// @Override (but super-class method not visible)
public void unstableProviderDied(IContentProvider icp) {
// ignore
}
};
private FakeContext() { private FakeContext() {
super(Workarounds.getSystemContext()); super(Workarounds.getSystemContext());
} }
@ -86,9 +49,4 @@ public final class FakeContext extends ContextWrapper {
public Context getApplicationContext() { public Context getApplicationContext() {
return this; return this;
} }
@Override
public ContentResolver getContentResolver() {
return contentResolver;
}
} }

View File

@ -2,9 +2,7 @@ package com.genymobile.scrcpy;
import com.genymobile.scrcpy.audio.AudioCodec; import com.genymobile.scrcpy.audio.AudioCodec;
import com.genymobile.scrcpy.audio.AudioSource; import com.genymobile.scrcpy.audio.AudioSource;
import com.genymobile.scrcpy.device.Device;
import com.genymobile.scrcpy.device.NewDisplay; import com.genymobile.scrcpy.device.NewDisplay;
import com.genymobile.scrcpy.device.Orientation;
import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.util.CodecOption; import com.genymobile.scrcpy.util.CodecOption;
import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.Ln;
@ -14,7 +12,6 @@ import com.genymobile.scrcpy.video.VideoCodec;
import com.genymobile.scrcpy.video.VideoSource; import com.genymobile.scrcpy.video.VideoSource;
import android.graphics.Rect; import android.graphics.Rect;
import android.util.Pair;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
@ -34,7 +31,7 @@ public class Options {
private int videoBitRate = 8000000; private int videoBitRate = 8000000;
private int audioBitRate = 128000; private int audioBitRate = 128000;
private float maxFps; private float maxFps;
private float angle; private int lockVideoOrientation = -1;
private boolean tunnelForward; private boolean tunnelForward;
private Rect crop; private Rect crop;
private boolean control = true; private boolean control = true;
@ -47,7 +44,6 @@ public class Options {
private boolean cameraHighSpeed; private boolean cameraHighSpeed;
private boolean showTouches; private boolean showTouches;
private boolean stayAwake; private boolean stayAwake;
private int screenOffTimeout = -1;
private List<CodecOption> videoCodecOptions; private List<CodecOption> videoCodecOptions;
private List<CodecOption> audioCodecOptions; private List<CodecOption> audioCodecOptions;
@ -60,11 +56,6 @@ public class Options {
private boolean powerOn = true; private boolean powerOn = true;
private NewDisplay newDisplay; 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 listEncoders;
private boolean listDisplays; private boolean listDisplays;
@ -130,8 +121,8 @@ public class Options {
return maxFps; return maxFps;
} }
public float getAngle() { public int getLockVideoOrientation() {
return angle; return lockVideoOrientation;
} }
public boolean isTunnelForward() { public boolean isTunnelForward() {
@ -182,10 +173,6 @@ public class Options {
return stayAwake; return stayAwake;
} }
public int getScreenOffTimeout() {
return screenOffTimeout;
}
public List<CodecOption> getVideoCodecOptions() { public List<CodecOption> getVideoCodecOptions() {
return videoCodecOptions; return videoCodecOptions;
} }
@ -226,22 +213,6 @@ public class Options {
return newDisplay; 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() { public boolean getList() {
return listEncoders || listDisplays || listCameras || listCameraSizes || listApps; return listEncoders || listDisplays || listCameras || listCameraSizes || listApps;
} }
@ -364,8 +335,8 @@ public class Options {
case "max_fps": case "max_fps":
options.maxFps = parseFloat("max_fps", value); options.maxFps = parseFloat("max_fps", value);
break; break;
case "angle": case "lock_video_orientation":
options.angle = parseFloat("angle", value); options.lockVideoOrientation = Integer.parseInt(value);
break; break;
case "tunnel_forward": case "tunnel_forward":
options.tunnelForward = Boolean.parseBoolean(value); options.tunnelForward = Boolean.parseBoolean(value);
@ -387,12 +358,6 @@ public class Options {
case "stay_awake": case "stay_awake":
options.stayAwake = Boolean.parseBoolean(value); options.stayAwake = Boolean.parseBoolean(value);
break; break;
case "screen_off_timeout":
options.screenOffTimeout = Integer.parseInt(value);
if (options.screenOffTimeout < -1) {
throw new IllegalArgumentException("Invalid screen off timeout: " + options.screenOffTimeout);
}
break;
case "video_codec_options": case "video_codec_options":
options.videoCodecOptions = CodecOption.parse(value); options.videoCodecOptions = CodecOption.parse(value);
break; break;
@ -471,17 +436,6 @@ public class Options {
case "new_display": case "new_display":
options.newDisplay = parseNewDisplay(value); options.newDisplay = parseNewDisplay(value);
break; 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": case "send_device_meta":
options.sendDeviceMeta = Boolean.parseBoolean(value); options.sendDeviceMeta = Boolean.parseBoolean(value);
break; break;
@ -509,11 +463,6 @@ public class Options {
} }
} }
if (options.newDisplay != null) {
assert options.displayId == 0 : "Must not set both displayId and newDisplay";
options.displayId = Device.DISPLAY_ID_NONE;
}
return options; return options;
} }
@ -605,25 +554,4 @@ public class Options {
return new NewDisplay(size, dpi); return new NewDisplay(size, dpi);
} }
private static Pair<Orientation.Lock, Orientation> parseCaptureOrientation(String value) {
if (value.isEmpty()) {
throw new IllegalArgumentException("Empty capture orientation string");
}
Orientation.Lock lock;
if (value.charAt(0) == '@') {
// Consume '@'
value = value.substring(1);
if (value.isEmpty()) {
// Only '@': lock to the initial orientation (orientation is unused)
return Pair.create(Orientation.Lock.LockedInitial, Orientation.Orient0);
}
lock = Orientation.Lock.LockedValue;
} else {
lock = Orientation.Lock.Unlocked;
}
return Pair.create(lock, Orientation.getByName(value));
}
} }

View File

@ -14,9 +14,10 @@ import com.genymobile.scrcpy.device.DesktopConnection;
import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.Device;
import com.genymobile.scrcpy.device.NewDisplay; import com.genymobile.scrcpy.device.NewDisplay;
import com.genymobile.scrcpy.device.Streamer; import com.genymobile.scrcpy.device.Streamer;
import com.genymobile.scrcpy.opengl.OpenGLRunner;
import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.LogUtils; import com.genymobile.scrcpy.util.LogUtils;
import com.genymobile.scrcpy.util.Settings;
import com.genymobile.scrcpy.util.SettingsException;
import com.genymobile.scrcpy.video.CameraCapture; import com.genymobile.scrcpy.video.CameraCapture;
import com.genymobile.scrcpy.video.NewDisplayCapture; import com.genymobile.scrcpy.video.NewDisplayCapture;
import com.genymobile.scrcpy.video.ScreenCapture; import com.genymobile.scrcpy.video.ScreenCapture;
@ -24,6 +25,7 @@ import com.genymobile.scrcpy.video.SurfaceCapture;
import com.genymobile.scrcpy.video.SurfaceEncoder; import com.genymobile.scrcpy.video.SurfaceEncoder;
import com.genymobile.scrcpy.video.VideoSource; import com.genymobile.scrcpy.video.VideoSource;
import android.os.BatteryManager;
import android.os.Build; import android.os.Build;
import java.io.File; import java.io.File;
@ -74,6 +76,51 @@ public final class Server {
// not instantiable // not instantiable
} }
private static void initAndCleanUp(Options options, CleanUp cleanUp) {
// This method is called from its own thread, so it may only configure cleanup actions which are NOT dynamic (i.e. they are configured once
// and for all, they cannot be changed from another thread)
if (options.getShowTouches()) {
try {
String oldValue = Settings.getAndPutValue(Settings.TABLE_SYSTEM, "show_touches", "1");
// If "show touches" was disabled, it must be disabled back on clean up
if (!"1".equals(oldValue)) {
if (!cleanUp.setDisableShowTouches(true)) {
Ln.e("Could not disable show touch on exit");
}
}
} catch (SettingsException e) {
Ln.e("Could not change \"show_touches\"", e);
}
}
if (options.getStayAwake()) {
int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS;
try {
String oldValue = Settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn));
try {
int restoreStayOn = Integer.parseInt(oldValue);
if (restoreStayOn != stayOn) {
// Restore only if the current value is different
if (!cleanUp.setRestoreStayOn(restoreStayOn)) {
Ln.e("Could not restore stay on on exit");
}
}
} catch (NumberFormatException e) {
// ignore
}
} catch (SettingsException e) {
Ln.e("Could not change \"stay_on_while_plugged_in\"", e);
}
}
if (options.getPowerOffScreenOnClose()) {
if (!cleanUp.setPowerOffScreen(true)) {
Ln.e("Could not power off screen on exit");
}
}
}
private static void scrcpy(Options options) throws IOException, ConfigurationException { private static void scrcpy(Options options) throws IOException, ConfigurationException {
if (Build.VERSION.SDK_INT < AndroidVersions.API_31_ANDROID_12 && options.getVideoSource() == VideoSource.CAMERA) { if (Build.VERSION.SDK_INT < AndroidVersions.API_31_ANDROID_12 && options.getVideoSource() == VideoSource.CAMERA) {
Ln.e("Camera mirroring is not supported before Android 12"); Ln.e("Camera mirroring is not supported before Android 12");
@ -86,9 +133,14 @@ public final class Server {
} }
CleanUp cleanUp = null; CleanUp cleanUp = null;
Thread initThread = null;
NewDisplay newDisplay = options.getNewDisplay();
int displayId = newDisplay == null ? options.getDisplayId() : Device.DISPLAY_ID_NONE;
if (options.getCleanup()) { if (options.getCleanup()) {
cleanUp = CleanUp.start(options); cleanUp = CleanUp.configure(displayId);
initThread = startInitThread(options, cleanUp);
} }
int scid = options.getScid(); int scid = options.getScid();
@ -112,7 +164,7 @@ public final class Server {
if (control) { if (control) {
ControlChannel controlChannel = connection.getControlChannel(); ControlChannel controlChannel = connection.getControlChannel();
controller = new Controller(controlChannel, cleanUp, options); controller = new Controller(displayId, controlChannel, cleanUp, options.getClipboardAutosync(), options.getPowerOn());
asyncProcessors.add(controller); asyncProcessors.add(controller);
} }
@ -131,7 +183,8 @@ public final class Server {
if (audioCodec == AudioCodec.RAW) { if (audioCodec == AudioCodec.RAW) {
audioRecorder = new AudioRawRecorder(audioCapture, audioStreamer); audioRecorder = new AudioRawRecorder(audioCapture, audioStreamer);
} else { } else {
audioRecorder = new AudioEncoder(audioCapture, audioStreamer, options); audioRecorder = new AudioEncoder(audioCapture, audioStreamer, options.getAudioBitRate(), options.getAudioCodecOptions(),
options.getAudioEncoder());
} }
asyncProcessors.add(audioRecorder); asyncProcessors.add(audioRecorder);
} }
@ -141,22 +194,20 @@ public final class Server {
options.getSendFrameMeta()); options.getSendFrameMeta());
SurfaceCapture surfaceCapture; SurfaceCapture surfaceCapture;
if (options.getVideoSource() == VideoSource.DISPLAY) { if (options.getVideoSource() == VideoSource.DISPLAY) {
NewDisplay newDisplay = options.getNewDisplay();
if (newDisplay != null) { if (newDisplay != null) {
surfaceCapture = new NewDisplayCapture(controller, options); surfaceCapture = new NewDisplayCapture(controller, newDisplay, options.getMaxSize());
} else { } else {
assert options.getDisplayId() != Device.DISPLAY_ID_NONE; assert displayId != Device.DISPLAY_ID_NONE;
surfaceCapture = new ScreenCapture(controller, options); surfaceCapture = new ScreenCapture(controller, displayId, options.getMaxSize(), options.getCrop(),
options.getLockVideoOrientation());
} }
} else { } else {
surfaceCapture = new CameraCapture(options); surfaceCapture = new CameraCapture(options.getCameraId(), options.getCameraFacing(), options.getCameraSize(),
options.getMaxSize(), options.getCameraAspectRatio(), options.getCameraFps(), options.getCameraHighSpeed());
} }
SurfaceEncoder surfaceEncoder = new SurfaceEncoder(surfaceCapture, videoStreamer, options); SurfaceEncoder surfaceEncoder = new SurfaceEncoder(surfaceCapture, videoStreamer, options.getVideoBitRate(), options.getMaxFps(),
options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError());
asyncProcessors.add(surfaceEncoder); asyncProcessors.add(surfaceEncoder);
if (controller != null) {
controller.setSurfaceCapture(surfaceCapture);
}
} }
Completion completion = new Completion(asyncProcessors.size()); Completion completion = new Completion(asyncProcessors.size());
@ -168,25 +219,22 @@ public final class Server {
completion.await(); completion.await();
} finally { } finally {
if (cleanUp != null) { if (initThread != null) {
cleanUp.interrupt(); initThread.interrupt();
} }
for (AsyncProcessor asyncProcessor : asyncProcessors) { for (AsyncProcessor asyncProcessor : asyncProcessors) {
asyncProcessor.stop(); asyncProcessor.stop();
} }
OpenGLRunner.quit(); // quit the OpenGL thread, if any
connection.shutdown(); connection.shutdown();
try { try {
if (cleanUp != null) { if (initThread != null) {
cleanUp.join(); initThread.join();
} }
for (AsyncProcessor asyncProcessor : asyncProcessors) { for (AsyncProcessor asyncProcessor : asyncProcessors) {
asyncProcessor.join(); asyncProcessor.join();
} }
OpenGLRunner.join();
} catch (InterruptedException e) { } catch (InterruptedException e) {
// ignore // ignore
} }
@ -195,6 +243,12 @@ public final class Server {
} }
} }
private static Thread startInitThread(final Options options, final CleanUp cleanUp) {
Thread thread = new Thread(() -> initAndCleanUp(options, cleanUp), "init-cleanup");
thread.start();
return thread;
}
public static void main(String... args) { public static void main(String... args) {
int status = 0; int status = 0;
try { try {

View File

@ -2,7 +2,6 @@ package com.genymobile.scrcpy.audio;
import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.AsyncProcessor; import com.genymobile.scrcpy.AsyncProcessor;
import com.genymobile.scrcpy.Options;
import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.device.ConfigurationException;
import com.genymobile.scrcpy.device.Streamer; import com.genymobile.scrcpy.device.Streamer;
import com.genymobile.scrcpy.util.Codec; import com.genymobile.scrcpy.util.Codec;
@ -68,12 +67,12 @@ public final class AudioEncoder implements AsyncProcessor {
private boolean ended; private boolean ended;
public AudioEncoder(AudioCapture capture, Streamer streamer, Options options) { public AudioEncoder(AudioCapture capture, Streamer streamer, int bitRate, List<CodecOption> codecOptions, String encoderName) {
this.capture = capture; this.capture = capture;
this.streamer = streamer; this.streamer = streamer;
this.bitRate = options.getAudioBitRate(); this.bitRate = bitRate;
this.codecOptions = options.getAudioCodecOptions(); this.codecOptions = codecOptions;
this.encoderName = options.getAudioEncoder(); this.encoderName = encoderName;
} }
private static MediaFormat createFormat(String mimeType, int bitRate, List<CodecOption> codecOptions) { private static MediaFormat createFormat(String mimeType, int bitRate, List<CodecOption> codecOptions) {

View File

@ -17,14 +17,13 @@ public final class ControlMessage {
public static final int TYPE_COLLAPSE_PANELS = 7; public static final int TYPE_COLLAPSE_PANELS = 7;
public static final int TYPE_GET_CLIPBOARD = 8; public static final int TYPE_GET_CLIPBOARD = 8;
public static final int TYPE_SET_CLIPBOARD = 9; public static final int TYPE_SET_CLIPBOARD = 9;
public static final int TYPE_SET_DISPLAY_POWER = 10; public static final int TYPE_SET_SCREEN_POWER_MODE = 10;
public static final int TYPE_ROTATE_DEVICE = 11; public static final int TYPE_ROTATE_DEVICE = 11;
public static final int TYPE_UHID_CREATE = 12; public static final int TYPE_UHID_CREATE = 12;
public static final int TYPE_UHID_INPUT = 13; public static final int TYPE_UHID_INPUT = 13;
public static final int TYPE_UHID_DESTROY = 14; public static final int TYPE_UHID_DESTROY = 14;
public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 15; public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 15;
public static final int TYPE_START_APP = 16; public static final int TYPE_START_APP = 16;
public static final int TYPE_RESET_VIDEO = 17;
public static final long SEQUENCE_INVALID = 0; public static final long SEQUENCE_INVALID = 0;
@ -35,7 +34,7 @@ public final class ControlMessage {
private int type; private int type;
private String text; private String text;
private int metaState; // KeyEvent.META_* private int metaState; // KeyEvent.META_*
private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or POWER_MODE_*
private int keycode; // KeyEvent.KEYCODE_* private int keycode; // KeyEvent.KEYCODE_*
private int actionButton; // MotionEvent.BUTTON_* private int actionButton; // MotionEvent.BUTTON_*
private int buttons; // MotionEvent.BUTTON_* private int buttons; // MotionEvent.BUTTON_*
@ -50,9 +49,6 @@ public final class ControlMessage {
private long sequence; private long sequence;
private int id; private int id;
private byte[] data; private byte[] data;
private boolean on;
private int vendorId;
private int productId;
private ControlMessage() { private ControlMessage() {
} }
@ -120,10 +116,13 @@ public final class ControlMessage {
return msg; return msg;
} }
public static ControlMessage createSetDisplayPower(boolean on) { /**
* @param mode one of the {@code Device.SCREEN_POWER_MODE_*} constants
*/
public static ControlMessage createSetScreenPowerMode(int mode) {
ControlMessage msg = new ControlMessage(); ControlMessage msg = new ControlMessage();
msg.type = TYPE_SET_DISPLAY_POWER; msg.type = TYPE_SET_SCREEN_POWER_MODE;
msg.on = on; msg.action = mode;
return msg; return msg;
} }
@ -133,12 +132,10 @@ public final class ControlMessage {
return msg; return msg;
} }
public static ControlMessage createUhidCreate(int id, int vendorId, int productId, String name, byte[] reportDesc) { public static ControlMessage createUhidCreate(int id, String name, byte[] reportDesc) {
ControlMessage msg = new ControlMessage(); ControlMessage msg = new ControlMessage();
msg.type = TYPE_UHID_CREATE; msg.type = TYPE_UHID_CREATE;
msg.id = id; msg.id = id;
msg.vendorId = vendorId;
msg.productId = productId;
msg.text = name; msg.text = name;
msg.data = reportDesc; msg.data = reportDesc;
return msg; return msg;
@ -237,16 +234,4 @@ public final class ControlMessage {
public byte[] getData() { public byte[] getData() {
return data; return data;
} }
public boolean getOn() {
return on;
}
public int getVendorId() {
return vendorId;
}
public int getProductId() {
return productId;
}
} }

View File

@ -39,14 +39,13 @@ public class ControlMessageReader {
return parseGetClipboard(); return parseGetClipboard();
case ControlMessage.TYPE_SET_CLIPBOARD: case ControlMessage.TYPE_SET_CLIPBOARD:
return parseSetClipboard(); return parseSetClipboard();
case ControlMessage.TYPE_SET_DISPLAY_POWER: case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
return parseSetDisplayPower(); return parseSetScreenPowerMode();
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL: case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL:
case ControlMessage.TYPE_COLLAPSE_PANELS: case ControlMessage.TYPE_COLLAPSE_PANELS:
case ControlMessage.TYPE_ROTATE_DEVICE: case ControlMessage.TYPE_ROTATE_DEVICE:
case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS: case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
case ControlMessage.TYPE_RESET_VIDEO:
return ControlMessage.createEmpty(type); return ControlMessage.createEmpty(type);
case ControlMessage.TYPE_UHID_CREATE: case ControlMessage.TYPE_UHID_CREATE:
return parseUhidCreate(); return parseUhidCreate();
@ -135,18 +134,16 @@ public class ControlMessageReader {
return ControlMessage.createSetClipboard(sequence, text, paste); return ControlMessage.createSetClipboard(sequence, text, paste);
} }
private ControlMessage parseSetDisplayPower() throws IOException { private ControlMessage parseSetScreenPowerMode() throws IOException {
boolean on = dis.readBoolean(); int mode = dis.readUnsignedByte();
return ControlMessage.createSetDisplayPower(on); return ControlMessage.createSetScreenPowerMode(mode);
} }
private ControlMessage parseUhidCreate() throws IOException { private ControlMessage parseUhidCreate() throws IOException {
int id = dis.readUnsignedShort(); int id = dis.readUnsignedShort();
int vendorId = dis.readUnsignedShort();
int productId = dis.readUnsignedShort();
String name = parseString(1); String name = parseString(1);
byte[] data = parseByteArray(2); byte[] data = parseByteArray(2);
return ControlMessage.createUhidCreate(id, vendorId, productId, name, data); return ControlMessage.createUhidCreate(id, name, data);
} }
private ControlMessage parseUhidInput() throws IOException { private ControlMessage parseUhidInput() throws IOException {

View File

@ -3,15 +3,12 @@ package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.AsyncProcessor; import com.genymobile.scrcpy.AsyncProcessor;
import com.genymobile.scrcpy.CleanUp; import com.genymobile.scrcpy.CleanUp;
import com.genymobile.scrcpy.Options;
import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.Device;
import com.genymobile.scrcpy.device.DeviceApp; import com.genymobile.scrcpy.device.DeviceApp;
import com.genymobile.scrcpy.device.Point; import com.genymobile.scrcpy.device.Point;
import com.genymobile.scrcpy.device.Position; import com.genymobile.scrcpy.device.Position;
import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.LogUtils; import com.genymobile.scrcpy.util.LogUtils;
import com.genymobile.scrcpy.video.SurfaceCapture;
import com.genymobile.scrcpy.video.VirtualDisplayListener; import com.genymobile.scrcpy.video.VirtualDisplayListener;
import com.genymobile.scrcpy.wrappers.ClipboardManager; import com.genymobile.scrcpy.wrappers.ClipboardManager;
import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.InputManager;
@ -21,7 +18,6 @@ import android.content.IOnPrimaryClipChangedListener;
import android.content.Intent; import android.content.Intent;
import android.os.Build; import android.os.Build;
import android.os.SystemClock; import android.os.SystemClock;
import android.util.Pair;
import android.view.InputDevice; import android.view.InputDevice;
import android.view.KeyCharacterMap; import android.view.KeyCharacterMap;
import android.view.KeyEvent; import android.view.KeyEvent;
@ -95,17 +91,14 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS]; private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS];
private final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[PointersState.MAX_POINTERS]; private final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[PointersState.MAX_POINTERS];
private boolean keepDisplayPowerOff; private boolean keepPowerModeOff;
// Used for resetting video encoding on RESET_VIDEO message public Controller(int displayId, ControlChannel controlChannel, CleanUp cleanUp, boolean clipboardAutosync, boolean powerOn) {
private SurfaceCapture surfaceCapture; this.displayId = displayId;
public Controller(ControlChannel controlChannel, CleanUp cleanUp, Options options) {
this.displayId = options.getDisplayId();
this.controlChannel = controlChannel; this.controlChannel = controlChannel;
this.cleanUp = cleanUp; this.cleanUp = cleanUp;
this.clipboardAutosync = options.getClipboardAutosync(); this.clipboardAutosync = clipboardAutosync;
this.powerOn = options.getPowerOn(); this.powerOn = powerOn;
initPointers(); initPointers();
sender = new DeviceMessageSender(controlChannel); sender = new DeviceMessageSender(controlChannel);
@ -150,10 +143,6 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
} }
} }
public void setSurfaceCapture(SurfaceCapture surfaceCapture) {
this.surfaceCapture = surfaceCapture;
}
private UhidManager getUhidManager() { private UhidManager getUhidManager() {
if (uhidManager == null) { if (uhidManager == null) {
uhidManager = new UhidManager(sender); uhidManager = new UhidManager(sender);
@ -177,7 +166,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
private void control() throws IOException { private void control() throws IOException {
// on start, power on the device // on start, power on the device
if (powerOn && displayId == 0 && !Device.isScreenOn(displayId)) { if (powerOn && displayId != Device.DISPLAY_ID_NONE && !Device.isScreenOn()) {
Device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC); Device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC);
// dirty hack // dirty hack
@ -281,16 +270,25 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
case ControlMessage.TYPE_SET_CLIPBOARD: case ControlMessage.TYPE_SET_CLIPBOARD:
setClipboard(msg.getText(), msg.getPaste(), msg.getSequence()); setClipboard(msg.getText(), msg.getPaste(), msg.getSequence());
break; break;
case ControlMessage.TYPE_SET_DISPLAY_POWER: case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
if (supportsInputEvents) { if (supportsInputEvents) {
setDisplayPower(msg.getOn()); int mode = msg.getAction();
boolean setPowerModeOk = Device.setScreenPowerMode(mode);
if (setPowerModeOk) {
keepPowerModeOff = mode == Device.POWER_MODE_OFF;
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
if (cleanUp != null) {
boolean mustRestoreOnExit = mode != Device.POWER_MODE_NORMAL;
cleanUp.setRestoreNormalPowerMode(mustRestoreOnExit);
}
}
} }
break; break;
case ControlMessage.TYPE_ROTATE_DEVICE: case ControlMessage.TYPE_ROTATE_DEVICE:
Device.rotateDevice(getActionDisplayId()); Device.rotateDevice(getActionDisplayId());
break; break;
case ControlMessage.TYPE_UHID_CREATE: case ControlMessage.TYPE_UHID_CREATE:
getUhidManager().open(msg.getId(), msg.getVendorId(), msg.getProductId(), msg.getText(), msg.getData()); getUhidManager().open(msg.getId(), msg.getText(), msg.getData());
break; break;
case ControlMessage.TYPE_UHID_INPUT: case ControlMessage.TYPE_UHID_INPUT:
getUhidManager().writeInput(msg.getId(), msg.getData()); getUhidManager().writeInput(msg.getId(), msg.getData());
@ -304,9 +302,6 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
case ControlMessage.TYPE_START_APP: case ControlMessage.TYPE_START_APP:
startAppAsync(msg.getText()); startAppAsync(msg.getText());
break; break;
case ControlMessage.TYPE_RESET_VIDEO:
resetVideo();
break;
default: default:
// do nothing // do nothing
} }
@ -315,9 +310,8 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
} }
private boolean injectKeycode(int action, int keycode, int repeat, int metaState) { private boolean injectKeycode(int action, int keycode, int repeat, int metaState) {
if (keepDisplayPowerOff && action == KeyEvent.ACTION_UP && (keycode == KeyEvent.KEYCODE_POWER || keycode == KeyEvent.KEYCODE_WAKEUP)) { if (keepPowerModeOff && action == KeyEvent.ACTION_UP && (keycode == KeyEvent.KEYCODE_POWER || keycode == KeyEvent.KEYCODE_WAKEUP)) {
assert displayId != Device.DISPLAY_ID_NONE; schedulePowerModeOff();
scheduleDisplayPowerOff(displayId);
} }
return injectKeyEvent(action, keycode, repeat, metaState, Device.INJECT_MODE_ASYNC); return injectKeyEvent(action, keycode, repeat, metaState, Device.INJECT_MODE_ASYNC);
} }
@ -351,47 +345,20 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
return successCount; return successCount;
} }
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();
// In scrcpy, displayData should never be null (a touch event can only be generated from the client when a video frame is present).
// However, it is possible to send events without video playback when using scrcpy-server alone (except for virtual displays).
assert displayData != null || displayId != Device.DISPLAY_ID_NONE : "Cannot receive a positional event without a display";
Point point;
int targetDisplayId;
if (displayData != null) {
point = displayData.positionMapper.map(position);
if (point == null) {
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
Size eventSize = position.getScreenSize();
Size currentSize = displayData.positionMapper.getVideoSize();
Ln.v("Ignore positional event generated for size " + eventSize + " (current size is " + currentSize + ")");
}
return null;
}
targetDisplayId = displayData.virtualDisplayId;
} else {
// No display, use the raw coordinates
point = position.getPoint();
targetDisplayId = displayId;
}
return Pair.create(point, targetDisplayId);
}
private boolean injectTouch(int action, long pointerId, Position position, float pressure, int actionButton, int buttons) { private boolean injectTouch(int action, long pointerId, Position position, float pressure, int actionButton, int buttons) {
long now = SystemClock.uptimeMillis(); long now = SystemClock.uptimeMillis();
Pair<Point, Integer> pair = getEventPointAndDisplayId(position); // it hides the field on purpose, to read it with atomic access
if (pair == null) { @SuppressWarnings("checkstyle:HiddenField")
DisplayData displayData = this.displayData.get();
assert displayData != null : "Cannot receive a touch event without a display";
Point point = displayData.positionMapper.map(position);
if (point == null) {
Ln.w("Ignore touch event, it was generated for a different device size");
return false; return false;
} }
Point point = pair.first;
int targetDisplayId = pair.second;
int pointerIndex = pointersState.getPointerIndex(pointerId); int pointerIndex = pointersState.getPointerIndex(pointerId);
if (pointerIndex == -1) { if (pointerIndex == -1) {
Ln.w("Too many pointers for touch event"); Ln.w("Too many pointers for touch event");
@ -445,7 +412,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
// First button pressed: ACTION_DOWN // First button pressed: ACTION_DOWN
MotionEvent downEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_DOWN, pointerCount, pointerProperties, MotionEvent downEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_DOWN, pointerCount, pointerProperties,
pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0);
if (!Device.injectEvent(downEvent, targetDisplayId, Device.INJECT_MODE_ASYNC)) { if (!Device.injectEvent(downEvent, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC)) {
return false; return false;
} }
} }
@ -456,7 +423,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
if (!InputManager.setActionButton(pressEvent, actionButton)) { if (!InputManager.setActionButton(pressEvent, actionButton)) {
return false; return false;
} }
if (!Device.injectEvent(pressEvent, targetDisplayId, Device.INJECT_MODE_ASYNC)) { if (!Device.injectEvent(pressEvent, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC)) {
return false; return false;
} }
@ -470,7 +437,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
if (!InputManager.setActionButton(releaseEvent, actionButton)) { if (!InputManager.setActionButton(releaseEvent, actionButton)) {
return false; return false;
} }
if (!Device.injectEvent(releaseEvent, targetDisplayId, Device.INJECT_MODE_ASYNC)) { if (!Device.injectEvent(releaseEvent, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC)) {
return false; return false;
} }
@ -478,7 +445,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
// Last button released: ACTION_UP // Last button released: ACTION_UP
MotionEvent upEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_UP, pointerCount, pointerProperties, MotionEvent upEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_UP, pointerCount, pointerProperties,
pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0);
if (!Device.injectEvent(upEvent, targetDisplayId, Device.INJECT_MODE_ASYNC)) { if (!Device.injectEvent(upEvent, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC)) {
return false; return false;
} }
} }
@ -489,20 +456,23 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f,
DEFAULT_DEVICE_ID, 0, source, 0); DEFAULT_DEVICE_ID, 0, source, 0);
return Device.injectEvent(event, targetDisplayId, Device.INJECT_MODE_ASYNC); return Device.injectEvent(event, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC);
} }
private boolean injectScroll(Position position, float hScroll, float vScroll, int buttons) { private boolean injectScroll(Position position, float hScroll, float vScroll, int buttons) {
long now = SystemClock.uptimeMillis(); long now = SystemClock.uptimeMillis();
Pair<Point, Integer> pair = getEventPointAndDisplayId(position); // it hides the field on purpose, to read it with atomic access
if (pair == null) { @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");
return false; return false;
} }
Point point = pair.first;
int targetDisplayId = pair.second;
MotionEvent.PointerProperties props = pointerProperties[0]; MotionEvent.PointerProperties props = pointerProperties[0];
props.id = 0; props.id = 0;
@ -514,21 +484,21 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
MotionEvent event = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, MotionEvent event = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, buttons, 1f, 1f,
DEFAULT_DEVICE_ID, 0, InputDevice.SOURCE_MOUSE, 0); DEFAULT_DEVICE_ID, 0, InputDevice.SOURCE_MOUSE, 0);
return Device.injectEvent(event, targetDisplayId, Device.INJECT_MODE_ASYNC); return Device.injectEvent(event, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC);
} }
/** /**
* Schedule a call to set display power to off after a small delay. * Schedule a call to set power mode to off after a small delay.
*/ */
private static void scheduleDisplayPowerOff(int displayId) { private static void schedulePowerModeOff() {
EXECUTOR.schedule(() -> { EXECUTOR.schedule(() -> {
Ln.i("Forcing display off"); Ln.i("Forcing screen off");
Device.setDisplayPower(displayId, false); Device.setScreenPowerMode(Device.POWER_MODE_OFF);
}, 200, TimeUnit.MILLISECONDS); }, 200, TimeUnit.MILLISECONDS);
} }
private boolean pressBackOrTurnScreenOn(int action) { private boolean pressBackOrTurnScreenOn(int action) {
if (displayId == Device.DISPLAY_ID_NONE || Device.isScreenOn(displayId)) { if (Device.isScreenOn()) {
return injectKeyEvent(action, KeyEvent.KEYCODE_BACK, 0, 0, Device.INJECT_MODE_ASYNC); return injectKeyEvent(action, KeyEvent.KEYCODE_BACK, 0, 0, Device.INJECT_MODE_ASYNC);
} }
@ -539,9 +509,8 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
return true; return true;
} }
if (keepDisplayPowerOff) { if (keepPowerModeOff) {
assert displayId != Device.DISPLAY_ID_NONE; schedulePowerModeOff();
scheduleDisplayPowerOff(displayId);
} }
return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC); return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC);
} }
@ -706,26 +675,4 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
return data; 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();
}
}
} }

View File

@ -3,46 +3,46 @@ package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.device.Point; import com.genymobile.scrcpy.device.Point;
import com.genymobile.scrcpy.device.Position; import com.genymobile.scrcpy.device.Position;
import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.util.AffineMatrix; import com.genymobile.scrcpy.video.ScreenInfo;
import android.graphics.Rect;
public final class PositionMapper { public final class PositionMapper {
private final Size videoSize; private final Size videoSize;
private final AffineMatrix videoToDeviceMatrix; private final Rect contentRect;
private final int coordsRotation;
public PositionMapper(Size videoSize, AffineMatrix videoToDeviceMatrix) { public PositionMapper(Size videoSize, Rect contentRect, int videoRotation) {
this.videoSize = videoSize; this.videoSize = videoSize;
this.videoToDeviceMatrix = videoToDeviceMatrix; this.contentRect = contentRect;
this.coordsRotation = reverseRotation(videoRotation);
} }
public static PositionMapper create(Size videoSize, AffineMatrix filterTransform, Size targetSize) { public static PositionMapper from(ScreenInfo screenInfo) {
boolean convertToPixels = !videoSize.equals(targetSize) || filterTransform != null; // ignore the locked video orientation, the events will apply in coordinates considered in the physical device orientation
AffineMatrix transform = filterTransform; Size videoSize = screenInfo.getUnlockedVideoSize();
if (convertToPixels) { return new PositionMapper(videoSize, screenInfo.getContentRect(), screenInfo.getVideoRotation());
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) { public Point map(Position position) {
Size clientVideoSize = position.getScreenSize(); // reverse the video rotation to apply the events
Position devicePosition = position.rotate(coordsRotation);
Size clientVideoSize = devicePosition.getScreenSize();
if (!videoSize.equals(clientVideoSize)) { if (!videoSize.equals(clientVideoSize)) {
// The client sends a click relative to a video with wrong dimensions, // 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 // the device may have been rotated since the event was generated, so ignore the event
return null; return null;
} }
Point point = position.getPoint(); Point point = devicePosition.getPoint();
if (videoToDeviceMatrix != null) { int convertedX = contentRect.left + point.getX() * contentRect.width() / videoSize.getWidth();
point = videoToDeviceMatrix.apply(point); int convertedY = contentRect.top + point.getY() * contentRect.height() / videoSize.getHeight();
} return new Point(convertedX, convertedY);
return point;
} }
} }

View File

@ -48,7 +48,7 @@ public final class UhidManager {
} }
} }
public void open(int id, int vendorId, int productId, String name, byte[] reportDesc) throws IOException { public void open(int id, String name, byte[] reportDesc) throws IOException {
try { try {
FileDescriptor fd = Os.open("/dev/uhid", OsConstants.O_RDWR, 0); FileDescriptor fd = Os.open("/dev/uhid", OsConstants.O_RDWR, 0);
try { try {
@ -58,7 +58,7 @@ public final class UhidManager {
close(old); close(old);
} }
byte[] req = buildUhidCreate2Req(vendorId, productId, name, reportDesc); byte[] req = buildUhidCreate2Req(name, reportDesc);
Os.write(fd, req, 0, req.length); Os.write(fd, req, 0, req.length);
registerUhidListener(id, fd); registerUhidListener(id, fd);
@ -148,7 +148,7 @@ public final class UhidManager {
} }
} }
private static byte[] buildUhidCreate2Req(int vendorId, int productId, String name, byte[] reportDesc) { private static byte[] buildUhidCreate2Req(String name, byte[] reportDesc) {
/* /*
* struct uhid_event { * struct uhid_event {
* uint32_t type; * uint32_t type;
@ -174,7 +174,7 @@ public final class UhidManager {
ByteBuffer buf = ByteBuffer.allocate(280 + reportDesc.length).order(ByteOrder.nativeOrder()); ByteBuffer buf = ByteBuffer.allocate(280 + reportDesc.length).order(ByteOrder.nativeOrder());
buf.putInt(UHID_CREATE2); buf.putInt(UHID_CREATE2);
String actualName = name.isEmpty() ? "scrcpy" : name; String actualName = name.isEmpty() ? "scrcpy" : "scrcpy: " + name;
byte[] utf8Name = actualName.getBytes(StandardCharsets.UTF_8); byte[] utf8Name = actualName.getBytes(StandardCharsets.UTF_8);
int len = StringUtils.getUtf8TruncationIndex(utf8Name, 127); int len = StringUtils.getUtf8TruncationIndex(utf8Name, 127);
assert len <= 127; assert len <= 127;
@ -183,8 +183,8 @@ public final class UhidManager {
buf.putShort((short) reportDesc.length); buf.putShort((short) reportDesc.length);
buf.putShort(BUS_VIRTUAL); buf.putShort(BUS_VIRTUAL);
buf.putInt(vendorId); buf.putInt(0); // vendor id
buf.putInt(productId); buf.putInt(0); // product id
buf.putInt(0); // version buf.putInt(0); // version
buf.putInt(0); // country; buf.putInt(0); // country;
buf.put(reportDesc); buf.put(reportDesc);

View File

@ -40,9 +40,8 @@ public final class Device {
public static final int INJECT_MODE_WAIT_FOR_RESULT = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT; public static final int INJECT_MODE_WAIT_FOR_RESULT = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT;
public static final int INJECT_MODE_WAIT_FOR_FINISH = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH; public static final int INJECT_MODE_WAIT_FOR_FINISH = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH;
// The new display power method introduced in Android 15 does not work as expected: public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1;
// <https://github.com/Genymobile/scrcpy/issues/5530> public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2;
private static final boolean USE_ANDROID_15_DISPLAY_POWER = false;
private Device() { private Device() {
// not instantiable // not instantiable
@ -81,9 +80,8 @@ public final class Device {
&& injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0, displayId, injectMode); && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0, displayId, injectMode);
} }
public static boolean isScreenOn(int displayId) { public static boolean isScreenOn() {
assert displayId != DISPLAY_ID_NONE; return ServiceManager.getPowerManager().isScreenOn();
return ServiceManager.getPowerManager().isScreenOn(displayId);
} }
public static void expandNotificationPanel() { public static void expandNotificationPanel() {
@ -128,13 +126,10 @@ public final class Device {
return clipboardManager.setText(text); return clipboardManager.setText(text);
} }
public static boolean setDisplayPower(int displayId, boolean on) { /**
assert displayId != Device.DISPLAY_ID_NONE; * @param mode one of the {@code POWER_MODE_*} constants
*/
if (USE_ANDROID_15_DISPLAY_POWER && Build.VERSION.SDK_INT >= AndroidVersions.API_35_ANDROID_15) { public static boolean setScreenPowerMode(int mode) {
return ServiceManager.getDisplayManager().requestDisplayPower(displayId, on);
}
boolean applyToMultiPhysicalDisplays = Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10; boolean applyToMultiPhysicalDisplays = Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10;
if (applyToMultiPhysicalDisplays if (applyToMultiPhysicalDisplays
@ -147,7 +142,6 @@ public final class Device {
applyToMultiPhysicalDisplays = false; applyToMultiPhysicalDisplays = false;
} }
int mode = on ? POWER_MODE_NORMAL : POWER_MODE_OFF;
if (applyToMultiPhysicalDisplays) { if (applyToMultiPhysicalDisplays) {
// On Android 14, these internal methods have been moved to DisplayControl // On Android 14, these internal methods have been moved to DisplayControl
boolean useDisplayControl = boolean useDisplayControl =
@ -181,7 +175,7 @@ public final class Device {
public static boolean powerOffScreen(int displayId) { public static boolean powerOffScreen(int displayId) {
assert displayId != DISPLAY_ID_NONE; assert displayId != DISPLAY_ID_NONE;
if (!isScreenOn(displayId)) { if (!isScreenOn()) {
return true; return true;
} }
return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC); return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC);

View File

@ -1,47 +0,0 @@
package com.genymobile.scrcpy.device;
public enum Orientation {
// @formatter:off
Orient0("0"),
Orient90("90"),
Orient180("180"),
Orient270("270"),
Flip0("flip0"),
Flip90("flip90"),
Flip180("flip180"),
Flip270("flip270");
public enum Lock {
Unlocked, LockedInitial, LockedValue,
}
private final String name;
Orientation(String name) {
this.name = name;
}
public static Orientation getByName(String name) {
for (Orientation orientation : values()) {
if (orientation.name.equals(name)) {
return orientation;
}
}
throw new IllegalArgumentException("Unknown orientation: " + name);
}
public static Orientation fromRotation(int rotation) {
assert rotation >= 0 && rotation < 4;
return values()[rotation];
}
public boolean isFlipped() {
return (ordinal() & 4) != 0;
}
public int getRotation() {
return ordinal() & 3;
}
}

View File

@ -29,61 +29,6 @@ public final class Size {
return new Size(height, width); return new Size(height, width);
} }
public Size limit(int maxSize) {
assert maxSize >= 0 : "Max size may not be negative";
assert maxSize % 8 == 0 : "Max size must be a multiple of 8";
if (maxSize == 0) {
// No limit
return this;
}
boolean portrait = height > width;
int major = portrait ? height : width;
if (major <= maxSize) {
return this;
}
int minor = portrait ? width : height;
int newMajor = maxSize;
int newMinor = maxSize * minor / major;
int w = portrait ? newMinor : newMajor;
int h = portrait ? newMajor : newMinor;
return new Size(w, h);
}
/**
* Round both dimensions of this size to be a multiple of 8 (as required by many encoders).
*
* @return The current size rounded.
*/
public Size round8() {
if (isMultipleOf8()) {
// Already a multiple of 8
return this;
}
boolean portrait = height > width;
int major = portrait ? height : width;
int minor = portrait ? width : height;
major &= ~7; // round down to not exceed the initial size
minor = (minor + 4) & ~7; // round to the nearest to minimize aspect ratio distortion
if (minor > major) {
minor = major;
}
int w = portrait ? minor : major;
int h = portrait ? major : minor;
return new Size(w, h);
}
public boolean isMultipleOf8() {
return (width & 7) == 0 && (height & 7) == 0;
}
public Rect toRect() { public Rect toRect() {
return new Rect(0, 0, width, height); return new Rect(0, 0, width, height);
} }
@ -107,6 +52,6 @@ public final class Size {
@Override @Override
public String toString() { public String toString() {
return width + "x" + height; return "Size{" + "width=" + width + ", height=" + height + '}';
} }
} }

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