Compare commits

..

1 Commits

Author SHA1 Message Date
e39adb1b79 Detect codec/encoder mismatch
Fail with an explicit error when the requested encoder does not match
the requested codec.

Refs #5066 <https://github.com/Genymobile/scrcpy/issues/5066>
2024-09-20 08:18:42 +02:00
141 changed files with 2389 additions and 6412 deletions

View File

@ -1,502 +0,0 @@
name: Build
on:
workflow_dispatch:
inputs:
name:
description: 'Version name (default is ref name)'
env:
# $VERSION is used by release scripts
VERSION: ${{ github.event.inputs.name || github.ref_name }}
jobs:
test-scrcpy-server:
runs-on: ubuntu-latest
env:
GRADLE: gradle # use native gradle instead of ./gradlew in scripts
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup JDK
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
- name: Test scrcpy-server
run: release/test_server.sh
build-scrcpy-server:
runs-on: ubuntu-latest
env:
GRADLE: gradle # use native gradle instead of ./gradlew in scripts
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup JDK
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
- name: Build
run: release/build_server.sh
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: scrcpy-server
path: release/work/build-server/server/scrcpy-server
test-build-scrcpy-server-without-gradle:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup JDK
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
- name: Build without gradle
run: server/build_without_gradle.sh
test-client:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \
libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \
libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \
libv4l-dev
- name: Test
run: release/test_client.sh
build-linux-x86_64:
runs-on: ubuntu-latest
steps:
- name: 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
run: release/build_linux.sh x86_64
# upload-artifact does not preserve permissions
- name: Tar
run: |
cd release/work/build-linux-x86_64
mkdir dist-tar
cd dist-tar
tar -C .. -cvf dist.tar.gz dist/
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: build-linux-x86_64-intermediate
path: release/work/build-linux-x86_64/dist-tar/
build-win32:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \
libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \
libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \
mingw-w64 mingw-w64-tools libz-mingw-w64-dev
- name: Workaround for old meson version run by Github Actions
run: sed -i 's/^pkg-config/pkgconfig/' cross_win32.txt
- name: Build
run: release/build_windows.sh 32
# upload-artifact does not preserve permissions
- name: Tar
run: |
cd release/work/build-win32
mkdir dist-tar
cd dist-tar
tar -C .. -cvf dist.tar.gz dist/
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: build-win32-intermediate
path: release/work/build-win32/dist-tar/
build-win64:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \
libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \
libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \
mingw-w64 mingw-w64-tools libz-mingw-w64-dev
- name: Workaround for old meson version run by Github Actions
run: sed -i 's/^pkg-config/pkgconfig/' cross_win64.txt
- name: Build
run: release/build_windows.sh 64
# upload-artifact does not preserve permissions
- name: Tar
run: |
cd release/work/build-win64
mkdir dist-tar
cd dist-tar
tar -C .. -cvf dist.tar.gz dist/
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: build-win64-intermediate
path: release/work/build-win64/dist-tar/
build-macos-aarch64:
runs-on: macos-latest
steps:
- name: 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:
- build-scrcpy-server
- build-win32
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download scrcpy-server
uses: actions/download-artifact@v4
with:
name: scrcpy-server
path: release/work/build-server/server/
- name: Download build-win32
uses: actions/download-artifact@v4
with:
name: build-win32-intermediate
path: release/work/build-win32/dist-tar/
# upload-artifact does not preserve permissions
- name: Detar
run: |
cd release/work/build-win32
tar xf dist-tar/dist.tar.gz
- name: Package
run: release/package_client.sh win32 zip
- name: Upload release
uses: actions/upload-artifact@v4
with:
name: release-win32
path: release/output/
package-win64:
needs:
- build-scrcpy-server
- build-win64
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download scrcpy-server
uses: actions/download-artifact@v4
with:
name: scrcpy-server
path: release/work/build-server/server/
- name: Download build-win64
uses: actions/download-artifact@v4
with:
name: build-win64-intermediate
path: release/work/build-win64/dist-tar/
# upload-artifact does not preserve permissions
- name: Detar
run: |
cd release/work/build-win64
tar xf dist-tar/dist.tar.gz
- name: Package
run: release/package_client.sh win64 zip
- name: Upload release
uses: actions/upload-artifact@v4
with:
name: release-win64
path: release/output
package-macos-aarch64:
needs:
- build-scrcpy-server
- build-macos-aarch64
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download scrcpy-server
uses: actions/download-artifact@v4
with:
name: scrcpy-server
path: release/work/build-server/server/
- name: Download build-macos-aarch64
uses: actions/download-artifact@v4
with:
name: build-macos-aarch64-intermediate
path: release/work/build-macos-aarch64/dist-tar/
# upload-artifact does not preserve permissions
- name: Detar
run: |
cd release/work/build-macos-aarch64
tar xf dist-tar/dist.tar.gz
- name: Package
run: release/package_client.sh macos-aarch64 tar.gz
- name: Upload release
uses: actions/upload-artifact@v4
with:
name: release-macos-aarch64
path: release/output/
package-macos-x86_64:
needs:
- build-scrcpy-server
- build-macos-x86_64
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download scrcpy-server
uses: actions/download-artifact@v4
with:
name: scrcpy-server
path: release/work/build-server/server/
- name: Download build-macos
uses: actions/download-artifact@v4
with:
name: build-macos-x86_64-intermediate
path: release/work/build-macos-x86_64/dist-tar/
# upload-artifact does not preserve permissions
- name: Detar
run: |
cd release/work/build-macos-x86_64
tar xf dist-tar/dist.tar.gz
- name: Package
run: release/package_client.sh macos-x86_64 tar.gz
- name: Upload release
uses: actions/upload-artifact@v4
with:
name: release-macos-x86_64
path: release/output/
release:
needs:
- build-scrcpy-server
- package-linux-x86_64
- package-win32
- package-win64
- package-macos-aarch64
- package-macos-x86_64
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download scrcpy-server
uses: actions/download-artifact@v4
with:
name: scrcpy-server
path: release/work/build-server/server/
- name: Download release-linux-x86_64
uses: actions/download-artifact@v4
with:
name: release-linux-x86_64
path: release/output/
- name: Download release-win32
uses: actions/download-artifact@v4
with:
name: release-win32
path: release/output/
- name: Download release-win64
uses: actions/download-artifact@v4
with:
name: release-win64
path: release/output/
- name: Download release-macos-aarch64
uses: actions/download-artifact@v4
with:
name: release-macos-aarch64
path: release/output/
- name: Download release-macos-x86_64
uses: actions/download-artifact@v4
with:
name: release-macos-x86_64
path: release/output/
- name: Package server
run: release/package_server.sh
- name: Generate checksums
run: release/generate_checksums.sh
- name: Upload release artifact
uses: actions/upload-artifact@v4
with:
name: scrcpy-release-${{ env.VERSION }}
path: release/output

View File

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

View File

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

View File

@ -9,7 +9,6 @@ local arguments
arguments=(
'--always-on-top[Make scrcpy window always on top \(above other windows\)]'
'--angle=[Rotate the video content by a custom angle, in degrees]'
'--audio-bit-rate=[Encode the audio at the given bit-rate]'
'--audio-buffer=[Configure the audio buffering delay (in milliseconds)]'
'--audio-codec=[Select the audio codec]:codec:(opus aac flac raw)'
@ -25,10 +24,10 @@ arguments=(
'--camera-facing=[Select the device camera by its facing direction]:facing:(front back external)'
'--camera-fps=[Specify the camera capture frame rate]'
'--camera-size=[Specify an explicit camera capture size]'
'--capture-orientation=[Set the capture video orientation]:orientation:(0 90 180 270 flip0 flip90 flip180 flip270 @0 @90 @180 @270 @flip0 @flip90 @flip180 @flip270)'
'--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]'
{-d,--select-usb}'[Use USB device]'
'--disable-screensaver[Disable screensaver while scrcpy is running]'
'--display-buffer=[Add a buffering delay \(in milliseconds\) before displaying]'
'--display-id=[Specify the display id to mirror]'
'--display-orientation=[Set the initial display orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)'
{-e,--select-tcpip}'[Use TCP/IP device]'
@ -41,11 +40,11 @@ arguments=(
'--keyboard=[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)'
'--kill-adb-on-close[Kill adb when scrcpy terminates]'
'--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]'
'--list-apps[List Android apps installed on the device]'
'--list-camera-sizes[List the valid camera capture sizes]'
'--list-cameras[List cameras available on the device]'
'--list-displays[List displays available on the device]'
'--list-encoders[List video and audio encoders available on the device]'
'--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 90 180 270)'
{-m,--max-size=}'[Limit both the width and height of the video to value]'
'-M[Use UHID/AOA mouse (same as --mouse=uhid or --mouse=aoa, depending on OTG mode)]'
'--max-fps=[Limit the frame rate of screen capture]'
@ -53,7 +52,6 @@ arguments=(
'--mouse-bind=[Configure bindings of secondary clicks]'
{-n,--no-control}'[Disable device control \(mirror the device in read only\)]'
{-N,--no-playback}'[Disable video and audio playback]'
'--new-display=[Create a new display]'
'--no-audio[Disable audio forwarding]'
'--no-audio-playback[Disable audio playback]'
'--no-cleanup[Disable device cleanup actions on exit]'
@ -63,7 +61,6 @@ arguments=(
'--no-mipmaps[Disable the generation of mipmaps]'
'--no-mouse-hover[Do not forward mouse hover events]'
'--no-power-on[Do not power on the device on start]'
'--no-vd-system-decorations[Disable virtual display system decorations flag]'
'--no-video[Disable video forwarding]'
'--no-video-playback[Disable video playback]'
'--orientation=[Set the video orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)'
@ -82,9 +79,7 @@ arguments=(
'--require-audio=[Make scrcpy fail if audio is enabled but does not work]'
{-s,--serial=}'[The device serial number \(mandatory for multiple devices only\)]:serial:($("${ADB-adb}" devices | awk '\''$2 == "device" {print $1}'\''))'
{-S,--turn-screen-off}'[Turn the device screen off immediately]'
'--screen-off-timeout=[Set the screen off timeout in seconds]'
'--shortcut-mod=[\[key1,key2+key3,...\] Specify the modifiers to use for scrcpy shortcuts]:shortcut mod:(lctrl rctrl lalt ralt lsuper rsuper)'
'--start-app=[Start an Android app]'
{-t,--show-touches}'[Show physical touches]'
'--tcpip[\(optional \[ip\:port\]\) Configure and connect the device over TCP/IP]'
'--time-limit=[Set the maximum mirroring time, in seconds]'
@ -94,7 +89,6 @@ arguments=(
'--v4l2-sink=[\[\/dev\/videoN\] Output to v4l2loopback device]'
{-v,--version}'[Print the version of scrcpy]'
{-V,--verbosity=}'[Set the log level]:verbosity:(verbose debug info warn error)'
'--video-buffer=[Add a buffering delay \(in milliseconds\) before displaying video frames]'
'--video-codec=[Select the video codec]:codec:(h264 h265 av1)'
'--video-codec-options=[Set a list of comma-separated key\:type=value options for the device video encoder]'
'--video-encoder=[Use a specific MediaCodec video encoder]'

View File

@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
VERSION=35.0.2
FILENAME=platform-tools_r$VERSION-win.zip
PROJECT_DIR=platform-tools-$VERSION-windows
SHA256SUM=2975a3eac0b19182748d64195375ad056986561d994fffbdc64332a516300bb9
VERSION=35.0.0
FILENAME=platform-tools_r$VERSION-windows.zip
PROJECT_DIR=platform-tools-$VERSION
SHA256SUM=7ab78a8f8b305ae4d0de647d99c43599744de61a0838d3a47bda0cdffefee87e
cd "$SOURCES_DIR"
@ -27,6 +27,6 @@ else
rmdir "$ZIP_PREFIX"
fi
mkdir -p "$INSTALL_DIR/adb-windows"
cd "$INSTALL_DIR/adb-windows"
cp -r "$SOURCES_DIR/$PROJECT_DIR"/. "$INSTALL_DIR/adb-windows/"
mkdir -p "$INSTALL_DIR/$HOST/bin"
cd "$INSTALL_DIR/$HOST/bin"
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
# This file is intended to be sourced by other scripts, not executed
process_args() {
if [[ $# != 3 ]]
then
# <host>: win32 or win64
# <build_type>: native or cross
# <link_type>: static or shared
echo "Syntax: $0 <host> <build_type> <link_type>" >&2
exit 1
fi
if [[ $# != 1 ]]
then
# <host>: win32 or win64
echo "Syntax: $0 <host>" >&2
exit 1
fi
HOST="$1"
BUILD_TYPE="$2" # native or cross
LINK_TYPE="$3" # static or shared
DIRNAME="$HOST-$BUILD_TYPE-$LINK_TYPE"
HOST="$1"
if [[ "$BUILD_TYPE" != native && "$BUILD_TYPE" != cross ]]
then
echo "Unsupported build type (expected native or cross): $BUILD_TYPE" >&2
exit 1
fi
if [[ "$LINK_TYPE" != static && "$LINK_TYPE" != shared ]]
then
echo "Unsupported link type (expected static or shared): $LINK_TYPE" >&2
exit 1
fi
if [[ "$BUILD_TYPE" == cross ]]
then
if [[ "$HOST" = win32 ]]
then
HOST_TRIPLET=i686-w64-mingw32
elif [[ "$HOST" = win64 ]]
then
HOST_TRIPLET=x86_64-w64-mingw32
else
echo "Unsupported cross-build to host: $HOST" >&2
exit 1
fi
fi
}
if [[ "$HOST" = win32 ]]
then
HOST_TRIPLET=i686-w64-mingw32
elif [[ "$HOST" = win64 ]]
then
HOST_TRIPLET=x86_64-w64-mingw32
else
echo "Unsupported host: $HOST" >&2
exit 1
fi
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
@ -59,7 +37,7 @@ checksum() {
local file="$1"
local sum="$2"
echo "$file: verifying checksum..."
echo "$sum $file" | shasum -a256 -c
echo "$sum $file" | sha256sum -c
}
get_file() {

View File

@ -3,12 +3,11 @@ set -ex
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
process_args "$@"
VERSION=7.1
VERSION=7.0.2
FILENAME=ffmpeg-$VERSION.tar.xz
PROJECT_DIR=ffmpeg-$VERSION
SHA256SUM=40973D44970DBC83EF302B0609F2E74982BE2D85916DD2EE7472D30678A7ABE6
SHA256SUM=8646515b638a3ad303e23af6a3587734447cb8fc0a0c064ecdb8e95c4fd8b389
cd "$SOURCES_DIR"
@ -23,121 +22,68 @@ fi
mkdir -p "$BUILD_DIR/$PROJECT_DIR"
cd "$BUILD_DIR/$PROJECT_DIR"
if [[ -d "$DIRNAME" ]]
if [[ "$HOST" = win32 ]]
then
echo "'$PWD/$DIRNAME' already exists, not reconfigured"
cd "$DIRNAME"
ARCH=x86
elif [[ "$HOST" = win64 ]]
then
ARCH=x86_64
else
mkdir "$DIRNAME"
cd "$DIRNAME"
echo "Unsupported host: $HOST" >&2
exit 1
fi
if [[ "$HOST" == win* ]]
then
# -static-libgcc to avoid missing libgcc_s_dw2-1.dll
# -static to avoid dynamic dependency to zlib
export CFLAGS='-static-libgcc -static'
export CXXFLAGS="$CFLAGS"
export LDFLAGS='-static-libgcc -static'
elif [[ "$HOST" == "macos" ]]
then
export LDFLAGS="$LDFLAGS -L/opt/homebrew/opt/zlib/lib"
export CPPFLAGS="$CPPFLAGS -I/opt/homebrew/opt/zlib/include"
# -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'
export LDFLAGS="$LDFLAGS-L/opt/homebrew/opt/libiconv/lib"
export CPPFLAGS="$CPPFLAGS -I/opt/homebrew/opt/libiconv/include"
export PKG_CONFIG_PATH="/opt/homebrew/opt/zlib/lib/pkgconfig"
fi
if [[ -d "$HOST" ]]
then
echo "'$PWD/$HOST' already exists, not reconfigured"
cd "$HOST"
else
mkdir "$HOST"
cd "$HOST"
conf=(
--prefix="$INSTALL_DIR/$DIRNAME"
--extra-cflags="-O2 -fPIC"
--disable-programs
--disable-doc
--disable-swscale
--disable-postproc
--disable-avfilter
--disable-network
--disable-everything
"$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
--disable-vaapi
--disable-vdpau
--enable-swresample
--enable-decoder=h264
--enable-decoder=hevc
--enable-decoder=av1
--enable-decoder=pcm_s16le
--enable-decoder=opus
--enable-decoder=aac
--enable-decoder=flac
--enable-decoder=png
--enable-protocol=file
--enable-demuxer=image2
--enable-parser=png
--enable-zlib
--enable-muxer=matroska
--enable-muxer=mp4
--enable-muxer=opus
--enable-muxer=flac
--enable-muxer=wav
)
if [[ "$HOST" == linux ]]
then
conf+=(
--enable-libv4l2
--enable-outdev=v4l2
--enable-encoder=rawvideo
)
else
# libavdevice is only used for V4L2 on Linux
conf+=(
--disable-avdevice
)
fi
if [[ "$LINK_TYPE" == static ]]
then
conf+=(
--enable-static
--disable-shared
)
else
conf+=(
--disable-static
--enable-shared
)
fi
if [[ "$BUILD_TYPE" == cross ]]
then
conf+=(
--enable-cross-compile
--cross-prefix="${HOST_TRIPLET}-"
--cc="${HOST_TRIPLET}-gcc"
)
case "$HOST" in
win32)
conf+=(
--target-os=mingw32
--arch=x86
)
;;
win64)
conf+=(
--target-os=mingw32
--arch=x86_64
)
;;
*)
echo "Unsupported host: $HOST" >&2
exit 1
esac
fi
"$SOURCES_DIR/$PROJECT_DIR"/configure "${conf[@]}"
fi
make -j

View File

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

View File

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

View File

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

View File

@ -13,7 +13,7 @@ BEGIN
VALUE "LegalCopyright", "Romain Vimont, Genymobile"
VALUE "OriginalFilename", "scrcpy.exe"
VALUE "ProductName", "scrcpy"
VALUE "ProductVersion", "3.0"
VALUE "ProductVersion", "2.7"
END
END
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
Make scrcpy window always on top (above other windows).
.TP
.BI "\-\-angle " degrees
Rotate the video content by a custom angle, in degrees (clockwise).
.TP
.BI "\-\-audio\-bit\-rate " value
Encode the audio at the given bit rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000).
@ -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").
.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
.B \-\-camera\-high\-speed
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.
.TP
.BI "\-\-camera\-size " width\fRx\fIheight
Specify an explicit camera capture size.
.BI "\-\-camera\-facing " facing
Select the device camera by its facing direction.
Possible values are "front", "back" and "external".
.TP
.BI "\-\-capture\-orientation " value
Possible values are 0, 90, 180, 270, flip0, flip90, flip180 and flip270, possibly prefixed by '@'.
.BI "\-\-camera\-fps " fps
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.
If '@' is passed alone, then the rotation is locked to the initial device orientation.
Default is 0.
.TP
.BI "\-\-camera\-size " width\fRx\fIheight
Specify an explicit camera capture size.
.TP
.BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy
Crop the device screen on the server.
The values are expressed in the device natural orientation (typically, portrait for a phone, landscape for a tablet).
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
.B \-d, \-\-select\-usb
@ -153,6 +139,12 @@ Also see \fB\-e\fR (\fB\-\-select\-tcpip\fR).
.BI "\-\-disable\-screensaver"
Disable screensaver while scrcpy is running.
.TP
.BI "\-\-display\-buffer " ms
Add a buffering delay (in milliseconds) before displaying. This increases latency to compensate for jitter.
Default is 0 (no buffering).
.TP
.BI "\-\-display\-id " id
Specify the device display id to mirror.
@ -235,10 +227,6 @@ Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+S
This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically.
.TP
.B \-\-list\-apps
List Android apps installed on the device.
.TP
.B \-\-list\-camera\-sizes
List the valid camera capture sizes.
@ -255,6 +243,16 @@ List video and audio encoders available on the device.
.B \-\-list\-displays
List displays available on the device.
.TP
\fB\-\-lock\-video\-orientation\fR[=\fIvalue\fR]
Lock capture video orientation to \fIvalue\fR.
Possible values are "unlocked", "initial" (locked to the initial orientation), 0, 90, 180, and 270. The values represent the clockwise rotation from the natural device orientation, in degrees.
Default is "unlocked".
Passing the option without argument is equivalent to passing "initial".
.TP
.BI "\-m, \-\-max\-size " value
Limit both the width and height of the video to \fIvalue\fR. The other dimension is computed so that the device aspect\-ratio is preserved.
@ -316,17 +314,6 @@ Disable device control (mirror the device in read\-only).
.B \-N, \-\-no\-playback
Disable video and audio playback on the computer (equivalent to \fB\-\-no\-video\-playback \-\-no\-audio\-playback\fR).
.TP
\fB\-\-new\-display\fR[=[\fIwidth\fRx\fIheight\fR][/\fIdpi\fR]]
Create a new display with the specified resolution and density. If not provided, they default to the main display dimensions and DPI.
Examples:
\-\-new\-display=1920x1080
\-\-new\-display=1920x1080/420
\-\-new\-display # main display size and density
\-\-new\-display=/240 # main display size and 240 dpi
.TP
.B \-\-no\-audio
Disable audio forwarding.
@ -369,10 +356,6 @@ Do not forward mouse hover (mouse motion without any clicks) events.
.B \-\-no\-power\-on
Do not power on the device on start.
.TP
.B \-\-no\-vd\-system\-decorations
Disable virtual display system decorations flag.
.TP
.B \-\-no\-video
Disable video forwarding.
@ -495,22 +478,6 @@ For example, to use either LCtrl or LSuper for scrcpy shortcuts, pass "lctrl,lsu
Default is "lalt,lsuper" (left-Alt or left-Super).
.TP
.BI "\-\-start\-app " name
Start an Android app, by its exact package name.
Add a '?' prefix to select an app whose name starts with the given name, case-insensitive (retrieving app names on the device may take some time):
scrcpy --start-app=?firefox
Add a '+' prefix to force-stop before starting the app:
scrcpy --new-display --start-app=+org.mozilla.firefox
Both prefixes can be used, in that order:
scrcpy --start-app=+?firefox
.TP
.B \-t, \-\-show\-touches
Enable "show touches" on start, restore the initial value on exit.
@ -555,19 +522,13 @@ Default is "info" for release builds, "debug" for debug builds.
.BI "\-\-v4l2-sink " /dev/videoN
Output to v4l2loopback device.
It requires to lock the video orientation (see \fB\-\-lock\-video\-orientation\fR).
.TP
.BI "\-\-v4l2-buffer " ms
Add a buffering delay (in milliseconds) before pushing frames. This increases latency to compensate for jitter.
This option is similar to \fB\-\-video\-buffer\fR, but specific to V4L2 sink.
Default is 0 (no buffering).
.TP
.BI "\-\-video\-buffer " ms
Add a buffering delay (in milliseconds) before displaying video frames.
This increases latency to compensate for jitter.
This option is similar to \fB\-\-display\-buffer\fR, but specific to V4L2 sink.
Default is 0 (no buffering).
@ -676,10 +637,6 @@ Pause or re-pause display
.B MOD+Shift+z
Unpause display
.TP
.B MOD+Shift+r
Reset video capture/encoding
.TP
.B MOD+g
Resize window to 1:1 (pixel\-perfect)
@ -770,11 +727,7 @@ Pinch-to-zoom and rotate from the center of the screen
.TP
.B Shift+click-and-move
Tilt vertically (slide with 2 fingers)
.TP
.B Ctrl+Shift+click-and-move
Tilt horizontally (slide with 2 fingers)
Tilt (slide vertically with two fingers)
.TP
.B Drag & drop APK file

View File

@ -7,7 +7,6 @@
#include "adb_device.h"
#include "adb_parser.h"
#include "util/env.h"
#include "util/file.h"
#include "util/log.h"
#include "util/process_intr.h"
@ -25,45 +24,15 @@
*/
#define SC_ADB_COMMAND(...) { sc_adb_get_executable(), __VA_ARGS__, NULL }
static char *adb_executable;
bool
sc_adb_init(void) {
adb_executable = sc_get_env("ADB");
if (adb_executable) {
LOGD("Using adb: %s", adb_executable);
return true;
}
#if !defined(PORTABLE) || defined(_WIN32)
adb_executable = strdup("adb");
if (!adb_executable) {
LOG_OOM();
return false;
}
#else
// For portable builds, use the absolute path to the adb executable
// in the same directory as scrcpy (except on Windows, where "adb"
// is sufficient)
adb_executable = sc_file_get_local_path("adb");
if (!adb_executable) {
// Error already logged
return false;
}
LOGD("Using adb (portable): %s", adb_executable);
#endif
return true;
}
void
sc_adb_destroy(void) {
free(adb_executable);
}
static const char *adb_executable;
const char *
sc_adb_get_executable(void) {
if (!adb_executable) {
adb_executable = getenv("ADB");
if (!adb_executable)
adb_executable = "adb";
}
return adb_executable;
}
@ -770,21 +739,3 @@ sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags) {
return sc_adb_parse_device_ip(buf);
}
uint16_t
sc_adb_get_device_sdk_version(struct sc_intr *intr, const char *serial) {
char *sdk_version =
sc_adb_getprop(intr, serial, "ro.build.version.sdk", SC_ADB_SILENT);
if (!sdk_version) {
return 0;
}
long value;
bool ok = sc_str_parse_integer(sdk_version, &value);
free(sdk_version);
if (!ok || value < 0 || value > 0xFFFF) {
return 0;
}
return value;
}

View File

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

View File

@ -1,23 +1,138 @@
#include "audio_player.h"
#include <libavcodec/avcodec.h>
#include <libavutil/opt.h>
#include "util/log.h"
//#define SC_AUDIO_PLAYER_DEBUG // uncomment to debug
/**
* Real-time audio player with configurable latency
*
* As input, the player regularly receives AVFrames of decoded audio samples.
* As output, an SDL callback regularly requests audio samples to be played.
* In the middle, an audio buffer stores the samples produced but not consumed
* yet.
*
* The goal of the player is to feed the audio output with a latency as low as
* possible while avoiding buffer underrun (i.e. not being able to provide
* samples when requested).
*
* The player aims to feed the audio output with as little latency as possible
* while avoiding buffer underrun. To achieve this, it attempts to maintain the
* average buffering (the number of samples present in the buffer) around a
* target value. If this target buffering is too low, then buffer underrun will
* occur frequently. If it is too high, then latency will become unacceptable.
* This target value is configured using the scrcpy option --audio-buffer.
*
* The player cannot adjust the sample input rate (it receives samples produced
* in real-time) or the sample output rate (it must provide samples as
* requested by the audio output callback). Therefore, it may only apply
* compensation by resampling (converting _m_ input samples to _n_ output
* samples).
*
* The compensation itself is applied by libswresample (FFmpeg). It is
* configured using swr_set_compensation(). An important work for the player
* is to estimate the compensation value regularly and apply it.
*
* The estimated buffering level is the result of averaging the "natural"
* buffering (samples are produced and consumed by blocks, so it must be
* smoothed), and making instant adjustments resulting of its own actions
* (explicit compensation and silence insertion on underflow), which are not
* smoothed.
*
* Buffer underflow events can occur when packets arrive too late. In that case,
* the player inserts silence. Once the packets finally arrive (late), one
* strategy could be to drop the samples that were replaced by silence, in
* order to keep a minimal latency. However, dropping samples in case of buffer
* underflow is inadvisable, as it would temporarily increase the underflow
* even more and cause very noticeable audio glitches.
*
* Therefore, the player doesn't drop any sample on underflow. The compensation
* mechanism will absorb the delay introduced by the inserted silence.
*/
/** Downcast frame_sink to sc_audio_player */
#define DOWNCAST(SINK) container_of(SINK, struct sc_audio_player, frame_sink)
#define SC_AV_SAMPLE_FMT AV_SAMPLE_FMT_FLT
#define SC_SDL_SAMPLE_FMT AUDIO_F32
#define TO_BYTES(SAMPLES) sc_audiobuf_to_bytes(&ap->buf, (SAMPLES))
#define TO_SAMPLES(BYTES) sc_audiobuf_to_samples(&ap->buf, (BYTES))
static void SDLCALL
sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) {
struct sc_audio_player *ap = userdata;
// This callback is called with the lock used by SDL_LockAudioDevice()
assert(len_int > 0);
size_t len = len_int;
uint32_t count = TO_SAMPLES(len);
assert(len % ap->audioreg.sample_size == 0);
uint32_t out_samples = len / ap->audioreg.sample_size;
#ifdef SC_AUDIO_PLAYER_DEBUG
LOGD("[Audio] SDL callback requests %" PRIu32 " samples", count);
#endif
sc_audio_regulator_pull(&ap->audioreg, stream, out_samples);
bool played = atomic_load_explicit(&ap->played, memory_order_relaxed);
if (!played) {
uint32_t buffered_samples = sc_audiobuf_can_read(&ap->buf);
// Wait until the buffer is filled up to at least target_buffering
// before playing
if (buffered_samples < ap->target_buffering) {
LOGV("[Audio] Inserting initial buffering silence: %" PRIu32
" samples", count);
// Delay playback starting to reach the target buffering. Fill the
// whole buffer with silence (len is small compared to the
// arbitrary margin value).
memset(stream, 0, len);
return;
}
}
uint32_t read = sc_audiobuf_read(&ap->buf, stream, count);
if (read < count) {
uint32_t silence = count - read;
// Insert silence. In theory, the inserted silent samples replace the
// missing real samples, which will arrive later, so they should be
// dropped to keep the latency minimal. However, this would cause very
// audible glitches, so let the clock compensation restore the target
// latency.
LOGD("[Audio] Buffer underflow, inserting silence: %" PRIu32 " samples",
silence);
memset(stream + TO_BYTES(read), 0, TO_BYTES(silence));
bool received = atomic_load_explicit(&ap->received,
memory_order_relaxed);
if (received) {
// Inserting additional samples immediately increases buffering
atomic_fetch_add_explicit(&ap->underflow, silence,
memory_order_relaxed);
}
}
atomic_store_explicit(&ap->played, true, memory_order_relaxed);
}
static uint8_t *
sc_audio_player_get_swr_buf(struct sc_audio_player *ap, uint32_t min_samples) {
size_t min_buf_size = TO_BYTES(min_samples);
if (min_buf_size > ap->swr_buf_alloc_size) {
size_t new_size = min_buf_size + 4096;
uint8_t *buf = realloc(ap->swr_buf, new_size);
if (!buf) {
LOG_OOM();
// Could not realloc to the requested size
return NULL;
}
ap->swr_buf = buf;
ap->swr_buf_alloc_size = new_size;
}
return ap->swr_buf;
}
static bool
@ -25,21 +140,209 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
const AVFrame *frame) {
struct sc_audio_player *ap = DOWNCAST(sink);
return sc_audio_regulator_push(&ap->audioreg, frame);
SwrContext *swr_ctx = ap->swr_ctx;
int64_t swr_delay = swr_get_delay(swr_ctx, ap->sample_rate);
// No need to av_rescale_rnd(), input and output sample rates are the same.
// Add more space (256) for clock compensation.
int dst_nb_samples = swr_delay + frame->nb_samples + 256;
uint8_t *swr_buf = sc_audio_player_get_swr_buf(ap, dst_nb_samples);
if (!swr_buf) {
return false;
}
int ret = swr_convert(swr_ctx, &swr_buf, dst_nb_samples,
(const uint8_t **) frame->data, frame->nb_samples);
if (ret < 0) {
LOGE("Resampling failed: %d", ret);
return false;
}
// swr_convert() returns the number of samples which would have been
// written if the buffer was big enough.
uint32_t samples = MIN(ret, dst_nb_samples);
#ifdef SC_AUDIO_PLAYER_DEBUG
LOGD("[Audio] %" PRIu32 " samples written to buffer", samples);
#endif
uint32_t cap = sc_audiobuf_capacity(&ap->buf);
if (samples > cap) {
// Very very unlikely: a single resampled frame should never
// exceed the audio buffer size (or something is very wrong).
// Ignore the first bytes in swr_buf to avoid memory corruption anyway.
swr_buf += TO_BYTES(samples - cap);
samples = cap;
}
uint32_t skipped_samples = 0;
uint32_t written = sc_audiobuf_write(&ap->buf, swr_buf, samples);
if (written < samples) {
uint32_t remaining = samples - written;
// All samples that could be written without locking have been written,
// now we need to lock to drop/consume old samples
SDL_LockAudioDevice(ap->device);
// Retry with the lock
written += sc_audiobuf_write(&ap->buf,
swr_buf + TO_BYTES(written),
remaining);
if (written < samples) {
remaining = samples - written;
// Still insufficient, drop old samples to make space
skipped_samples = sc_audiobuf_read(&ap->buf, NULL, remaining);
assert(skipped_samples == remaining);
}
SDL_UnlockAudioDevice(ap->device);
if (written < samples) {
// Now there is enough space
uint32_t w = sc_audiobuf_write(&ap->buf,
swr_buf + TO_BYTES(written),
remaining);
assert(w == remaining);
(void) w;
}
}
uint32_t underflow = 0;
uint32_t max_buffered_samples;
bool played = atomic_load_explicit(&ap->played, memory_order_relaxed);
if (played) {
underflow = atomic_exchange_explicit(&ap->underflow, 0,
memory_order_relaxed);
max_buffered_samples = ap->target_buffering
+ 12 * ap->output_buffer
+ ap->target_buffering / 10;
} else {
// SDL playback not started yet, do not accumulate more than
// max_initial_buffering samples, this would cause unnecessary delay
// (and glitches to compensate) on start.
max_buffered_samples = ap->target_buffering + 2 * ap->output_buffer;
}
uint32_t can_read = sc_audiobuf_can_read(&ap->buf);
if (can_read > max_buffered_samples) {
uint32_t skip_samples = 0;
SDL_LockAudioDevice(ap->device);
can_read = sc_audiobuf_can_read(&ap->buf);
if (can_read > max_buffered_samples) {
skip_samples = can_read - max_buffered_samples;
uint32_t r = sc_audiobuf_read(&ap->buf, NULL, skip_samples);
assert(r == skip_samples);
(void) r;
skipped_samples += skip_samples;
}
SDL_UnlockAudioDevice(ap->device);
if (skip_samples) {
if (played) {
LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32
" samples", skip_samples);
#ifdef SC_AUDIO_PLAYER_DEBUG
} else {
LOGD("[Audio] Playback not started, skipping %" PRIu32
" samples", skip_samples);
#endif
}
}
}
atomic_store_explicit(&ap->received, true, memory_order_relaxed);
if (!played) {
// Nothing more to do
return true;
}
// Number of samples added (or removed, if negative) for compensation
int32_t instant_compensation = (int32_t) written - frame->nb_samples;
// Inserting silence instantly increases buffering
int32_t inserted_silence = (int32_t) underflow;
// Dropping input samples instantly decreases buffering
int32_t dropped = (int32_t) skipped_samples;
// The compensation must apply instantly, it must not be smoothed
ap->avg_buffering.avg += instant_compensation + inserted_silence - dropped;
if (ap->avg_buffering.avg < 0) {
// Since dropping samples instantly reduces buffering, the difference
// is applied immediately to the average value, assuming that the delay
// between the producer and the consumer will be caught up.
//
// However, when this assumption is not valid, the average buffering
// may decrease indefinitely. Prevent it to become negative to limit
// the consequences.
ap->avg_buffering.avg = 0;
}
// However, the buffering level must be smoothed
sc_average_push(&ap->avg_buffering, can_read);
#ifdef SC_AUDIO_PLAYER_DEBUG
LOGD("[Audio] can_read=%" PRIu32 " avg_buffering=%f",
can_read, sc_average_get(&ap->avg_buffering));
#endif
ap->samples_since_resync += written;
if (ap->samples_since_resync >= ap->sample_rate) {
// Recompute compensation every second
ap->samples_since_resync = 0;
float avg = sc_average_get(&ap->avg_buffering);
int diff = ap->target_buffering - avg;
// Enable compensation when the difference exceeds +/- 4ms.
// Disable compensation when the difference is lower than +/- 1ms.
int threshold = ap->compensation != 0
? ap->sample_rate / 1000 /* 1ms */
: ap->sample_rate * 4 / 1000; /* 4ms */
if (abs(diff) < threshold) {
// Do not compensate for small values, the error is just noise
diff = 0;
} else if (diff < 0 && can_read < ap->target_buffering) {
// Do not accelerate if the instant buffering level is below the
// target, this would increase underflow
diff = 0;
}
// Compensate the diff over 4 seconds (but will be recomputed after 1
// second)
int distance = 4 * ap->sample_rate;
// Limit compensation rate to 2%
int abs_max_diff = distance / 50;
diff = CLAMP(diff, -abs_max_diff, abs_max_diff);
LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32
" compensation=%d", ap->target_buffering, avg, can_read, diff);
if (diff != ap->compensation) {
int ret = swr_set_compensation(swr_ctx, diff, distance);
if (ret < 0) {
LOGW("Resampling compensation failed: %d", ret);
// not fatal
} else {
ap->compensation = diff;
}
}
}
return true;
}
static bool
sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
const AVCodecContext *ctx) {
struct sc_audio_player *ap = DOWNCAST(sink);
#ifdef SCRCPY_LAVU_HAS_CHLAYOUT
assert(ctx->ch_layout.nb_channels > 0 && ctx->ch_layout.nb_channels < 256);
uint8_t nb_channels = ctx->ch_layout.nb_channels;
assert(ctx->ch_layout.nb_channels > 0);
unsigned nb_channels = ctx->ch_layout.nb_channels;
#else
int tmp = av_get_channel_layout_nb_channels(ctx->channel_layout);
assert(tmp > 0 && tmp < 256);
uint8_t nb_channels = tmp;
assert(tmp > 0);
unsigned nb_channels = tmp;
#endif
assert(ctx->sample_rate > 0);
@ -47,19 +350,17 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
int out_bytes_per_sample = av_get_bytes_per_sample(SC_AV_SAMPLE_FMT);
assert(out_bytes_per_sample > 0);
uint32_t target_buffering_samples =
ap->target_buffering_delay * ctx->sample_rate / SC_TICK_FREQ;
ap->sample_rate = ctx->sample_rate;
ap->nb_channels = nb_channels;
ap->out_bytes_per_sample = out_bytes_per_sample;
size_t sample_size = nb_channels * out_bytes_per_sample;
bool ok = sc_audio_regulator_init(&ap->audioreg, sample_size, ctx,
target_buffering_samples);
if (!ok) {
return false;
}
ap->target_buffering = ap->target_buffering_delay * ap->sample_rate
/ SC_TICK_FREQ;
uint64_t aout_samples = ap->output_buffer_duration * ctx->sample_rate
uint64_t aout_samples = ap->output_buffer_duration * ap->sample_rate
/ SC_TICK_FREQ;
assert(aout_samples <= 0xFFFF);
ap->output_buffer = (uint16_t) aout_samples;
SDL_AudioSpec desired = {
.freq = ctx->sample_rate,
@ -74,10 +375,69 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
ap->device = SDL_OpenAudioDevice(NULL, 0, &desired, &obtained, 0);
if (!ap->device) {
LOGE("Could not open audio device: %s", SDL_GetError());
sc_audio_regulator_destroy(&ap->audioreg);
return false;
}
SwrContext *swr_ctx = swr_alloc();
if (!swr_ctx) {
LOG_OOM();
goto error_close_audio_device;
}
ap->swr_ctx = swr_ctx;
#ifdef SCRCPY_LAVU_HAS_CHLAYOUT
av_opt_set_chlayout(swr_ctx, "in_chlayout", &ctx->ch_layout, 0);
av_opt_set_chlayout(swr_ctx, "out_chlayout", &ctx->ch_layout, 0);
#else
av_opt_set_channel_layout(swr_ctx, "in_channel_layout",
ctx->channel_layout, 0);
av_opt_set_channel_layout(swr_ctx, "out_channel_layout",
ctx->channel_layout, 0);
#endif
av_opt_set_int(swr_ctx, "in_sample_rate", ctx->sample_rate, 0);
av_opt_set_int(swr_ctx, "out_sample_rate", ctx->sample_rate, 0);
av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt", ctx->sample_fmt, 0);
av_opt_set_sample_fmt(swr_ctx, "out_sample_fmt", SC_AV_SAMPLE_FMT, 0);
int ret = swr_init(swr_ctx);
if (ret) {
LOGE("Failed to initialize the resampling context");
goto error_free_swr_ctx;
}
// Use a ring-buffer of the target buffering size plus 1 second between the
// producer and the consumer. It's too big on purpose, to guarantee that
// the producer and the consumer will be able to access it in parallel
// without locking.
uint32_t audiobuf_samples = ap->target_buffering + ap->sample_rate;
size_t sample_size = ap->nb_channels * ap->out_bytes_per_sample;
bool ok = sc_audiobuf_init(&ap->buf, sample_size, audiobuf_samples);
if (!ok) {
goto error_free_swr_ctx;
}
size_t initial_swr_buf_size = TO_BYTES(4096);
ap->swr_buf = malloc(initial_swr_buf_size);
if (!ap->swr_buf) {
LOG_OOM();
goto error_destroy_audiobuf;
}
ap->swr_buf_alloc_size = initial_swr_buf_size;
// Samples are produced and consumed by blocks, so the buffering must be
// smoothed to get a relatively stable value.
sc_average_init(&ap->avg_buffering, 128);
ap->samples_since_resync = 0;
ap->received = false;
atomic_init(&ap->played, false);
atomic_init(&ap->received, false);
atomic_init(&ap->underflow, 0);
ap->compensation = 0;
// The thread calling open() is the thread calling push(), which fills the
// audio buffer consumed by the SDL audio thread.
ok = sc_thread_set_priority(SC_THREAD_PRIORITY_TIME_CRITICAL);
@ -89,6 +449,15 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
SDL_PauseAudioDevice(ap->device, 0);
return true;
error_destroy_audiobuf:
sc_audiobuf_destroy(&ap->buf);
error_free_swr_ctx:
swr_free(&ap->swr_ctx);
error_close_audio_device:
SDL_CloseAudioDevice(ap->device);
return false;
}
static void
@ -99,7 +468,9 @@ sc_audio_player_frame_sink_close(struct sc_frame_sink *sink) {
SDL_PauseAudioDevice(ap->device, 1);
SDL_CloseAudioDevice(ap->device);
sc_audio_regulator_destroy(&ap->audioreg);
free(ap->swr_buf);
sc_audiobuf_destroy(&ap->buf);
swr_free(&ap->swr_ctx);
}
void

View File

@ -5,27 +5,76 @@
#include <stdatomic.h>
#include <stdbool.h>
#include <libavformat/avformat.h>
#include <libswresample/swresample.h>
#include <SDL2/SDL.h>
#include "audio_regulator.h"
#include "trait/frame_sink.h"
#include "util/audiobuf.h"
#include "util/average.h"
#include "util/thread.h"
#include "util/tick.h"
struct sc_audio_player {
struct sc_frame_sink frame_sink;
SDL_AudioDeviceID device;
// The target buffering between the producer and the consumer. This value
// is directly use for compensation.
// Since audio capture and/or encoding on the device typically produce
// blocks of 960 samples (20ms) or 1024 samples (~21.3ms), this target
// value should be higher.
sc_tick target_buffering_delay;
uint32_t target_buffering; // in samples
// SDL audio output buffer size
// SDL audio output buffer size.
sc_tick output_buffer_duration;
uint16_t output_buffer;
SDL_AudioDeviceID device;
struct sc_audio_regulator audioreg;
// Audio buffer to communicate between the receiver and the SDL audio
// callback
struct sc_audiobuf buf;
// Resampler (only used from the receiver thread)
struct SwrContext *swr_ctx;
// The sample rate is the same for input and output
unsigned sample_rate;
// The number of channels is the same for input and output
unsigned nb_channels;
// The number of bytes per sample for a single channel
size_t out_bytes_per_sample;
// Target buffer for resampling (only used by the receiver thread)
uint8_t *swr_buf;
size_t swr_buf_alloc_size;
// Number of buffered samples (may be negative on underflow) (only used by
// the receiver thread)
struct sc_average avg_buffering;
// Count the number of samples to trigger a compensation update regularly
// (only used by the receiver thread)
uint32_t samples_since_resync;
// Number of silence samples inserted since the last received packet
atomic_uint_least32_t underflow;
// Current applied compensation value (only used by the receiver thread)
int compensation;
// Set to true the first time a sample is received
atomic_bool received;
// Set to true the first time the SDL callback is called
atomic_bool played;
const struct sc_audio_player_callbacks *cbs;
void *cbs_userdata;
};
struct sc_audio_player_callbacks {
void (*on_ended)(struct sc_audio_player *ap, bool success, void *userdata);
};
void

View File

@ -1,413 +0,0 @@
#include "audio_regulator.h"
#include <libavcodec/avcodec.h>
#include <libavutil/opt.h>
#include "util/log.h"
//#define SC_AUDIO_REGULATOR_DEBUG // uncomment to debug
/**
* Real-time audio regulator with configurable latency
*
* As input, the regulator regularly receives AVFrames of decoded audio samples.
* As output, the audio player regularly requests audio samples to be played.
* In the middle, an audio buffer stores the samples produced but not consumed
* yet.
*
* The goal of the regulator is to feed the audio player with a latency as low
* as possible while avoiding buffer underrun (i.e. not being able to provide
* samples when requested).
*
* To achieve this, it attempts to maintain the average buffering (the number
* of samples present in the buffer) around a target value. If this target
* buffering is too low, then buffer underrun will occur frequently. If it is
* too high, then latency will become unacceptable. This target value is
* configured using the scrcpy option --audio-buffer.
*
* The regulator cannot adjust the sample input rate (it receives samples
* produced in real-time) or the sample output rate (it must provide samples as
* requested by the audio player). Therefore, it may only apply compensation by
* resampling (converting _m_ input samples to _n_ output samples).
*
* The compensation itself is applied by libswresample (FFmpeg). It is
* configured using swr_set_compensation(). An important work for the regulator
* is to estimate the compensation value regularly and apply it.
*
* The estimated buffering level is the result of averaging the "natural"
* buffering (samples are produced and consumed by blocks, so it must be
* smoothed), and making instant adjustments resulting of its own actions
* (explicit compensation and silence insertion on underflow), which are not
* smoothed.
*
* Buffer underflow events can occur when packets arrive too late. In that case,
* the regulator inserts silence. Once the packets finally arrive (late), one
* strategy could be to drop the samples that were replaced by silence, in
* order to keep a minimal latency. However, dropping samples in case of buffer
* underflow is inadvisable, as it would temporarily increase the underflow
* even more and cause very noticeable audio glitches.
*
* Therefore, the regulator doesn't drop any sample on underflow. The
* compensation mechanism will absorb the delay introduced by the inserted
* silence.
*/
#define TO_BYTES(SAMPLES) sc_audiobuf_to_bytes(&ar->buf, (SAMPLES))
#define TO_SAMPLES(BYTES) sc_audiobuf_to_samples(&ar->buf, (BYTES))
void
sc_audio_regulator_pull(struct sc_audio_regulator *ar, uint8_t *out,
uint32_t out_samples) {
#ifdef SC_AUDIO_REGULATOR_DEBUG
LOGD("[Audio] Audio regulator pulls %" PRIu32 " samples", out_samples);
#endif
// A lock is necessary in the rare case where the producer needs to drop
// samples already pushed (when the buffer is full)
sc_mutex_lock(&ar->mutex);
bool played = atomic_load_explicit(&ar->played, memory_order_relaxed);
if (!played) {
uint32_t buffered_samples = sc_audiobuf_can_read(&ar->buf);
// Wait until the buffer is filled up to at least target_buffering
// before playing
if (buffered_samples < ar->target_buffering) {
LOGV("[Audio] Inserting initial buffering silence: %" PRIu32
" samples", out_samples);
// Delay playback starting to reach the target buffering. Fill the
// whole buffer with silence (len is small compared to the
// arbitrary margin value).
memset(out, 0, out_samples * ar->sample_size);
sc_mutex_unlock(&ar->mutex);
return;
}
}
uint32_t read = sc_audiobuf_read(&ar->buf, out, out_samples);
sc_mutex_unlock(&ar->mutex);
if (read < out_samples) {
uint32_t silence = out_samples - read;
// Insert silence. In theory, the inserted silent samples replace the
// missing real samples, which will arrive later, so they should be
// dropped to keep the latency minimal. However, this would cause very
// audible glitches, so let the clock compensation restore the target
// latency.
LOGD("[Audio] Buffer underflow, inserting silence: %" PRIu32 " samples",
silence);
memset(out + TO_BYTES(read), 0, TO_BYTES(silence));
bool received = atomic_load_explicit(&ar->received,
memory_order_relaxed);
if (received) {
// Inserting additional samples immediately increases buffering
atomic_fetch_add_explicit(&ar->underflow, silence,
memory_order_relaxed);
}
}
atomic_store_explicit(&ar->played, true, memory_order_relaxed);
}
static uint8_t *
sc_audio_regulator_get_swr_buf(struct sc_audio_regulator *ar,
uint32_t min_samples) {
size_t min_buf_size = TO_BYTES(min_samples);
if (min_buf_size > ar->swr_buf_alloc_size) {
size_t new_size = min_buf_size + 4096;
uint8_t *buf = realloc(ar->swr_buf, new_size);
if (!buf) {
LOG_OOM();
// Could not realloc to the requested size
return NULL;
}
ar->swr_buf = buf;
ar->swr_buf_alloc_size = new_size;
}
return ar->swr_buf;
}
bool
sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) {
SwrContext *swr_ctx = ar->swr_ctx;
int64_t swr_delay = swr_get_delay(swr_ctx, ar->sample_rate);
// No need to av_rescale_rnd(), input and output sample rates are the same.
// Add more space (256) for clock compensation.
int dst_nb_samples = swr_delay + frame->nb_samples + 256;
uint8_t *swr_buf = sc_audio_regulator_get_swr_buf(ar, dst_nb_samples);
if (!swr_buf) {
return false;
}
int ret = swr_convert(swr_ctx, &swr_buf, dst_nb_samples,
(const uint8_t **) frame->data, frame->nb_samples);
if (ret < 0) {
LOGE("Resampling failed: %d", ret);
return false;
}
// swr_convert() returns the number of samples which would have been
// written if the buffer was big enough.
uint32_t samples = MIN(ret, dst_nb_samples);
#ifdef SC_AUDIO_REGULATOR_DEBUG
LOGD("[Audio] %" PRIu32 " samples written to buffer", samples);
#endif
uint32_t cap = sc_audiobuf_capacity(&ar->buf);
if (samples > cap) {
// Very very unlikely: a single resampled frame should never
// exceed the audio buffer size (or something is very wrong).
// Ignore the first bytes in swr_buf to avoid memory corruption anyway.
swr_buf += TO_BYTES(samples - cap);
samples = cap;
}
uint32_t skipped_samples = 0;
uint32_t written = sc_audiobuf_write(&ar->buf, swr_buf, samples);
if (written < samples) {
uint32_t remaining = samples - written;
// All samples that could be written without locking have been written,
// now we need to lock to drop/consume old samples
sc_mutex_lock(&ar->mutex);
// Retry with the lock
written += sc_audiobuf_write(&ar->buf,
swr_buf + TO_BYTES(written),
remaining);
if (written < samples) {
remaining = samples - written;
// Still insufficient, drop old samples to make space
skipped_samples = sc_audiobuf_read(&ar->buf, NULL, remaining);
assert(skipped_samples == remaining);
}
sc_mutex_unlock(&ar->mutex);
if (written < samples) {
// Now there is enough space
uint32_t w = sc_audiobuf_write(&ar->buf,
swr_buf + TO_BYTES(written),
remaining);
assert(w == remaining);
(void) w;
}
}
uint32_t underflow = 0;
uint32_t max_buffered_samples;
bool played = atomic_load_explicit(&ar->played, memory_order_relaxed);
if (played) {
underflow = atomic_exchange_explicit(&ar->underflow, 0,
memory_order_relaxed);
max_buffered_samples = ar->target_buffering * 11 / 10
+ 60 * ar->sample_rate / 1000 /* 60 ms */;
} else {
// Playback not started yet, do not accumulate more than
// max_initial_buffering samples, this would cause unnecessary delay
// (and glitches to compensate) on start.
max_buffered_samples = ar->target_buffering
+ 10 * ar->sample_rate / 1000 /* 10 ms */;
}
uint32_t can_read = sc_audiobuf_can_read(&ar->buf);
if (can_read > max_buffered_samples) {
uint32_t skip_samples = 0;
sc_mutex_lock(&ar->mutex);
can_read = sc_audiobuf_can_read(&ar->buf);
if (can_read > max_buffered_samples) {
skip_samples = can_read - max_buffered_samples;
uint32_t r = sc_audiobuf_read(&ar->buf, NULL, skip_samples);
assert(r == skip_samples);
(void) r;
skipped_samples += skip_samples;
}
sc_mutex_unlock(&ar->mutex);
if (skip_samples) {
if (played) {
LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32
" samples", skip_samples);
#ifdef SC_AUDIO_REGULATOR_DEBUG
} else {
LOGD("[Audio] Playback not started, skipping %" PRIu32
" samples", skip_samples);
#endif
}
}
}
atomic_store_explicit(&ar->received, true, memory_order_relaxed);
if (!played) {
// Nothing more to do
return true;
}
// Number of samples added (or removed, if negative) for compensation
int32_t instant_compensation = (int32_t) written - frame->nb_samples;
// Inserting silence instantly increases buffering
int32_t inserted_silence = (int32_t) underflow;
// Dropping input samples instantly decreases buffering
int32_t dropped = (int32_t) skipped_samples;
// The compensation must apply instantly, it must not be smoothed
ar->avg_buffering.avg += instant_compensation + inserted_silence - dropped;
if (ar->avg_buffering.avg < 0) {
// Since dropping samples instantly reduces buffering, the difference
// is applied immediately to the average value, assuming that the delay
// between the producer and the consumer will be caught up.
//
// However, when this assumption is not valid, the average buffering
// may decrease indefinitely. Prevent it to become negative to limit
// the consequences.
ar->avg_buffering.avg = 0;
}
// However, the buffering level must be smoothed
sc_average_push(&ar->avg_buffering, can_read);
#ifdef SC_AUDIO_REGULATOR_DEBUG
LOGD("[Audio] can_read=%" PRIu32 " avg_buffering=%f",
can_read, sc_average_get(&ar->avg_buffering));
#endif
ar->samples_since_resync += written;
if (ar->samples_since_resync >= ar->sample_rate) {
// Recompute compensation every second
ar->samples_since_resync = 0;
float avg = sc_average_get(&ar->avg_buffering);
int diff = ar->target_buffering - avg;
// Enable compensation when the difference exceeds +/- 4ms.
// Disable compensation when the difference is lower than +/- 1ms.
int threshold = ar->compensation_active
? ar->sample_rate / 1000 /* 1ms */
: ar->sample_rate * 4 / 1000; /* 4ms */
if (abs(diff) < threshold) {
// Do not compensate for small values, the error is just noise
diff = 0;
} else if (diff < 0 && can_read < ar->target_buffering) {
// Do not accelerate if the instant buffering level is below the
// target, this would increase underflow
diff = 0;
}
// Compensate the diff over 4 seconds (but will be recomputed after 1
// second)
int distance = 4 * ar->sample_rate;
// Limit compensation rate to 2%
int abs_max_diff = distance / 50;
diff = CLAMP(diff, -abs_max_diff, abs_max_diff);
LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32
" compensation=%d", ar->target_buffering, avg, can_read, diff);
int ret = swr_set_compensation(swr_ctx, diff, distance);
if (ret < 0) {
LOGW("Resampling compensation failed: %d", ret);
// not fatal
} else {
ar->compensation_active = diff != 0;
}
}
return true;
}
bool
sc_audio_regulator_init(struct sc_audio_regulator *ar, size_t sample_size,
const AVCodecContext *ctx, uint32_t target_buffering) {
SwrContext *swr_ctx = swr_alloc();
if (!swr_ctx) {
LOG_OOM();
return false;
}
ar->swr_ctx = swr_ctx;
#ifdef SCRCPY_LAVU_HAS_CHLAYOUT
av_opt_set_chlayout(swr_ctx, "in_chlayout", &ctx->ch_layout, 0);
av_opt_set_chlayout(swr_ctx, "out_chlayout", &ctx->ch_layout, 0);
#else
av_opt_set_channel_layout(swr_ctx, "in_channel_layout",
ctx->channel_layout, 0);
av_opt_set_channel_layout(swr_ctx, "out_channel_layout",
ctx->channel_layout, 0);
#endif
av_opt_set_int(swr_ctx, "in_sample_rate", ctx->sample_rate, 0);
av_opt_set_int(swr_ctx, "out_sample_rate", ctx->sample_rate, 0);
av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt", ctx->sample_fmt, 0);
av_opt_set_sample_fmt(swr_ctx, "out_sample_fmt", SC_AV_SAMPLE_FMT, 0);
int ret = swr_init(swr_ctx);
if (ret) {
LOGE("Failed to initialize the resampling context");
goto error_free_swr_ctx;
}
bool ok = sc_mutex_init(&ar->mutex);
if (!ok) {
goto error_free_swr_ctx;
}
ar->target_buffering = target_buffering;
ar->sample_size = sample_size;
ar->sample_rate = ctx->sample_rate;
// Use a ring-buffer of the target buffering size plus 1 second between the
// producer and the consumer. It's too big on purpose, to guarantee that
// the producer and the consumer will be able to access it in parallel
// without locking.
uint32_t audiobuf_samples = target_buffering + ar->sample_rate;
ok = sc_audiobuf_init(&ar->buf, sample_size, audiobuf_samples);
if (!ok) {
goto error_destroy_mutex;
}
size_t initial_swr_buf_size = TO_BYTES(4096);
ar->swr_buf = malloc(initial_swr_buf_size);
if (!ar->swr_buf) {
LOG_OOM();
goto error_destroy_audiobuf;
}
ar->swr_buf_alloc_size = initial_swr_buf_size;
// Samples are produced and consumed by blocks, so the buffering must be
// smoothed to get a relatively stable value.
sc_average_init(&ar->avg_buffering, 128);
ar->samples_since_resync = 0;
ar->received = false;
atomic_init(&ar->played, false);
atomic_init(&ar->received, false);
atomic_init(&ar->underflow, 0);
ar->compensation_active = false;
return true;
error_destroy_audiobuf:
sc_audiobuf_destroy(&ar->buf);
error_destroy_mutex:
sc_mutex_destroy(&ar->mutex);
error_free_swr_ctx:
swr_free(&ar->swr_ctx);
return false;
}
void
sc_audio_regulator_destroy(struct sc_audio_regulator *ar) {
free(ar->swr_buf);
sc_audiobuf_destroy(&ar->buf);
sc_mutex_destroy(&ar->mutex);
swr_free(&ar->swr_ctx);
}

View File

@ -1,71 +0,0 @@
#ifndef SC_AUDIO_REGULATOR_H
#define SC_AUDIO_REGULATOR_H
#include "common.h"
#include <stdatomic.h>
#include <stdbool.h>
#include <libavcodec/avcodec.h>
#include <libswresample/swresample.h>
#include "util/audiobuf.h"
#include "util/average.h"
#include "util/thread.h"
#define SC_AV_SAMPLE_FMT AV_SAMPLE_FMT_FLT
struct sc_audio_regulator {
sc_mutex mutex;
// Target buffering between the producer and the consumer (in samples)
uint32_t target_buffering;
// Audio buffer to communicate between the receiver and the player
struct sc_audiobuf buf;
// Resampler (only used from the receiver thread)
struct SwrContext *swr_ctx;
// The sample rate is the same for input and output
uint32_t sample_rate;
// The number of bytes per sample (for all channels)
size_t sample_size;
// Target buffer for resampling (only used by the receiver thread)
uint8_t *swr_buf;
size_t swr_buf_alloc_size;
// Number of buffered samples (may be negative on underflow) (only used by
// the receiver thread)
struct sc_average avg_buffering;
// Count the number of samples to trigger a compensation update regularly
// (only used by the receiver thread)
uint32_t samples_since_resync;
// Number of silence samples inserted since the last received packet
atomic_uint_least32_t underflow;
// Non-zero compensation applied (only used by the receiver thread)
bool compensation_active;
// Set to true the first time a sample is received
atomic_bool received;
// Set to true the first time samples are pulled by the player
atomic_bool played;
};
bool
sc_audio_regulator_init(struct sc_audio_regulator *ar, size_t sample_size,
const AVCodecContext *ctx, uint32_t target_buffering);
void
sc_audio_regulator_destroy(struct sc_audio_regulator *ar);
bool
sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame);
void
sc_audio_regulator_pull(struct sc_audio_regulator *ar, uint8_t *out,
uint32_t samples);
#endif

View File

@ -50,7 +50,6 @@ enum {
OPT_POWER_OFF_ON_CLOSE,
OPT_V4L2_SINK,
OPT_DISPLAY_BUFFER,
OPT_VIDEO_BUFFER,
OPT_V4L2_BUFFER,
OPT_TUNNEL_HOST,
OPT_TUNNEL_PORT,
@ -103,13 +102,6 @@ enum {
OPT_NO_MOUSE_HOVER,
OPT_AUDIO_DUP,
OPT_GAMEPAD,
OPT_NEW_DISPLAY,
OPT_LIST_APPS,
OPT_START_APP,
OPT_SCREEN_OFF_TIMEOUT,
OPT_CAPTURE_ORIENTATION,
OPT_ANGLE,
OPT_NO_VD_SYSTEM_DECORATIONS,
};
struct sc_option {
@ -151,13 +143,6 @@ static const struct sc_option options[] = {
.longopt = "always-on-top",
.text = "Make scrcpy window always on top (above other windows).",
},
{
.longopt_id = OPT_ANGLE,
.longopt = "angle",
.argdesc = "degrees",
.text = "Rotate the video content by a custom angle, in degrees "
"(clockwise).",
},
{
.longopt_id = OPT_AUDIO_BIT_RATE,
.longopt = "audio-bit-rate",
@ -255,6 +240,14 @@ static const struct sc_option options[] = {
"ratio), \"<num>:<den>\" (e.g. \"4:3\") or \"<value>\" (e.g. "
"\"1.6\")."
},
{
.longopt_id = OPT_CAMERA_ID,
.longopt = "camera-id",
.argdesc = "id",
.text = "Specify the device camera id to mirror.\n"
"The available camera ids can be listed by:\n"
" scrcpy --list-cameras",
},
{
.longopt_id = OPT_CAMERA_FACING,
.longopt = "camera-facing",
@ -262,14 +255,6 @@ static const struct sc_option options[] = {
.text = "Select the device camera by its facing direction.\n"
"Possible values are \"front\", \"back\" and \"external\".",
},
{
.longopt_id = OPT_CAMERA_FPS,
.longopt = "camera-fps",
.argdesc = "value",
.text = "Specify the camera capture frame rate.\n"
"If not specified, Android's default frame rate (30 fps) is "
"used.",
},
{
.longopt_id = OPT_CAMERA_HIGH_SPEED,
.longopt = "camera-high-speed",
@ -277,14 +262,6 @@ static const struct sc_option options[] = {
"This mode is restricted to specific resolutions and frame "
"rates, listed by --list-camera-sizes.",
},
{
.longopt_id = OPT_CAMERA_ID,
.longopt = "camera-id",
.argdesc = "id",
.text = "Specify the device camera id to mirror.\n"
"The available camera ids can be listed by:\n"
" scrcpy --list-cameras",
},
{
.longopt_id = OPT_CAMERA_SIZE,
.longopt = "camera-size",
@ -292,21 +269,12 @@ static const struct sc_option options[] = {
.text = "Specify an explicit camera capture size.",
},
{
.longopt_id = OPT_CAPTURE_ORIENTATION,
.longopt = "capture-orientation",
.longopt_id = OPT_CAMERA_FPS,
.longopt = "camera-fps",
.argdesc = "value",
.text = "Set the capture video orientation.\n"
"Possible values are 0, 90, 180, 270, flip0, flip90, flip180 "
"and flip270, possibly prefixed by '@'.\n"
"The number represents the clockwise rotation in degrees; the "
"flip\" keyword applies a horizontal flip before the "
"rotation.\n"
"If a leading '@' is passed (@90) for display capture, then "
"the rotation is locked, and is relative to the natural device "
"orientation.\n"
"If '@' is passed alone, then the rotation is locked to the "
"initial device orientation.\n"
"Default is 0.",
.text = "Specify the camera capture frame rate.\n"
"If not specified, Android's default frame rate (30 fps) is "
"used.",
},
{
// Not really deprecated (--codec has never been released), but without
@ -329,7 +297,8 @@ static const struct sc_option options[] = {
.argdesc = "width:height:x:y",
.text = "Crop the device screen on the server.\n"
"The values are expressed in the device natural orientation "
"(typically, portrait for a phone, landscape for a tablet).",
"(typically, portrait for a phone, landscape for a tablet). "
"Any --max-size value is computed on the cropped size.",
},
{
.shortopt = 'd',
@ -349,10 +318,12 @@ static const struct sc_option options[] = {
.argdesc = "id",
},
{
// deprecated
.longopt_id = OPT_DISPLAY_BUFFER,
.longopt = "display-buffer",
.argdesc = "ms",
.text = "Add a buffering delay (in milliseconds) before displaying. "
"This increases latency to compensate for jitter.\n"
"Default is 0 (no buffering).",
},
{
.longopt_id = OPT_DISPLAY_ID,
@ -471,11 +442,6 @@ static const struct sc_option options[] = {
"This is a workaround for some devices not behaving as "
"expected when setting the device clipboard programmatically.",
},
{
.longopt_id = OPT_LIST_APPS,
.longopt = "list-apps",
.text = "List Android apps installed on the device.",
},
{
.longopt_id = OPT_LIST_CAMERAS,
.longopt = "list-cameras",
@ -497,10 +463,18 @@ static const struct sc_option options[] = {
.text = "List video and audio encoders available on the device.",
},
{
// deprecated
.longopt_id = OPT_LOCK_VIDEO_ORIENTATION,
.longopt = "lock-video-orientation",
.argdesc = "value",
.optional_arg = true,
.text = "Lock capture video orientation to value.\n"
"Possible values are \"unlocked\", \"initial\" (locked to the "
"initial orientation), 0, 90, 180 and 270. The values "
"represent the clockwise rotation from the natural device "
"orientation, in degrees.\n"
"Default is \"unlocked\".\n"
"Passing the option without argument is equivalent to passing "
"\"initial\".",
},
{
.shortopt = 'm',
@ -583,20 +557,6 @@ static const struct sc_option options[] = {
.text = "Disable video and audio playback on the computer (equivalent "
"to --no-video-playback --no-audio-playback).",
},
{
.longopt_id = OPT_NEW_DISPLAY,
.longopt = "new-display",
.argdesc = "[<width>x<height>][/<dpi>]",
.optional_arg = true,
.text = "Create a new display with the specified resolution and "
"density. If not provided, they default to the main display "
"dimensions and DPI.\n"
"Examples:\n"
" --new-display=1920x1080\n"
" --new-display=1920x1080/420 # force 420 dpi\n"
" --new-display # main display size and density\n"
" --new-display=/240 # main display size and 240 dpi",
},
{
.longopt_id = OPT_NO_AUDIO,
.longopt = "no-audio",
@ -659,11 +619,6 @@ static const struct sc_option options[] = {
.longopt = "no-power-on",
.text = "Do not power on the device on start.",
},
{
.longopt_id = OPT_NO_VD_SYSTEM_DECORATIONS,
.longopt = "no-vd-system-decorations",
.text = "Disable virtual display system decorations flag.",
},
{
.longopt_id = OPT_NO_VIDEO,
.longopt = "no-video",
@ -816,13 +771,6 @@ static const struct sc_option options[] = {
.longopt = "turn-screen-off",
.text = "Turn the device screen off immediately.",
},
{
.longopt_id = OPT_SCREEN_OFF_TIMEOUT,
.longopt = "screen-off-timeout",
.argdesc = "seconds",
.text = "Set the screen off timeout while scrcpy is running (restore "
"the initial value on exit).",
},
{
.longopt_id = OPT_SHORTCUT_MOD,
.longopt = "shortcut-mod",
@ -836,20 +784,6 @@ static const struct sc_option options[] = {
"shortcuts, pass \"lctrl,lsuper\".\n"
"Default is \"lalt,lsuper\" (left-Alt or left-Super).",
},
{
.longopt_id = OPT_START_APP,
.longopt = "start-app",
.argdesc = "name",
.text = "Start an Android app, by its exact package name.\n"
"Add a '?' prefix to select an app whose name starts with the "
"given name, case-insensitive (retrieving app names on the "
"device may take some time):\n"
" scrcpy --start-app=?firefox\n"
"Add a '+' prefix to force-stop before starting the app:\n"
" scrcpy --new-display --start-app=+org.mozilla.firefox\n"
"Both prefixes can be used, in that order:\n"
" scrcpy --start-app=+?firefox",
},
{
.shortopt = 't',
.longopt = "show-touches",
@ -917,6 +851,8 @@ static const struct sc_option options[] = {
.longopt = "v4l2-sink",
.argdesc = "/dev/videoN",
.text = "Output to v4l2loopback device.\n"
"It requires to lock the video orientation (see "
"--lock-video-orientation).\n"
"This feature is only available on Linux.",
},
{
@ -925,20 +861,11 @@ static const struct sc_option options[] = {
.argdesc = "ms",
.text = "Add a buffering delay (in milliseconds) before pushing "
"frames. This increases latency to compensate for jitter.\n"
"This option is similar to --video-buffer, but specific to "
"This option is similar to --display-buffer, but specific to "
"V4L2 sink.\n"
"Default is 0 (no buffering).\n"
"This option is only available on Linux.",
},
{
.longopt_id = OPT_VIDEO_BUFFER,
.longopt = "video-buffer",
.argdesc = "ms",
.text = "Add a buffering delay (in milliseconds) before displaying "
"video frames.\n"
"This increases latency to compensate for jitter.\n"
"Default is 0 (no buffering).",
},
{
.longopt_id = OPT_VIDEO_CODEC,
.longopt = "video-codec",
@ -1050,10 +977,6 @@ static const struct sc_shortcut shortcuts[] = {
.shortcuts = { "MOD+Shift+z" },
.text = "Unpause display",
},
{
.shortcuts = { "MOD+Shift+r" },
.text = "Reset video capture/encoding",
},
{
.shortcuts = { "MOD+g" },
.text = "Resize window to 1:1 (pixel-perfect)",
@ -1149,11 +1072,7 @@ static const struct sc_shortcut shortcuts[] = {
},
{
.shortcuts = { "Shift+click-and-move" },
.text = "Tilt vertically (slide with 2 fingers)",
},
{
.shortcuts = { "Ctrl+Shift+click-and-move" },
.text = "Tilt horizontally (slide with 2 fingers)",
.text = "Tilt (slide vertically with two fingers)",
},
{
.shortcuts = { "Drag & drop APK file" },
@ -1602,6 +1521,78 @@ parse_audio_output_buffer(const char *s, sc_tick *tick) {
return true;
}
static bool
parse_lock_video_orientation(const char *s,
enum sc_lock_video_orientation *lock_mode) {
if (!s || !strcmp(s, "initial")) {
// Without argument, lock the initial orientation
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_INITIAL;
return true;
}
if (!strcmp(s, "unlocked")) {
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED;
return true;
}
if (!strcmp(s, "0")) {
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_0;
return true;
}
if (!strcmp(s, "90")) {
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_90;
return true;
}
if (!strcmp(s, "180")) {
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_180;
return true;
}
if (!strcmp(s, "270")) {
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_270;
return true;
}
if (!strcmp(s, "1")) {
LOGW("--lock-video-orientation=1 is deprecated, use "
"--lock-video-orientation=270 instead.");
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_270;
return true;
}
if (!strcmp(s, "2")) {
LOGW("--lock-video-orientation=2 is deprecated, use "
"--lock-video-orientation=180 instead.");
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_180;
return true;
}
if (!strcmp(s, "3")) {
LOGW("--lock-video-orientation=3 is deprecated, use "
"--lock-video-orientation=90 instead.");
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_90;
return true;
}
LOGE("Unsupported --lock-video-orientation value: %s (expected initial, "
"unlocked, 0, 90, 180 or 270).", s);
return false;
}
static bool
parse_rotation(const char *s, uint8_t *rotation) {
long value;
bool ok = parse_integer_arg(s, &value, false, 0, 3, "rotation");
if (!ok) {
return false;
}
*rotation = (uint8_t) value;
return true;
}
static bool
parse_orientation(const char *s, enum sc_orientation *orientation) {
if (!strcmp(s, "0")) {
@ -1641,32 +1632,6 @@ parse_orientation(const char *s, enum sc_orientation *orientation) {
return false;
}
static bool
parse_capture_orientation(const char *s, enum sc_orientation *orientation,
enum sc_orientation_lock *lock) {
if (*s == '\0') {
LOGE("Capture orientation may not be empty (expected 0, 90, 180, 270, "
"flip0, flip90, flip180 or flip270, possibly prefixed by '@')");
return false;
}
// Lock the orientation by a leading '@'
if (s[0] == '@') {
// Consume '@'
++s;
if (*s == '\0') {
// Only '@': lock to the initial orientation (orientation is unused)
*lock = SC_ORIENTATION_LOCKED_INITIAL;
return true;
}
*lock = SC_ORIENTATION_LOCKED_VALUE;
} else {
*lock = SC_ORIENTATION_UNLOCKED;
}
return parse_orientation(s, orientation);
}
static bool
parse_window_position(const char *s, int16_t *position) {
// special value for "auto"
@ -2137,20 +2102,6 @@ parse_time_limit(const char *s, sc_tick *tick) {
return true;
}
static bool
parse_screen_off_timeout(const char *s, sc_tick *tick) {
long value;
// value in seconds, but must fit in 31 bits in milliseconds
bool ok = parse_integer_arg(s, &value, false, 0, 0x7FFFFFFF / 1000,
"screen off timeout");
if (!ok) {
return false;
}
*tick = SC_TICK_FROM_SEC(value);
return true;
}
static bool
parse_pause_on_exit(const char *s, enum sc_pause_on_exit *pause_on_exit) {
if (!s || !strcmp(s, "true")) {
@ -2276,8 +2227,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
opts->crop = optarg;
break;
case OPT_DISPLAY:
LOGE("--display has been removed, use --display-id instead.");
return false;
LOGW("--display is deprecated, use --display-id instead.");
// fall through
case OPT_DISPLAY_ID:
if (!parse_display_id(optarg, &opts->display_id)) {
return false;
@ -2341,13 +2292,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
"--mouse=uhid instead.");
return false;
case OPT_LOCK_VIDEO_ORIENTATION:
LOGE("--lock-video-orientation has been removed, use "
"--capture-orientation instead.");
return false;
case OPT_CAPTURE_ORIENTATION:
if (!parse_capture_orientation(optarg,
&opts->capture_orientation,
&opts->capture_orientation_lock)) {
if (!parse_lock_video_orientation(optarg,
&opts->lock_video_orientation)) {
return false;
}
break;
@ -2365,9 +2311,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
opts->control = false;
break;
case OPT_NO_DISPLAY:
LOGE("--no-display has been removed, use --no-playback "
"instead.");
return false;
LOGW("--no-display is deprecated, use --no-playback instead.");
// fall through
case 'N':
opts->video_playback = false;
opts->audio_playback = false;
@ -2453,9 +2398,32 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
opts->key_inject_mode = SC_KEY_INJECT_MODE_RAW;
break;
case OPT_ROTATION:
LOGE("--rotation has been removed, use --orientation or "
"--capture-orientation instead.");
return false;
LOGW("--rotation is deprecated, use --display-orientation "
"instead.");
uint8_t rotation;
if (!parse_rotation(optarg, &rotation)) {
return false;
}
assert(rotation <= 3);
switch (rotation) {
case 0:
opts->display_orientation = SC_ORIENTATION_0;
break;
case 1:
// rotation 1 was 90° counterclockwise, but orientation
// is expressed clockwise
opts->display_orientation = SC_ORIENTATION_270;
break;
case 2:
opts->display_orientation = SC_ORIENTATION_180;
break;
case 3:
// rotation 3 was 270° counterclockwise, but orientation
// is expressed clockwise
opts->display_orientation = SC_ORIENTATION_90;
break;
}
break;
case OPT_DISPLAY_ORIENTATION:
if (!parse_orientation(optarg, &opts->display_orientation)) {
return false;
@ -2516,9 +2484,23 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
}
break;
case OPT_FORWARD_ALL_CLICKS:
LOGE("--forward-all-clicks has been removed, "
LOGW("--forward-all-clicks is deprecated, "
"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:
opts->legacy_paste = true;
break;
@ -2526,11 +2508,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
opts->power_off_on_close = true;
break;
case OPT_DISPLAY_BUFFER:
LOGE("--display-buffer has been removed, use --video-buffer "
"instead.");
return false;
case OPT_VIDEO_BUFFER:
if (!parse_buffering_time(optarg, &opts->video_buffer)) {
if (!parse_buffering_time(optarg, &opts->display_buffer)) {
return false;
}
break;
@ -2613,9 +2591,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
case OPT_LIST_CAMERA_SIZES:
opts->list |= SC_OPTION_LIST_CAMERA_SIZES;
break;
case OPT_LIST_APPS:
opts->list |= SC_OPTION_LIST_APPS;
break;
case OPT_REQUIRE_AUDIO:
opts->require_audio = true;
break;
@ -2689,24 +2664,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
return false;
}
break;
case OPT_NEW_DISPLAY:
opts->new_display = optarg ? optarg : "";
break;
case OPT_START_APP:
opts->start_app = optarg;
break;
case OPT_SCREEN_OFF_TIMEOUT:
if (!parse_screen_off_timeout(optarg,
&opts->screen_off_timeout)) {
return false;
}
break;
case OPT_ANGLE:
opts->angle = optarg;
break;
case OPT_NO_VD_SYSTEM_DECORATIONS:
opts->vd_system_decorations = optarg;
break;
default:
// getopt prints the error message on stderr
return false;
@ -2801,6 +2758,13 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
return false;
}
if (opts->lock_video_orientation ==
SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) {
LOGI("Video orientation is locked for v4l2 sink. "
"See --lock-video-orientation.");
opts->lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_INITIAL;
}
// V4L2 could not handle size change.
// Do not log because downsizing on error is the default behavior,
// not an explicit request from the user.
@ -2808,7 +2772,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
}
if (opts->v4l2_buffer && !opts->v4l2_device) {
LOGE("V4L2 buffer value without V4L2 sink");
LOGE("V4L2 buffer value without V4L2 sink\n");
return false;
}
#endif
@ -2827,8 +2791,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
if (otg) {
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_AOA;
} else if (!opts->video_playback) {
LOGI("No video mirroring, SDK mouse disabled");
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_DISABLED;
LOGI("No video mirroring, mouse mode switched to UHID");
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_UHID;
} else {
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_SDK;
}
@ -2880,18 +2844,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
}
}
if (opts->new_display) {
if (opts->video_source != SC_VIDEO_SOURCE_DISPLAY) {
LOGE("--new-display is only available with --video-source=display");
return false;
}
if (!opts->video) {
LOGE("--new-display is incompatible with --no-video");
return false;
}
}
if (otg) {
if (!opts->control) {
LOGE("--no-control is not allowed in OTG mode");
@ -2998,11 +2950,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
return false;
}
if (opts->display_id != 0 && opts->new_display) {
LOGE("Cannot specify both --display-id and --new-display");
return false;
}
if (opts->audio && opts->audio_source == SC_AUDIO_SOURCE_AUTO) {
// Select the audio source according to the video source
if (opts->video_source == SC_VIDEO_SOURCE_DISPLAY) {
@ -3135,10 +3082,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
LOGE("Cannot request power off on close if control is disabled");
return false;
}
if (opts->start_app) {
LOGE("Cannot start an Android app if control is disabled");
return false;
}
}
# ifdef _WIN32

View File

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

View File

@ -35,14 +35,18 @@ enum sc_control_msg_type {
SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS,
SC_CONTROL_MSG_TYPE_GET_CLIPBOARD,
SC_CONTROL_MSG_TYPE_SET_CLIPBOARD,
SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER,
SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE,
SC_CONTROL_MSG_TYPE_ROTATE_DEVICE,
SC_CONTROL_MSG_TYPE_UHID_CREATE,
SC_CONTROL_MSG_TYPE_UHID_INPUT,
SC_CONTROL_MSG_TYPE_UHID_DESTROY,
SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS,
SC_CONTROL_MSG_TYPE_START_APP,
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 {
@ -90,8 +94,8 @@ struct sc_control_msg {
bool paste;
} set_clipboard;
struct {
bool on;
} set_display_power;
enum sc_screen_power_mode mode;
} set_screen_power_mode;
struct {
uint16_t id;
const char *name; // pointer to static data
@ -106,9 +110,6 @@ struct sc_control_msg {
struct {
uint16_t id;
} uhid_destroy;
struct {
char *name;
} start_app;
};
};

View File

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

View File

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

View File

@ -5,9 +5,53 @@
#include "input_events.h"
#include "screen.h"
#include "shortcut_mod.h"
#include "util/log.h"
#define SC_SDL_SHORTCUT_MODS_MASK (KMOD_CTRL | KMOD_ALT | KMOD_GUI)
static inline uint16_t
to_sdl_mod(uint8_t shortcut_mod) {
uint16_t sdl_mod = 0;
if (shortcut_mod & SC_SHORTCUT_MOD_LCTRL) {
sdl_mod |= KMOD_LCTRL;
}
if (shortcut_mod & SC_SHORTCUT_MOD_RCTRL) {
sdl_mod |= KMOD_RCTRL;
}
if (shortcut_mod & SC_SHORTCUT_MOD_LALT) {
sdl_mod |= KMOD_LALT;
}
if (shortcut_mod & SC_SHORTCUT_MOD_RALT) {
sdl_mod |= KMOD_RALT;
}
if (shortcut_mod & SC_SHORTCUT_MOD_LSUPER) {
sdl_mod |= KMOD_LGUI;
}
if (shortcut_mod & SC_SHORTCUT_MOD_RSUPER) {
sdl_mod |= KMOD_RGUI;
}
return sdl_mod;
}
static bool
is_shortcut_mod(struct sc_input_manager *im, uint16_t sdl_mod) {
// keep only the relevant modifier keys
sdl_mod &= SC_SDL_SHORTCUT_MODS_MASK;
// at least one shortcut mod pressed?
return sdl_mod & im->sdl_shortcut_mods;
}
static bool
is_shortcut_key(struct sc_input_manager *im, SDL_Keycode keycode) {
return (im->sdl_shortcut_mods & KMOD_LCTRL && keycode == SDLK_LCTRL)
|| (im->sdl_shortcut_mods & KMOD_RCTRL && keycode == SDLK_RCTRL)
|| (im->sdl_shortcut_mods & KMOD_LALT && keycode == SDLK_LALT)
|| (im->sdl_shortcut_mods & KMOD_RALT && keycode == SDLK_RALT)
|| (im->sdl_shortcut_mods & KMOD_LGUI && keycode == SDLK_LGUI)
|| (im->sdl_shortcut_mods & KMOD_RGUI && keycode == SDLK_RGUI);
}
void
sc_input_manager_init(struct sc_input_manager *im,
const struct sc_input_manager_params *params) {
@ -29,7 +73,7 @@ sc_input_manager_init(struct sc_input_manager *im,
im->legacy_paste = params->legacy_paste;
im->clipboard_autosync = params->clipboard_autosync;
im->sdl_shortcut_mods = sc_shortcut_mods_to_sdl(params->shortcut_mods);
im->sdl_shortcut_mods = to_sdl_mod(params->shortcut_mods);
im->vfinger_down = false;
im->vfinger_invert_x = false;
@ -203,12 +247,13 @@ set_device_clipboard(struct sc_input_manager *im, bool paste,
}
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);
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER;
msg.set_display_power.on = on;
msg.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE;
msg.set_screen_power_mode.mode = mode;
if (!sc_controller_push_msg(im->controller, &msg)) {
LOGW("Could not request 'set screen power mode'");
@ -284,18 +329,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
apply_orientation_transform(struct sc_input_manager *im,
enum sc_orientation transform) {
@ -313,8 +346,7 @@ sc_input_manager_process_text_input(struct sc_input_manager *im,
return;
}
if (sc_shortcut_mods_is_shortcut_mod(im->sdl_shortcut_mods,
SDL_GetModState())) {
if (is_shortcut_mod(im, SDL_GetModState())) {
// A shortcut must never generate text events
return;
}
@ -381,9 +413,8 @@ sc_input_manager_process_key(struct sc_input_manager *im,
// press/release is a modifier key.
// The second condition is necessary to ignore the release of the modifier
// key (because in this case mod is 0).
uint16_t mods = im->sdl_shortcut_mods;
bool is_shortcut = sc_shortcut_mods_is_shortcut_mod(mods, mod)
|| sc_shortcut_mods_is_shortcut_key(mods, sdl_keycode);
bool is_shortcut = is_shortcut_mod(im, mod)
|| is_shortcut_key(im, sdl_keycode);
if (down && !repeat) {
if (sdl_keycode == im->last_keycode && mod == im->last_mod) {
@ -426,8 +457,10 @@ sc_input_manager_process_key(struct sc_input_manager *im,
return;
case SDLK_o:
if (control && !repeat && down && !paused) {
bool on = shift;
set_display_power(im, on);
enum sc_screen_power_mode mode = shift
? SC_SCREEN_POWER_MODE_NORMAL
: SC_SCREEN_POWER_MODE_OFF;
set_screen_power_mode(im, mode);
}
return;
case SDLK_z:
@ -503,7 +536,7 @@ sc_input_manager_process_key(struct sc_input_manager *im,
return;
case SDLK_f:
if (video && !shift && !repeat && down) {
sc_screen_toggle_fullscreen(im->screen);
sc_screen_switch_fullscreen(im->screen);
}
return;
case SDLK_w:
@ -533,12 +566,8 @@ sc_input_manager_process_key(struct sc_input_manager *im,
}
return;
case SDLK_r:
if (control && !repeat && down && !paused) {
if (shift) {
reset_video(im);
} else {
rotate_device(im);
}
if (control && !shift && !repeat && down && !paused) {
rotate_device(im);
}
return;
case SDLK_k:
@ -807,7 +836,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
}
bool change_vfinger = event->button == SDL_BUTTON_LEFT &&
((down && !im->vfinger_down && (ctrl_pressed || shift_pressed)) ||
((down && !im->vfinger_down && (ctrl_pressed ^ shift_pressed)) ||
(!down && im->vfinger_down));
bool use_finger = im->vfinger_down || change_vfinger;
@ -839,28 +868,16 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
// In other words, the center of the rotation/scaling is the center of the
// screen.
//
// To simulate a vertical tilt gesture (a vertical slide with two fingers),
// Shift can be used instead of Ctrl. The "virtual finger" has a position
// To simulate a tilt gesture (a vertical slide with two fingers), Shift
// can be used instead of Ctrl. The "virtual finger" has a position
// inverted with respect to the vertical axis of symmetry in the middle of
// the screen.
//
// To simulate a horizontal tilt gesture (a horizontal slide with two
// fingers), Ctrl+Shift can be used. The "virtual finger" has a position
// inverted with respect to the horizontal axis of symmetry in the middle
// of the screen. It is expected to be less frequently used, that's why the
// one-mod shortcuts are assigned to rotation and vertical tilt.
if (change_vfinger) {
struct sc_point mouse =
sc_screen_convert_window_to_frame_coords(im->screen, event->x,
event->y);
if (down) {
// Ctrl Shift invert_x invert_y
// ---- ----- ==> -------- --------
// 0 0 0 0 -
// 0 1 1 0 vertical tilt
// 1 0 1 1 rotate
// 1 1 0 1 horizontal tilt
im->vfinger_invert_x = ctrl_pressed ^ shift_pressed;
im->vfinger_invert_x = ctrl_pressed || shift_pressed;
im->vfinger_invert_y = ctrl_pressed;
}
struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size,

View File

@ -45,10 +45,6 @@ convert_keycode(enum sc_keycode from, enum android_keycode *to, uint16_t mod,
{SC_KEYCODE_RCTRL, AKEYCODE_CTRL_RIGHT},
{SC_KEYCODE_LSHIFT, AKEYCODE_SHIFT_LEFT},
{SC_KEYCODE_RSHIFT, AKEYCODE_SHIFT_RIGHT},
{SC_KEYCODE_LALT, AKEYCODE_ALT_LEFT},
{SC_KEYCODE_RALT, AKEYCODE_ALT_RIGHT},
{SC_KEYCODE_LGUI, AKEYCODE_META_LEFT},
{SC_KEYCODE_RGUI, AKEYCODE_META_RIGHT},
};
// Numpad navigation keys.
@ -170,7 +166,11 @@ convert_keycode(enum sc_keycode from, enum android_keycode *to, uint16_t mod,
return false;
}
// Handle letters and space
if (mod & (SC_MOD_LALT | SC_MOD_RALT | SC_MOD_LGUI | SC_MOD_RGUI)) {
return false;
}
// if ALT and META are not pressed, also handle letters and space
entry = SC_INTMAP_FIND_ENTRY(alphaspace_keys, from);
if (entry) {
*to = entry->value;

View File

@ -1,123 +0,0 @@
#include "mouse_capture.h"
#include "shortcut_mod.h"
#include "util/log.h"
void
sc_mouse_capture_init(struct sc_mouse_capture *mc, SDL_Window *window,
uint8_t shortcut_mods) {
mc->window = window;
mc->sdl_mouse_capture_keys = sc_shortcut_mods_to_sdl(shortcut_mods);
mc->mouse_capture_key_pressed = SDLK_UNKNOWN;
}
static inline bool
sc_mouse_capture_is_capture_key(struct sc_mouse_capture *mc, SDL_Keycode key) {
return sc_shortcut_mods_is_shortcut_key(mc->sdl_mouse_capture_keys, key);
}
bool
sc_mouse_capture_handle_event(struct sc_mouse_capture *mc,
const SDL_Event *event) {
switch (event->type) {
case SDL_WINDOWEVENT:
if (event->window.event == SDL_WINDOWEVENT_FOCUS_LOST) {
sc_mouse_capture_set_active(mc, false);
return true;
}
break;
case SDL_KEYDOWN: {
SDL_Keycode key = event->key.keysym.sym;
if (sc_mouse_capture_is_capture_key(mc, key)) {
if (!mc->mouse_capture_key_pressed) {
mc->mouse_capture_key_pressed = key;
} else {
// Another mouse capture key has been pressed, cancel
// mouse (un)capture
mc->mouse_capture_key_pressed = 0;
}
// Mouse capture keys are never forwarded to the device
return true;
}
break;
}
case SDL_KEYUP: {
SDL_Keycode key = event->key.keysym.sym;
SDL_Keycode cap = mc->mouse_capture_key_pressed;
mc->mouse_capture_key_pressed = 0;
if (sc_mouse_capture_is_capture_key(mc, key)) {
if (key == cap) {
// A mouse capture key has been pressed then released:
// toggle the capture mouse mode
sc_mouse_capture_toggle(mc);
}
// Mouse capture keys are never forwarded to the device
return true;
}
break;
}
case SDL_MOUSEWHEEL:
case SDL_MOUSEMOTION:
case SDL_MOUSEBUTTONDOWN:
if (!sc_mouse_capture_is_active(mc)) {
// The mouse will be captured on SDL_MOUSEBUTTONUP, so consume
// the event
return true;
}
break;
case SDL_MOUSEBUTTONUP:
if (!sc_mouse_capture_is_active(mc)) {
sc_mouse_capture_set_active(mc, true);
return true;
}
break;
case SDL_FINGERMOTION:
case SDL_FINGERDOWN:
case SDL_FINGERUP:
// Touch events are not compatible with relative mode
// (coordinates are not relative), so consume the event
return true;
}
return false;
}
void
sc_mouse_capture_set_active(struct sc_mouse_capture *mc, bool capture) {
#ifdef __APPLE__
// Workaround for SDL bug on macOS:
// <https://github.com/libsdl-org/SDL/issues/5340>
if (capture) {
int mouse_x, mouse_y;
SDL_GetGlobalMouseState(&mouse_x, &mouse_y);
int x, y, w, h;
SDL_GetWindowPosition(mc->window, &x, &y);
SDL_GetWindowSize(mc->window, &w, &h);
bool outside_window = mouse_x < x || mouse_x >= x + w
|| mouse_y < y || mouse_y >= y + h;
if (outside_window) {
SDL_WarpMouseInWindow(mc->window, w / 2, h / 2);
}
}
#else
(void) mc;
#endif
if (SDL_SetRelativeMouseMode(capture)) {
LOGE("Could not set relative mouse mode to %s: %s",
capture ? "true" : "false", SDL_GetError());
}
}
bool
sc_mouse_capture_is_active(struct sc_mouse_capture *mc) {
(void) mc;
return SDL_GetRelativeMouseMode();
}
void
sc_mouse_capture_toggle(struct sc_mouse_capture *mc) {
bool new_value = !sc_mouse_capture_is_active(mc);
sc_mouse_capture_set_active(mc, new_value);
}

View File

@ -1,38 +0,0 @@
#ifndef SC_MOUSE_CAPTURE_H
#define SC_MOUSE_CAPTURE_H
#include "common.h"
#include <stdbool.h>
#include <SDL2/SDL.h>
struct sc_mouse_capture {
SDL_Window *window;
uint16_t sdl_mouse_capture_keys;
// To enable/disable mouse capture, a mouse capture key (LALT, LGUI or
// RGUI) must be pressed. This variable tracks the pressed capture key.
SDL_Keycode mouse_capture_key_pressed;
};
void
sc_mouse_capture_init(struct sc_mouse_capture *mc, SDL_Window *window,
uint8_t shortcut_mods);
void
sc_mouse_capture_set_active(struct sc_mouse_capture *mc, bool capture);
bool
sc_mouse_capture_is_active(struct sc_mouse_capture *mc);
void
sc_mouse_capture_toggle(struct sc_mouse_capture *mc);
// Return true if it consumed the event
bool
sc_mouse_capture_handle_event(struct sc_mouse_capture *mc,
const SDL_Event *event);
#endif

View File

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

View File

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

View File

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

View File

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

View File

@ -162,6 +162,47 @@ sc_screen_is_relative_mode(struct sc_screen *screen) {
return screen->im.mp && screen->im.mp->relative_mode;
}
static void
sc_screen_set_mouse_capture(struct sc_screen *screen, bool capture) {
#ifdef __APPLE__
// Workaround for SDL bug on macOS:
// <https://github.com/libsdl-org/SDL/issues/5340>
if (capture) {
int mouse_x, mouse_y;
SDL_GetGlobalMouseState(&mouse_x, &mouse_y);
int x, y, w, h;
SDL_GetWindowPosition(screen->window, &x, &y);
SDL_GetWindowSize(screen->window, &w, &h);
bool outside_window = mouse_x < x || mouse_x >= x + w
|| mouse_y < y || mouse_y >= y + h;
if (outside_window) {
SDL_WarpMouseInWindow(screen->window, w / 2, h / 2);
}
}
#else
(void) screen;
#endif
if (SDL_SetRelativeMouseMode(capture)) {
LOGE("Could not set relative mouse mode to %s: %s",
capture ? "true" : "false", SDL_GetError());
}
}
static inline bool
sc_screen_get_mouse_capture(struct sc_screen *screen) {
(void) screen;
return SDL_GetRelativeMouseMode();
}
static inline void
sc_screen_toggle_mouse_capture(struct sc_screen *screen) {
(void) screen;
bool new_value = !sc_screen_get_mouse_capture(screen);
sc_screen_set_mouse_capture(screen, new_value);
}
static void
sc_screen_update_content_rect(struct sc_screen *screen) {
assert(screen->video);
@ -330,6 +371,7 @@ sc_screen_init(struct sc_screen *screen,
screen->fullscreen = false;
screen->maximized = false;
screen->minimized = false;
screen->mouse_capture_key_pressed = 0;
screen->paused = false;
screen->resume_frame = NULL;
screen->orientation = SC_ORIENTATION_0;
@ -444,9 +486,6 @@ sc_screen_init(struct sc_screen *screen,
sc_input_manager_init(&screen->im, &im_params);
// Initialize even if not used for simplicity
sc_mouse_capture_init(&screen->mc, screen->window, params->shortcut_mods);
#ifdef CONTINUOUS_RESIZING_WORKAROUND
if (screen->video) {
SDL_AddEventWatch(event_watcher, screen);
@ -467,7 +506,7 @@ sc_screen_init(struct sc_screen *screen,
if (!screen->video && sc_screen_is_relative_mode(screen)) {
// Capture mouse immediately if video mirroring is disabled
sc_mouse_capture_set_active(&screen->mc, true);
sc_screen_set_mouse_capture(screen, true);
}
return true;
@ -499,7 +538,7 @@ sc_screen_show_initial_window(struct sc_screen *screen) {
SDL_SetWindowPosition(screen->window, x, y);
if (screen->req.fullscreen) {
sc_screen_toggle_fullscreen(screen);
sc_screen_switch_fullscreen(screen);
}
if (screen->req.start_fps_counter) {
@ -674,7 +713,7 @@ sc_screen_apply_frame(struct sc_screen *screen) {
if (sc_screen_is_relative_mode(screen)) {
// Capture mouse on start
sc_mouse_capture_set_active(&screen->mc, true);
sc_screen_set_mouse_capture(screen, true);
}
}
@ -735,7 +774,7 @@ sc_screen_set_paused(struct sc_screen *screen, bool paused) {
}
void
sc_screen_toggle_fullscreen(struct sc_screen *screen) {
sc_screen_switch_fullscreen(struct sc_screen *screen) {
assert(screen->video);
uint32_t new_mode = screen->fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP;
@ -798,8 +837,15 @@ sc_screen_resize_to_pixel_perfect(struct sc_screen *screen) {
content_size.height);
}
static inline bool
sc_screen_is_mouse_capture_key(SDL_Keycode key) {
return key == SDLK_LALT || key == SDLK_LGUI || key == SDLK_RGUI;
}
bool
sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
bool relative_mode = sc_screen_is_relative_mode(screen);
switch (event->type) {
case SC_EVENT_SCREEN_INIT_SIZE: {
// The initial size is passed via screen->frame_size
@ -857,14 +903,69 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
apply_pending_resize(screen);
sc_screen_render(screen, true);
break;
case SDL_WINDOWEVENT_FOCUS_LOST:
if (relative_mode) {
sc_screen_set_mouse_capture(screen, false);
}
break;
}
return true;
}
if (sc_screen_is_relative_mode(screen)
&& sc_mouse_capture_handle_event(&screen->mc, event)) {
// The mouse capture handler consumed the event
return true;
case SDL_KEYDOWN:
if (relative_mode) {
SDL_Keycode key = event->key.keysym.sym;
if (sc_screen_is_mouse_capture_key(key)) {
if (!screen->mouse_capture_key_pressed) {
screen->mouse_capture_key_pressed = key;
} else {
// Another mouse capture key has been pressed, cancel
// mouse (un)capture
screen->mouse_capture_key_pressed = 0;
}
// Mouse capture keys are never forwarded to the device
return true;
}
}
break;
case SDL_KEYUP:
if (relative_mode) {
SDL_Keycode key = event->key.keysym.sym;
SDL_Keycode cap = screen->mouse_capture_key_pressed;
screen->mouse_capture_key_pressed = 0;
if (sc_screen_is_mouse_capture_key(key)) {
if (key == cap) {
// A mouse capture key has been pressed then released:
// toggle the capture mouse mode
sc_screen_toggle_mouse_capture(screen);
}
// Mouse capture keys are never forwarded to the device
return true;
}
}
break;
case SDL_MOUSEWHEEL:
case SDL_MOUSEMOTION:
case SDL_MOUSEBUTTONDOWN:
if (relative_mode && !sc_screen_get_mouse_capture(screen)) {
// Do not forward to input manager, the mouse will be captured
// on SDL_MOUSEBUTTONUP
return true;
}
break;
case SDL_FINGERMOTION:
case SDL_FINGERDOWN:
case SDL_FINGERUP:
if (relative_mode) {
// Touch events are not compatible with relative mode
// (coordinates are not relative)
return true;
}
break;
case SDL_MOUSEBUTTONUP:
if (relative_mode && !sc_screen_get_mouse_capture(screen)) {
sc_screen_set_mouse_capture(screen, true);
return true;
}
break;
}
sc_input_manager_handle_event(&screen->im, event);

View File

@ -13,7 +13,6 @@
#include "fps_counter.h"
#include "frame_buffer.h"
#include "input_manager.h"
#include "mouse_capture.h"
#include "opengl.h"
#include "options.h"
#include "trait/key_processor.h"
@ -31,7 +30,6 @@ struct sc_screen {
struct sc_display display;
struct sc_input_manager im;
struct sc_mouse_capture mc; // only used in mouse relative mode
struct sc_frame_buffer fb;
struct sc_fps_counter fps_counter;
@ -63,6 +61,10 @@ struct sc_screen {
bool maximized;
bool minimized;
// To enable/disable mouse capture, a mouse capture key (LALT, LGUI or
// RGUI) must be pressed. This variable tracks the pressed capture key.
SDL_Keycode mouse_capture_key_pressed;
AVFrame *frame;
bool paused;
@ -124,9 +126,9 @@ sc_screen_destroy(struct sc_screen *screen);
void
sc_screen_hide_window(struct sc_screen *screen);
// toggle the fullscreen mode
// switch the fullscreen mode
void
sc_screen_toggle_fullscreen(struct sc_screen *screen);
sc_screen_switch_fullscreen(struct sc_screen *screen);
// resize window to optimal size (remove black borders)
void

View File

@ -9,7 +9,6 @@
#include "adb/adb.h"
#include "util/binary.h"
#include "util/env.h"
#include "util/file.h"
#include "util/log.h"
#include "util/net_intr.h"
@ -26,22 +25,35 @@
static char *
get_server_path(void) {
char *server_path = sc_get_env("SCRCPY_SERVER_PATH");
if (server_path) {
#ifdef __WINDOWS__
const wchar_t *server_path_env = _wgetenv(L"SCRCPY_SERVER_PATH");
#else
const char *server_path_env = getenv("SCRCPY_SERVER_PATH");
#endif
if (server_path_env) {
// if the envvar is set, use it
#ifdef __WINDOWS__
char *server_path = sc_str_from_wchars(server_path_env);
#else
char *server_path = strdup(server_path_env);
#endif
if (!server_path) {
LOG_OOM();
return NULL;
}
LOGD("Using SCRCPY_SERVER_PATH: %s", server_path);
return server_path;
}
#ifndef PORTABLE
LOGD("Using server: " SC_SERVER_PATH_DEFAULT);
server_path = strdup(SC_SERVER_PATH_DEFAULT);
char *server_path = strdup(SC_SERVER_PATH_DEFAULT);
if (!server_path) {
LOG_OOM();
return NULL;
}
#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) {
LOGE("Could not get local file path, "
"using " SC_SERVER_FILENAME " from current directory");
@ -54,6 +66,56 @@ get_server_path(void) {
return server_path;
}
static void
sc_server_params_destroy(struct sc_server_params *params) {
// The server stores a copy of the params provided by the user
free((char *) params->req_serial);
free((char *) params->crop);
free((char *) params->video_codec_options);
free((char *) params->audio_codec_options);
free((char *) params->video_encoder);
free((char *) params->audio_encoder);
free((char *) params->tcpip_dst);
free((char *) params->camera_id);
free((char *) params->camera_ar);
}
static bool
sc_server_params_copy(struct sc_server_params *dst,
const struct sc_server_params *src) {
*dst = *src;
// The params reference user-allocated memory, so we must copy them to
// handle them from another thread
#define COPY(FIELD) do { \
dst->FIELD = NULL; \
if (src->FIELD) { \
dst->FIELD = strdup(src->FIELD); \
if (!dst->FIELD) { \
goto error; \
} \
} \
} while(0)
COPY(req_serial);
COPY(crop);
COPY(video_codec_options);
COPY(audio_codec_options);
COPY(video_encoder);
COPY(audio_encoder);
COPY(tcpip_dst);
COPY(camera_id);
COPY(camera_ar);
#undef COPY
return true;
error:
sc_server_params_destroy(dst);
return false;
}
static bool
push_server(struct sc_intr *intr, const char *serial) {
char *server_path = get_server_path();
@ -189,31 +251,18 @@ execute_server(struct sc_server *server,
cmd[count++] = "app_process";
#ifdef SERVER_DEBUGGER
uint16_t sdk_version = sc_adb_get_device_sdk_version(&server->intr, serial);
if (!sdk_version) {
LOGE("Could not determine SDK version");
return 0;
}
# define SERVER_DEBUGGER_PORT "5005"
const char *dbg;
if (sdk_version < 28) {
// Android < 9
dbg = "-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address="
SERVER_DEBUGGER_PORT;
} else if (sdk_version < 30) {
// Android >= 9 && Android < 11
dbg = "-XjdwpProvider:internal -XjdwpOptions:transport=dt_socket,"
"suspend=y,server=y,address=" SERVER_DEBUGGER_PORT;
} else {
// Android >= 11
// Contrary to the other methods, this does not suspend on start.
// <https://github.com/Genymobile/scrcpy/pull/5466>
dbg = "-XjdwpProvider:adbconnection";
}
cmd[count++] = dbg;
cmd[count++] =
# ifdef SERVER_DEBUGGER_METHOD_NEW
/* Android 9 and above */
"-XjdwpProvider:internal -XjdwpOptions:transport=dt_socket,suspend=y,"
"server=y,address="
# else
/* Android 8 and below */
"-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address="
# endif
SERVER_DEBUGGER_PORT;
#endif
cmd[count++] = "/"; // unused
cmd[count++] = "com.genymobile.scrcpy.Server";
cmd[count++] = SCRCPY_VERSION;
@ -275,21 +324,9 @@ execute_server(struct sc_server *server,
VALIDATE_STRING(params->max_fps);
ADD_PARAM("max_fps=%s", params->max_fps);
}
if (params->angle) {
VALIDATE_STRING(params->angle);
ADD_PARAM("angle=%s", params->angle);
}
if (params->capture_orientation_lock != SC_ORIENTATION_UNLOCKED
|| params->capture_orientation != SC_ORIENTATION_0) {
if (params->capture_orientation_lock == SC_ORIENTATION_LOCKED_INITIAL) {
ADD_PARAM("capture_orientation=@");
} else {
const char *orient =
sc_orientation_get_name(params->capture_orientation);
bool locked =
params->capture_orientation_lock != SC_ORIENTATION_UNLOCKED;
ADD_PARAM("capture_orientation=%s%s", locked ? "@" : "", orient);
}
if (params->lock_video_orientation != SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) {
ADD_PARAM("lock_video_orientation=%" PRIi8,
params->lock_video_orientation);
}
if (server->tunnel.forward) {
ADD_PARAM("tunnel_forward=true");
@ -333,11 +370,6 @@ execute_server(struct sc_server *server,
if (params->stay_awake) {
ADD_PARAM("stay_awake=true");
}
if (params->screen_off_timeout != -1) {
assert(params->screen_off_timeout >= 0);
uint64_t ms = SC_TICK_TO_MS(params->screen_off_timeout);
ADD_PARAM("screen_off_timeout=%" PRIu64, ms);
}
if (params->video_codec_options) {
VALIDATE_STRING(params->video_codec_options);
ADD_PARAM("video_codec_options=%s", params->video_codec_options);
@ -373,13 +405,6 @@ execute_server(struct sc_server *server,
// By default, power_on is true
ADD_PARAM("power_on=false");
}
if (params->new_display) {
VALIDATE_STRING(params->new_display);
ADD_PARAM("new_display=%s", params->new_display);
}
if (!params->vd_system_decorations) {
ADD_PARAM("vd_system_decorations=false");
}
if (params->list & SC_OPTION_LIST_ENCODERS) {
ADD_PARAM("list_encoders=true");
}
@ -392,23 +417,16 @@ execute_server(struct sc_server *server,
if (params->list & SC_OPTION_LIST_CAMERA_SIZES) {
ADD_PARAM("list_camera_sizes=true");
}
if (params->list & SC_OPTION_LIST_APPS) {
ADD_PARAM("list_apps=true");
}
#undef ADD_PARAM
cmd[count++] = NULL;
#ifdef SERVER_DEBUGGER
LOGI("Server debugger listening%s...",
sdk_version < 30 ? " on port " SERVER_DEBUGGER_PORT : "");
// For Android < 11, from the computer:
// - run `adb forward tcp:5005 tcp:5005`
// For Android >= 11:
// - execute `adb jdwp` to get the jdwp port
// - run `adb forward tcp:5005 jdwp:XXXX` (replace XXXX)
//
LOGI("Server debugger waiting for a client on device port "
SERVER_DEBUGGER_PORT "...");
// From the computer, run
// adb forward tcp:5005 tcp:5005
// Then, from Android Studio: Run > Debug > Edit configurations...
// On the left, click on '+', "Remote", with:
// Host: localhost
@ -481,25 +499,22 @@ connect_to_server(struct sc_server *server, unsigned attempts, sc_tick delay,
bool
sc_server_init(struct sc_server *server, const struct sc_server_params *params,
const struct sc_server_callbacks *cbs, void *cbs_userdata) {
// The allocated data in params (const char *) must remain valid until the
// end of the program
server->params = *params;
bool ok = sc_adb_init();
bool ok = sc_server_params_copy(&server->params, params);
if (!ok) {
LOG_OOM();
return false;
}
ok = sc_mutex_init(&server->mutex);
if (!ok) {
sc_adb_destroy();
sc_server_params_destroy(&server->params);
return false;
}
ok = sc_cond_init(&server->cond_stopped);
if (!ok) {
sc_mutex_destroy(&server->mutex);
sc_adb_destroy();
sc_server_params_destroy(&server->params);
return false;
}
@ -507,7 +522,7 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params,
if (!ok) {
sc_cond_destroy(&server->cond_stopped);
sc_mutex_destroy(&server->mutex);
sc_adb_destroy();
sc_server_params_destroy(&server->params);
return false;
}
@ -1146,9 +1161,8 @@ sc_server_destroy(struct sc_server *server) {
free(server->serial);
free(server->device_socket_name);
sc_server_params_destroy(&server->params);
sc_intr_destroy(&server->intr);
sc_cond_destroy(&server->cond_stopped);
sc_mutex_destroy(&server->mutex);
sc_adb_destroy();
}

View File

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

View File

@ -1,60 +0,0 @@
#ifndef SC_SHORTCUT_MOD_H
#define SC_SHORTCUT_MOD_H
#include "common.h"
#include <stdbool.h>
#include <stdint.h>
#include <SDL2/SDL_keycode.h>
#include "options.h"
#define SC_SDL_SHORTCUT_MODS_MASK (KMOD_CTRL | KMOD_ALT | KMOD_GUI)
// input: OR of enum sc_shortcut_mod
// output: OR of SDL_Keymod
static inline uint16_t
sc_shortcut_mods_to_sdl(uint8_t shortcut_mods) {
uint16_t sdl_mod = 0;
if (shortcut_mods & SC_SHORTCUT_MOD_LCTRL) {
sdl_mod |= KMOD_LCTRL;
}
if (shortcut_mods & SC_SHORTCUT_MOD_RCTRL) {
sdl_mod |= KMOD_RCTRL;
}
if (shortcut_mods & SC_SHORTCUT_MOD_LALT) {
sdl_mod |= KMOD_LALT;
}
if (shortcut_mods & SC_SHORTCUT_MOD_RALT) {
sdl_mod |= KMOD_RALT;
}
if (shortcut_mods & SC_SHORTCUT_MOD_LSUPER) {
sdl_mod |= KMOD_LGUI;
}
if (shortcut_mods & SC_SHORTCUT_MOD_RSUPER) {
sdl_mod |= KMOD_RGUI;
}
return sdl_mod;
}
static inline bool
sc_shortcut_mods_is_shortcut_mod(uint16_t sdl_shortcut_mods, uint16_t sdl_mod) {
// sdl_shortcut_mods must be within the mask
assert(!(sdl_shortcut_mods & ~SC_SDL_SHORTCUT_MODS_MASK));
// at least one shortcut mod pressed?
return sdl_mod & sdl_shortcut_mods;
}
static inline bool
sc_shortcut_mods_is_shortcut_key(uint16_t sdl_shortcut_mods,
SDL_Keycode keycode) {
return (sdl_shortcut_mods & KMOD_LCTRL && keycode == SDLK_LCTRL)
|| (sdl_shortcut_mods & KMOD_RCTRL && keycode == SDLK_RCTRL)
|| (sdl_shortcut_mods & KMOD_LALT && keycode == SDLK_LALT)
|| (sdl_shortcut_mods & KMOD_RALT && keycode == SDLK_RALT)
|| (sdl_shortcut_mods & KMOD_LGUI && keycode == SDLK_LGUI)
|| (sdl_shortcut_mods & KMOD_RGUI && keycode == SDLK_RGUI);
}
#endif

View File

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

View File

@ -1,8 +1,6 @@
#ifndef SC_AOA_HID_H
#define SC_AOA_HID_H
#include "common.h"
#include <stdint.h>
#include <stdbool.h>

View File

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

View File

@ -4,6 +4,47 @@
#include "options.h"
#include "util/log.h"
static void
sc_screen_otg_set_mouse_capture(struct sc_screen_otg *screen, bool capture) {
#ifdef __APPLE__
// Workaround for SDL bug on macOS:
// <https://github.com/libsdl-org/SDL/issues/5340>
if (capture) {
int mouse_x, mouse_y;
SDL_GetGlobalMouseState(&mouse_x, &mouse_y);
int x, y, w, h;
SDL_GetWindowPosition(screen->window, &x, &y);
SDL_GetWindowSize(screen->window, &w, &h);
bool outside_window = mouse_x < x || mouse_x >= x + w
|| mouse_y < y || mouse_y >= y + h;
if (outside_window) {
SDL_WarpMouseInWindow(screen->window, w / 2, h / 2);
}
}
#else
(void) screen;
#endif
if (SDL_SetRelativeMouseMode(capture)) {
LOGE("Could not set relative mouse mode to %s: %s",
capture ? "true" : "false", SDL_GetError());
}
}
static inline bool
sc_screen_otg_get_mouse_capture(struct sc_screen_otg *screen) {
(void) screen;
return SDL_GetRelativeMouseMode();
}
static inline void
sc_screen_otg_toggle_mouse_capture(struct sc_screen_otg *screen) {
(void) screen;
bool new_value = !sc_screen_otg_get_mouse_capture(screen);
sc_screen_otg_set_mouse_capture(screen, new_value);
}
static void
sc_screen_otg_render(struct sc_screen_otg *screen) {
SDL_RenderClear(screen->renderer);
@ -20,6 +61,8 @@ sc_screen_otg_init(struct sc_screen_otg *screen,
screen->mouse = params->mouse;
screen->gamepad = params->gamepad;
screen->mouse_capture_key_pressed = 0;
const char *title = params->window_title;
assert(title);
@ -70,11 +113,9 @@ sc_screen_otg_init(struct sc_screen_otg *screen,
LOGW("Could not load icon");
}
sc_mouse_capture_init(&screen->mc, screen->window, params->shortcut_mods);
if (screen->mouse) {
// Capture mouse on start
sc_mouse_capture_set_active(&screen->mc, true);
sc_screen_otg_set_mouse_capture(screen, true);
}
return true;
@ -96,6 +137,11 @@ sc_screen_otg_destroy(struct sc_screen_otg *screen) {
SDL_DestroyWindow(screen->window);
}
static inline bool
sc_screen_otg_is_mouse_capture_key(SDL_Keycode key) {
return key == SDLK_LALT || key == SDLK_LGUI || key == SDLK_RGUI;
}
static void
sc_screen_otg_process_key(struct sc_screen_otg *screen,
const SDL_KeyboardEvent *event) {
@ -252,46 +298,80 @@ sc_screen_otg_process_gamepad_button(struct sc_screen_otg *screen,
void
sc_screen_otg_handle_event(struct sc_screen_otg *screen, SDL_Event *event) {
if (sc_mouse_capture_handle_event(&screen->mc, event)) {
// The mouse capture handler consumed the event
return;
}
switch (event->type) {
case SDL_WINDOWEVENT:
switch (event->window.event) {
case SDL_WINDOWEVENT_EXPOSED:
sc_screen_otg_render(screen);
break;
case SDL_WINDOWEVENT_FOCUS_LOST:
if (screen->mouse) {
sc_screen_otg_set_mouse_capture(screen, false);
}
break;
}
return;
case SDL_KEYDOWN:
if (screen->mouse) {
SDL_Keycode key = event->key.keysym.sym;
if (sc_screen_otg_is_mouse_capture_key(key)) {
if (!screen->mouse_capture_key_pressed) {
screen->mouse_capture_key_pressed = key;
} else {
// Another mouse capture key has been pressed, cancel
// mouse (un)capture
screen->mouse_capture_key_pressed = 0;
}
// Mouse capture keys are never forwarded to the device
return;
}
}
if (screen->keyboard) {
sc_screen_otg_process_key(screen, &event->key);
}
break;
case SDL_KEYUP:
if (screen->mouse) {
SDL_Keycode key = event->key.keysym.sym;
SDL_Keycode cap = screen->mouse_capture_key_pressed;
screen->mouse_capture_key_pressed = 0;
if (sc_screen_otg_is_mouse_capture_key(key)) {
if (key == cap) {
// A mouse capture key has been pressed then released:
// toggle the capture mouse mode
sc_screen_otg_toggle_mouse_capture(screen);
}
// Mouse capture keys are never forwarded to the device
return;
}
}
if (screen->keyboard) {
sc_screen_otg_process_key(screen, &event->key);
}
break;
case SDL_MOUSEMOTION:
if (screen->mouse) {
if (screen->mouse && sc_screen_otg_get_mouse_capture(screen)) {
sc_screen_otg_process_mouse_motion(screen, &event->motion);
}
break;
case SDL_MOUSEBUTTONDOWN:
if (screen->mouse) {
if (screen->mouse && sc_screen_otg_get_mouse_capture(screen)) {
sc_screen_otg_process_mouse_button(screen, &event->button);
}
break;
case SDL_MOUSEBUTTONUP:
if (screen->mouse) {
sc_screen_otg_process_mouse_button(screen, &event->button);
if (sc_screen_otg_get_mouse_capture(screen)) {
sc_screen_otg_process_mouse_button(screen, &event->button);
} else {
sc_screen_otg_set_mouse_capture(screen, true);
}
}
break;
case SDL_MOUSEWHEEL:
if (screen->mouse) {
if (screen->mouse && sc_screen_otg_get_mouse_capture(screen)) {
sc_screen_otg_process_mouse_wheel(screen, &event->wheel);
}
break;

View File

@ -8,7 +8,6 @@
#include "keyboard_aoa.h"
#include "mouse_aoa.h"
#include "mouse_capture.h"
#include "gamepad_aoa.h"
struct sc_screen_otg {
@ -20,7 +19,8 @@ struct sc_screen_otg {
SDL_Renderer *renderer;
SDL_Texture *texture;
struct sc_mouse_capture mc;
// See equivalent mechanism in screen.h
SDL_Keycode mouse_capture_key_pressed;
};
struct sc_screen_otg_params {
@ -35,7 +35,6 @@ struct sc_screen_otg_params {
uint16_t window_width;
uint16_t window_height;
bool window_borderless;
uint8_t shortcut_mods; // OR of enum sc_shortcut_mod values
};
bool

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

View File

@ -7,36 +7,21 @@
#include <stdint.h>
#ifdef _WIN32
# include <winsock2.h>
typedef SOCKET sc_raw_socket;
# define SC_RAW_SOCKET_NONE INVALID_SOCKET
#else // not _WIN32
# include <sys/socket.h>
typedef int sc_raw_socket;
# define SC_RAW_SOCKET_NONE -1
#endif
#if defined(_WIN32) || defined(__APPLE__)
// On Windows and macOS, shutdown() does not interrupt accept() or read()
// calls, so net_interrupt() must call close() instead, and net_close() must
// behave accordingly.
// This causes a small race condition (once the socket is closed, its
// handle becomes invalid and may in theory be reassigned before another
// thread calls accept() or read()), but it is deemed acceptable as a
// workaround.
# define SC_SOCKET_CLOSE_ON_INTERRUPT
#endif
#ifdef SC_SOCKET_CLOSE_ON_INTERRUPT
# include <stdatomic.h>
# define SC_SOCKET_NONE NULL
typedef struct sc_socket_wrapper {
sc_raw_socket socket;
typedef struct sc_socket_windows {
SOCKET socket;
atomic_flag closed;
} *sc_socket;
#else
#else // not _WIN32
# include <sys/socket.h>
# define SC_SOCKET_NONE -1
typedef sc_raw_socket sc_socket;
typedef int sc_socket;
#endif
#define IPV4_LOCALHOST 0x7F000001

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.7.1'
classpath 'com.android.tools.build:gradle:8.3.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files

View File

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

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
# 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
sudo dnf install java-devel
@ -233,10 +233,10 @@ install` must be run as root)._
#### Option 2: Use prebuilt server
- [`scrcpy-server-v3.0`][direct-scrcpy-server]
<sub>SHA-256: `800044c62a94d5fc16f5ab9c86d45b1050eae3eb436514d1b0d2fe2646b894ea`</sub>
- [`scrcpy-server-v2.7`][direct-scrcpy-server]
<sub>SHA-256: `a23c5659f36c260f105c022d27bcb3eafffa26070e7baa9eda66d01377a1adba`</sub>
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-server-v3.0
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-server-v2.7
Download the prebuilt server somewhere, and specify its path during the Meson
configuration:

View File

@ -23,20 +23,14 @@ To control the device without mirroring:
scrcpy --no-video --no-audio
```
By default, the mouse is disabled when video playback is turned off.
To control the device using a relative mouse, enable UHID mouse mode:
```bash
scrcpy --no-video --no-audio --mouse=uhid
scrcpy --no-video --no-audio -M # short version
```
By default, mouse mode is switched to UHID if video mirroring is disabled (a
relative mouse mode is required).
To also use a UHID keyboard, set it explicitly:
```bash
scrcpy --no-video --no-audio --mouse=uhid --keyboard=uhid
scrcpy --no-video --no-audio -MK # short version
scrcpy --no-video --no-audio --keyboard=uhid
scrcpy --no-video --no-audio -K # short version
```
To use AOA instead (over USB only):
@ -100,18 +94,14 @@ the content (if supported by the app) relative to the center of the screen.
https://github.com/Genymobile/scrcpy/assets/543275/26c4a920-9805-43f1-8d4c-608752d04767
To simulate a vertical tilt gesture: <kbd>Shift</kbd>+_click-and-move-up-or-down_.
To simulate a tilt gesture: <kbd>Shift</kbd>+_click-and-move-up-or-down_.
https://github.com/Genymobile/scrcpy/assets/543275/1e252341-4a90-4b29-9d11-9153b324669f
Similarly, to simulate a horizontal tilt gesture:
<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+_click-and-move-left-or-right_.
Technically, _scrcpy_ generates additional touch events from a "virtual finger"
at a location inverted through the center of the screen. When pressing
<kbd>Ctrl</kbd> the _x_ and _y_ coordinates are inverted. Using <kbd>Shift</kbd>
only inverts _x_, whereas using <kbd>Ctrl</kbd>+<kbd>Shift</kbd> only inverts
_y_.
only inverts _x_.
This only works for the default mouse mode (`--mouse=sdk`).

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)
of the device screen, with some additional headers for each packet. The client
decodes the video frames, and displays them as soon as possible, without
buffering (unless `--video-buffer=delay` is specified) to minimize latency. The
client is not aware of the device rotation (which is handled by the server), it
just knows the dimensions of the video frames it receives.
buffering (unless `--display-buffer=delay` is specified) to minimize latency.
The client is not aware of the device rotation (which is handled by the server),
it just knows the dimensions of the video frames it receives.
Similarly, if audio is enabled, then the server sends a raw audio stream (OPUS
by default) of the device audio output (or the microphone if
@ -461,30 +461,26 @@ meson setup 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:
```bash
adb forward tcp:5005 tcp:5005
```
For Android >= 11, first find the listening port:
```bash
adb jdwp
# press Ctrl+C to interrupt
```
Then redirect the resulting PID:
```bash
adb forward tcp:5005 jdwp:XXXX # replace XXXX
```
In Android Studio, _Run_ > _Debug_ > _Edit configurations..._ On the left, click
on `+`, _Remote_, and fill the form:
In Android Studio, _Run_ > _Debug_ > _Edit configurations..._ On the left, click on
`+`, _Remote_, and fill the form:
- Host: `localhost`
- Port: `5005`

View File

@ -18,21 +18,6 @@ The initial state is restored when _scrcpy_ is closed.
If the device is not plugged in (i.e. only connected over TCP/IP),
`--stay-awake` has no effect (this is the Android behavior).
This changes the value of [`stay_on_while_plugged_in`], setting which can be
changed manually:
[`stay_on_while_plugged_in`]: https://developer.android.com/reference/android/provider/Settings.Global#STAY_ON_WHILE_PLUGGED_IN
```bash
# get the current show_touches value
adb shell settings get global stay_on_while_plugged_in
# enable for AC/USB/wireless chargers
adb shell settings put global stay_on_while_plugged_in 7
# disable
adb shell settings put global stay_on_while_plugged_in 0
```
## Turn screen off
@ -61,40 +46,6 @@ scrcpy --turn-screen-off --stay-awake
scrcpy -Sw # short version
```
Since Android 15, it is possible to change this setting manually:
```
# turn screen off (0 for main display)
adb shell cmd display power-off 0
# turn screen on
adb shell cmd display power-on 0
```
## Screen off timeout
The Android screen automatically turns off after some delay.
To change this delay while scrcpy is running:
```bash
scrcpy --screen-off-timeout=300 # 300 seconds (5 minutes)
```
The initial value is restored on exit.
It is possible to change this setting manually:
```bash
# get the current screen_off_timeout value
adb shell settings get system screen_off_timeout
# set a new value (in milliseconds)
adb shell settings put system screen_off_timeout 30000
```
Note that the Android value is in milliseconds, but the scrcpy command line
argument is in seconds.
## Show touches
@ -111,16 +62,6 @@ scrcpy -t # short version
Note that it only shows _physical_ touches (by a finger on the device).
It is possible to change this setting manually:
```bash
# get the current show_touches value
adb shell settings get system show_touches
# enable show_touches
adb shell settings put system show_touches 1
# disable show_touches
adb shell settings put system show_touches 0
```
## Power off on close
@ -137,48 +78,3 @@ By default, on start, the device is powered on. To prevent this behavior:
```bash
scrcpy --no-power-on
```
## Start Android app
To list the Android apps installed on the device:
```bash
scrcpy --list-apps
```
An app, selected by its package name, can be launched on start:
```
scrcpy --start-app=org.mozilla.firefox
```
This feature can be used to run an app in a [virtual
display](virtual_display.md):
```
scrcpy --new-display=1920x1080 --start-app=org.videolan.vlc
```
The app can be optionally forced-stop before being started, by adding a `+`
prefix:
```
scrcpy --start-app=+org.mozilla.firefox
```
For convenience, it is also possible to select an app by its name, by adding a
`?` prefix:
```
scrcpy --start-app=?firefox
```
But retrieving app names may take some time (sometimes several seconds), so
passing the package name is recommended.
The `+` and `?` prefixes can be combined (in that order):
```
scrcpy --start-app=+?firefox
```

View File

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

View File

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

View File

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

View File

@ -30,7 +30,6 @@ _<kbd>[Super]</kbd> is typically the <kbd>Windows</kbd> or <kbd>Cmd</kbd> key._
| Flip display vertically | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd></kbd> _(up)_ \| <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd></kbd> _(down)_
| Pause or re-pause display | <kbd>MOD</kbd>+<kbd>z</kbd>
| Unpause display | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>z</kbd>
| Reset video capture/encoding | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>r</kbd>
| Resize window to 1:1 (pixel-perfect) | <kbd>MOD</kbd>+<kbd>g</kbd>
| Resize window to remove black borders | <kbd>MOD</kbd>+<kbd>w</kbd> \| _Double-left-click¹_
| Click on `HOME` | <kbd>MOD</kbd>+<kbd>h</kbd> \| _Middle-click_
@ -54,8 +53,7 @@ _<kbd>[Super]</kbd> is typically the <kbd>Windows</kbd> or <kbd>Cmd</kbd> key._
| Open keyboard settings (HID keyboard only) | <kbd>MOD</kbd>+<kbd>k</kbd>
| Enable/disable FPS counter (on stdout) | <kbd>MOD</kbd>+<kbd>i</kbd>
| Pinch-to-zoom/rotate | <kbd>Ctrl</kbd>+_click-and-move_
| Tilt vertically (slide with 2 fingers) | <kbd>Shift</kbd>+_click-and-move_
| Tilt horizontally (slide with 2 fingers) | <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+_click-and-move_
| Tilt (slide vertically with 2 fingers) | <kbd>Shift</kbd>+_click-and-move_
| Drag & drop APK file | Install APK from computer
| Drag & drop non-APK file | [Push file to device](control.md#push-file-to-device)

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
(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
@ -96,7 +93,7 @@ Sometimes, the default encoder may have issues or even crash, so it is useful to
try another one:
```bash
scrcpy --video-codec=h264 --video-encoder=OMX.qcom.video.encoder.avc
scrcpy --video-codec=h264 --video-encoder='OMX.qcom.video.encoder.avc'
```
@ -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
device to switch between portrait and landscape (the current running app may
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
recording.
- `--orientation` is applied on the client side, and affects display and
recording. For the display, it can be changed dynamically using
[shortcuts](shortcuts.md).
To capture the video with a specific orientation:
To lock the mirroring orientation (on the capture side):
```bash
scrcpy --capture-orientation=0
scrcpy --capture-orientation=90 # 90° clockwise
scrcpy --capture-orientation=180 # 180°
scrcpy --capture-orientation=270 # 270° clockwise
scrcpy --capture-orientation=flip0 # hflip
scrcpy --capture-orientation=flip90 # hflip + 90° clockwise
scrcpy --capture-orientation=flip180 # hflip + 180°
scrcpy --capture-orientation=flip270 # hflip + 270° clockwise
scrcpy --lock-video-orientation # initial (current) orientation
scrcpy --lock-video-orientation=0 # natural orientation
scrcpy --lock-video-orientation=90 # 90° clockwise
scrcpy --lock-video-orientation=180 # 180°
scrcpy --lock-video-orientation=270 # 270° clockwise
```
The capture orientation can be locked by using `@`, so that a physical device
rotation does not change the captured video orientation:
```bash
scrcpy --capture-orientation=@ # locked to the initial orientation
scrcpy --capture-orientation=@0 # locked to 0°
scrcpy --capture-orientation=@90 # locked to 90° clockwise
scrcpy --capture-orientation=@180 # locked to 180°
scrcpy --capture-orientation=@270 # locked to 270° clockwise
scrcpy --capture-orientation=@flip0 # locked to hflip
scrcpy --capture-orientation=@flip90 # locked to hflip + 90° clockwise
scrcpy --capture-orientation=@flip180 # locked to hflip + 180°
scrcpy --capture-orientation=@flip270 # locked to hflip + 270° clockwise
```
The capture orientation transform is applied after `--crop`, but before
`--angle`.
To orient the video (on the client side):
To orient the video (on the rendering side):
```bash
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.
## Angle
To rotate the video content by a custom angle (in degrees, clockwise):
```
scrcpy --angle=23
```
The center of rotation is the center of the visible area.
This transformation is applied after `--crop` and `--capture-orientation`.
## Crop
The device screen may be cropped to mirror only part of the screen.
@ -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
phone, landscape for a tablet).
Cropping is performed before `--capture-orientation` and `--angle`.
For display mirroring, `--max-size` is applied after cropping. For camera,
`--max-size` is applied first (because it selects the source size rather than
resizing the content).
If `--max-size` is also specified, resizing is applied after cropping.
## Display
@ -216,8 +175,6 @@ scrcpy --list-displays
A secondary display may only be controlled if the device runs at least Android
10 (otherwise it is mirrored as read-only).
It is also possible to create a [virtual display](virtual_display.md).
## Buffering
@ -232,15 +189,15 @@ The configuration is available independently for the display,
[v4l2 sinks](video.md#video4linux) and [audio](audio.md#buffering) playback.
```bash
scrcpy --video-buffer=50 # add 50ms buffering for video playback
scrcpy --audio-buffer=200 # set 200ms buffering for audio playback
scrcpy --display-buffer=50 # add 50ms buffering for display
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:
```bash
scrcpy --video-buffer=50 --v4l2-buffer=300
scrcpy --display-buffer=50 --v4l2-buffer=300
```

View File

@ -1,52 +0,0 @@
# Virtual display
## New display
To mirror a new virtual display instead of the device screen:
```bash
scrcpy --new-display=1920x1080
scrcpy --new-display=1920x1080/420 # force 420 dpi
scrcpy --new-display # use the main display size and density
scrcpy --new-display=/240 # use the main display size and 240 dpi
```
## Start app
On some devices, a launcher is available in the virtual display.
When no launcher is available (or if is explicitly disabled by
[`--no-vd-system-decorations`](#system-decorations)), the virtual display is
empty. In that case, you must [start an Android
app](device.md#start-android-app).
For example:
```bash
scrcpy --new-display=1920x1080 --start-app=org.videolan.vlc
```
The app may itself be a launcher. For example, to run the open source [Fossify
Launcher]:
```bash
scrcpy --new-display=1920x1080 --no-vd-system-decorations --start-app=org.fossify.home
```
[Fossify Launcher]: https://f-droid.org/en/packages/org.fossify.home/
## System decorations
By default, virtual display system decorations are enabled. To disable them, use
`--no-vd-system-decorations`:
```
scrcpy --new-display --no-vd-system-decorations
```
This is useful for some devices which might display a broken UI, or to disable
any default launcher UI available in virtual displays.
Note that if no app is started, no content will be rendered, so no video frame
will be produced at all.

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
project('scrcpy', 'c',
version: '3.0',
version: '2.7',
meson_version: '>= 0.48',
default_options: [
'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('prebuilt_server', type: 'string', description: 'Path of the prebuilt server')
option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server from the same directory as the scrcpy executable')
option('static', type: 'boolean', value: false, description: 'Use static dependencies')
option('server_debugger', type: 'boolean', value: false, description: 'Run a server debugger and wait for a client to be attached')
option('server_debugger_method', type: 'combo', choices: ['old', 'new'], value: 'new', description: 'Select the debugger method (Android < 9: "old", Android >= 9: "new")')
option('v4l2', type: 'boolean', value: true, description: 'Enable V4L2 feature when supported')
option('usb', type: 'boolean', value: true, description: 'Enable HID/OTG features when supported')

131
release.mk Normal file
View File

@ -0,0 +1,131 @@
# This makefile provides recipes to build a "portable" version of scrcpy for
# Windows.
#
# Here, "portable" means that the client and server binaries are expected to be
# anywhere, but in the same directory, instead of well-defined separate
# locations (e.g. /usr/bin/scrcpy and /usr/share/scrcpy/scrcpy-server).
#
# In particular, this implies to change the location from where the client push
# the server to the device.
.PHONY: default clean \
test \
build-server \
prepare-deps \
build-win32 build-win64 \
dist-win32 dist-win64 \
zip-win32 zip-win64 \
release
GRADLE ?= ./gradlew
TEST_BUILD_DIR := build-test
SERVER_BUILD_DIR := build-server
WIN32_BUILD_DIR := build-win32
WIN64_BUILD_DIR := build-win64
VERSION ?= $(shell git describe --tags --exclude='*install-release' --always)
DIST := dist
WIN32_TARGET_DIR := scrcpy-win32-$(VERSION)
WIN64_TARGET_DIR := scrcpy-win64-$(VERSION)
WIN32_TARGET := $(WIN32_TARGET_DIR).zip
WIN64_TARGET := $(WIN64_TARGET_DIR).zip
RELEASE_DIR := release-$(VERSION)
release: clean test build-server zip-win32 zip-win64
mkdir -p "$(RELEASE_DIR)"
cp "$(SERVER_BUILD_DIR)/server/scrcpy-server" \
"$(RELEASE_DIR)/scrcpy-server-$(VERSION)"
cp "$(DIST)/$(WIN32_TARGET)" "$(RELEASE_DIR)"
cp "$(DIST)/$(WIN64_TARGET)" "$(RELEASE_DIR)"
cd "$(RELEASE_DIR)" && \
sha256sum "scrcpy-server-$(VERSION)" \
"scrcpy-win32-$(VERSION).zip" \
"scrcpy-win64-$(VERSION).zip" > SHA256SUMS.txt
@echo "Release generated in $(RELEASE_DIR)/"
clean:
$(GRADLE) clean
rm -rf "$(DIST)" "$(TEST_BUILD_DIR)" "$(SERVER_BUILD_DIR)" \
"$(WIN32_BUILD_DIR)" "$(WIN64_BUILD_DIR)"
test:
[ -d "$(TEST_BUILD_DIR)" ] || ( mkdir "$(TEST_BUILD_DIR)" && \
meson setup "$(TEST_BUILD_DIR)" -Db_sanitize=address )
ninja -C "$(TEST_BUILD_DIR)"
$(GRADLE) -p server check
build-server:
[ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \
meson setup "$(SERVER_BUILD_DIR)" --buildtype release -Dcompile_app=false )
ninja -C "$(SERVER_BUILD_DIR)"
prepare-deps-win32:
@app/deps/adb.sh win32
@app/deps/sdl.sh win32
@app/deps/ffmpeg.sh win32
@app/deps/libusb.sh win32
prepare-deps-win64:
@app/deps/adb.sh win64
@app/deps/sdl.sh win64
@app/deps/ffmpeg.sh win64
@app/deps/libusb.sh win64
build-win32: prepare-deps-win32
rm -rf "$(WIN32_BUILD_DIR)"
mkdir -p "$(WIN32_BUILD_DIR)/local"
meson setup "$(WIN32_BUILD_DIR)" \
--pkg-config-path="app/deps/work/install/win32/lib/pkgconfig" \
-Dc_args="-I$(PWD)/app/deps/work/install/win32/include" \
-Dc_link_args="-L$(PWD)/app/deps/work/install/win32/lib" \
--cross-file=cross_win32.txt \
--buildtype=release --strip -Db_lto=true \
-Dcompile_server=false \
-Dportable=true
ninja -C "$(WIN32_BUILD_DIR)"
build-win64: prepare-deps-win64
rm -rf "$(WIN64_BUILD_DIR)"
mkdir -p "$(WIN64_BUILD_DIR)/local"
meson setup "$(WIN64_BUILD_DIR)" \
--pkg-config-path="app/deps/work/install/win64/lib/pkgconfig" \
-Dc_args="-I$(PWD)/app/deps/work/install/win64/include" \
-Dc_link_args="-L$(PWD)/app/deps/work/install/win64/lib" \
--cross-file=cross_win64.txt \
--buildtype=release --strip -Db_lto=true \
-Dcompile_server=false \
-Dportable=true
ninja -C "$(WIN64_BUILD_DIR)"
dist-win32: build-server build-win32
mkdir -p "$(DIST)/$(WIN32_TARGET_DIR)"
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN32_TARGET_DIR)/"
cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/data/scrcpy-console.bat "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/deps/work/install/win32/bin/*.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/deps/work/install/win32/bin/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
dist-win64: build-server build-win64
mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)"
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN64_TARGET_DIR)/"
cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/data/scrcpy-console.bat "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/deps/work/install/win64/bin/*.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/deps/work/install/win64/bin/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
zip-win32: dist-win32
cd "$(DIST)"; \
zip -r "$(WIN32_TARGET)" "$(WIN32_TARGET_DIR)"
zip-win64: dist-win64
cd "$(DIST)"; \
zip -r "$(WIN64_TARGET)" "$(WIN64_TARGET_DIR)"

2
release.sh Executable file
View File

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

2
release/.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@ -1,32 +0,0 @@
package com.genymobile.scrcpy;
import android.os.Build;
/**
* Android version code constants, done right.
* <p/>
* <a href="https://apilevels.com/">API levels</a>
*/
public final class AndroidVersions {
private AndroidVersions() {
// not instantiable
}
public static final int API_21_ANDROID_5_0 = Build.VERSION_CODES.LOLLIPOP;
public static final int API_22_ANDROID_5_1 = Build.VERSION_CODES.LOLLIPOP_MR1;
public static final int API_23_ANDROID_6_0 = Build.VERSION_CODES.M;
public static final int API_24_ANDROID_7_0 = Build.VERSION_CODES.N;
public static final int API_25_ANDROID_7_1 = Build.VERSION_CODES.N_MR1;
public static final int API_26_ANDROID_8_0 = Build.VERSION_CODES.O;
public static final int API_27_ANDROID_8_1 = Build.VERSION_CODES.O_MR1;
public static final int API_28_ANDROID_9 = Build.VERSION_CODES.P;
public static final int API_29_ANDROID_10 = Build.VERSION_CODES.Q;
public static final int API_30_ANDROID_11 = Build.VERSION_CODES.R;
public static final int API_31_ANDROID_12 = Build.VERSION_CODES.S;
public static final int API_32_ANDROID_12L = Build.VERSION_CODES.S_V2;
public static final int API_33_ANDROID_13 = Build.VERSION_CODES.TIRAMISU;
public static final int API_34_ANDROID_14 = Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
public static final int API_35_ANDROID_15 = Build.VERSION_CODES.VANILLA_ICE_CREAM;
}

View File

@ -5,8 +5,6 @@ import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.Settings;
import com.genymobile.scrcpy.util.SettingsException;
import android.os.BatteryManager;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
@ -18,132 +16,59 @@ import java.io.OutputStream;
*/
public final class CleanUp {
// Dynamic options
private static final int PENDING_CHANGE_DISPLAY_POWER = 1 << 0;
private int pendingChanges;
private boolean pendingRestoreDisplayPower;
private static final int MSG_TYPE_MASK = 0b11;
private static final int MSG_TYPE_RESTORE_STAY_ON = 0;
private static final int MSG_TYPE_DISABLE_SHOW_TOUCHES = 1;
private static final int MSG_TYPE_RESTORE_NORMAL_POWER_MODE = 2;
private static final int MSG_TYPE_POWER_OFF_SCREEN = 3;
private Thread thread;
private static final int MSG_PARAM_SHIFT = 2;
private CleanUp(Options options) {
thread = new Thread(() -> runCleanUp(options), "cleanup");
thread.start();
private final OutputStream out;
public CleanUp(OutputStream out) {
this.out = out;
}
public static CleanUp start(Options options) {
return new CleanUp(options);
}
public void interrupt() {
thread.interrupt();
}
public void join() throws InterruptedException {
thread.join();
}
private void runCleanUp(Options options) {
boolean disableShowTouches = false;
if (options.getShowTouches()) {
try {
String oldValue = Settings.getAndPutValue(Settings.TABLE_SYSTEM, "show_touches", "1");
// If "show touches" was disabled, it must be disabled back on clean up
disableShowTouches = !"1".equals(oldValue);
} catch (SettingsException e) {
Ln.e("Could not change \"show_touches\"", e);
}
}
int restoreStayOn = -1;
if (options.getStayAwake()) {
int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS;
try {
String oldValue = Settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn));
try {
int currentStayOn = Integer.parseInt(oldValue);
// Restore only if the current value is different
if (currentStayOn != stayOn) {
restoreStayOn = currentStayOn;
}
} catch (NumberFormatException e) {
// ignore
}
} catch (SettingsException e) {
Ln.e("Could not change \"stay_on_while_plugged_in\"", e);
}
}
int restoreScreenOffTimeout = -1;
int screenOffTimeout = options.getScreenOffTimeout();
if (screenOffTimeout != -1) {
try {
String oldValue = Settings.getAndPutValue(Settings.TABLE_SYSTEM, "screen_off_timeout", String.valueOf(screenOffTimeout));
try {
int currentScreenOffTimeout = Integer.parseInt(oldValue);
// Restore only if the current value is different
if (currentScreenOffTimeout != screenOffTimeout) {
restoreScreenOffTimeout = currentScreenOffTimeout;
}
} catch (NumberFormatException e) {
// ignore
}
} catch (SettingsException e) {
Ln.e("Could not change \"screen_off_timeout\"", e);
}
}
boolean powerOffScreen = options.getPowerOffScreenOnClose();
int displayId = options.getDisplayId();
try {
run(displayId, restoreStayOn, disableShowTouches, powerOffScreen, restoreScreenOffTimeout);
} catch (InterruptedException e) {
// ignore
} catch (IOException e) {
Ln.e("Clean up I/O exception", e);
}
}
private void run(int displayId, int restoreStayOn, boolean disableShowTouches, boolean powerOffScreen, int restoreScreenOffTimeout)
throws IOException, InterruptedException {
String[] cmd = {
"app_process",
"/",
CleanUp.class.getName(),
String.valueOf(displayId),
String.valueOf(restoreStayOn),
String.valueOf(disableShowTouches),
String.valueOf(powerOffScreen),
String.valueOf(restoreScreenOffTimeout),
};
public static CleanUp configure(int displayId) throws IOException {
String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(displayId)};
ProcessBuilder builder = new ProcessBuilder(cmd);
builder.environment().put("CLASSPATH", Server.SERVER_PATH);
Process process = builder.start();
OutputStream out = process.getOutputStream();
return new CleanUp(process.getOutputStream());
}
while (true) {
int localPendingChanges;
boolean localPendingRestoreDisplayPower;
synchronized (this) {
while (pendingChanges == 0) {
wait();
}
localPendingChanges = pendingChanges;
localPendingRestoreDisplayPower = pendingRestoreDisplayPower;
pendingChanges = 0;
}
if ((localPendingChanges & PENDING_CHANGE_DISPLAY_POWER) != 0) {
out.write(localPendingRestoreDisplayPower ? 1 : 0);
out.flush();
}
private boolean sendMessage(int type, int param) {
assert (type & ~MSG_TYPE_MASK) == 0;
int msg = type | param << MSG_PARAM_SHIFT;
try {
out.write(msg);
out.flush();
return true;
} catch (IOException e) {
Ln.w("Could not configure cleanup (type=" + type + ", param=" + param + ")", e);
return false;
}
}
public synchronized void setRestoreDisplayPower(boolean restoreDisplayPower) {
pendingRestoreDisplayPower = restoreDisplayPower;
pendingChanges |= PENDING_CHANGE_DISPLAY_POWER;
notify();
public boolean setRestoreStayOn(int restoreValue) {
// Restore the value (between 0 and 7), -1 to not restore
// <https://developer.android.com/reference/android/provider/Settings.Global#STAY_ON_WHILE_PLUGGED_IN>
assert restoreValue >= -1 && restoreValue <= 7;
return sendMessage(MSG_TYPE_RESTORE_STAY_ON, restoreValue & 0b1111);
}
public boolean setDisableShowTouches(boolean disableOnExit) {
return sendMessage(MSG_TYPE_DISABLE_SHOW_TOUCHES, disableOnExit ? 1 : 0);
}
public boolean setRestoreNormalPowerMode(boolean restoreOnExit) {
return sendMessage(MSG_TYPE_RESTORE_NORMAL_POWER_MODE, restoreOnExit ? 1 : 0);
}
public boolean setPowerOffScreen(boolean powerOffScreenOnExit) {
return sendMessage(MSG_TYPE_POWER_OFF_SCREEN, powerOffScreenOnExit ? 1 : 0);
}
public static void unlinkSelf() {
@ -158,21 +83,35 @@ public final class CleanUp {
unlinkSelf();
int displayId = Integer.parseInt(args[0]);
int restoreStayOn = Integer.parseInt(args[1]);
boolean disableShowTouches = Boolean.parseBoolean(args[2]);
boolean powerOffScreen = Boolean.parseBoolean(args[3]);
int restoreScreenOffTimeout = Integer.parseInt(args[4]);
// Dynamic option
boolean restoreDisplayPower = false;
int restoreStayOn = -1;
boolean disableShowTouches = false;
boolean restoreNormalPowerMode = false;
boolean powerOffScreen = false;
try {
// Wait for the server to die
int msg;
while ((msg = System.in.read()) != -1) {
// Only restore display power
assert msg == 0 || msg == 1;
restoreDisplayPower = msg != 0;
int type = msg & MSG_TYPE_MASK;
int param = msg >> MSG_PARAM_SHIFT;
switch (type) {
case MSG_TYPE_RESTORE_STAY_ON:
restoreStayOn = param > 7 ? -1 : param;
break;
case MSG_TYPE_DISABLE_SHOW_TOUCHES:
disableShowTouches = param != 0;
break;
case MSG_TYPE_RESTORE_NORMAL_POWER_MODE:
restoreNormalPowerMode = param != 0;
break;
case MSG_TYPE_POWER_OFF_SCREEN:
powerOffScreen = param != 0;
break;
default:
Ln.w("Unexpected msg type: " + type);
break;
}
}
} catch (IOException e) {
// Expected when the server is dead
@ -198,24 +137,13 @@ public final class CleanUp {
}
}
if (restoreScreenOffTimeout != -1) {
Ln.i("Restoring \"screen off timeout\"");
try {
Settings.putValue(Settings.TABLE_SYSTEM, "screen_off_timeout", String.valueOf(restoreScreenOffTimeout));
} catch (SettingsException e) {
Ln.e("Could not restore \"screen_off_timeout\"", e);
}
}
// Change the power of the main display when mirroring a virtual display
int targetDisplayId = displayId != Device.DISPLAY_ID_NONE ? displayId : 0;
if (Device.isScreenOn(targetDisplayId)) {
if (Device.isScreenOn()) {
if (powerOffScreen) {
Ln.i("Power off screen");
Device.powerOffScreen(targetDisplayId);
} else if (restoreDisplayPower) {
Ln.i("Restoring display power");
Device.setDisplayPower(targetDisplayId, true);
Device.powerOffScreen(displayId);
} else if (restoreNormalPowerMode) {
Ln.i("Restoring normal power mode");
Device.setScreenPowerMode(Device.POWER_MODE_NORMAL);
}
}

View File

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

View File

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

View File

@ -9,21 +9,22 @@ import com.genymobile.scrcpy.audio.AudioRawRecorder;
import com.genymobile.scrcpy.audio.AudioSource;
import com.genymobile.scrcpy.control.ControlChannel;
import com.genymobile.scrcpy.control.Controller;
import com.genymobile.scrcpy.control.DeviceMessage;
import com.genymobile.scrcpy.device.ConfigurationException;
import com.genymobile.scrcpy.device.DesktopConnection;
import com.genymobile.scrcpy.device.Device;
import com.genymobile.scrcpy.device.NewDisplay;
import com.genymobile.scrcpy.device.Streamer;
import com.genymobile.scrcpy.opengl.OpenGLRunner;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.LogUtils;
import com.genymobile.scrcpy.util.Settings;
import com.genymobile.scrcpy.util.SettingsException;
import com.genymobile.scrcpy.video.CameraCapture;
import com.genymobile.scrcpy.video.NewDisplayCapture;
import com.genymobile.scrcpy.video.ScreenCapture;
import com.genymobile.scrcpy.video.SurfaceCapture;
import com.genymobile.scrcpy.video.SurfaceEncoder;
import com.genymobile.scrcpy.video.VideoSource;
import android.os.BatteryManager;
import android.os.Build;
import java.io.File;
@ -74,21 +75,63 @@ public final class Server {
// not instantiable
}
private static void initAndCleanUp(Options options, CleanUp cleanUp) {
// This method is called from its own thread, so it may only configure cleanup actions which are NOT dynamic (i.e. they are configured once
// and for all, they cannot be changed from another thread)
if (options.getShowTouches()) {
try {
String oldValue = Settings.getAndPutValue(Settings.TABLE_SYSTEM, "show_touches", "1");
// If "show touches" was disabled, it must be disabled back on clean up
if (!"1".equals(oldValue)) {
if (!cleanUp.setDisableShowTouches(true)) {
Ln.e("Could not disable show touch on exit");
}
}
} catch (SettingsException e) {
Ln.e("Could not change \"show_touches\"", e);
}
}
if (options.getStayAwake()) {
int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS;
try {
String oldValue = Settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn));
try {
int restoreStayOn = Integer.parseInt(oldValue);
if (restoreStayOn != stayOn) {
// Restore only if the current value is different
if (!cleanUp.setRestoreStayOn(restoreStayOn)) {
Ln.e("Could not restore stay on on exit");
}
}
} catch (NumberFormatException e) {
// ignore
}
} catch (SettingsException e) {
Ln.e("Could not change \"stay_on_while_plugged_in\"", e);
}
}
if (options.getPowerOffScreenOnClose()) {
if (!cleanUp.setPowerOffScreen(true)) {
Ln.e("Could not power off screen on exit");
}
}
}
private static void scrcpy(Options options) throws IOException, ConfigurationException {
if (Build.VERSION.SDK_INT < AndroidVersions.API_31_ANDROID_12 && options.getVideoSource() == VideoSource.CAMERA) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S && options.getVideoSource() == VideoSource.CAMERA) {
Ln.e("Camera mirroring is not supported before Android 12");
throw new ConfigurationException("Camera mirroring is not supported");
}
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10 && options.getNewDisplay() != null) {
Ln.e("New virtual display is not supported before Android 10");
throw new ConfigurationException("New virtual display is not supported");
}
CleanUp cleanUp = null;
Thread initThread = null;
if (options.getCleanup()) {
cleanUp = CleanUp.start(options);
cleanUp = CleanUp.configure(options.getDisplayId());
initThread = startInitThread(options, cleanUp);
}
int scid = options.getScid();
@ -97,6 +140,9 @@ public final class Server {
boolean video = options.getVideo();
boolean audio = options.getAudio();
boolean sendDummyByte = options.getSendDummyByte();
boolean camera = video && options.getVideoSource() == VideoSource.CAMERA;
final Device device = camera ? null : new Device(options);
Workarounds.apply();
@ -108,11 +154,13 @@ public final class Server {
connection.sendDeviceMeta(Device.getDeviceName());
}
Controller controller = null;
if (control) {
ControlChannel controlChannel = connection.getControlChannel();
controller = new Controller(controlChannel, cleanUp, options);
Controller controller = new Controller(device, controlChannel, cleanUp, options.getClipboardAutosync(), options.getPowerOn());
device.setClipboardListener(text -> {
DeviceMessage msg = DeviceMessage.createClipboard(text);
controller.getSender().send(msg);
});
asyncProcessors.add(controller);
}
@ -131,7 +179,8 @@ public final class Server {
if (audioCodec == AudioCodec.RAW) {
audioRecorder = new AudioRawRecorder(audioCapture, audioStreamer);
} else {
audioRecorder = new AudioEncoder(audioCapture, audioStreamer, options);
audioRecorder = new AudioEncoder(audioCapture, audioStreamer, options.getAudioBitRate(), options.getAudioCodecOptions(),
options.getAudioEncoder());
}
asyncProcessors.add(audioRecorder);
}
@ -141,22 +190,14 @@ public final class Server {
options.getSendFrameMeta());
SurfaceCapture surfaceCapture;
if (options.getVideoSource() == VideoSource.DISPLAY) {
NewDisplay newDisplay = options.getNewDisplay();
if (newDisplay != null) {
surfaceCapture = new NewDisplayCapture(controller, options);
} else {
assert options.getDisplayId() != Device.DISPLAY_ID_NONE;
surfaceCapture = new ScreenCapture(controller, options);
}
surfaceCapture = new ScreenCapture(device);
} 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);
if (controller != null) {
controller.setSurfaceCapture(surfaceCapture);
}
}
Completion completion = new Completion(asyncProcessors.size());
@ -168,25 +209,22 @@ public final class Server {
completion.await();
} finally {
if (cleanUp != null) {
cleanUp.interrupt();
if (initThread != null) {
initThread.interrupt();
}
for (AsyncProcessor asyncProcessor : asyncProcessors) {
asyncProcessor.stop();
}
OpenGLRunner.quit(); // quit the OpenGL thread, if any
connection.shutdown();
try {
if (cleanUp != null) {
cleanUp.join();
if (initThread != null) {
initThread.join();
}
for (AsyncProcessor asyncProcessor : asyncProcessors) {
asyncProcessor.join();
}
OpenGLRunner.join();
} catch (InterruptedException e) {
// ignore
}
@ -195,6 +233,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) {
int status = 0;
try {
@ -238,11 +282,6 @@ public final class Server {
Workarounds.apply();
Ln.i(LogUtils.buildCameraListMessage(options.getListCameraSizes()));
}
if (options.getListApps()) {
Workarounds.apply();
Ln.i("Processing Android apps... (this may take some time)");
Ln.i(LogUtils.buildAppListMessage());
}
// Just print the requested data, do not mirror
return;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,9 @@
package com.genymobile.scrcpy.audio;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.AsyncProcessor;
import com.genymobile.scrcpy.device.Streamer;
import com.genymobile.scrcpy.util.IO;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.device.Streamer;
import android.media.MediaCodec;
import android.os.Build;
@ -25,7 +24,7 @@ public final class AudioRawRecorder implements AsyncProcessor {
}
private void record() throws IOException, AudioCaptureException {
if (Build.VERSION.SDK_INT < AndroidVersions.API_30_ANDROID_11) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
Ln.w("Audio disabled: it is not supported before Android 11");
streamer.writeDisableStream(false);
return;

View File

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

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