mirror of
https://github.com/Genymobile/scrcpy.git
synced 2026-02-25 07:44:29 +01:00
Compare commits
75 Commits
dev4
...
renderer.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac82761932 | ||
|
|
0f93b7fea1 | ||
|
|
aa2f826a9a | ||
|
|
9fb194c3ad | ||
|
|
cf47819c18 | ||
|
|
4c32823852 | ||
|
|
19c015dd91 | ||
|
|
66dda394d7 | ||
|
|
123df8e20e | ||
|
|
ffc7847608 | ||
|
|
b8d25586bf | ||
|
|
73b503427c | ||
|
|
57acb665c0 | ||
|
|
665c586d76 | ||
|
|
b5580dd2e1 | ||
|
|
71305012a0 | ||
|
|
9ca85aadef | ||
|
|
3b13a68c7a | ||
|
|
362df5cc84 | ||
|
|
373b366fa5 | ||
|
|
5607f12f2a | ||
|
|
8ac04d39f4 | ||
|
|
af355804ef | ||
|
|
1018f3e9be | ||
|
|
48fcfdd104 | ||
|
|
553813e97d | ||
|
|
968f178205 | ||
|
|
1b13d0a22d | ||
|
|
078565d40b | ||
|
|
cc7c07d4a3 | ||
|
|
24b46f11a5 | ||
|
|
f348a1f307 | ||
|
|
f8846aa76d | ||
|
|
1e51d2c83f | ||
|
|
826076f753 | ||
|
|
c36433999c | ||
|
|
78cba1b7c2 | ||
|
|
1015b42e53 | ||
|
|
f4cc07da24 | ||
|
|
fde02a7dfa | ||
|
|
dee1fd46a6 | ||
|
|
a90a039f50 | ||
|
|
ab04e0348d | ||
|
|
99b955f390 | ||
|
|
bbb855b5b0 | ||
|
|
4879950a06 | ||
|
|
170b7a02e7 | ||
|
|
9f1aac41a6 | ||
|
|
02989249f6 | ||
|
|
f8e0b9be4b | ||
|
|
ed62ca124c | ||
|
|
3571fe62ed | ||
|
|
bca98133d1 | ||
|
|
881c71b2e8 | ||
|
|
09eed565fc | ||
|
|
b0da401e6d | ||
|
|
dba2a3778f | ||
|
|
cda4387058 | ||
|
|
42632d3f53 | ||
|
|
db2f72a58a | ||
|
|
fb6381f5b9 | ||
|
|
06fd3b4786 | ||
|
|
1d1a4103cc | ||
|
|
5b51396a8c | ||
|
|
7e66062086 | ||
|
|
6f9eb31d52 | ||
|
|
9cfa5b197a | ||
|
|
b08093d1c0 | ||
|
|
7dd9bcaf60 | ||
|
|
3530851071 | ||
|
|
d0047b2110 | ||
|
|
3281fda6ef | ||
|
|
925949d54a | ||
|
|
f3d4fde15b | ||
|
|
eee3f24739 |
38
.github/workflows/release.yml
vendored
38
.github/workflows/release.yml
vendored
@@ -72,13 +72,19 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Same as build-linux-x86_64
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \
|
||||
libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \
|
||||
libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \
|
||||
libv4l-dev
|
||||
sudo apt install -y meson ninja-build nasm ffmpeg libavcodec-dev \
|
||||
libavdevice-dev libavformat-dev libavutil-dev libswresample-dev \
|
||||
libusb-1.0-0 libusb-1.0-0-dev libv4l-dev \
|
||||
libasound2-dev libpulse-dev \
|
||||
libaudio-dev libfribidi-dev libjack-dev libsndio-dev libx11-dev libxext-dev \
|
||||
libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev libxtst-dev \
|
||||
libxkbcommon-dev libdrm-dev libgbm-dev libgl1-mesa-dev libgles2-mesa-dev \
|
||||
libegl1-mesa-dev libdbus-1-dev libibus-1.0-dev libudev-dev libthai-dev \
|
||||
libpipewire-0.3-dev libwayland-dev libdecor-0-dev liburing-dev
|
||||
|
||||
# SDL3 is not available in Ubuntu yet
|
||||
- name: Install SDL3
|
||||
@@ -106,13 +112,19 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# https://wiki.libsdl.org/SDL3/README-linux#build-dependencies
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \
|
||||
libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \
|
||||
libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \
|
||||
libv4l-dev
|
||||
sudo apt install -y meson ninja-build nasm ffmpeg libavcodec-dev \
|
||||
libavdevice-dev libavformat-dev libavutil-dev libswresample-dev \
|
||||
libusb-1.0-0 libusb-1.0-0-dev libv4l-dev \
|
||||
libasound2-dev libpulse-dev \
|
||||
libaudio-dev libfribidi-dev libjack-dev libsndio-dev libx11-dev libxext-dev \
|
||||
libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev libxtst-dev \
|
||||
libxkbcommon-dev libdrm-dev libgbm-dev libgl1-mesa-dev libgles2-mesa-dev \
|
||||
libegl1-mesa-dev libdbus-1-dev libibus-1.0-dev libudev-dev libthai-dev \
|
||||
libpipewire-0.3-dev libwayland-dev libdecor-0-dev liburing-dev
|
||||
|
||||
- name: Build
|
||||
run: release/build_linux.sh x86_64
|
||||
@@ -140,9 +152,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \
|
||||
libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \
|
||||
libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \
|
||||
sudo apt install -y meson ninja-build nasm \
|
||||
mingw-w64 mingw-w64-tools libz-mingw-w64-dev
|
||||
|
||||
- name: Build
|
||||
@@ -171,9 +181,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \
|
||||
libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \
|
||||
libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \
|
||||
sudo apt install -y meson ninja-build nasm \
|
||||
mingw-w64 mingw-w64-tools libz-mingw-w64-dev
|
||||
|
||||
- name: Build
|
||||
@@ -237,7 +245,7 @@ jobs:
|
||||
path: release/work/build-macos-aarch64/dist-tar/
|
||||
|
||||
build-macos-x86_64:
|
||||
runs-on: macos-13
|
||||
runs-on: macos-15-intel
|
||||
steps:
|
||||
- name: Check architecture
|
||||
run: |
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
source for the project. Do not download releases from random websites, even if
|
||||
their name contains `scrcpy`.**
|
||||
|
||||
# scrcpy (v3.3.3)
|
||||
# scrcpy (v3.3.4)
|
||||
|
||||
<img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />
|
||||
|
||||
@@ -129,7 +129,7 @@ Here are just some common examples.
|
||||
scrcpy --otg
|
||||
```
|
||||
|
||||
- Control the device using gamepad controllers plugged into the computer:
|
||||
- Control the device using gamepads plugged into the computer:
|
||||
|
||||
```bash
|
||||
scrcpy --gamepad=uhid
|
||||
|
||||
@@ -18,6 +18,8 @@ _scrcpy() {
|
||||
--camera-fps=
|
||||
--camera-high-speed
|
||||
--camera-size=
|
||||
--camera-torch
|
||||
--camera-zoom=
|
||||
--capture-orientation=
|
||||
--crop=
|
||||
-d --select-usb
|
||||
@@ -197,6 +199,8 @@ _scrcpy() {
|
||||
|--camera-id \
|
||||
|--camera-fps \
|
||||
|--camera-size \
|
||||
|--camera-torch \
|
||||
|--camera-zoom \
|
||||
|--crop \
|
||||
|--display-id \
|
||||
|--max-fps \
|
||||
|
||||
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
@@ -25,6 +25,8 @@ 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]'
|
||||
'--camera-torch[Turn on the camera torch when the camera starts]'
|
||||
'--camera-zoom[Specify the camera zoom initial value]'
|
||||
'--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]'
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
#!/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
|
||||
# <host>: linux, macos, win32 or win64
|
||||
# <build_type>: native or cross
|
||||
# <link_type>: static or shared
|
||||
echo "Syntax: $0 <host> <build_type> <link_type>" >&2
|
||||
@@ -12,8 +11,8 @@ process_args() {
|
||||
fi
|
||||
|
||||
HOST="$1"
|
||||
BUILD_TYPE="$2" # native or cross
|
||||
LINK_TYPE="$3" # static or shared
|
||||
BUILD_TYPE="$2"
|
||||
LINK_TYPE="$3"
|
||||
DIRNAME="$HOST-$BUILD_TYPE-$LINK_TYPE"
|
||||
|
||||
if [[ "$BUILD_TYPE" != native && "$BUILD_TYPE" != cross ]]
|
||||
@@ -1,21 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
set -ex
|
||||
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
|
||||
cd "$DEPS_DIR"
|
||||
. common
|
||||
. $(dirname ${BASH_SOURCE[0]})/_init "$@"
|
||||
|
||||
VERSION=36.0.0
|
||||
FILENAME=platform-tools_r$VERSION-linux.zip
|
||||
PROJECT_DIR=platform-tools-$VERSION-linux
|
||||
URL="https://dl.google.com/android/repository/platform-tools_r$VERSION-linux.zip"
|
||||
SHA256SUM=0ead642c943ffe79701fccca8f5f1c69c4ce4f43df2eefee553f6ccb27cbfbe8
|
||||
|
||||
PROJECT_DIR="platform-tools-$VERSION-linux"
|
||||
FILENAME="$PROJECT_DIR.zip"
|
||||
|
||||
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"
|
||||
get_file "$URL" "$FILENAME" "$SHA256SUM"
|
||||
mkdir -p "$PROJECT_DIR"
|
||||
cd "$PROJECT_DIR"
|
||||
ZIP_PREFIX=platform-tools
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
set -ex
|
||||
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
|
||||
cd "$DEPS_DIR"
|
||||
. common
|
||||
. $(dirname ${BASH_SOURCE[0]})/_init "$@"
|
||||
|
||||
VERSION=36.0.0
|
||||
FILENAME=platform-tools_r$VERSION-darwin.zip
|
||||
PROJECT_DIR=platform-tools-$VERSION-darwin
|
||||
URL="https://dl.google.com/android/repository/platform-tools_r$VERSION-darwin.zip"
|
||||
SHA256SUM=d3e9fa1df3345cf728586908426615a60863d2632f73f1ce14f0f1349ef000fd
|
||||
|
||||
PROJECT_DIR="platform-tools-$VERSION-darwin"
|
||||
FILENAME="$PROJECT_DIR.zip"
|
||||
|
||||
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"
|
||||
get_file "$URL" "$FILENAME" "$SHA256SUM"
|
||||
mkdir -p "$PROJECT_DIR"
|
||||
cd "$PROJECT_DIR"
|
||||
ZIP_PREFIX=platform-tools
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
set -ex
|
||||
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
|
||||
cd "$DEPS_DIR"
|
||||
. common
|
||||
. $(dirname ${BASH_SOURCE[0]})/_init "$@"
|
||||
|
||||
VERSION=36.0.0
|
||||
FILENAME=platform-tools_r$VERSION-win.zip
|
||||
PROJECT_DIR=platform-tools-$VERSION-windows
|
||||
URL="https://dl.google.com/android/repository/platform-tools_r$VERSION-win.zip"
|
||||
SHA256SUM=12c2841f354e92a0eb2fd7bf6f0f9bf8538abce7bd6b060ac8349d6f6a61107c
|
||||
|
||||
PROJECT_DIR="platform-tools-$VERSION-windows"
|
||||
FILENAME="$PROJECT_DIR.zip"
|
||||
|
||||
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"
|
||||
get_file "$URL" "$FILENAME" "$SHA256SUM"
|
||||
mkdir -p "$PROJECT_DIR"
|
||||
cd "$PROJECT_DIR"
|
||||
ZIP_PREFIX=platform-tools
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
set -ex
|
||||
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
|
||||
cd "$DEPS_DIR"
|
||||
. common
|
||||
. $(dirname ${BASH_SOURCE[0]})/_init
|
||||
process_args "$@"
|
||||
|
||||
VERSION=1.5.0
|
||||
FILENAME=dav1d-$VERSION.tar.gz
|
||||
PROJECT_DIR=dav1d-$VERSION
|
||||
URL="https://code.videolan.org/videolan/dav1d/-/archive/$VERSION/dav1d-$VERSION.tar.gz"
|
||||
SHA256SUM=78b15d9954b513ea92d27f39362535ded2243e1b0924fde39f37a31ebed5f76b
|
||||
|
||||
PROJECT_DIR="dav1d-$VERSION"
|
||||
FILENAME="$PROJECT_DIR.tar.gz"
|
||||
|
||||
cd "$SOURCES_DIR"
|
||||
|
||||
if [[ -d "$PROJECT_DIR" ]]
|
||||
then
|
||||
echo "$PWD/$PROJECT_DIR" found
|
||||
else
|
||||
get_file "https://code.videolan.org/videolan/dav1d/-/archive/$VERSION/$FILENAME" "$FILENAME" "$SHA256SUM"
|
||||
get_file "$URL" "$FILENAME" "$SHA256SUM"
|
||||
tar xf "$FILENAME" # First level directory is "$PROJECT_DIR"
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
set -ex
|
||||
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
|
||||
cd "$DEPS_DIR"
|
||||
. common
|
||||
. $(dirname ${BASH_SOURCE[0]})/_init
|
||||
process_args "$@"
|
||||
|
||||
VERSION=7.1.1
|
||||
FILENAME=ffmpeg-$VERSION.tar.xz
|
||||
PROJECT_DIR=ffmpeg-$VERSION
|
||||
URL="https://ffmpeg.org/releases/ffmpeg-$VERSION.tar.xz"
|
||||
SHA256SUM=733984395e0dbbe5c046abda2dc49a5544e7e0e1e2366bba849222ae9e3a03b1
|
||||
|
||||
PROJECT_DIR="ffmpeg-$VERSION"
|
||||
FILENAME="$PROJECT_DIR.tar.xz"
|
||||
|
||||
cd "$SOURCES_DIR"
|
||||
|
||||
if [[ -d "$PROJECT_DIR" ]]
|
||||
then
|
||||
echo "$PWD/$PROJECT_DIR" found
|
||||
else
|
||||
get_file "https://ffmpeg.org/releases/$FILENAME" "$FILENAME" "$SHA256SUM"
|
||||
get_file "$URL" "$FILENAME" "$SHA256SUM"
|
||||
tar xf "$FILENAME" # First level directory is "$PROJECT_DIR"
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
set -ex
|
||||
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
|
||||
cd "$DEPS_DIR"
|
||||
. common
|
||||
. $(dirname ${BASH_SOURCE[0]})/_init
|
||||
process_args "$@"
|
||||
|
||||
VERSION=1.0.29
|
||||
FILENAME=libusb-$VERSION.tar.gz
|
||||
PROJECT_DIR=libusb-$VERSION
|
||||
URL="https://github.com/libusb/libusb/archive/refs/tags/v$VERSION.tar.gz"
|
||||
SHA256SUM=7c2dd39c0b2589236e48c93247c986ae272e27570942b4163cb00a060fcf1b74
|
||||
|
||||
PROJECT_DIR="libusb-$VERSION"
|
||||
FILENAME="$PROJECT_DIR.tar.gz"
|
||||
|
||||
cd "$SOURCES_DIR"
|
||||
|
||||
if [[ -d "$PROJECT_DIR" ]]
|
||||
then
|
||||
echo "$PWD/$PROJECT_DIR" found
|
||||
else
|
||||
get_file "https://github.com/libusb/libusb/archive/refs/tags/v$VERSION.tar.gz" "$FILENAME" "$SHA256SUM"
|
||||
get_file "$URL" "$FILENAME" "$SHA256SUM"
|
||||
tar xf "$FILENAME" # First level directory is "$PROJECT_DIR"
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
set -ex
|
||||
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
|
||||
cd "$DEPS_DIR"
|
||||
. common
|
||||
. $(dirname ${BASH_SOURCE[0]})/_init
|
||||
process_args "$@"
|
||||
|
||||
VERSION=3.2.18
|
||||
FILENAME=SDL-$VERSION.tar.gz
|
||||
PROJECT_DIR=SDL-release-$VERSION
|
||||
SHA256SUM=51539fa13e546bc50c632beed3f34257de2baa38a4c642048de56377903b4265
|
||||
VERSION=3.4.0
|
||||
URL="https://github.com/libsdl-org/SDL/archive/refs/tags/release-$VERSION.tar.gz"
|
||||
SHA256SUM=9614b9696abc4597ffce6b888829dc6537ae500423474c342ac4a67222c5654c
|
||||
|
||||
PROJECT_DIR="sdl-$VERSION"
|
||||
FILENAME="$PROJECT_DIR.tar.gz"
|
||||
|
||||
cd "$SOURCES_DIR"
|
||||
|
||||
@@ -16,8 +16,9 @@ if [[ -d "$PROJECT_DIR" ]]
|
||||
then
|
||||
echo "$PWD/$PROJECT_DIR" found
|
||||
else
|
||||
get_file "https://github.com/libsdl-org/SDL/archive/refs/tags/release-$VERSION.tar.gz" "$FILENAME" "$SHA256SUM"
|
||||
tar xf "$FILENAME" # First level directory is "$PROJECT_DIR"
|
||||
get_file "$URL" "$FILENAME" "$SHA256SUM"
|
||||
tar xf "$FILENAME" # First level directory is "SDL-release-$VERSION"
|
||||
mv "SDL-release-$VERSION" "$PROJECT_DIR"
|
||||
fi
|
||||
|
||||
mkdir -p "$BUILD_DIR/$PROJECT_DIR"
|
||||
@@ -36,6 +37,7 @@ else
|
||||
|
||||
conf=(
|
||||
-DCMAKE_INSTALL_PREFIX="$INSTALL_DIR/$DIRNAME"
|
||||
-DSDL_TESTS=OFF
|
||||
)
|
||||
|
||||
if [[ "$HOST" == linux ]]
|
||||
|
||||
@@ -15,7 +15,6 @@ src = [
|
||||
'src/delay_buffer.c',
|
||||
'src/demuxer.c',
|
||||
'src/device_msg.c',
|
||||
'src/display.c',
|
||||
'src/events.c',
|
||||
'src/icon.c',
|
||||
'src/file_pusher.c',
|
||||
@@ -33,6 +32,7 @@ src = [
|
||||
'src/scrcpy.c',
|
||||
'src/screen.c',
|
||||
'src/server.c',
|
||||
'src/texture.c',
|
||||
'src/version.c',
|
||||
'src/hid/hid_gamepad.c',
|
||||
'src/hid/hid_keyboard.c',
|
||||
@@ -57,13 +57,14 @@ src = [
|
||||
'src/util/process.c',
|
||||
'src/util/process_intr.c',
|
||||
'src/util/rand.c',
|
||||
'src/util/rect.c',
|
||||
'src/util/sdl.c',
|
||||
'src/util/strbuf.c',
|
||||
'src/util/str.c',
|
||||
'src/util/term.c',
|
||||
'src/util/thread.c',
|
||||
'src/util/tick.c',
|
||||
'src/util/timeout.c',
|
||||
'src/util/window.c',
|
||||
]
|
||||
|
||||
conf = configuration_data()
|
||||
@@ -191,8 +192,7 @@ executable('scrcpy', src,
|
||||
datadir = get_option('datadir') # by default 'share'
|
||||
|
||||
install_man('scrcpy.1')
|
||||
install_data('data/icon.png',
|
||||
rename: 'scrcpy.png',
|
||||
install_data('data/scrcpy.png',
|
||||
install_dir: datadir / 'icons/hicolor/256x256/apps')
|
||||
install_data('data/zsh-completion/_scrcpy',
|
||||
install_dir: datadir / 'zsh/site-functions')
|
||||
@@ -276,13 +276,13 @@ if get_option('buildtype') == 'debug'
|
||||
exe = executable(t[0], sources,
|
||||
include_directories: src_dir,
|
||||
dependencies: dependencies,
|
||||
c_args: ['-DSDL_MAIN_HANDLED', '-DSC_TEST'])
|
||||
c_args: ['-DSC_TEST'])
|
||||
test(t[0], exe)
|
||||
endforeach
|
||||
endif
|
||||
|
||||
if meson.version().version_compare('>= 0.58.0')
|
||||
devenv = environment()
|
||||
devenv.set('SCRCPY_ICON_PATH', meson.current_source_dir() / 'data/icon.png')
|
||||
devenv.set('SCRCPY_ICON_DIR', meson.current_source_dir() / 'data')
|
||||
meson.add_devenv(devenv)
|
||||
endif
|
||||
|
||||
@@ -13,7 +13,7 @@ BEGIN
|
||||
VALUE "LegalCopyright", "Romain Vimont, Genymobile"
|
||||
VALUE "OriginalFilename", "scrcpy.exe"
|
||||
VALUE "ProductName", "scrcpy"
|
||||
VALUE "ProductVersion", "3.3.3"
|
||||
VALUE "ProductVersion", "3.3.4"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
28
app/scrcpy.1
28
app/scrcpy.1
@@ -131,6 +131,14 @@ The available camera ids can be listed by \fB\-\-list\-cameras\fR.
|
||||
.BI "\-\-camera\-size " width\fRx\fIheight
|
||||
Specify an explicit camera capture size.
|
||||
|
||||
.TP
|
||||
.BI \-\-camera\-torch
|
||||
Turn on the camera torch when the camera starts.
|
||||
|
||||
.TP
|
||||
.BI "\-\-camera-zoom " zoom
|
||||
Specify the camera zoom initial value.
|
||||
|
||||
.TP
|
||||
.BI "\-\-capture\-orientation " value
|
||||
Possible values are 0, 90, 180, 270, flip0, flip90, flip180 and flip270, possibly prefixed by '@'.
|
||||
@@ -815,6 +823,22 @@ Install APK from computer
|
||||
.B Drag & drop non-APK file
|
||||
Push file to device (see \fB\-\-push\-target\fR)
|
||||
|
||||
.TP
|
||||
.B MOD+t
|
||||
Turn on the camera torch (camera mode only)
|
||||
|
||||
.TP
|
||||
.B MOD+Shift+t
|
||||
Turn off the camera torch (camera mode only)
|
||||
|
||||
.TP
|
||||
.B MOD+Up
|
||||
Zoom camera in (camera mode only)
|
||||
|
||||
.TP
|
||||
.B MOD+Down
|
||||
Zoom camera out (camera mode only)
|
||||
|
||||
|
||||
.SH Environment variables
|
||||
|
||||
@@ -827,8 +851,8 @@ Path to adb.
|
||||
Device serial to use if no selector (\fB-s\fR, \fB-d\fR, \fB-e\fR or \fB\-\-tcpip=\fIaddr\fR) is specified.
|
||||
|
||||
.TP
|
||||
.B SCRCPY_ICON_PATH
|
||||
Path to the program icon.
|
||||
.B SCRCPY_ICON_DIR
|
||||
Path to the icon directory.
|
||||
|
||||
.TP
|
||||
.B SCRCPY_SERVER_PATH
|
||||
|
||||
@@ -120,14 +120,6 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
|
||||
ap->device = SDL_GetAudioStreamDevice(ap->stream);
|
||||
assert(ap->device);
|
||||
|
||||
// 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);
|
||||
if (!ok) {
|
||||
ok = sc_thread_set_priority(SC_THREAD_PRIORITY_HIGH);
|
||||
(void) ok; // We don't care if it worked, at least we tried
|
||||
}
|
||||
|
||||
ok = SDL_ResumeAudioDevice(ap->device);
|
||||
if (!ok) {
|
||||
LOGE("Could not resume audio device: %s", SDL_GetError());
|
||||
|
||||
@@ -114,6 +114,8 @@ enum {
|
||||
OPT_NO_VD_SYSTEM_DECORATIONS,
|
||||
OPT_NO_VD_DESTROY_CONTENT,
|
||||
OPT_DISPLAY_IME_POLICY,
|
||||
OPT_CAMERA_TORCH,
|
||||
OPT_CAMERA_ZOOM,
|
||||
};
|
||||
|
||||
struct sc_option {
|
||||
@@ -313,6 +315,17 @@ static const struct sc_option options[] = {
|
||||
.argdesc = "<width>x<height>",
|
||||
.text = "Specify an explicit camera capture size.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_CAMERA_TORCH,
|
||||
.longopt = "camera-torch",
|
||||
.text = "Turn on the camera torch when the camera starts.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_CAMERA_ZOOM,
|
||||
.longopt = "camera-zoom",
|
||||
.argdesc = "zoom",
|
||||
.text = "Specify the camera zoom initial value.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_CAPTURE_ORIENTATION,
|
||||
.longopt = "capture-orientation",
|
||||
@@ -1207,6 +1220,22 @@ static const struct sc_shortcut shortcuts[] = {
|
||||
.shortcuts = { "Drag & drop non-APK file" },
|
||||
.text = "Push file to device (see --push-target)",
|
||||
},
|
||||
{
|
||||
.shortcuts = { "MOD+t" },
|
||||
.text = "Turn on the camera torch (camera mode only)",
|
||||
},
|
||||
{
|
||||
.shortcuts = { "MOD+Shift+t" },
|
||||
.text = "Turn off the camera torch (camera mode only)",
|
||||
},
|
||||
{
|
||||
.shortcuts = { "MOD+Up" },
|
||||
.text = "Zoom camera in (camera mode only)",
|
||||
},
|
||||
{
|
||||
.shortcuts = { "MOD+Down" },
|
||||
.text = "Zoom camera out (camera mode only)",
|
||||
},
|
||||
};
|
||||
|
||||
static const struct sc_envvar envvars[] = {
|
||||
@@ -1220,8 +1249,8 @@ static const struct sc_envvar envvars[] = {
|
||||
"--tcpip=<addr>) is specified",
|
||||
},
|
||||
{
|
||||
.name = "SCRCPY_ICON_PATH",
|
||||
.text = "Path to the program icon",
|
||||
.name = "SCRCPY_ICON_DIR",
|
||||
.text = "Path to the icon directory",
|
||||
},
|
||||
{
|
||||
.name = "SCRCPY_SERVER_PATH",
|
||||
@@ -2780,6 +2809,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
case OPT_CAMERA_HIGH_SPEED:
|
||||
opts->camera_high_speed = true;
|
||||
break;
|
||||
case OPT_CAMERA_TORCH:
|
||||
opts->camera_torch = true;
|
||||
break;
|
||||
case OPT_CAMERA_ZOOM:
|
||||
opts->camera_zoom = optarg;
|
||||
break;
|
||||
case OPT_NO_WINDOW:
|
||||
opts->window = false;
|
||||
break;
|
||||
@@ -2928,7 +2963,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
}
|
||||
#endif
|
||||
|
||||
if (opts->control) {
|
||||
if (opts->control && opts->video_source == SC_VIDEO_SOURCE_DISPLAY) {
|
||||
if (opts->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AUTO) {
|
||||
opts->keyboard_input_mode = otg ? SC_KEYBOARD_INPUT_MODE_AOA
|
||||
: SC_KEYBOARD_INPUT_MODE_SDK;
|
||||
@@ -3106,8 +3141,10 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
}
|
||||
|
||||
if (opts->control) {
|
||||
LOGI("Camera video source: control disabled");
|
||||
opts->control = false;
|
||||
// Disable all inputs for camera
|
||||
opts->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_DISABLED;
|
||||
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_DISABLED;
|
||||
opts->gamepad_input_mode = SC_GAMEPAD_INPUT_MODE_DISABLED;
|
||||
}
|
||||
} else if (opts->camera_id
|
||||
|| opts->camera_ar
|
||||
|
||||
@@ -182,12 +182,17 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) {
|
||||
size_t len = write_string_tiny(&buf[1], msg->start_app.name, 255);
|
||||
return 1 + len;
|
||||
}
|
||||
case SC_CONTROL_MSG_TYPE_CAMERA_SET_TORCH:
|
||||
buf[1] = msg->camera_set_torch.on ? 1 : 0;
|
||||
return 2;
|
||||
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:
|
||||
case SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_IN:
|
||||
case SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_OUT:
|
||||
// no additional data
|
||||
return 1;
|
||||
default:
|
||||
@@ -318,6 +323,16 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
|
||||
case SC_CONTROL_MSG_TYPE_RESET_VIDEO:
|
||||
LOG_CMSG("reset video");
|
||||
break;
|
||||
case SC_CONTROL_MSG_TYPE_CAMERA_SET_TORCH:
|
||||
LOG_CMSG("camera set torch %s",
|
||||
msg->camera_set_torch.on ? "on" : "off");
|
||||
break;
|
||||
case SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_IN:
|
||||
LOG_CMSG("camera zoom in");
|
||||
break;
|
||||
case SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_OUT:
|
||||
LOG_CMSG("camera zoom out");
|
||||
break;
|
||||
default:
|
||||
LOG_CMSG("unknown type: %u", (unsigned) msg->type);
|
||||
break;
|
||||
|
||||
@@ -43,6 +43,9 @@ enum sc_control_msg_type {
|
||||
SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS,
|
||||
SC_CONTROL_MSG_TYPE_START_APP,
|
||||
SC_CONTROL_MSG_TYPE_RESET_VIDEO,
|
||||
SC_CONTROL_MSG_TYPE_CAMERA_SET_TORCH,
|
||||
SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_IN,
|
||||
SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_OUT,
|
||||
};
|
||||
|
||||
enum sc_copy_key {
|
||||
@@ -111,6 +114,9 @@ struct sc_control_msg {
|
||||
struct {
|
||||
char *name;
|
||||
} start_app;
|
||||
struct {
|
||||
bool on;
|
||||
} camera_set_torch;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,428 +0,0 @@
|
||||
#include "display.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <inttypes.h>
|
||||
#include <string.h>
|
||||
#include <libavutil/pixfmt.h>
|
||||
|
||||
#include "util/log.h"
|
||||
|
||||
static bool
|
||||
sc_display_init_novideo_icon(struct sc_display *display,
|
||||
SDL_Surface *icon_novideo) {
|
||||
assert(icon_novideo);
|
||||
|
||||
bool ok = SDL_SetRenderLogicalPresentation(display->renderer,
|
||||
icon_novideo->w,
|
||||
icon_novideo->h,
|
||||
SDL_LOGICAL_PRESENTATION_LETTERBOX);
|
||||
if (!ok) {
|
||||
LOGW("Could not set renderer logical size: %s", SDL_GetError());
|
||||
// don't fail
|
||||
}
|
||||
|
||||
display->texture = SDL_CreateTextureFromSurface(display->renderer,
|
||||
icon_novideo);
|
||||
if (!display->texture) {
|
||||
LOGE("Could not create texture: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
sc_display_init(struct sc_display *display, SDL_Window *window,
|
||||
SDL_Surface *icon_novideo, bool mipmaps) {
|
||||
display->renderer = SDL_CreateRenderer(window, NULL);
|
||||
if (!display->renderer) {
|
||||
LOGE("Could not create renderer: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *renderer_name = SDL_GetRendererName(display->renderer);
|
||||
LOGI("Renderer: %s", renderer_name ? renderer_name : "(unknown)");
|
||||
|
||||
display->mipmaps = false;
|
||||
|
||||
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
|
||||
display->gl_context = NULL;
|
||||
#endif
|
||||
|
||||
// starts with "opengl"
|
||||
bool use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6);
|
||||
if (use_opengl) {
|
||||
|
||||
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
|
||||
// Persuade macOS to give us something better than OpenGL 2.1.
|
||||
// If we create a Core Profile context, we get the best OpenGL version.
|
||||
bool ok = SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK,
|
||||
SDL_GL_CONTEXT_PROFILE_CORE);
|
||||
if (!ok) {
|
||||
LOGW("Could not set a GL Core Profile Context");
|
||||
}
|
||||
|
||||
LOGD("Creating OpenGL Core Profile context");
|
||||
display->gl_context = SDL_GL_CreateContext(window);
|
||||
if (!display->gl_context) {
|
||||
LOGE("Could not create OpenGL context: %s", SDL_GetError());
|
||||
SDL_DestroyRenderer(display->renderer);
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
struct sc_opengl *gl = &display->gl;
|
||||
sc_opengl_init(gl);
|
||||
|
||||
LOGI("OpenGL version: %s", gl->version);
|
||||
|
||||
if (mipmaps) {
|
||||
bool supports_mipmaps =
|
||||
sc_opengl_version_at_least(gl, 3, 0, /* OpenGL 3.0+ */
|
||||
2, 0 /* OpenGL ES 2.0+ */);
|
||||
if (supports_mipmaps) {
|
||||
LOGI("Trilinear filtering enabled");
|
||||
display->mipmaps = true;
|
||||
} else {
|
||||
LOGW("Trilinear filtering disabled "
|
||||
"(OpenGL 3.0+ or ES 2.0+ required)");
|
||||
}
|
||||
} else {
|
||||
LOGI("Trilinear filtering disabled");
|
||||
}
|
||||
} else if (mipmaps) {
|
||||
LOGD("Trilinear filtering disabled (not an OpenGL renderer)");
|
||||
}
|
||||
|
||||
display->texture = NULL;
|
||||
display->pending.flags = 0;
|
||||
display->pending.frame = NULL;
|
||||
|
||||
if (icon_novideo) {
|
||||
// Without video, set a static scrcpy icon as window content
|
||||
bool ok = sc_display_init_novideo_icon(display, icon_novideo);
|
||||
if (!ok) {
|
||||
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
|
||||
SDL_GL_DestroyContext(display->gl_context);
|
||||
#endif
|
||||
SDL_DestroyRenderer(display->renderer);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
sc_display_destroy(struct sc_display *display) {
|
||||
if (display->pending.frame) {
|
||||
av_frame_free(&display->pending.frame);
|
||||
}
|
||||
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
|
||||
SDL_GL_DestroyContext(display->gl_context);
|
||||
#endif
|
||||
if (display->texture) {
|
||||
SDL_DestroyTexture(display->texture);
|
||||
}
|
||||
SDL_DestroyRenderer(display->renderer);
|
||||
}
|
||||
|
||||
static enum SDL_Colorspace
|
||||
sc_display_to_sdl_color_space(enum AVColorSpace color_space,
|
||||
enum AVColorRange color_range) {
|
||||
bool full_range = color_range == AVCOL_RANGE_JPEG;
|
||||
|
||||
switch (color_space) {
|
||||
case AVCOL_SPC_BT709:
|
||||
case AVCOL_SPC_RGB:
|
||||
return full_range ? SDL_COLORSPACE_BT709_FULL
|
||||
: SDL_COLORSPACE_BT709_LIMITED;
|
||||
case AVCOL_SPC_BT470BG:
|
||||
case AVCOL_SPC_SMPTE170M:
|
||||
return full_range ? SDL_COLORSPACE_BT601_FULL
|
||||
: SDL_COLORSPACE_BT601_LIMITED;
|
||||
case AVCOL_SPC_BT2020_NCL:
|
||||
case AVCOL_SPC_BT2020_CL:
|
||||
return full_range ? SDL_COLORSPACE_BT2020_FULL
|
||||
: SDL_COLORSPACE_BT2020_LIMITED;
|
||||
default:
|
||||
return SDL_COLORSPACE_JPEG;
|
||||
}
|
||||
}
|
||||
|
||||
static SDL_Texture *
|
||||
sc_display_create_texture(struct sc_display *display,
|
||||
struct sc_size size, enum AVColorSpace color_space,
|
||||
enum AVColorRange color_range) {
|
||||
SDL_PropertiesID props = SDL_CreateProperties();
|
||||
if (!props) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
enum SDL_Colorspace sdl_color_space =
|
||||
sc_display_to_sdl_color_space(color_space, color_range);
|
||||
|
||||
bool ok =
|
||||
SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_FORMAT_NUMBER,
|
||||
SDL_PIXELFORMAT_YV12);
|
||||
ok &= SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_ACCESS_NUMBER,
|
||||
SDL_TEXTUREACCESS_STREAMING);
|
||||
ok &= SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_WIDTH_NUMBER,
|
||||
size.width);
|
||||
ok &= SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_HEIGHT_NUMBER,
|
||||
size.height);
|
||||
ok &= SDL_SetNumberProperty(props,
|
||||
SDL_PROP_TEXTURE_CREATE_COLORSPACE_NUMBER,
|
||||
sdl_color_space);
|
||||
|
||||
if (!ok) {
|
||||
LOGE("Could not set texture properties");
|
||||
SDL_DestroyProperties(props);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
SDL_Renderer *renderer = display->renderer;
|
||||
SDL_Texture *texture = SDL_CreateTextureWithProperties(renderer, props);
|
||||
SDL_DestroyProperties(props);
|
||||
if (!texture) {
|
||||
LOGD("Could not create texture: %s", SDL_GetError());
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (display->mipmaps) {
|
||||
struct sc_opengl *gl = &display->gl;
|
||||
|
||||
SDL_PropertiesID props = SDL_GetTextureProperties(texture);
|
||||
if (!props) {
|
||||
LOGE("Could not get texture properties: %s", SDL_GetError());
|
||||
SDL_DestroyTexture(texture);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char *renderer_name = SDL_GetRendererName(display->renderer);
|
||||
const char *key = !renderer_name || !strcmp(renderer_name, "opengl")
|
||||
? SDL_PROP_TEXTURE_OPENGL_TEXTURE_NUMBER
|
||||
: SDL_PROP_TEXTURE_OPENGLES2_TEXTURE_NUMBER;
|
||||
|
||||
int64_t texture_id = SDL_GetNumberProperty(props, key, 0);
|
||||
SDL_DestroyProperties(props);
|
||||
if (!texture_id) {
|
||||
LOGE("Could not get texture id: %s", SDL_GetError());
|
||||
SDL_DestroyTexture(texture);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
assert(!(texture_id & ~0xFFFFFFFF)); // fits in uint32_t
|
||||
display->texture_id = texture_id;
|
||||
gl->BindTexture(GL_TEXTURE_2D, display->texture_id);
|
||||
|
||||
// Enable trilinear filtering for downscaling
|
||||
gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
|
||||
GL_LINEAR_MIPMAP_LINEAR);
|
||||
gl->TexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -1.f);
|
||||
|
||||
gl->BindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
|
||||
return texture;
|
||||
}
|
||||
|
||||
static inline void
|
||||
sc_display_set_pending_texture(struct sc_display *display,
|
||||
struct sc_size size,
|
||||
enum AVColorRange color_range) {
|
||||
assert(!display->texture);
|
||||
display->pending.texture.size = size;
|
||||
display->pending.texture.color_range = color_range;
|
||||
display->pending.flags |= SC_DISPLAY_PENDING_FLAG_TEXTURE;
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_display_set_pending_frame(struct sc_display *display, const AVFrame *frame) {
|
||||
if (!display->pending.frame) {
|
||||
display->pending.frame = av_frame_alloc();
|
||||
if (!display->pending.frame) {
|
||||
LOG_OOM();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
av_frame_unref(display->pending.frame);
|
||||
int r = av_frame_ref(display->pending.frame, frame);
|
||||
if (r) {
|
||||
LOGE("Could not ref frame: %d", r);
|
||||
return false;
|
||||
}
|
||||
|
||||
display->pending.flags |= SC_DISPLAY_PENDING_FLAG_FRAME;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Forward declaration
|
||||
static bool
|
||||
sc_display_update_texture_internal(struct sc_display *display,
|
||||
const AVFrame *frame);
|
||||
|
||||
static bool
|
||||
sc_display_apply_pending(struct sc_display *display) {
|
||||
if (display->pending.flags & SC_DISPLAY_PENDING_FLAG_TEXTURE) {
|
||||
assert(!display->texture);
|
||||
display->texture =
|
||||
sc_display_create_texture(display,
|
||||
display->pending.texture.size,
|
||||
display->pending.texture.color_space,
|
||||
display->pending.texture.color_range);
|
||||
if (!display->texture) {
|
||||
return false;
|
||||
}
|
||||
|
||||
display->pending.flags &= ~SC_DISPLAY_PENDING_FLAG_TEXTURE;
|
||||
}
|
||||
|
||||
if (display->pending.flags & SC_DISPLAY_PENDING_FLAG_FRAME) {
|
||||
assert(display->pending.frame);
|
||||
bool ok = sc_display_update_texture_internal(display,
|
||||
display->pending.frame);
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
av_frame_unref(display->pending.frame);
|
||||
display->pending.flags &= ~SC_DISPLAY_PENDING_FLAG_FRAME;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_display_prepare_texture_internal(struct sc_display *display,
|
||||
struct sc_size size,
|
||||
enum AVColorSpace color_space,
|
||||
enum AVColorRange color_range) {
|
||||
assert(size.width && size.height);
|
||||
|
||||
if (display->texture) {
|
||||
SDL_DestroyTexture(display->texture);
|
||||
}
|
||||
|
||||
display->texture =
|
||||
sc_display_create_texture(display, size, color_space, color_range);
|
||||
if (!display->texture) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LOGI("Texture: %" PRIu16 "x%" PRIu16, size.width, size.height);
|
||||
return true;
|
||||
}
|
||||
|
||||
enum sc_display_result
|
||||
sc_display_prepare_texture(struct sc_display *display, struct sc_size size,
|
||||
enum AVColorSpace color_space,
|
||||
enum AVColorRange color_range) {
|
||||
bool ok = sc_display_prepare_texture_internal(display, size, color_space,
|
||||
color_range);
|
||||
if (!ok) {
|
||||
sc_display_set_pending_texture(display, size, color_range);
|
||||
return SC_DISPLAY_RESULT_PENDING;
|
||||
|
||||
}
|
||||
|
||||
return SC_DISPLAY_RESULT_OK;
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_display_update_texture_internal(struct sc_display *display,
|
||||
const AVFrame *frame) {
|
||||
bool ok = SDL_UpdateYUVTexture(display->texture, NULL,
|
||||
frame->data[0], frame->linesize[0],
|
||||
frame->data[1], frame->linesize[1],
|
||||
frame->data[2], frame->linesize[2]);
|
||||
if (!ok) {
|
||||
LOGD("Could not update texture: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (display->mipmaps) {
|
||||
assert(display->texture_id);
|
||||
struct sc_opengl *gl = &display->gl;
|
||||
|
||||
gl->BindTexture(GL_TEXTURE_2D, display->texture_id);
|
||||
display->gl.GenerateMipmap(GL_TEXTURE_2D);
|
||||
gl->BindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
enum sc_display_result
|
||||
sc_display_update_texture(struct sc_display *display, const AVFrame *frame) {
|
||||
bool ok = sc_display_update_texture_internal(display, frame);
|
||||
if (!ok) {
|
||||
ok = sc_display_set_pending_frame(display, frame);
|
||||
if (!ok) {
|
||||
LOGE("Could not set pending frame");
|
||||
return SC_DISPLAY_RESULT_ERROR;
|
||||
}
|
||||
|
||||
return SC_DISPLAY_RESULT_PENDING;
|
||||
}
|
||||
|
||||
return SC_DISPLAY_RESULT_OK;
|
||||
}
|
||||
|
||||
enum sc_display_result
|
||||
sc_display_render(struct sc_display *display, const SDL_Rect *geometry,
|
||||
enum sc_orientation orientation) {
|
||||
bool always_ok = SDL_RenderClear(display->renderer);
|
||||
(void) always_ok;
|
||||
assert(always_ok);
|
||||
|
||||
if (display->pending.flags) {
|
||||
bool ok = sc_display_apply_pending(display);
|
||||
if (!ok) {
|
||||
return SC_DISPLAY_RESULT_PENDING;
|
||||
}
|
||||
}
|
||||
|
||||
SDL_Renderer *renderer = display->renderer;
|
||||
SDL_Texture *texture = display->texture;
|
||||
|
||||
if (orientation == SC_ORIENTATION_0) {
|
||||
SDL_FRect frect;
|
||||
SDL_RectToFRect(geometry, &frect);
|
||||
bool ok = SDL_RenderTexture(renderer, texture, NULL, &frect);
|
||||
if (!ok) {
|
||||
LOGE("Could not render texture: %s", SDL_GetError());
|
||||
return SC_DISPLAY_RESULT_ERROR;
|
||||
}
|
||||
} else {
|
||||
unsigned cw_rotation = sc_orientation_get_rotation(orientation);
|
||||
double angle = 90 * cw_rotation;
|
||||
|
||||
SDL_FRect frect;
|
||||
if (sc_orientation_is_swap(orientation)) {
|
||||
frect.x = geometry->x + (geometry->w - geometry->h) / 2.f;
|
||||
frect.y = geometry->y + (geometry->h - geometry->w) / 2.f;
|
||||
frect.w = geometry->h;
|
||||
frect.h = geometry->w;
|
||||
} else {
|
||||
SDL_RectToFRect(geometry, &frect);
|
||||
}
|
||||
|
||||
SDL_FlipMode flip = sc_orientation_is_mirror(orientation)
|
||||
? SDL_FLIP_HORIZONTAL : 0;
|
||||
|
||||
bool ok = SDL_RenderTextureRotated(renderer, texture, NULL, &frect,
|
||||
angle, NULL, flip);
|
||||
if (!ok) {
|
||||
LOGE("Could not render texture: %s", SDL_GetError());
|
||||
return SC_DISPLAY_RESULT_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
always_ok = SDL_RenderPresent(display->renderer);
|
||||
assert(always_ok);
|
||||
|
||||
return SC_DISPLAY_RESULT_OK;
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
#ifndef SC_DISPLAY_H
|
||||
#define SC_DISPLAY_H
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <libavutil/frame.h>
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include "coords.h"
|
||||
#include "opengl.h"
|
||||
#include "options.h"
|
||||
|
||||
#ifdef __APPLE__
|
||||
# define SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
|
||||
#endif
|
||||
|
||||
struct sc_display {
|
||||
SDL_Renderer *renderer;
|
||||
SDL_Texture *texture;
|
||||
|
||||
struct sc_opengl gl;
|
||||
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
|
||||
SDL_GLContext gl_context;
|
||||
#endif
|
||||
|
||||
bool mipmaps;
|
||||
uint32_t texture_id; // only set if mipmaps is enabled
|
||||
|
||||
struct {
|
||||
#define SC_DISPLAY_PENDING_FLAG_TEXTURE 1
|
||||
#define SC_DISPLAY_PENDING_FLAG_FRAME 2
|
||||
int8_t flags;
|
||||
struct {
|
||||
struct sc_size size;
|
||||
enum AVColorSpace color_space;
|
||||
enum AVColorRange color_range;
|
||||
} texture;
|
||||
AVFrame *frame;
|
||||
} pending;
|
||||
};
|
||||
|
||||
enum sc_display_result {
|
||||
SC_DISPLAY_RESULT_OK,
|
||||
SC_DISPLAY_RESULT_PENDING,
|
||||
SC_DISPLAY_RESULT_ERROR,
|
||||
};
|
||||
|
||||
bool
|
||||
sc_display_init(struct sc_display *display, SDL_Window *window,
|
||||
SDL_Surface *icon_novideo, bool mipmaps);
|
||||
|
||||
void
|
||||
sc_display_destroy(struct sc_display *display);
|
||||
|
||||
enum sc_display_result
|
||||
sc_display_prepare_texture(struct sc_display *display, struct sc_size size,
|
||||
enum AVColorSpace color_space,
|
||||
enum AVColorRange color_range);
|
||||
|
||||
enum sc_display_result
|
||||
sc_display_update_texture(struct sc_display *display, const AVFrame *frame);
|
||||
|
||||
enum sc_display_result
|
||||
sc_display_render(struct sc_display *display, const SDL_Rect *geometry,
|
||||
enum sc_orientation orientation);
|
||||
|
||||
#endif
|
||||
@@ -9,7 +9,7 @@
|
||||
#include "hid/hid_event.h"
|
||||
#include "input_events.h"
|
||||
|
||||
// See "SDL2/SDL_scancode.h".
|
||||
// See "SDL3/SDL_scancode.h".
|
||||
// Maybe SDL_Keycode is used by most people, but SDL_Scancode is taken from USB
|
||||
// HID protocol.
|
||||
// 0x65 is Application, typically AT-101 Keyboard ends here.
|
||||
|
||||
@@ -14,33 +14,37 @@
|
||||
|
||||
#include "config.h"
|
||||
#include "util/env.h"
|
||||
#ifdef PORTABLE
|
||||
# include "util/file.h"
|
||||
#endif
|
||||
#include "util/file.h"
|
||||
#include "util/log.h"
|
||||
|
||||
#define SCRCPY_PORTABLE_ICON_FILENAME "icon.png"
|
||||
#define SCRCPY_DEFAULT_ICON_PATH \
|
||||
PREFIX "/share/icons/hicolor/256x256/apps/scrcpy.png"
|
||||
#define SCRCPY_DEFAULT_ICON_DIR PREFIX "/share/icons/hicolor/256x256/apps"
|
||||
|
||||
static char *
|
||||
get_icon_path(void) {
|
||||
char *icon_path = sc_get_env("SCRCPY_ICON_PATH");
|
||||
if (icon_path) {
|
||||
get_icon_path(const char *filename) {
|
||||
char *icon_path;
|
||||
|
||||
char *icon_dir = sc_get_env("SCRCPY_ICON_DIR");
|
||||
if (icon_dir) {
|
||||
// if the envvar is set, use it
|
||||
LOGD("Using SCRCPY_ICON_PATH: %s", icon_path);
|
||||
icon_path = sc_file_build_path(icon_dir, filename);
|
||||
free(icon_dir);
|
||||
if (!icon_path) {
|
||||
LOG_OOM();
|
||||
return NULL;
|
||||
}
|
||||
LOGD("Using icon from SCRCPY_ICON_DIR: %s", icon_path);
|
||||
return icon_path;
|
||||
}
|
||||
|
||||
#ifndef PORTABLE
|
||||
LOGD("Using icon: " SCRCPY_DEFAULT_ICON_PATH);
|
||||
icon_path = strdup(SCRCPY_DEFAULT_ICON_PATH);
|
||||
icon_path = sc_file_build_path(SCRCPY_DEFAULT_ICON_DIR, filename);
|
||||
if (!icon_path) {
|
||||
LOG_OOM();
|
||||
return NULL;
|
||||
}
|
||||
LOGD("Using icon: %s", icon_path);
|
||||
#else
|
||||
icon_path = sc_file_get_local_path(SCRCPY_PORTABLE_ICON_FILENAME);
|
||||
icon_path = sc_file_get_local_path(filename);
|
||||
if (!icon_path) {
|
||||
LOGE("Could not get icon path");
|
||||
return NULL;
|
||||
@@ -177,7 +181,7 @@ to_sdl_pixel_format(enum AVPixelFormat fmt) {
|
||||
}
|
||||
|
||||
static SDL_Surface *
|
||||
load_from_path(const char *path) {
|
||||
sc_icon_load_from_full_path(const char *path) {
|
||||
AVFrame *frame = decode_image(path);
|
||||
if (!frame) {
|
||||
return NULL;
|
||||
@@ -261,7 +265,7 @@ load_from_path(const char *path) {
|
||||
// frame owns the data
|
||||
bool ok = SDL_SetPointerProperty(props, "sc_frame", frame);
|
||||
if (!ok) {
|
||||
LOGE("Could not get surface properties: %s", SDL_GetError());
|
||||
LOGE("Could not set pointer property: %s", SDL_GetError());
|
||||
SDL_DestroySurface(surface);
|
||||
goto error;
|
||||
}
|
||||
@@ -274,19 +278,19 @@ error:
|
||||
}
|
||||
|
||||
SDL_Surface *
|
||||
scrcpy_icon_load(void) {
|
||||
char *icon_path = get_icon_path();
|
||||
sc_icon_load(const char *filename) {
|
||||
char *icon_path = get_icon_path(filename);
|
||||
if (!icon_path) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
SDL_Surface *icon = load_from_path(icon_path);
|
||||
SDL_Surface *icon = sc_icon_load_from_full_path(icon_path);
|
||||
free(icon_path);
|
||||
return icon;
|
||||
}
|
||||
|
||||
void
|
||||
scrcpy_icon_destroy(SDL_Surface *icon) {
|
||||
sc_icon_destroy(SDL_Surface *icon) {
|
||||
SDL_PropertiesID props = SDL_GetSurfaceProperties(icon);
|
||||
assert(props);
|
||||
AVFrame *frame = SDL_GetPointerProperty(props, "sc_frame", NULL);
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
|
||||
#include <SDL3/SDL_surface.h>
|
||||
|
||||
#define SC_ICON_FILENAME_SCRCPY "scrcpy.png"
|
||||
|
||||
SDL_Surface *
|
||||
scrcpy_icon_load(void);
|
||||
sc_icon_load(const char *filename);
|
||||
|
||||
void
|
||||
scrcpy_icon_destroy(SDL_Surface *icon);
|
||||
sc_icon_destroy(SDL_Surface *icon);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
* for simplicity.
|
||||
*
|
||||
* This scrcpy input events API is designed to be consumed by input event
|
||||
* processors (sc_key_processor and sc_mouse_processor, see app/src/trait/).
|
||||
* processors (sc_key_processor, sc_mouse_processor and sc_gamepad_processor,
|
||||
* see app/src/trait/).
|
||||
*
|
||||
* One major semantic difference between SDL input events and scrcpy input
|
||||
* events is their frame of reference (for mouse and touch events): SDL events
|
||||
@@ -322,9 +323,6 @@ enum sc_mouse_button {
|
||||
SC_MOUSE_BUTTON_X2 = SDL_BUTTON_MASK(SDL_BUTTON_X2),
|
||||
};
|
||||
|
||||
// Use the naming from SDL3 for gamepad axis and buttons:
|
||||
// <https://wiki.libsdl.org/SDL3/README/migration>
|
||||
|
||||
enum sc_gamepad_axis {
|
||||
SC_GAMEPAD_AXIS_UNKNOWN = -1,
|
||||
SC_GAMEPAD_AXIS_LEFTX = SDL_GAMEPAD_AXIS_LEFTX,
|
||||
@@ -411,10 +409,9 @@ struct sc_touch_event {
|
||||
float pressure;
|
||||
};
|
||||
|
||||
// As documented in <https://wiki.libsdl.org/SDL2/SDL_JoystickID>:
|
||||
// The ID value starts at 0 and increments from there. The value -1 is an
|
||||
// invalid ID.
|
||||
#define SC_GAMEPAD_ID_INVALID UINT32_C(-1)
|
||||
// As documented in <https://wiki.libsdl.org/SDL3/SDL_JoystickID>:
|
||||
// The value 0 is an invalid ID.
|
||||
#define SC_GAMEPAD_ID_INVALID 0
|
||||
|
||||
struct sc_gamepad_device_event {
|
||||
uint32_t gamepad_id;
|
||||
@@ -500,7 +497,7 @@ static inline enum sc_gamepad_axis
|
||||
sc_gamepad_axis_from_sdl(uint8_t axis) {
|
||||
if (axis <= SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) {
|
||||
// SC_GAMEPAD_AXIS_* constants are initialized from
|
||||
// SDL_CONTROLLER_AXIS_*
|
||||
// SDL_GAMEPAD_AXIS_*
|
||||
return axis;
|
||||
}
|
||||
return SC_GAMEPAD_AXIS_UNKNOWN;
|
||||
@@ -510,14 +507,14 @@ static inline enum sc_gamepad_button
|
||||
sc_gamepad_button_from_sdl(uint8_t button) {
|
||||
if (button <= SDL_GAMEPAD_BUTTON_DPAD_RIGHT) {
|
||||
// SC_GAMEPAD_BUTTON_* constants are initialized from
|
||||
// SDL_CONTROLLER_BUTTON_*
|
||||
// SDL_GAMEPAD_BUTTON_*
|
||||
return button;
|
||||
}
|
||||
return SC_GAMEPAD_BUTTON_UNKNOWN;
|
||||
}
|
||||
|
||||
static inline enum sc_action
|
||||
sc_action_from_sdl_controllerbutton_type(uint32_t type) {
|
||||
sc_action_from_sdl_gamepad_button_type(uint32_t type) {
|
||||
assert(type == SDL_EVENT_GAMEPAD_BUTTON_DOWN || type == SDL_EVENT_GAMEPAD_BUTTON_UP);
|
||||
if (type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
|
||||
return SC_ACTION_DOWN;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "screen.h"
|
||||
#include "shortcut_mod.h"
|
||||
#include "util/log.h"
|
||||
#include "util/sdl.h"
|
||||
|
||||
void
|
||||
sc_input_manager_init(struct sc_input_manager *im,
|
||||
@@ -28,6 +29,7 @@ sc_input_manager_init(struct sc_input_manager *im,
|
||||
im->kp = params->kp;
|
||||
im->mp = params->mp;
|
||||
im->gp = params->gp;
|
||||
im->camera = params->camera;
|
||||
|
||||
im->mouse_bindings = params->mouse_bindings;
|
||||
im->legacy_paste = params->legacy_paste;
|
||||
@@ -51,7 +53,7 @@ sc_input_manager_init(struct sc_input_manager *im,
|
||||
static void
|
||||
send_keycode(struct sc_input_manager *im, enum android_keycode keycode,
|
||||
enum sc_action action, const char *name) {
|
||||
assert(im->controller && im->kp);
|
||||
assert(im->controller && im->kp && !im->camera);
|
||||
|
||||
// send DOWN event
|
||||
struct sc_control_msg msg;
|
||||
@@ -108,7 +110,7 @@ action_menu(struct sc_input_manager *im, enum sc_action action) {
|
||||
static void
|
||||
press_back_or_turn_screen_on(struct sc_input_manager *im,
|
||||
enum sc_action action) {
|
||||
assert(im->controller && im->kp);
|
||||
assert(im->controller && im->kp && !im->camera);
|
||||
|
||||
struct sc_control_msg msg;
|
||||
msg.type = SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON;
|
||||
@@ -123,7 +125,7 @@ press_back_or_turn_screen_on(struct sc_input_manager *im,
|
||||
|
||||
static void
|
||||
expand_notification_panel(struct sc_input_manager *im) {
|
||||
assert(im->controller);
|
||||
assert(im->controller && !im->camera);
|
||||
|
||||
struct sc_control_msg msg;
|
||||
msg.type = SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL;
|
||||
@@ -135,7 +137,7 @@ expand_notification_panel(struct sc_input_manager *im) {
|
||||
|
||||
static void
|
||||
expand_settings_panel(struct sc_input_manager *im) {
|
||||
assert(im->controller);
|
||||
assert(im->controller && !im->camera);
|
||||
|
||||
struct sc_control_msg msg;
|
||||
msg.type = SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL;
|
||||
@@ -147,7 +149,7 @@ expand_settings_panel(struct sc_input_manager *im) {
|
||||
|
||||
static void
|
||||
collapse_panels(struct sc_input_manager *im) {
|
||||
assert(im->controller);
|
||||
assert(im->controller && !im->camera);
|
||||
|
||||
struct sc_control_msg msg;
|
||||
msg.type = SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS;
|
||||
@@ -159,7 +161,7 @@ collapse_panels(struct sc_input_manager *im) {
|
||||
|
||||
static bool
|
||||
get_device_clipboard(struct sc_input_manager *im, enum sc_copy_key copy_key) {
|
||||
assert(im->controller && im->kp);
|
||||
assert(im->controller && im->kp && !im->camera);
|
||||
|
||||
struct sc_control_msg msg;
|
||||
msg.type = SC_CONTROL_MSG_TYPE_GET_CLIPBOARD;
|
||||
@@ -176,7 +178,7 @@ get_device_clipboard(struct sc_input_manager *im, enum sc_copy_key copy_key) {
|
||||
static bool
|
||||
set_device_clipboard(struct sc_input_manager *im, bool paste,
|
||||
uint64_t sequence) {
|
||||
assert(im->controller && im->kp);
|
||||
assert(im->controller && im->kp && !im->camera);
|
||||
|
||||
char *text = SDL_GetClipboardText();
|
||||
if (!text) {
|
||||
@@ -208,7 +210,7 @@ set_device_clipboard(struct sc_input_manager *im, bool paste,
|
||||
|
||||
static void
|
||||
set_display_power(struct sc_input_manager *im, bool on) {
|
||||
assert(im->controller);
|
||||
assert(im->controller && !im->camera);
|
||||
|
||||
struct sc_control_msg msg;
|
||||
msg.type = SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER;
|
||||
@@ -235,7 +237,7 @@ switch_fps_counter_state(struct sc_input_manager *im) {
|
||||
|
||||
static void
|
||||
clipboard_paste(struct sc_input_manager *im) {
|
||||
assert(im->controller && im->kp);
|
||||
assert(im->controller && im->kp && !im->camera);
|
||||
|
||||
char *text = SDL_GetClipboardText();
|
||||
if (!text) {
|
||||
@@ -266,7 +268,7 @@ clipboard_paste(struct sc_input_manager *im) {
|
||||
|
||||
static void
|
||||
rotate_device(struct sc_input_manager *im) {
|
||||
assert(im->controller);
|
||||
assert(im->controller && !im->camera);
|
||||
|
||||
struct sc_control_msg msg;
|
||||
msg.type = SC_CONTROL_MSG_TYPE_ROTATE_DEVICE;
|
||||
@@ -278,7 +280,7 @@ rotate_device(struct sc_input_manager *im) {
|
||||
|
||||
static void
|
||||
open_hard_keyboard_settings(struct sc_input_manager *im) {
|
||||
assert(im->controller);
|
||||
assert(im->controller && !im->camera);
|
||||
|
||||
struct sc_control_msg msg;
|
||||
msg.type = SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS;
|
||||
@@ -300,6 +302,43 @@ reset_video(struct sc_input_manager *im) {
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
camera_set_torch(struct sc_input_manager *im, bool on) {
|
||||
assert(im->controller && im->camera);
|
||||
|
||||
struct sc_control_msg msg;
|
||||
msg.type = SC_CONTROL_MSG_TYPE_CAMERA_SET_TORCH;
|
||||
msg.camera_set_torch.on = on;
|
||||
|
||||
if (!sc_controller_push_msg(im->controller, &msg)) {
|
||||
LOGW("Could not request setting camera torch");
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
camera_zoom_in(struct sc_input_manager *im) {
|
||||
assert(im->controller && im->camera);
|
||||
|
||||
struct sc_control_msg msg;
|
||||
msg.type = SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_IN;
|
||||
|
||||
if (!sc_controller_push_msg(im->controller, &msg)) {
|
||||
LOGW("Could not request camera zoom in");
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
camera_zoom_out(struct sc_input_manager *im) {
|
||||
assert(im->controller && im->camera);
|
||||
|
||||
struct sc_control_msg msg;
|
||||
msg.type = SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_OUT;
|
||||
|
||||
if (!sc_controller_push_msg(im->controller, &msg)) {
|
||||
LOGW("Could not request camera zoom out");
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
apply_orientation_transform(struct sc_input_manager *im,
|
||||
enum sc_orientation transform) {
|
||||
@@ -312,6 +351,10 @@ apply_orientation_transform(struct sc_input_manager *im,
|
||||
static void
|
||||
sc_input_manager_process_text_input(struct sc_input_manager *im,
|
||||
const SDL_TextInputEvent *event) {
|
||||
if (im->camera || !im->kp || im->screen->paused) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!im->kp->ops->process_text) {
|
||||
// The key processor does not support text input
|
||||
return;
|
||||
@@ -369,6 +412,9 @@ inverse_point(struct sc_point point, struct sc_size size,
|
||||
static void
|
||||
sc_input_manager_process_key(struct sc_input_manager *im,
|
||||
const SDL_KeyboardEvent *event) {
|
||||
// some key events do not interact with the device, so process the event
|
||||
// even if control is disabled
|
||||
|
||||
// controller is NULL if --no-control is requested
|
||||
bool control = im->controller;
|
||||
bool paused = im->screen->paused;
|
||||
@@ -402,106 +448,45 @@ sc_input_manager_process_key(struct sc_input_manager *im,
|
||||
if (is_shortcut) {
|
||||
enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP;
|
||||
switch (sdl_keycode) {
|
||||
case SDLK_H:
|
||||
if (im->kp && !shift && !repeat && !paused) {
|
||||
action_home(im, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_B: // fall-through
|
||||
case SDLK_BACKSPACE:
|
||||
if (im->kp && !shift && !repeat && !paused) {
|
||||
action_back(im, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_S:
|
||||
if (im->kp && !shift && !repeat && !paused) {
|
||||
action_app_switch(im, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_M:
|
||||
if (im->kp && !shift && !repeat && !paused) {
|
||||
action_menu(im, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_P:
|
||||
if (im->kp && !shift && !repeat && !paused) {
|
||||
action_power(im, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_O:
|
||||
if (control && !repeat && down && !paused) {
|
||||
bool on = shift;
|
||||
set_display_power(im, on);
|
||||
}
|
||||
return;
|
||||
case SDLK_Z:
|
||||
if (video && down && !repeat) {
|
||||
sc_screen_set_paused(im->screen, !shift);
|
||||
}
|
||||
return;
|
||||
case SDLK_DOWN:
|
||||
// Only capture if shift is set
|
||||
if (shift) {
|
||||
if (video && !repeat && down) {
|
||||
apply_orientation_transform(im,
|
||||
SC_ORIENTATION_FLIP_180);
|
||||
}
|
||||
} else if (im->kp && !paused) {
|
||||
// forward repeated events
|
||||
action_volume_down(im, action);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
break;
|
||||
case SDLK_UP:
|
||||
// Only capture if shift is set
|
||||
if (shift) {
|
||||
if (video && !repeat && down) {
|
||||
apply_orientation_transform(im,
|
||||
SC_ORIENTATION_FLIP_180);
|
||||
apply_orientation_transform(im, SC_ORIENTATION_FLIP_180);
|
||||
}
|
||||
} else if (im->kp && !paused) {
|
||||
// forward repeated events
|
||||
action_volume_up(im, action);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
break;
|
||||
case SDLK_LEFT:
|
||||
if (video && !repeat && down) {
|
||||
if (shift) {
|
||||
apply_orientation_transform(im,
|
||||
SC_ORIENTATION_FLIP_0);
|
||||
apply_orientation_transform(im, SC_ORIENTATION_FLIP_0);
|
||||
} else {
|
||||
apply_orientation_transform(im,
|
||||
SC_ORIENTATION_270);
|
||||
apply_orientation_transform(im, SC_ORIENTATION_270);
|
||||
}
|
||||
}
|
||||
return;
|
||||
case SDLK_RIGHT:
|
||||
if (video && !repeat && down) {
|
||||
if (shift) {
|
||||
apply_orientation_transform(im,
|
||||
SC_ORIENTATION_FLIP_0);
|
||||
apply_orientation_transform(im, SC_ORIENTATION_FLIP_0);
|
||||
} else {
|
||||
apply_orientation_transform(im,
|
||||
SC_ORIENTATION_90);
|
||||
}
|
||||
}
|
||||
return;
|
||||
case SDLK_C:
|
||||
if (im->kp && !shift && !repeat && down && !paused) {
|
||||
get_device_clipboard(im, SC_COPY_KEY_COPY);
|
||||
}
|
||||
return;
|
||||
case SDLK_X:
|
||||
if (im->kp && !shift && !repeat && down && !paused) {
|
||||
get_device_clipboard(im, SC_COPY_KEY_CUT);
|
||||
}
|
||||
return;
|
||||
case SDLK_V:
|
||||
if (im->kp && !repeat && down && !paused) {
|
||||
if (shift || im->legacy_paste) {
|
||||
// inject the text as input events
|
||||
clipboard_paste(im);
|
||||
} else {
|
||||
// store the text in the device clipboard and paste,
|
||||
// without requesting an acknowledgment
|
||||
set_device_clipboard(im, true, SC_SEQUENCE_INVALID);
|
||||
apply_orientation_transform(im, SC_ORIENTATION_90);
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -525,33 +510,134 @@ sc_input_manager_process_key(struct sc_input_manager *im,
|
||||
switch_fps_counter_state(im);
|
||||
}
|
||||
return;
|
||||
case SDLK_N:
|
||||
if (control && !repeat && down && !paused) {
|
||||
if (shift) {
|
||||
collapse_panels(im);
|
||||
} else if (im->key_repeat == 0) {
|
||||
expand_notification_panel(im);
|
||||
} else {
|
||||
expand_settings_panel(im);
|
||||
}
|
||||
}
|
||||
return;
|
||||
case SDLK_R:
|
||||
if (control && !repeat && down && !paused) {
|
||||
if (shift) {
|
||||
}
|
||||
|
||||
// Flatten conditions to avoid additional indentation levels
|
||||
if (control) {
|
||||
// Controls for all sources
|
||||
switch (sdl_keycode) {
|
||||
case SDLK_R:
|
||||
if (!repeat && shift && down && !paused) {
|
||||
reset_video(im);
|
||||
} else {
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (control && !im->camera) {
|
||||
switch (sdl_keycode) {
|
||||
case SDLK_H:
|
||||
if (im->kp && !shift && !repeat && !paused) {
|
||||
action_home(im, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_B: // fall-through
|
||||
case SDLK_BACKSPACE:
|
||||
if (im->kp && !shift && !repeat && !paused) {
|
||||
action_back(im, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_S:
|
||||
if (im->kp && !shift && !repeat && !paused) {
|
||||
action_app_switch(im, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_M:
|
||||
if (im->kp && !shift && !repeat && !paused) {
|
||||
action_menu(im, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_P:
|
||||
if (im->kp && !shift && !repeat && !paused) {
|
||||
action_power(im, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_O:
|
||||
if (control && !repeat && down && !paused) {
|
||||
bool on = shift;
|
||||
set_display_power(im, on);
|
||||
}
|
||||
return;
|
||||
case SDLK_DOWN:
|
||||
if (im->kp && !shift && !paused) {
|
||||
// forward repeated events
|
||||
action_volume_down(im, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_UP:
|
||||
if (im->kp && !shift && !paused) {
|
||||
// forward repeated events
|
||||
action_volume_up(im, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_C:
|
||||
if (im->kp && !shift && !repeat && down && !paused) {
|
||||
get_device_clipboard(im, SC_COPY_KEY_COPY);
|
||||
}
|
||||
return;
|
||||
case SDLK_X:
|
||||
if (im->kp && !shift && !repeat && down && !paused) {
|
||||
get_device_clipboard(im, SC_COPY_KEY_CUT);
|
||||
}
|
||||
return;
|
||||
case SDLK_V:
|
||||
if (im->kp && !repeat && down && !paused) {
|
||||
if (shift || im->legacy_paste) {
|
||||
// inject the text as input events
|
||||
clipboard_paste(im);
|
||||
} else {
|
||||
// store the text in the device clipboard and paste,
|
||||
// without requesting an acknowledgment
|
||||
set_device_clipboard(im, true, SC_SEQUENCE_INVALID);
|
||||
}
|
||||
}
|
||||
return;
|
||||
case SDLK_N:
|
||||
if (!repeat && down && !paused) {
|
||||
if (shift) {
|
||||
collapse_panels(im);
|
||||
} else if (im->key_repeat == 0) {
|
||||
expand_notification_panel(im);
|
||||
} else {
|
||||
expand_settings_panel(im);
|
||||
}
|
||||
}
|
||||
return;
|
||||
case SDLK_R:
|
||||
if (!repeat && !shift && down && !paused) {
|
||||
rotate_device(im);
|
||||
}
|
||||
}
|
||||
return;
|
||||
case SDLK_K:
|
||||
if (control && !shift && !repeat && down && !paused
|
||||
&& im->kp && im->kp->hid) {
|
||||
// Only if the current keyboard is hid
|
||||
open_hard_keyboard_settings(im);
|
||||
}
|
||||
return;
|
||||
return;
|
||||
case SDLK_K:
|
||||
if (!shift && !repeat && down && !paused
|
||||
&& im->kp && im->kp->hid) {
|
||||
// Only if the current keyboard is hid
|
||||
open_hard_keyboard_settings(im);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (control && im->camera) {
|
||||
switch (sdl_keycode) {
|
||||
case SDLK_T:
|
||||
if (!repeat && down) {
|
||||
camera_set_torch(im, !shift);
|
||||
}
|
||||
return;
|
||||
case SDLK_DOWN:
|
||||
if (!shift && down && !paused) {
|
||||
// forward repeated events
|
||||
camera_zoom_out(im);
|
||||
}
|
||||
return;
|
||||
case SDLK_UP:
|
||||
if (!shift && down && !paused) {
|
||||
// forward repeated events
|
||||
camera_zoom_in(im);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -561,6 +647,8 @@ sc_input_manager_process_key(struct sc_input_manager *im,
|
||||
return;
|
||||
}
|
||||
|
||||
assert(!im->camera);
|
||||
|
||||
uint64_t ack_to_wait = SC_SEQUENCE_INVALID;
|
||||
bool is_ctrl_v = ctrl && !shift && sdl_keycode == SDLK_V && down && !repeat;
|
||||
if (im->clipboard_autosync && is_ctrl_v) {
|
||||
@@ -632,6 +720,10 @@ sc_input_manager_get_position(struct sc_input_manager *im, int32_t x,
|
||||
static void
|
||||
sc_input_manager_process_mouse_motion(struct sc_input_manager *im,
|
||||
const SDL_MouseMotionEvent *event) {
|
||||
if (im->camera || !im->mp || im->screen->paused) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event->which == SDL_TOUCH_MOUSEID) {
|
||||
// simulated from touch events, so it's a duplicate
|
||||
return;
|
||||
@@ -667,20 +759,21 @@ sc_input_manager_process_mouse_motion(struct sc_input_manager *im,
|
||||
static void
|
||||
sc_input_manager_process_touch(struct sc_input_manager *im,
|
||||
const SDL_TouchFingerEvent *event) {
|
||||
if (im->camera || !im->mp || im->screen->paused) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!im->mp->ops->process_touch) {
|
||||
// The mouse processor does not support touch events
|
||||
return;
|
||||
}
|
||||
|
||||
int dw;
|
||||
int dh;
|
||||
bool always_ok = SDL_GetWindowSizeInPixels(im->screen->window, &dw, &dh);
|
||||
(void) always_ok;
|
||||
assert(always_ok);
|
||||
struct sc_size drawable_size =
|
||||
sc_sdl_get_window_size_in_pixels(im->screen->window);
|
||||
|
||||
// SDL touch event coordinates are normalized in the range [0; 1]
|
||||
int32_t x = event->x * dw;
|
||||
int32_t y = event->y * dh;
|
||||
int32_t x = event->x * (int32_t) drawable_size.width;
|
||||
int32_t y = event->y * (int32_t) drawable_size.height;
|
||||
|
||||
struct sc_touch_event evt = {
|
||||
.position = {
|
||||
@@ -718,6 +811,13 @@ sc_input_manager_get_binding(const struct sc_mouse_binding_set *bindings,
|
||||
static void
|
||||
sc_input_manager_process_mouse_button(struct sc_input_manager *im,
|
||||
const SDL_MouseButtonEvent *event) {
|
||||
// some mouse events do not interact with the device, so process the event
|
||||
// even if control is disabled
|
||||
|
||||
if (im->camera) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event->which == SDL_TOUCH_MOUSEID) {
|
||||
// simulated from touch events, so it's a duplicate
|
||||
return;
|
||||
@@ -792,7 +892,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
|
||||
int32_t x = event->x;
|
||||
int32_t y = event->y;
|
||||
sc_screen_hidpi_scale_coords(im->screen, &x, &y);
|
||||
SDL_Rect *r = &im->screen->rect;
|
||||
SDL_FRect *r = &im->screen->rect;
|
||||
bool outside = x < r->x || x >= r->x + r->w
|
||||
|| y < r->y || y >= r->y + r->h;
|
||||
if (outside) {
|
||||
@@ -885,6 +985,10 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
|
||||
static void
|
||||
sc_input_manager_process_mouse_wheel(struct sc_input_manager *im,
|
||||
const SDL_MouseWheelEvent *event) {
|
||||
if (im->camera || !im->kp || im->screen->paused) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!im->mp->ops->process_mouse_scroll) {
|
||||
// The mouse processor does not support scroll events
|
||||
return;
|
||||
@@ -909,17 +1013,23 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im,
|
||||
static void
|
||||
sc_input_manager_process_gamepad_device(struct sc_input_manager *im,
|
||||
const SDL_GamepadDeviceEvent *event) {
|
||||
// Handle device added or removed even if paused
|
||||
|
||||
if (im->camera || !im->gp) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event->type == SDL_EVENT_GAMEPAD_ADDED) {
|
||||
SDL_Gamepad *gc = SDL_OpenGamepad(event->which);
|
||||
if (!gc) {
|
||||
LOGW("Could not open game controller");
|
||||
SDL_Gamepad *sdl_gamepad = SDL_OpenGamepad(event->which);
|
||||
if (!sdl_gamepad) {
|
||||
LOGW("Could not open gamepad");
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_Joystick *joystick = SDL_GetGamepadJoystick(gc);
|
||||
SDL_Joystick *joystick = SDL_GetGamepadJoystick(sdl_gamepad);
|
||||
if (!joystick) {
|
||||
LOGW("Could not get controller joystick");
|
||||
SDL_CloseGamepad(gc);
|
||||
LOGW("Could not get gamepad joystick");
|
||||
SDL_CloseGamepad(sdl_gamepad);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -930,9 +1040,9 @@ sc_input_manager_process_gamepad_device(struct sc_input_manager *im,
|
||||
} else if (event->type == SDL_EVENT_GAMEPAD_REMOVED) {
|
||||
SDL_JoystickID id = event->which;
|
||||
|
||||
SDL_Gamepad *gc = SDL_GetGamepadFromID(id);
|
||||
if (gc) {
|
||||
SDL_CloseGamepad(gc);
|
||||
SDL_Gamepad *sdl_gamepad = SDL_GetGamepadFromID(id);
|
||||
if (sdl_gamepad) {
|
||||
SDL_CloseGamepad(sdl_gamepad);
|
||||
} else {
|
||||
LOGW("Unknown gamepad device removed");
|
||||
}
|
||||
@@ -950,6 +1060,10 @@ sc_input_manager_process_gamepad_device(struct sc_input_manager *im,
|
||||
static void
|
||||
sc_input_manager_process_gamepad_axis(struct sc_input_manager *im,
|
||||
const SDL_GamepadAxisEvent *event) {
|
||||
if (im->camera || !im->gp || im->screen->paused) {
|
||||
return;
|
||||
}
|
||||
|
||||
enum sc_gamepad_axis axis = sc_gamepad_axis_from_sdl(event->axis);
|
||||
if (axis == SC_GAMEPAD_AXIS_UNKNOWN) {
|
||||
return;
|
||||
@@ -966,6 +1080,10 @@ sc_input_manager_process_gamepad_axis(struct sc_input_manager *im,
|
||||
static void
|
||||
sc_input_manager_process_gamepad_button(struct sc_input_manager *im,
|
||||
const SDL_GamepadButtonEvent *event) {
|
||||
if (im->camera || !im->gp || im->screen->paused) {
|
||||
return;
|
||||
}
|
||||
|
||||
enum sc_gamepad_button button = sc_gamepad_button_from_sdl(event->button);
|
||||
if (button == SC_GAMEPAD_BUTTON_UNKNOWN) {
|
||||
return;
|
||||
@@ -973,7 +1091,7 @@ sc_input_manager_process_gamepad_button(struct sc_input_manager *im,
|
||||
|
||||
struct sc_gamepad_button_event evt = {
|
||||
.gamepad_id = event->which,
|
||||
.action = sc_action_from_sdl_controllerbutton_type(event->type),
|
||||
.action = sc_action_from_sdl_gamepad_button_type(event->type),
|
||||
.button = button,
|
||||
};
|
||||
im->gp->ops->process_gamepad_button(im->gp, &evt);
|
||||
@@ -988,6 +1106,10 @@ is_apk(const char *file) {
|
||||
static void
|
||||
sc_input_manager_process_file(struct sc_input_manager *im,
|
||||
const SDL_DropEvent *event) {
|
||||
if (im->camera || !im->controller) {
|
||||
return;
|
||||
}
|
||||
|
||||
assert(event->type == SDL_EVENT_DROP_FILE);
|
||||
char *file = strdup(event->data);
|
||||
if (!file) {
|
||||
@@ -1010,72 +1132,41 @@ sc_input_manager_process_file(struct sc_input_manager *im,
|
||||
void
|
||||
sc_input_manager_handle_event(struct sc_input_manager *im,
|
||||
const SDL_Event *event) {
|
||||
bool control = im->controller;
|
||||
bool paused = im->screen->paused;
|
||||
switch (event->type) {
|
||||
case SDL_EVENT_TEXT_INPUT:
|
||||
if (!im->kp || paused) {
|
||||
break;
|
||||
}
|
||||
sc_input_manager_process_text_input(im, &event->text);
|
||||
break;
|
||||
case SDL_EVENT_KEY_DOWN:
|
||||
case SDL_EVENT_KEY_UP:
|
||||
// some key events do not interact with the device, so process the
|
||||
// event even if control is disabled
|
||||
sc_input_manager_process_key(im, &event->key);
|
||||
break;
|
||||
case SDL_EVENT_MOUSE_MOTION:
|
||||
if (!im->mp || paused) {
|
||||
break;
|
||||
}
|
||||
sc_input_manager_process_mouse_motion(im, &event->motion);
|
||||
break;
|
||||
case SDL_EVENT_MOUSE_WHEEL:
|
||||
if (!im->mp || paused) {
|
||||
break;
|
||||
}
|
||||
sc_input_manager_process_mouse_wheel(im, &event->wheel);
|
||||
break;
|
||||
case SDL_EVENT_MOUSE_BUTTON_DOWN:
|
||||
case SDL_EVENT_MOUSE_BUTTON_UP:
|
||||
// some mouse events do not interact with the device, so process
|
||||
// the event even if control is disabled
|
||||
sc_input_manager_process_mouse_button(im, &event->button);
|
||||
break;
|
||||
case SDL_EVENT_FINGER_MOTION:
|
||||
case SDL_EVENT_FINGER_DOWN:
|
||||
case SDL_EVENT_FINGER_UP:
|
||||
if (!im->mp || paused) {
|
||||
break;
|
||||
}
|
||||
sc_input_manager_process_touch(im, &event->tfinger);
|
||||
break;
|
||||
case SDL_EVENT_GAMEPAD_ADDED:
|
||||
case SDL_EVENT_GAMEPAD_REMOVED:
|
||||
// Handle device added or removed even if paused
|
||||
if (!im->gp) {
|
||||
break;
|
||||
}
|
||||
sc_input_manager_process_gamepad_device(im, &event->gdevice);
|
||||
break;
|
||||
case SDL_EVENT_GAMEPAD_AXIS_MOTION:
|
||||
if (!im->gp || paused) {
|
||||
break;
|
||||
}
|
||||
sc_input_manager_process_gamepad_axis(im, &event->gaxis);
|
||||
break;
|
||||
case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
|
||||
case SDL_EVENT_GAMEPAD_BUTTON_UP:
|
||||
if (!im->gp || paused) {
|
||||
break;
|
||||
}
|
||||
sc_input_manager_process_gamepad_button(im, &event->gbutton);
|
||||
break;
|
||||
case SDL_EVENT_DROP_FILE: {
|
||||
if (!control) {
|
||||
break;
|
||||
}
|
||||
sc_input_manager_process_file(im, &event->drop);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ struct sc_input_manager {
|
||||
struct sc_mouse_processor *mp;
|
||||
struct sc_gamepad_processor *gp;
|
||||
|
||||
bool camera;
|
||||
|
||||
struct sc_mouse_bindings mouse_bindings;
|
||||
bool legacy_paste;
|
||||
bool clipboard_autosync;
|
||||
@@ -53,6 +55,7 @@ struct sc_input_manager_params {
|
||||
struct sc_key_processor *kp;
|
||||
struct sc_mouse_processor *mp;
|
||||
struct sc_gamepad_processor *gp;
|
||||
bool camera;
|
||||
|
||||
struct sc_mouse_bindings mouse_bindings;
|
||||
bool legacy_paste;
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
#ifdef HAVE_V4L2
|
||||
# include <libavdevice/avdevice.h>
|
||||
#endif
|
||||
#define SDL_FUNCTION_POINTER_IS_VOID_POINTER
|
||||
#include <SDL3/SDL_stdinc.h>
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include "cli.h"
|
||||
#include "options.h"
|
||||
#include "scrcpy.h"
|
||||
#include "usb/scrcpy_otg.h"
|
||||
#ifdef HAVE_USB
|
||||
# include "usb/scrcpy_otg.h"
|
||||
#endif
|
||||
#include "util/log.h"
|
||||
#include "util/net.h"
|
||||
#include "util/thread.h"
|
||||
|
||||
@@ -57,8 +57,8 @@ sc_mouse_capture_handle_event(struct sc_mouse_capture *mc,
|
||||
case SDL_EVENT_MOUSE_MOTION:
|
||||
case SDL_EVENT_MOUSE_BUTTON_DOWN:
|
||||
if (!sc_mouse_capture_is_active(mc)) {
|
||||
// The mouse will be captured on SDL_MOUSEBUTTONUP, so consume
|
||||
// the event
|
||||
// The mouse will be captured on SDL_EVENT_MOUSE_BUTTON_UP, so
|
||||
// consume the event
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -16,6 +16,7 @@ const struct scrcpy_options scrcpy_options_default = {
|
||||
.camera_id = NULL,
|
||||
.camera_size = NULL,
|
||||
.camera_ar = NULL,
|
||||
.camera_zoom = NULL,
|
||||
.camera_fps = 0,
|
||||
.log_level = SC_LOG_LEVEL_INFO,
|
||||
.video_codec = SC_CODEC_H264,
|
||||
@@ -113,6 +114,7 @@ const struct scrcpy_options scrcpy_options_default = {
|
||||
.angle = NULL,
|
||||
.vd_destroy_content = true,
|
||||
.vd_system_decorations = true,
|
||||
.camera_torch = false,
|
||||
};
|
||||
|
||||
enum sc_orientation
|
||||
|
||||
@@ -241,6 +241,7 @@ struct scrcpy_options {
|
||||
const char *camera_id;
|
||||
const char *camera_size;
|
||||
const char *camera_ar;
|
||||
const char *camera_zoom;
|
||||
uint16_t camera_fps;
|
||||
enum sc_log_level log_level;
|
||||
enum sc_codec video_codec;
|
||||
@@ -327,6 +328,7 @@ struct scrcpy_options {
|
||||
const char *start_app;
|
||||
bool vd_destroy_content;
|
||||
bool vd_system_decorations;
|
||||
bool camera_torch;
|
||||
};
|
||||
|
||||
extern const struct scrcpy_options scrcpy_options_default;
|
||||
|
||||
@@ -355,7 +355,7 @@ scrcpy_generate_scid(void) {
|
||||
|
||||
static void
|
||||
init_sdl_gamepads(void) {
|
||||
// Trigger a SDL_CONTROLLERDEVICEADDED event for all gamepads already
|
||||
// Trigger a SDL_EVENT_GAMEPAD_ADDED event for all gamepads already
|
||||
// connected
|
||||
int count;
|
||||
SDL_JoystickID *joysticks = SDL_GetJoysticks(&count);
|
||||
@@ -469,6 +469,8 @@ scrcpy(struct scrcpy_options *options) {
|
||||
.power_on = options->power_on,
|
||||
.kill_adb_on_close = options->kill_adb_on_close,
|
||||
.camera_high_speed = options->camera_high_speed,
|
||||
.camera_torch = options->camera_torch,
|
||||
.camera_zoom = options->camera_zoom,
|
||||
.vd_destroy_content = options->vd_destroy_content,
|
||||
.vd_system_decorations = options->vd_system_decorations,
|
||||
.list = options->list,
|
||||
@@ -802,6 +804,7 @@ aoa_complete:
|
||||
|
||||
struct sc_screen_params screen_params = {
|
||||
.video = options->video_playback,
|
||||
.camera = options->video_source == SC_VIDEO_SOURCE_CAMERA,
|
||||
.controller = controller,
|
||||
.fp = fp,
|
||||
.kp = kp,
|
||||
@@ -946,7 +949,7 @@ aoa_complete:
|
||||
terminate_event_loop();
|
||||
LOGD("quit...");
|
||||
|
||||
if (options->video_playback) {
|
||||
if (options->window) {
|
||||
// Close the window immediately on closing, because screen_destroy()
|
||||
// may only be called once the video demuxer thread is joined (it may
|
||||
// take time)
|
||||
|
||||
446
app/src/screen.c
446
app/src/screen.c
@@ -8,7 +8,8 @@
|
||||
#include "icon.h"
|
||||
#include "options.h"
|
||||
#include "util/log.h"
|
||||
#include "util/window.h"
|
||||
#include "util/rect.h"
|
||||
#include "util/sdl.h"
|
||||
|
||||
#define DISPLAY_MARGINS 96
|
||||
|
||||
@@ -27,45 +28,11 @@ get_oriented_size(struct sc_size size, enum sc_orientation orientation) {
|
||||
return oriented_size;
|
||||
}
|
||||
|
||||
// get the window size in a struct sc_size
|
||||
static struct sc_size
|
||||
get_window_size(const struct sc_screen *screen) {
|
||||
int width;
|
||||
int height;
|
||||
bool always_ok = SDL_GetWindowSize(screen->window, &width, &height);
|
||||
(void) always_ok;
|
||||
assert(always_ok);
|
||||
|
||||
struct sc_size size;
|
||||
size.width = width;
|
||||
size.height = height;
|
||||
return size;
|
||||
}
|
||||
|
||||
static struct sc_point
|
||||
get_window_position(const struct sc_screen *screen) {
|
||||
int x;
|
||||
int y;
|
||||
bool always_ok = SDL_GetWindowPosition(screen->window, &x, &y);
|
||||
(void) always_ok;
|
||||
assert(always_ok);
|
||||
|
||||
struct sc_point point;
|
||||
point.x = x;
|
||||
point.y = y;
|
||||
return point;
|
||||
}
|
||||
|
||||
// set the window size to be applied when fullscreen is disabled
|
||||
static void
|
||||
set_window_size(struct sc_screen *screen, struct sc_size new_size) {
|
||||
assert(!screen->fullscreen);
|
||||
assert(!screen->maximized);
|
||||
assert(!screen->minimized);
|
||||
bool always_ok =
|
||||
SDL_SetWindowSize(screen->window, new_size.width, new_size.height);
|
||||
(void) always_ok;
|
||||
assert(always_ok);
|
||||
static inline bool
|
||||
is_windowed(struct sc_screen *screen) {
|
||||
return !(SDL_GetWindowFlags(screen->window) & (SDL_WINDOW_FULLSCREEN
|
||||
| SDL_WINDOW_MINIMIZED
|
||||
| SDL_WINDOW_MAXIMIZED));
|
||||
}
|
||||
|
||||
// get the preferred display bounds (i.e. the screen bounds with some margins)
|
||||
@@ -73,6 +40,11 @@ static bool
|
||||
get_preferred_display_bounds(struct sc_size *bounds) {
|
||||
SDL_Rect rect;
|
||||
SDL_DisplayID display = SDL_GetPrimaryDisplay();
|
||||
if (!display) {
|
||||
LOGW("Could not get primary display: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = SDL_GetDisplayUsableBounds(display, &rect);
|
||||
if (!ok) {
|
||||
LOGW("Could not get display usable bounds: %s", SDL_GetError());
|
||||
@@ -84,16 +56,6 @@ get_preferred_display_bounds(struct sc_size *bounds) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
is_optimal_size(struct sc_size current_size, struct sc_size content_size) {
|
||||
// The size is optimal if we can recompute one dimension of the current
|
||||
// size from the other
|
||||
return current_size.height == current_size.width * content_size.height
|
||||
/ content_size.width
|
||||
|| current_size.width == current_size.height * content_size.width
|
||||
/ content_size.height;
|
||||
}
|
||||
|
||||
// return the optimal size of the window, with the following constraints:
|
||||
// - it attempts to keep at least one dimension of the current_size (i.e. it
|
||||
// crops the black borders)
|
||||
@@ -119,7 +81,7 @@ get_optimal_size(struct sc_size current_size, struct sc_size content_size,
|
||||
window_size.height = MIN(current_size.height, display_size.height);
|
||||
}
|
||||
|
||||
if (is_optimal_size(window_size, content_size)) {
|
||||
if (sc_rect_is_optimal_size(window_size, content_size)) {
|
||||
return window_size;
|
||||
}
|
||||
|
||||
@@ -174,43 +136,13 @@ sc_screen_is_relative_mode(struct sc_screen *screen) {
|
||||
|
||||
static void
|
||||
sc_screen_update_content_rect(struct sc_screen *screen) {
|
||||
assert(screen->video);
|
||||
// Only upscale video frames, not icon
|
||||
bool can_upscale = screen->video;
|
||||
|
||||
int dw;
|
||||
int dh;
|
||||
bool always_ok = SDL_GetWindowSizeInPixels(screen->window, &dw, &dh);
|
||||
(void) always_ok;
|
||||
assert(always_ok);
|
||||
|
||||
struct sc_size content_size = screen->content_size;
|
||||
// The drawable size is the window size * the HiDPI scale
|
||||
struct sc_size drawable_size = {dw, dh};
|
||||
|
||||
SDL_Rect *rect = &screen->rect;
|
||||
|
||||
if (is_optimal_size(drawable_size, content_size)) {
|
||||
rect->x = 0;
|
||||
rect->y = 0;
|
||||
rect->w = drawable_size.width;
|
||||
rect->h = drawable_size.height;
|
||||
return;
|
||||
}
|
||||
|
||||
bool keep_width = content_size.width * drawable_size.height
|
||||
> content_size.height * drawable_size.width;
|
||||
if (keep_width) {
|
||||
rect->x = 0;
|
||||
rect->w = drawable_size.width;
|
||||
rect->h = drawable_size.width * content_size.height
|
||||
/ content_size.width;
|
||||
rect->y = (drawable_size.height - rect->h) / 2;
|
||||
} else {
|
||||
rect->y = 0;
|
||||
rect->h = drawable_size.height;
|
||||
rect->w = drawable_size.height * content_size.width
|
||||
/ content_size.height;
|
||||
rect->x = (drawable_size.width - rect->w) / 2;
|
||||
}
|
||||
struct sc_size render_size =
|
||||
sc_sdl_get_render_output_size(screen->renderer);
|
||||
sc_rect_get_content_location(render_size, screen->content_size, can_upscale,
|
||||
&screen->rect);
|
||||
}
|
||||
|
||||
// render the texture to the renderer
|
||||
@@ -219,23 +151,56 @@ sc_screen_update_content_rect(struct sc_screen *screen) {
|
||||
// changed, so that the content rectangle is recomputed
|
||||
static void
|
||||
sc_screen_render(struct sc_screen *screen, bool update_content_rect) {
|
||||
assert(screen->video);
|
||||
assert(screen->has_video_window);
|
||||
assert(!screen->video || screen->has_video_window);
|
||||
|
||||
if (update_content_rect) {
|
||||
sc_screen_update_content_rect(screen);
|
||||
}
|
||||
|
||||
enum sc_display_result res =
|
||||
sc_display_render(&screen->display, &screen->rect, screen->orientation);
|
||||
(void) res; // any error already logged
|
||||
}
|
||||
SDL_Renderer *renderer = screen->renderer;
|
||||
sc_sdl_render_clear(renderer);
|
||||
|
||||
static void
|
||||
sc_screen_render_novideo(struct sc_screen *screen) {
|
||||
enum sc_display_result res =
|
||||
sc_display_render(&screen->display, NULL, SC_ORIENTATION_0);
|
||||
(void) res; // any error already logged
|
||||
bool ok = false;
|
||||
SDL_Texture *texture = screen->tex.texture;
|
||||
if (!texture) {
|
||||
LOGW("No texture to render");
|
||||
goto end;
|
||||
}
|
||||
|
||||
SDL_FRect *geometry = &screen->rect;
|
||||
enum sc_orientation orientation = screen->orientation;
|
||||
|
||||
if (orientation == SC_ORIENTATION_0) {
|
||||
ok = SDL_RenderTexture(renderer, texture, NULL, geometry);
|
||||
} else {
|
||||
unsigned cw_rotation = sc_orientation_get_rotation(orientation);
|
||||
double angle = 90 * cw_rotation;
|
||||
|
||||
const SDL_FRect *dstrect = NULL;
|
||||
SDL_FRect rect;
|
||||
if (sc_orientation_is_swap(orientation)) {
|
||||
rect.x = geometry->x + (geometry->w - geometry->h) / 2.f;
|
||||
rect.y = geometry->y + (geometry->h - geometry->w) / 2.f;
|
||||
rect.w = geometry->h;
|
||||
rect.h = geometry->w;
|
||||
dstrect = ▭
|
||||
} else {
|
||||
dstrect = geometry;
|
||||
}
|
||||
|
||||
SDL_FlipMode flip = sc_orientation_is_mirror(orientation)
|
||||
? SDL_FLIP_HORIZONTAL : 0;
|
||||
|
||||
ok = SDL_RenderTextureRotated(renderer, texture, NULL, dstrect, angle,
|
||||
NULL, flip);
|
||||
}
|
||||
|
||||
if (!ok) {
|
||||
LOGE("Could not render texture: %s", SDL_GetError());
|
||||
}
|
||||
|
||||
end:
|
||||
sc_sdl_render_present(renderer);
|
||||
}
|
||||
|
||||
#if defined(__APPLE__) || defined(_WIN32)
|
||||
@@ -253,7 +218,7 @@ event_watcher(void *data, SDL_Event *event) {
|
||||
struct sc_screen *screen = data;
|
||||
assert(screen->video);
|
||||
|
||||
if (event->type == SDL_EVENT_WINDOW_EXPOSED) {
|
||||
if (event->type == SDL_EVENT_WINDOW_RESIZED) {
|
||||
// In practice, it seems to always be called from the same thread in
|
||||
// that specific case. Anyway, it's just a workaround.
|
||||
sc_screen_render(screen, true);
|
||||
@@ -331,14 +296,12 @@ sc_screen_init(struct sc_screen *screen,
|
||||
screen->resize_pending = false;
|
||||
screen->has_frame = false;
|
||||
screen->has_video_window = false;
|
||||
screen->fullscreen = false;
|
||||
screen->maximized = false;
|
||||
screen->minimized = false;
|
||||
screen->paused = false;
|
||||
screen->resume_frame = NULL;
|
||||
screen->orientation = SC_ORIENTATION_0;
|
||||
|
||||
screen->video = params->video;
|
||||
screen->camera = params->camera;
|
||||
|
||||
screen->req.x = params->window_x;
|
||||
screen->req.y = params->window_y;
|
||||
@@ -364,7 +327,8 @@ sc_screen_init(struct sc_screen *screen,
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t window_flags = SDL_WINDOW_HIGH_PIXEL_DENSITY;
|
||||
// Always create the window hidden to prevent blinking during initialization
|
||||
uint32_t window_flags = SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_HIDDEN;
|
||||
if (params->always_on_top) {
|
||||
window_flags |= SDL_WINDOW_ALWAYS_ON_TOP;
|
||||
}
|
||||
@@ -373,8 +337,7 @@ sc_screen_init(struct sc_screen *screen,
|
||||
}
|
||||
if (params->video) {
|
||||
// The window will be shown on first frame
|
||||
window_flags |= SDL_WINDOW_HIDDEN
|
||||
| SDL_WINDOW_RESIZABLE;
|
||||
window_flags |= SDL_WINDOW_RESIZABLE;
|
||||
}
|
||||
|
||||
const char *title = params->window_title;
|
||||
@@ -399,47 +362,85 @@ sc_screen_init(struct sc_screen *screen,
|
||||
|
||||
// The window will be positioned and sized on first video frame
|
||||
screen->window =
|
||||
sc_create_sdl_window(title, x, y, width, height, window_flags);
|
||||
sc_sdl_create_window(title, x, y, width, height, window_flags);
|
||||
if (!screen->window) {
|
||||
LOGE("Could not create window: %s", SDL_GetError());
|
||||
goto error_destroy_fps_counter;
|
||||
}
|
||||
|
||||
ok = SDL_StartTextInput(screen->window);
|
||||
if (!ok) {
|
||||
LOGE("Could not enable text input: %s", SDL_GetError());
|
||||
screen->renderer = SDL_CreateRenderer(screen->window, NULL);
|
||||
if (!screen->renderer) {
|
||||
LOGE("Could not create renderer: %s", SDL_GetError());
|
||||
goto error_destroy_window;
|
||||
}
|
||||
|
||||
SDL_Surface *icon = scrcpy_icon_load();
|
||||
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
|
||||
screen->gl_context = NULL;
|
||||
|
||||
// starts with "opengl"
|
||||
const char *renderer_name = SDL_GetRendererName(screen->renderer);
|
||||
bool use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6);
|
||||
if (use_opengl) {
|
||||
// Persuade macOS to give us something better than OpenGL 2.1.
|
||||
// If we create a Core Profile context, we get the best OpenGL version.
|
||||
bool ok = SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK,
|
||||
SDL_GL_CONTEXT_PROFILE_CORE);
|
||||
if (!ok) {
|
||||
LOGW("Could not set a GL Core Profile Context");
|
||||
}
|
||||
|
||||
LOGD("Creating OpenGL Core Profile context");
|
||||
screen->gl_context = SDL_GL_CreateContext(screen->window);
|
||||
if (!screen->gl_context) {
|
||||
LOGE("Could not create OpenGL context: %s", SDL_GetError());
|
||||
goto error_destroy_renderer;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
bool mipmaps = params->video;
|
||||
ok = sc_texture_init(&screen->tex, screen->renderer, mipmaps);
|
||||
if (!ok) {
|
||||
goto error_destroy_renderer;
|
||||
}
|
||||
|
||||
ok = SDL_StartTextInput(screen->window);
|
||||
if (!ok) {
|
||||
LOGE("Could not enable text input: %s", SDL_GetError());
|
||||
goto error_destroy_texture;
|
||||
}
|
||||
|
||||
SDL_Surface *icon = sc_icon_load(SC_ICON_FILENAME_SCRCPY);
|
||||
if (icon) {
|
||||
if (!SDL_SetWindowIcon(screen->window, icon)) {
|
||||
LOGW("Could not set window icon: %s", SDL_GetError());
|
||||
}
|
||||
} else if (params->video) {
|
||||
// just a warning
|
||||
LOGW("Could not load icon");
|
||||
} else {
|
||||
// without video, the icon is used as window content, it must be present
|
||||
LOGE("Could not load icon");
|
||||
goto error_destroy_window;
|
||||
}
|
||||
|
||||
SDL_Surface *icon_novideo = params->video ? NULL : icon;
|
||||
bool mipmaps = params->video && params->mipmaps;
|
||||
ok = sc_display_init(&screen->display, screen->window, icon_novideo,
|
||||
mipmaps);
|
||||
if (icon) {
|
||||
scrcpy_icon_destroy(icon);
|
||||
}
|
||||
if (!ok) {
|
||||
goto error_destroy_window;
|
||||
if (!params->video) {
|
||||
screen->content_size.width = icon->w;
|
||||
screen->content_size.height = icon->h;
|
||||
ok = sc_texture_set_from_surface(&screen->tex, icon);
|
||||
if (!ok) {
|
||||
LOGE("Could not set icon: %s", SDL_GetError());
|
||||
}
|
||||
}
|
||||
|
||||
sc_icon_destroy(icon);
|
||||
} else {
|
||||
// not fatal
|
||||
LOGE("Could not load icon");
|
||||
|
||||
if (!params->video) {
|
||||
// Make sure the content size is initialized
|
||||
screen->content_size.width = 256;
|
||||
screen->content_size.height = 256;
|
||||
}
|
||||
}
|
||||
|
||||
screen->frame = av_frame_alloc();
|
||||
if (!screen->frame) {
|
||||
LOG_OOM();
|
||||
goto error_destroy_display;
|
||||
goto error_destroy_texture;
|
||||
}
|
||||
|
||||
struct sc_input_manager_params im_params = {
|
||||
@@ -449,6 +450,7 @@ sc_screen_init(struct sc_screen *screen,
|
||||
.kp = params->kp,
|
||||
.mp = params->mp,
|
||||
.gp = params->gp,
|
||||
.camera = params->camera,
|
||||
.mouse_bindings = params->mouse_bindings,
|
||||
.legacy_paste = params->legacy_paste,
|
||||
.clipboard_autosync = params->clipboard_autosync,
|
||||
@@ -482,15 +484,27 @@ sc_screen_init(struct sc_screen *screen,
|
||||
screen->open = false;
|
||||
#endif
|
||||
|
||||
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);
|
||||
if (!screen->video) {
|
||||
// Show the window immediately
|
||||
sc_sdl_show_window(screen->window);
|
||||
|
||||
if (sc_screen_is_relative_mode(screen)) {
|
||||
// Capture mouse immediately if video mirroring is disabled
|
||||
sc_mouse_capture_set_active(&screen->mc, true);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
error_destroy_display:
|
||||
sc_display_destroy(&screen->display);
|
||||
error_destroy_texture:
|
||||
sc_texture_destroy(&screen->tex);
|
||||
error_destroy_renderer:
|
||||
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
|
||||
if (screen->gl_context) {
|
||||
SDL_GL_DestroyContext(screen->gl_context);
|
||||
}
|
||||
#endif
|
||||
SDL_DestroyRenderer(screen->renderer);
|
||||
error_destroy_window:
|
||||
SDL_DestroyWindow(screen->window);
|
||||
error_destroy_fps_counter:
|
||||
@@ -507,18 +521,18 @@ sc_screen_show_initial_window(struct sc_screen *screen) {
|
||||
? screen->req.x : (int) SDL_WINDOWPOS_CENTERED;
|
||||
int y = screen->req.y != SC_WINDOW_POSITION_UNDEFINED
|
||||
? screen->req.y : (int) SDL_WINDOWPOS_CENTERED;
|
||||
struct sc_point position = {
|
||||
.x = x,
|
||||
.y = y,
|
||||
};
|
||||
|
||||
struct sc_size window_size =
|
||||
get_initial_optimal_size(screen->content_size, screen->req.width,
|
||||
screen->req.height);
|
||||
|
||||
set_window_size(screen, window_size);
|
||||
|
||||
bool always_ok;
|
||||
(void) always_ok;
|
||||
|
||||
always_ok = SDL_SetWindowPosition(screen->window, x, y);
|
||||
assert(always_ok);
|
||||
assert(is_windowed(screen));
|
||||
sc_sdl_set_window_size(screen->window, window_size);
|
||||
sc_sdl_set_window_position(screen->window, position);
|
||||
|
||||
if (screen->req.fullscreen) {
|
||||
sc_screen_toggle_fullscreen(screen);
|
||||
@@ -528,17 +542,13 @@ sc_screen_show_initial_window(struct sc_screen *screen) {
|
||||
sc_fps_counter_start(&screen->fps_counter);
|
||||
}
|
||||
|
||||
always_ok = SDL_ShowWindow(screen->window);
|
||||
assert(always_ok);
|
||||
|
||||
sc_sdl_show_window(screen->window);
|
||||
sc_screen_update_content_rect(screen);
|
||||
}
|
||||
|
||||
void
|
||||
sc_screen_hide_window(struct sc_screen *screen) {
|
||||
bool always_ok = SDL_HideWindow(screen->window);
|
||||
(void) always_ok;
|
||||
assert(always_ok);
|
||||
sc_sdl_hide_window(screen->window);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -556,8 +566,12 @@ sc_screen_destroy(struct sc_screen *screen) {
|
||||
#ifndef NDEBUG
|
||||
assert(!screen->open);
|
||||
#endif
|
||||
sc_display_destroy(&screen->display);
|
||||
sc_texture_destroy(&screen->tex);
|
||||
av_frame_free(&screen->frame);
|
||||
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
|
||||
SDL_GL_DestroyContext(screen->gl_context);
|
||||
#endif
|
||||
SDL_DestroyRenderer(screen->renderer);
|
||||
SDL_DestroyWindow(screen->window);
|
||||
sc_fps_counter_destroy(&screen->fps_counter);
|
||||
sc_frame_buffer_destroy(&screen->fb);
|
||||
@@ -568,7 +582,7 @@ resize_for_content(struct sc_screen *screen, struct sc_size old_content_size,
|
||||
struct sc_size new_content_size) {
|
||||
assert(screen->video);
|
||||
|
||||
struct sc_size window_size = get_window_size(screen);
|
||||
struct sc_size window_size = sc_sdl_get_window_size(screen->window);
|
||||
struct sc_size target_size = {
|
||||
.width = (uint32_t) window_size.width * new_content_size.width
|
||||
/ old_content_size.width,
|
||||
@@ -576,14 +590,15 @@ resize_for_content(struct sc_screen *screen, struct sc_size old_content_size,
|
||||
/ old_content_size.height,
|
||||
};
|
||||
target_size = get_optimal_size(target_size, new_content_size, true);
|
||||
set_window_size(screen, target_size);
|
||||
assert(is_windowed(screen));
|
||||
sc_sdl_set_window_size(screen->window, target_size);
|
||||
}
|
||||
|
||||
static void
|
||||
set_content_size(struct sc_screen *screen, struct sc_size new_content_size) {
|
||||
assert(screen->video);
|
||||
|
||||
if (!screen->fullscreen && !screen->maximized && !screen->minimized) {
|
||||
if (is_windowed(screen)) {
|
||||
resize_for_content(screen, screen->content_size, new_content_size);
|
||||
} else if (!screen->resize_pending) {
|
||||
// Store the windowed size to be able to compute the optimal size once
|
||||
@@ -599,9 +614,7 @@ static void
|
||||
apply_pending_resize(struct sc_screen *screen) {
|
||||
assert(screen->video);
|
||||
|
||||
assert(!screen->fullscreen);
|
||||
assert(!screen->maximized);
|
||||
assert(!screen->minimized);
|
||||
assert(is_windowed(screen));
|
||||
if (screen->resize_pending) {
|
||||
resize_for_content(screen, screen->windowed_content_size,
|
||||
screen->content_size);
|
||||
@@ -641,6 +654,7 @@ sc_screen_apply_frame(struct sc_screen *screen) {
|
||||
if (!screen->has_frame
|
||||
|| screen->frame_size.width != new_frame_size.width
|
||||
|| screen->frame_size.height != new_frame_size.height) {
|
||||
|
||||
// frame dimension changed
|
||||
screen->frame_size = new_frame_size;
|
||||
|
||||
@@ -654,28 +668,12 @@ sc_screen_apply_frame(struct sc_screen *screen) {
|
||||
screen->has_frame = true;
|
||||
screen->content_size = new_content_size;
|
||||
}
|
||||
|
||||
enum sc_display_result res =
|
||||
sc_display_prepare_texture(&screen->display, screen->frame_size,
|
||||
frame->colorspace, frame->color_range);
|
||||
if (res == SC_DISPLAY_RESULT_ERROR) {
|
||||
return false;
|
||||
}
|
||||
if (res == SC_DISPLAY_RESULT_PENDING) {
|
||||
// Not an error, but do not continue
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
enum sc_display_result res =
|
||||
sc_display_update_texture(&screen->display, frame);
|
||||
if (res == SC_DISPLAY_RESULT_ERROR) {
|
||||
bool ok = sc_texture_set_from_frame(&screen->tex, frame);
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
if (res == SC_DISPLAY_RESULT_PENDING) {
|
||||
// Not an error, but do not continue
|
||||
return true;
|
||||
}
|
||||
|
||||
assert(screen->has_frame);
|
||||
if (!screen->has_video_window) {
|
||||
@@ -731,7 +729,10 @@ sc_screen_set_paused(struct sc_screen *screen, bool paused) {
|
||||
av_frame_free(&screen->frame);
|
||||
screen->frame = screen->resume_frame;
|
||||
screen->resume_frame = NULL;
|
||||
sc_screen_apply_frame(screen);
|
||||
bool ok = sc_screen_apply_frame(screen);
|
||||
if (!ok) {
|
||||
LOGE("Resume frame update failed");
|
||||
}
|
||||
}
|
||||
|
||||
if (!paused) {
|
||||
@@ -749,31 +750,28 @@ void
|
||||
sc_screen_toggle_fullscreen(struct sc_screen *screen) {
|
||||
assert(screen->video);
|
||||
|
||||
bool ok = SDL_SetWindowFullscreen(screen->window, !screen->fullscreen);
|
||||
bool req_fullscreen =
|
||||
!(SDL_GetWindowFlags(screen->window) & SDL_WINDOW_FULLSCREEN);
|
||||
|
||||
bool ok = SDL_SetWindowFullscreen(screen->window, req_fullscreen);
|
||||
if (!ok) {
|
||||
LOGW("Could not switch fullscreen mode: %s", SDL_GetError());
|
||||
return;
|
||||
}
|
||||
|
||||
screen->fullscreen = !screen->fullscreen;
|
||||
if (!screen->fullscreen && !screen->maximized && !screen->minimized) {
|
||||
apply_pending_resize(screen);
|
||||
}
|
||||
|
||||
LOGD("Switched to %s mode", screen->fullscreen ? "fullscreen" : "windowed");
|
||||
sc_screen_render(screen, true);
|
||||
LOGD("Requested %s mode", req_fullscreen ? "fullscreen" : "windowed");
|
||||
}
|
||||
|
||||
void
|
||||
sc_screen_resize_to_fit(struct sc_screen *screen) {
|
||||
assert(screen->video);
|
||||
|
||||
if (screen->fullscreen || screen->maximized || screen->minimized) {
|
||||
if (!is_windowed(screen)) {
|
||||
return;
|
||||
}
|
||||
|
||||
struct sc_point point = get_window_position(screen);
|
||||
struct sc_size window_size = get_window_size(screen);
|
||||
struct sc_point point = sc_sdl_get_window_position(screen->window);
|
||||
struct sc_size window_size = sc_sdl_get_window_size(screen->window);
|
||||
|
||||
struct sc_size optimal_size =
|
||||
get_optimal_size(window_size, screen->content_size, false);
|
||||
@@ -781,19 +779,14 @@ sc_screen_resize_to_fit(struct sc_screen *screen) {
|
||||
// Center the window related to the device screen
|
||||
assert(optimal_size.width <= window_size.width);
|
||||
assert(optimal_size.height <= window_size.height);
|
||||
uint32_t new_x = point.x + (window_size.width - optimal_size.width) / 2;
|
||||
uint32_t new_y = point.y + (window_size.height - optimal_size.height) / 2;
|
||||
|
||||
bool always_ok;
|
||||
(void) always_ok;
|
||||
|
||||
always_ok = SDL_SetWindowSize(screen->window, optimal_size.width,
|
||||
optimal_size.height);
|
||||
assert(always_ok);
|
||||
|
||||
always_ok = SDL_SetWindowPosition(screen->window, new_x, new_y);
|
||||
assert(always_ok);
|
||||
struct sc_point new_position = {
|
||||
.x = point.x + (window_size.width - optimal_size.width) / 2,
|
||||
.y = point.y + (window_size.height - optimal_size.height) / 2,
|
||||
};
|
||||
|
||||
sc_sdl_set_window_size(screen->window, optimal_size);
|
||||
sc_sdl_set_window_position(screen->window, new_position);
|
||||
LOGD("Resized to optimal size: %ux%u", optimal_size.width,
|
||||
optimal_size.height);
|
||||
}
|
||||
@@ -802,24 +795,12 @@ void
|
||||
sc_screen_resize_to_pixel_perfect(struct sc_screen *screen) {
|
||||
assert(screen->video);
|
||||
|
||||
if (screen->fullscreen || screen->minimized) {
|
||||
if (!is_windowed(screen)) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool always_ok;
|
||||
(void) always_ok;
|
||||
|
||||
if (screen->maximized) {
|
||||
always_ok = SDL_RestoreWindow(screen->window);
|
||||
assert(always_ok);
|
||||
screen->maximized = false;
|
||||
}
|
||||
|
||||
struct sc_size content_size = screen->content_size;
|
||||
always_ok = SDL_SetWindowSize(screen->window, content_size.width,
|
||||
content_size.height);
|
||||
assert(always_ok);
|
||||
|
||||
sc_sdl_set_window_size(screen->window, content_size);
|
||||
LOGD("Resized to pixel-perfect: %ux%u", content_size.width,
|
||||
content_size.height);
|
||||
}
|
||||
@@ -833,14 +814,11 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
|
||||
bool ok = sc_screen_update_frame(screen);
|
||||
if (!ok) {
|
||||
LOGE("Frame update failed\n");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case SDL_EVENT_WINDOW_EXPOSED:
|
||||
if (!screen->video) {
|
||||
sc_screen_render_novideo(screen);
|
||||
} else if (screen->has_video_window) {
|
||||
if (!screen->video || screen->has_video_window) {
|
||||
sc_screen_render(screen, true);
|
||||
}
|
||||
return true;
|
||||
@@ -849,24 +827,20 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
|
||||
sc_screen_render(screen, true);
|
||||
}
|
||||
return true;
|
||||
case SDL_EVENT_WINDOW_MAXIMIZED:
|
||||
screen->maximized = true;
|
||||
return true;
|
||||
case SDL_EVENT_WINDOW_MINIMIZED:
|
||||
screen->minimized = true;
|
||||
return true;
|
||||
case SDL_EVENT_WINDOW_RESTORED:
|
||||
if (screen->fullscreen) {
|
||||
// On Windows, in maximized+fullscreen, disabling
|
||||
// fullscreen mode unexpectedly triggers the "restored"
|
||||
// then "maximized" events, leaving the window in a
|
||||
// weird state (maximized according to the events, but
|
||||
// not maximized visually).
|
||||
return true;
|
||||
if (screen->has_video_window && is_windowed(screen)) {
|
||||
apply_pending_resize(screen);
|
||||
sc_screen_render(screen, true);
|
||||
}
|
||||
screen->maximized = false;
|
||||
screen->minimized = false;
|
||||
if (screen->has_video_window) {
|
||||
return true;
|
||||
case SDL_EVENT_WINDOW_ENTER_FULLSCREEN:
|
||||
LOGD("Switched to fullscreen mode");
|
||||
assert(screen->has_video_window);
|
||||
return true;
|
||||
case SDL_EVENT_WINDOW_LEAVE_FULLSCREEN:
|
||||
LOGD("Switched to windowed mode");
|
||||
assert(screen->has_video_window);
|
||||
if (is_windowed(screen)) {
|
||||
apply_pending_resize(screen);
|
||||
sc_screen_render(screen, true);
|
||||
}
|
||||
@@ -948,16 +922,16 @@ sc_screen_convert_window_to_frame_coords(struct sc_screen *screen,
|
||||
|
||||
void
|
||||
sc_screen_hidpi_scale_coords(struct sc_screen *screen, int32_t *x, int32_t *y) {
|
||||
bool always_ok;
|
||||
(void) always_ok;
|
||||
|
||||
// take the HiDPI scaling (dw/ww and dh/wh) into account
|
||||
int ww, wh, dw, dh;
|
||||
always_ok = SDL_GetWindowSize(screen->window, &ww, &wh);
|
||||
assert(always_ok);
|
||||
|
||||
always_ok = SDL_GetWindowSizeInPixels(screen->window, &dw, &dh);
|
||||
assert(always_ok);
|
||||
struct sc_size window_size = sc_sdl_get_window_size(screen->window);
|
||||
int64_t ww = window_size.width;
|
||||
int64_t wh = window_size.height;
|
||||
|
||||
struct sc_size drawable_size =
|
||||
sc_sdl_get_window_size_in_pixels(screen->window);
|
||||
int64_t dw = drawable_size.width;
|
||||
int64_t dh = drawable_size.height;
|
||||
|
||||
// scale for HiDPI (64 bits for intermediate multiplications)
|
||||
*x = (int64_t) *x * dw / ww;
|
||||
|
||||
@@ -12,16 +12,20 @@
|
||||
|
||||
#include "controller.h"
|
||||
#include "coords.h"
|
||||
#include "display.h"
|
||||
#include "fps_counter.h"
|
||||
#include "frame_buffer.h"
|
||||
#include "input_manager.h"
|
||||
#include "mouse_capture.h"
|
||||
#include "options.h"
|
||||
#include "texture.h"
|
||||
#include "trait/key_processor.h"
|
||||
#include "trait/frame_sink.h"
|
||||
#include "trait/mouse_processor.h"
|
||||
|
||||
#ifdef __APPLE__
|
||||
# define SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
|
||||
#endif
|
||||
|
||||
struct sc_screen {
|
||||
struct sc_frame_sink frame_sink; // frame sink trait
|
||||
|
||||
@@ -30,8 +34,9 @@ struct sc_screen {
|
||||
#endif
|
||||
|
||||
bool video;
|
||||
bool camera;
|
||||
|
||||
struct sc_display display;
|
||||
struct sc_texture tex;
|
||||
struct sc_input_manager im;
|
||||
struct sc_mouse_capture mc; // only used in mouse relative mode
|
||||
struct sc_frame_buffer fb;
|
||||
@@ -48,6 +53,11 @@ struct sc_screen {
|
||||
} req;
|
||||
|
||||
SDL_Window *window;
|
||||
SDL_Renderer *renderer;
|
||||
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
|
||||
SDL_GLContext gl_context;
|
||||
#endif
|
||||
|
||||
struct sc_size frame_size;
|
||||
struct sc_size content_size; // rotated frame_size
|
||||
|
||||
@@ -59,12 +69,9 @@ struct sc_screen {
|
||||
// client orientation
|
||||
enum sc_orientation orientation;
|
||||
// rectangle of the content (excluding black borders)
|
||||
struct SDL_Rect rect;
|
||||
struct SDL_FRect rect;
|
||||
bool has_frame;
|
||||
bool has_video_window;
|
||||
bool fullscreen;
|
||||
bool maximized;
|
||||
bool minimized;
|
||||
|
||||
AVFrame *frame;
|
||||
|
||||
@@ -74,6 +81,7 @@ struct sc_screen {
|
||||
|
||||
struct sc_screen_params {
|
||||
bool video;
|
||||
bool camera;
|
||||
|
||||
struct sc_controller *controller;
|
||||
struct sc_file_pusher *fp;
|
||||
|
||||
@@ -357,6 +357,13 @@ execute_server(struct sc_server *server,
|
||||
if (params->camera_high_speed) {
|
||||
ADD_PARAM("camera_high_speed=true");
|
||||
}
|
||||
if (params->camera_torch) {
|
||||
ADD_PARAM("camera_torch=true");
|
||||
}
|
||||
if (params->camera_zoom) {
|
||||
VALIDATE_STRING(params->camera_zoom);
|
||||
ADD_PARAM("camera_zoom=%s", params->camera_zoom);
|
||||
}
|
||||
if (params->show_touches) {
|
||||
ADD_PARAM("show_touches=true");
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ struct sc_server_params {
|
||||
const char *camera_id;
|
||||
const char *camera_size;
|
||||
const char *camera_ar;
|
||||
const char *camera_zoom;
|
||||
uint16_t camera_fps;
|
||||
struct sc_port_range port_range;
|
||||
uint32_t tunnel_host;
|
||||
@@ -68,6 +69,7 @@ struct sc_server_params {
|
||||
bool power_on;
|
||||
bool kill_adb_on_close;
|
||||
bool camera_high_speed;
|
||||
bool camera_torch;
|
||||
bool vd_destroy_content;
|
||||
bool vd_system_decorations;
|
||||
uint8_t list;
|
||||
|
||||
227
app/src/texture.c
Normal file
227
app/src/texture.c
Normal file
@@ -0,0 +1,227 @@
|
||||
#include "texture.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <inttypes.h>
|
||||
#include <string.h>
|
||||
#include <libavutil/pixfmt.h>
|
||||
|
||||
#include "util/log.h"
|
||||
|
||||
bool
|
||||
sc_texture_init(struct sc_texture *tex, SDL_Renderer *renderer, bool mipmaps) {
|
||||
const char *renderer_name = SDL_GetRendererName(renderer);
|
||||
LOGI("Renderer: %s", renderer_name ? renderer_name : "(unknown)");
|
||||
|
||||
tex->mipmaps = false;
|
||||
|
||||
// starts with "opengl"
|
||||
bool use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6);
|
||||
if (use_opengl) {
|
||||
struct sc_opengl *gl = &tex->gl;
|
||||
sc_opengl_init(gl);
|
||||
|
||||
LOGI("OpenGL version: %s", gl->version);
|
||||
|
||||
if (mipmaps) {
|
||||
bool supports_mipmaps =
|
||||
sc_opengl_version_at_least(gl, 3, 0, /* OpenGL 3.0+ */
|
||||
2, 0 /* OpenGL ES 2.0+ */);
|
||||
if (supports_mipmaps) {
|
||||
LOGI("Trilinear filtering enabled");
|
||||
tex->mipmaps = true;
|
||||
} else {
|
||||
LOGW("Trilinear filtering disabled "
|
||||
"(OpenGL 3.0+ or ES 2.0+ required)");
|
||||
}
|
||||
} else {
|
||||
LOGI("Trilinear filtering disabled");
|
||||
}
|
||||
} else if (mipmaps) {
|
||||
LOGD("Trilinear filtering disabled (not an OpenGL renderer)");
|
||||
}
|
||||
|
||||
tex->renderer = renderer;
|
||||
tex->texture = NULL;
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
sc_texture_destroy(struct sc_texture *tex) {
|
||||
if (tex->texture) {
|
||||
SDL_DestroyTexture(tex->texture);
|
||||
}
|
||||
}
|
||||
|
||||
static enum SDL_Colorspace
|
||||
sc_texture_to_sdl_color_space(enum AVColorSpace color_space,
|
||||
enum AVColorRange color_range) {
|
||||
bool full_range = color_range == AVCOL_RANGE_JPEG;
|
||||
|
||||
switch (color_space) {
|
||||
case AVCOL_SPC_BT709:
|
||||
case AVCOL_SPC_RGB:
|
||||
return full_range ? SDL_COLORSPACE_BT709_FULL
|
||||
: SDL_COLORSPACE_BT709_LIMITED;
|
||||
case AVCOL_SPC_BT470BG:
|
||||
case AVCOL_SPC_SMPTE170M:
|
||||
return full_range ? SDL_COLORSPACE_BT601_FULL
|
||||
: SDL_COLORSPACE_BT601_LIMITED;
|
||||
case AVCOL_SPC_BT2020_NCL:
|
||||
case AVCOL_SPC_BT2020_CL:
|
||||
return full_range ? SDL_COLORSPACE_BT2020_FULL
|
||||
: SDL_COLORSPACE_BT2020_LIMITED;
|
||||
default:
|
||||
return SDL_COLORSPACE_JPEG;
|
||||
}
|
||||
}
|
||||
|
||||
static SDL_Texture *
|
||||
sc_texture_create_frame_texture(struct sc_texture *tex,
|
||||
struct sc_size size,
|
||||
enum AVColorSpace color_space,
|
||||
enum AVColorRange color_range) {
|
||||
SDL_PropertiesID props = SDL_CreateProperties();
|
||||
if (!props) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
enum SDL_Colorspace sdl_color_space =
|
||||
sc_texture_to_sdl_color_space(color_space, color_range);
|
||||
|
||||
bool ok =
|
||||
SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_FORMAT_NUMBER,
|
||||
SDL_PIXELFORMAT_YV12);
|
||||
ok &= SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_ACCESS_NUMBER,
|
||||
SDL_TEXTUREACCESS_STREAMING);
|
||||
ok &= SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_WIDTH_NUMBER,
|
||||
size.width);
|
||||
ok &= SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_HEIGHT_NUMBER,
|
||||
size.height);
|
||||
ok &= SDL_SetNumberProperty(props,
|
||||
SDL_PROP_TEXTURE_CREATE_COLORSPACE_NUMBER,
|
||||
sdl_color_space);
|
||||
|
||||
if (!ok) {
|
||||
LOGE("Could not set texture properties");
|
||||
SDL_DestroyProperties(props);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
SDL_Renderer *renderer = tex->renderer;
|
||||
SDL_Texture *texture = SDL_CreateTextureWithProperties(renderer, props);
|
||||
SDL_DestroyProperties(props);
|
||||
if (!texture) {
|
||||
LOGD("Could not create texture: %s", SDL_GetError());
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (tex->mipmaps) {
|
||||
struct sc_opengl *gl = &tex->gl;
|
||||
|
||||
SDL_PropertiesID props = SDL_GetTextureProperties(texture);
|
||||
if (!props) {
|
||||
LOGE("Could not get texture properties: %s", SDL_GetError());
|
||||
SDL_DestroyTexture(texture);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char *renderer_name = SDL_GetRendererName(tex->renderer);
|
||||
const char *key = !renderer_name || !strcmp(renderer_name, "opengl")
|
||||
? SDL_PROP_TEXTURE_OPENGL_TEXTURE_NUMBER
|
||||
: SDL_PROP_TEXTURE_OPENGLES2_TEXTURE_NUMBER;
|
||||
|
||||
int64_t texture_id = SDL_GetNumberProperty(props, key, 0);
|
||||
SDL_DestroyProperties(props);
|
||||
if (!texture_id) {
|
||||
LOGE("Could not get texture id: %s", SDL_GetError());
|
||||
SDL_DestroyTexture(texture);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
assert(!(texture_id & ~0xFFFFFFFF)); // fits in uint32_t
|
||||
tex->texture_id = texture_id;
|
||||
gl->BindTexture(GL_TEXTURE_2D, tex->texture_id);
|
||||
|
||||
// Enable trilinear filtering for downscaling
|
||||
gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
|
||||
GL_LINEAR_MIPMAP_LINEAR);
|
||||
gl->TexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -1.f);
|
||||
|
||||
gl->BindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
|
||||
return texture;
|
||||
}
|
||||
|
||||
bool
|
||||
sc_texture_set_from_frame(struct sc_texture *tex, const AVFrame *frame) {
|
||||
|
||||
struct sc_size size = {frame->width, frame->height};
|
||||
assert(size.width && size.height);
|
||||
|
||||
if (!tex->texture
|
||||
|| tex->texture_type != SC_TEXTURE_TYPE_FRAME
|
||||
|| tex->texture_size.width != size.width
|
||||
|| tex->texture_size.height != size.height) {
|
||||
// Incompatible texture, recreate it
|
||||
enum AVColorSpace color_space = frame->colorspace;
|
||||
enum AVColorRange color_range = frame->color_range;
|
||||
|
||||
if (tex->texture) {
|
||||
SDL_DestroyTexture(tex->texture);
|
||||
}
|
||||
|
||||
tex->texture = sc_texture_create_frame_texture(tex, size, color_space,
|
||||
color_range);
|
||||
if (!tex->texture) {
|
||||
return false;
|
||||
}
|
||||
|
||||
tex->texture_size = size;
|
||||
tex->texture_type = SC_TEXTURE_TYPE_FRAME;
|
||||
|
||||
LOGI("Texture: %" PRIu16 "x%" PRIu16, size.width, size.height);
|
||||
}
|
||||
|
||||
assert(tex->texture);
|
||||
assert(tex->texture_type == SC_TEXTURE_TYPE_FRAME);
|
||||
|
||||
bool ok = SDL_UpdateYUVTexture(tex->texture, NULL,
|
||||
frame->data[0], frame->linesize[0],
|
||||
frame->data[1], frame->linesize[1],
|
||||
frame->data[2], frame->linesize[2]);
|
||||
if (!ok) {
|
||||
LOGD("Could not update texture: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tex->mipmaps) {
|
||||
assert(tex->texture_id);
|
||||
struct sc_opengl *gl = &tex->gl;
|
||||
|
||||
gl->BindTexture(GL_TEXTURE_2D, tex->texture_id);
|
||||
gl->GenerateMipmap(GL_TEXTURE_2D);
|
||||
gl->BindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
sc_texture_set_from_surface(struct sc_texture *tex, SDL_Surface *surface) {
|
||||
if (tex->texture) {
|
||||
SDL_DestroyTexture(tex->texture);
|
||||
}
|
||||
|
||||
tex->texture = SDL_CreateTextureFromSurface(tex->renderer, surface);
|
||||
if (!tex->texture) {
|
||||
LOGE("Could not create texture: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
tex->texture_size.width = surface->w;
|
||||
tex->texture_size.height = surface->h;
|
||||
tex->texture_type = SC_TEXTURE_TYPE_ICON;
|
||||
|
||||
return true;
|
||||
}
|
||||
44
app/src/texture.h
Normal file
44
app/src/texture.h
Normal file
@@ -0,0 +1,44 @@
|
||||
#ifndef SC_DISPLAY_H
|
||||
#define SC_DISPLAY_H
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <libavutil/frame.h>
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include "coords.h"
|
||||
#include "opengl.h"
|
||||
|
||||
enum sc_texture_type {
|
||||
SC_TEXTURE_TYPE_FRAME,
|
||||
SC_TEXTURE_TYPE_ICON,
|
||||
};
|
||||
|
||||
struct sc_texture {
|
||||
SDL_Renderer *renderer; // owned by the caller
|
||||
SDL_Texture *texture;
|
||||
// Only valid if texture != NULL
|
||||
struct sc_size texture_size;
|
||||
enum sc_texture_type texture_type;
|
||||
|
||||
struct sc_opengl gl;
|
||||
|
||||
bool mipmaps;
|
||||
uint32_t texture_id; // only set if mipmaps is enabled
|
||||
};
|
||||
|
||||
bool
|
||||
sc_texture_init(struct sc_texture *tex, SDL_Renderer *renderer, bool mipmaps);
|
||||
|
||||
void
|
||||
sc_texture_destroy(struct sc_texture *tex);
|
||||
|
||||
bool
|
||||
sc_texture_set_from_frame(struct sc_texture *tex, const AVFrame *frame);
|
||||
|
||||
bool
|
||||
sc_texture_set_from_surface(struct sc_texture *tex, SDL_Surface *surface);
|
||||
|
||||
#endif
|
||||
@@ -74,10 +74,9 @@ sc_gamepad_processor_process_gamepad_added(struct sc_gamepad_processor *gp,
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_Gamepad * game_controller =
|
||||
SDL_GetGamepadFromID(event->gamepad_id);
|
||||
assert(game_controller);
|
||||
const char *name = SDL_GetGamepadName(game_controller);
|
||||
SDL_Gamepad *sdl_gamepad = SDL_GetGamepadFromID(event->gamepad_id);
|
||||
assert(sdl_gamepad);
|
||||
const char *name = SDL_GetGamepadName(sdl_gamepad);
|
||||
LOGI("Gamepad added: [%" PRIu32 "] %s", event->gamepad_id, name);
|
||||
|
||||
sc_gamepad_uhid_send_open(gamepad, &hid_open);
|
||||
|
||||
@@ -75,7 +75,7 @@ scrcpy_otg(struct scrcpy_options *options) {
|
||||
|
||||
if (options->gamepad_input_mode != SC_GAMEPAD_INPUT_MODE_DISABLED) {
|
||||
if (!SDL_Init(SDL_INIT_GAMEPAD)) {
|
||||
LOGE("Could not initialize SDL controller: %s", SDL_GetError());
|
||||
LOGE("Could not initialize SDL gamepad: %s", SDL_GetError());
|
||||
// Not fatal, keyboard/mouse should still work
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,24 +7,28 @@
|
||||
#include "options.h"
|
||||
#include "util/acksync.h"
|
||||
#include "util/log.h"
|
||||
#include "util/window.h"
|
||||
#include "util/rect.h"
|
||||
#include "util/sdl.h"
|
||||
|
||||
static void
|
||||
sc_screen_otg_render(struct sc_screen_otg *screen) {
|
||||
bool always_ok = SDL_RenderClear(screen->renderer);
|
||||
(void) always_ok;
|
||||
assert(always_ok);
|
||||
sc_sdl_render_clear(screen->renderer);
|
||||
|
||||
if (screen->texture) {
|
||||
bool ok =
|
||||
SDL_RenderTexture(screen->renderer, screen->texture, NULL, NULL);
|
||||
SDL_Texture *texture = screen->tex.texture;
|
||||
if (texture) {
|
||||
struct sc_size render_size =
|
||||
sc_sdl_get_render_output_size(screen->renderer);
|
||||
|
||||
SDL_FRect rect;
|
||||
sc_rect_get_content_location(render_size, screen->tex.texture_size,
|
||||
false, &rect);
|
||||
|
||||
bool ok = SDL_RenderTexture(screen->renderer, texture, NULL, &rect);
|
||||
if (!ok) {
|
||||
LOGW("Could not render texture: %s", SDL_GetError());
|
||||
}
|
||||
}
|
||||
|
||||
always_ok = SDL_RenderPresent(screen->renderer);
|
||||
assert(always_ok);
|
||||
sc_sdl_render_present(screen->renderer);
|
||||
}
|
||||
|
||||
bool
|
||||
@@ -53,7 +57,7 @@ sc_screen_otg_init(struct sc_screen_otg *screen,
|
||||
}
|
||||
|
||||
screen->window =
|
||||
sc_create_sdl_window(title, x, y, width, height, window_flags);
|
||||
sc_sdl_create_window(title, x, y, width, height, window_flags);
|
||||
if (!screen->window) {
|
||||
LOGE("Could not create window: %s", SDL_GetError());
|
||||
return false;
|
||||
@@ -65,30 +69,25 @@ sc_screen_otg_init(struct sc_screen_otg *screen,
|
||||
goto error_destroy_window;
|
||||
}
|
||||
|
||||
SDL_Surface *icon = scrcpy_icon_load();
|
||||
bool ok = sc_texture_init(&screen->tex, screen->renderer, false);
|
||||
if (!ok) {
|
||||
goto error_destroy_renderer;
|
||||
}
|
||||
|
||||
SDL_Surface *icon = sc_icon_load(SC_ICON_FILENAME_SCRCPY);
|
||||
if (icon) {
|
||||
bool ok = SDL_SetWindowIcon(screen->window, icon);
|
||||
if (!ok) {
|
||||
LOGW("Could not set window icon: %s", SDL_GetError());
|
||||
}
|
||||
|
||||
ok = SDL_SetRenderLogicalPresentation(screen->renderer, icon->w,
|
||||
icon->h,
|
||||
SDL_LOGICAL_PRESENTATION_LETTERBOX);
|
||||
ok = sc_texture_set_from_surface(&screen->tex, icon);
|
||||
sc_icon_destroy(icon);
|
||||
if (!ok) {
|
||||
LOGW("Could not set renderer logical size: %s", SDL_GetError());
|
||||
// don't fail
|
||||
}
|
||||
|
||||
screen->texture = SDL_CreateTextureFromSurface(screen->renderer, icon);
|
||||
scrcpy_icon_destroy(icon);
|
||||
if (!screen->texture) {
|
||||
goto error_destroy_renderer;
|
||||
LOGE("Could not set icon: %s", SDL_GetError());
|
||||
}
|
||||
} else {
|
||||
screen->texture = NULL;
|
||||
LOGW("Could not load icon");
|
||||
LOGE("Could not load icon");
|
||||
}
|
||||
|
||||
sc_mouse_capture_init(&screen->mc, screen->window, params->shortcut_mods);
|
||||
@@ -100,19 +99,17 @@ sc_screen_otg_init(struct sc_screen_otg *screen,
|
||||
|
||||
return true;
|
||||
|
||||
error_destroy_window:
|
||||
SDL_DestroyWindow(screen->window);
|
||||
error_destroy_renderer:
|
||||
SDL_DestroyRenderer(screen->renderer);
|
||||
error_destroy_window:
|
||||
SDL_DestroyWindow(screen->window);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void
|
||||
sc_screen_otg_destroy(struct sc_screen_otg *screen) {
|
||||
if (screen->texture) {
|
||||
SDL_DestroyTexture(screen->texture);
|
||||
}
|
||||
sc_texture_destroy(&screen->tex);
|
||||
SDL_DestroyRenderer(screen->renderer);
|
||||
SDL_DestroyWindow(screen->window);
|
||||
}
|
||||
@@ -197,16 +194,16 @@ sc_screen_otg_process_gamepad_device(struct sc_screen_otg *screen,
|
||||
struct sc_gamepad_processor *gp = &screen->gamepad->gamepad_processor;
|
||||
|
||||
if (event->type == SDL_EVENT_GAMEPAD_ADDED) {
|
||||
SDL_Gamepad *gc = SDL_OpenGamepad(event->which);
|
||||
if (!gc) {
|
||||
LOGW("Could not open game controller");
|
||||
SDL_Gamepad *sdl_gamepad = SDL_OpenGamepad(event->which);
|
||||
if (!sdl_gamepad) {
|
||||
LOGW("Could not open gamepad");
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_Joystick *joystick = SDL_GetGamepadJoystick(gc);
|
||||
SDL_Joystick *joystick = SDL_GetGamepadJoystick(sdl_gamepad);
|
||||
if (!joystick) {
|
||||
LOGW("Could not get controller joystick");
|
||||
SDL_CloseGamepad(gc);
|
||||
LOGW("Could not get gamepad joystick");
|
||||
SDL_CloseGamepad(sdl_gamepad);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -217,9 +214,9 @@ sc_screen_otg_process_gamepad_device(struct sc_screen_otg *screen,
|
||||
} else if (event->type == SDL_EVENT_GAMEPAD_REMOVED) {
|
||||
SDL_JoystickID id = event->which;
|
||||
|
||||
SDL_Gamepad *gc = SDL_GetGamepadFromID(id);
|
||||
if (gc) {
|
||||
SDL_CloseGamepad(gc);
|
||||
SDL_Gamepad *sdl_gamepad = SDL_GetGamepadFromID(id);
|
||||
if (sdl_gamepad) {
|
||||
SDL_CloseGamepad(sdl_gamepad);
|
||||
} else {
|
||||
LOGW("Unknown gamepad device removed");
|
||||
}
|
||||
@@ -263,7 +260,7 @@ sc_screen_otg_process_gamepad_button(struct sc_screen_otg *screen,
|
||||
|
||||
struct sc_gamepad_button_event evt = {
|
||||
.gamepad_id = event->which,
|
||||
.action = sc_action_from_sdl_controllerbutton_type(event->type),
|
||||
.action = sc_action_from_sdl_gamepad_button_type(event->type),
|
||||
.button = button,
|
||||
};
|
||||
gp->ops->process_gamepad_button(gp, &evt);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include "mouse_capture.h"
|
||||
#include "texture.h"
|
||||
#include "usb/gamepad_aoa.h"
|
||||
#include "usb/keyboard_aoa.h"
|
||||
#include "usb/mouse_aoa.h"
|
||||
@@ -19,7 +20,7 @@ struct sc_screen_otg {
|
||||
|
||||
SDL_Window *window;
|
||||
SDL_Renderer *renderer;
|
||||
SDL_Texture *texture;
|
||||
struct sc_texture tex;
|
||||
|
||||
struct sc_mouse_capture mc;
|
||||
};
|
||||
|
||||
@@ -5,6 +5,25 @@
|
||||
|
||||
#include "util/log.h"
|
||||
|
||||
char *
|
||||
sc_file_build_path(const char *dir, const char *name) {
|
||||
size_t dir_len = strlen(dir);
|
||||
size_t name_len = strlen(name);
|
||||
|
||||
size_t len = dir_len + name_len + 2; // +2: '/' and '\0'
|
||||
char *path = malloc(len);
|
||||
if (!path) {
|
||||
LOG_OOM();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
memcpy(path, dir, dir_len);
|
||||
path[dir_len] = SC_PATH_SEPARATOR;
|
||||
// namelen + 1 to copy the final '\0'
|
||||
memcpy(&path[dir_len + 1], name, name_len + 1);
|
||||
return path;
|
||||
}
|
||||
|
||||
char *
|
||||
sc_file_get_local_path(const char *name) {
|
||||
char *executable_path = sc_file_get_executable_path();
|
||||
@@ -25,24 +44,9 @@ sc_file_get_local_path(const char *name) {
|
||||
|
||||
*p = '\0'; // modify executable_path in place
|
||||
char *dir = executable_path;
|
||||
size_t dirlen = strlen(dir);
|
||||
size_t namelen = strlen(name);
|
||||
|
||||
size_t len = dirlen + namelen + 2; // +2: '/' and '\0'
|
||||
char *file_path = malloc(len);
|
||||
if (!file_path) {
|
||||
LOG_OOM();
|
||||
free(executable_path);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
memcpy(file_path, dir, dirlen);
|
||||
file_path[dirlen] = SC_PATH_SEPARATOR;
|
||||
// namelen + 1 to copy the final '\0'
|
||||
memcpy(&file_path[dirlen + 1], name, namelen + 1);
|
||||
char *file_path = sc_file_build_path(dir, name);
|
||||
|
||||
free(executable_path);
|
||||
|
||||
return file_path;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,15 @@ sc_file_get_executable_path(void);
|
||||
char *
|
||||
sc_file_get_local_path(const char *name);
|
||||
|
||||
/**
|
||||
* Return the concatenation of dir, the path separator and the filename.
|
||||
*
|
||||
* The result must be freed by the caller using free(). It may return NULL on
|
||||
* error.
|
||||
*/
|
||||
char *
|
||||
sc_file_build_path(const char *dir, const char *filename);
|
||||
|
||||
/**
|
||||
* Indicate if the file exists and is not a directory
|
||||
*/
|
||||
|
||||
52
app/src/util/rect.c
Normal file
52
app/src/util/rect.c
Normal file
@@ -0,0 +1,52 @@
|
||||
#include "rect.h"
|
||||
|
||||
bool
|
||||
sc_rect_is_optimal_size(struct sc_size current_size,
|
||||
struct sc_size content_size) {
|
||||
// The size is optimal if we can recompute one dimension of the current
|
||||
// size from the other
|
||||
return current_size.height == current_size.width * content_size.height
|
||||
/ content_size.width
|
||||
|| current_size.width == current_size.height * content_size.width
|
||||
/ content_size.height;
|
||||
}
|
||||
|
||||
// Compute the content location, preserving its aspect ratio
|
||||
void
|
||||
sc_rect_get_content_location(struct sc_size render_size,
|
||||
struct sc_size content_size, bool can_upscale,
|
||||
SDL_FRect *out) {
|
||||
if (sc_rect_is_optimal_size(render_size, content_size)) {
|
||||
out->x = 0;
|
||||
out->y = 0;
|
||||
out->w = render_size.width;
|
||||
out->h = render_size.height;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!can_upscale && content_size.width <= render_size.width
|
||||
&& content_size.height <= render_size.height) {
|
||||
// Center without upscaling
|
||||
out->x = (render_size.width - content_size.width) / 2.f;
|
||||
out->y = (render_size.height - content_size.height) / 2.f;
|
||||
out->w = content_size.width;
|
||||
out->h = content_size.height;
|
||||
return;
|
||||
}
|
||||
|
||||
bool keep_width = content_size.width * render_size.height
|
||||
> content_size.height * render_size.width;
|
||||
if (keep_width) {
|
||||
out->x = 0;
|
||||
out->w = render_size.width;
|
||||
out->h = (float) render_size.width * content_size.height
|
||||
/ content_size.width;
|
||||
out->y = (render_size.height - out->h) / 2.f;
|
||||
} else {
|
||||
out->y = 0;
|
||||
out->h = render_size.height;
|
||||
out->w = (float) render_size.height * content_size.width
|
||||
/ content_size.height;
|
||||
out->x = (render_size.width - out->w) / 2.f;
|
||||
}
|
||||
}
|
||||
24
app/src/util/rect.h
Normal file
24
app/src/util/rect.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#ifndef SC_RECT_H
|
||||
#define SC_RECT_H
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <SDL3/SDL_rect.h>
|
||||
|
||||
#include "coords.h"
|
||||
|
||||
// Return whether the size is optimal
|
||||
//
|
||||
// It is optimal if it does not require black borders to preserve the aspect
|
||||
// ratio, with rounding applied at pixel boundaries.
|
||||
bool
|
||||
sc_rect_is_optimal_size(struct sc_size current_size,
|
||||
struct sc_size content_size);
|
||||
|
||||
// Compute the content location, preserving its aspect ratio
|
||||
void
|
||||
sc_rect_get_content_location(struct sc_size render_size,
|
||||
struct sc_size content_size, bool can_upscale,
|
||||
SDL_FRect *out);
|
||||
|
||||
#endif
|
||||
168
app/src/util/sdl.c
Normal file
168
app/src/util/sdl.c
Normal file
@@ -0,0 +1,168 @@
|
||||
#include "sdl.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "util/log.h"
|
||||
|
||||
SDL_Window *
|
||||
sc_sdl_create_window(const char *title, int64_t x, int64_t y, int64_t width,
|
||||
int64_t height, int64_t flags) {
|
||||
SDL_Window *window = NULL;
|
||||
|
||||
SDL_PropertiesID props = SDL_CreateProperties();
|
||||
if (!props) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool ok =
|
||||
SDL_SetStringProperty(props, SDL_PROP_WINDOW_CREATE_TITLE_STRING,
|
||||
title);
|
||||
ok &= SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_X_NUMBER, x);
|
||||
ok &= SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_Y_NUMBER, y);
|
||||
ok &= SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER,
|
||||
width);
|
||||
ok &= SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER,
|
||||
height);
|
||||
ok &= SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_FLAGS_NUMBER,
|
||||
flags);
|
||||
|
||||
if (!ok) {
|
||||
SDL_DestroyProperties(props);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
window = SDL_CreateWindowWithProperties(props);
|
||||
SDL_DestroyProperties(props);
|
||||
return window;
|
||||
}
|
||||
|
||||
struct sc_size
|
||||
sc_sdl_get_window_size(SDL_Window *window) {
|
||||
int width;
|
||||
int height;
|
||||
bool ok = SDL_GetWindowSize(window, &width, &height);
|
||||
if (!ok) {
|
||||
LOGE("Could not get window size: %s", SDL_GetError());
|
||||
LOGE("Please report the error");
|
||||
// fatal error
|
||||
abort();
|
||||
}
|
||||
|
||||
struct sc_size size = {
|
||||
.width = width,
|
||||
.height = height,
|
||||
};
|
||||
return size;
|
||||
}
|
||||
|
||||
struct sc_size
|
||||
sc_sdl_get_window_size_in_pixels(SDL_Window *window) {
|
||||
int width;
|
||||
int height;
|
||||
bool ok = SDL_GetWindowSizeInPixels(window, &width, &height);
|
||||
if (!ok) {
|
||||
LOGE("Could not get window size: %s", SDL_GetError());
|
||||
LOGE("Please report the error");
|
||||
// fatal error
|
||||
abort();
|
||||
}
|
||||
|
||||
struct sc_size size = {
|
||||
.width = width,
|
||||
.height = height,
|
||||
};
|
||||
return size;
|
||||
}
|
||||
|
||||
void
|
||||
sc_sdl_set_window_size(SDL_Window *window, struct sc_size size) {
|
||||
bool ok = SDL_SetWindowSize(window, size.width, size.height);
|
||||
if (!ok) {
|
||||
LOGE("Could not set window size: %s", SDL_GetError());
|
||||
assert(!"unexpected");
|
||||
}
|
||||
}
|
||||
|
||||
struct sc_point
|
||||
sc_sdl_get_window_position(SDL_Window *window) {
|
||||
int x;
|
||||
int y;
|
||||
bool ok = SDL_GetWindowPosition(window, &x, &y);
|
||||
if (!ok) {
|
||||
LOGE("Could not get window position: %s", SDL_GetError());
|
||||
LOGE("Please report the error");
|
||||
// fatal error
|
||||
abort();
|
||||
}
|
||||
|
||||
struct sc_point point = {
|
||||
.x = x,
|
||||
.y = y,
|
||||
};
|
||||
return point;
|
||||
}
|
||||
|
||||
void
|
||||
sc_sdl_set_window_position(SDL_Window *window, struct sc_point point) {
|
||||
bool ok = SDL_SetWindowPosition(window, point.x, point.y);
|
||||
if (!ok) {
|
||||
LOGE("Could not set window position: %s", SDL_GetError());
|
||||
assert(!"unexpected");
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
sc_sdl_show_window(SDL_Window *window) {
|
||||
bool ok = SDL_ShowWindow(window);
|
||||
if (!ok) {
|
||||
LOGE("Could not show window: %s", SDL_GetError());
|
||||
assert(!"unexpected");
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
sc_sdl_hide_window(SDL_Window *window) {
|
||||
bool ok = SDL_HideWindow(window);
|
||||
if (!ok) {
|
||||
LOGE("Could not hide window: %s", SDL_GetError());
|
||||
assert(!"unexpected");
|
||||
}
|
||||
}
|
||||
|
||||
struct sc_size
|
||||
sc_sdl_get_render_output_size(SDL_Renderer *renderer) {
|
||||
int width;
|
||||
int height;
|
||||
bool ok = SDL_GetRenderOutputSize(renderer, &width, &height);
|
||||
if (!ok) {
|
||||
LOGE("Could not get render output size: %s", SDL_GetError());
|
||||
LOGE("Please report the error");
|
||||
// fatal error
|
||||
abort();
|
||||
}
|
||||
|
||||
struct sc_size size = {
|
||||
.width = width,
|
||||
.height = height,
|
||||
};
|
||||
return size;
|
||||
}
|
||||
|
||||
bool
|
||||
sc_sdl_render_clear(SDL_Renderer *renderer) {
|
||||
bool ok = SDL_RenderClear(renderer);
|
||||
if (!ok) {
|
||||
LOGW("Could not clear rendering: %s", SDL_GetError());
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
void
|
||||
sc_sdl_render_present(SDL_Renderer *renderer) {
|
||||
bool ok = SDL_RenderPresent(renderer);
|
||||
if (!ok) {
|
||||
LOGE("Could not render: %s", SDL_GetError());
|
||||
assert(!"unexpected");
|
||||
}
|
||||
}
|
||||
46
app/src/util/sdl.h
Normal file
46
app/src/util/sdl.h
Normal file
@@ -0,0 +1,46 @@
|
||||
#ifndef SC_SDL_H
|
||||
#define SC_SDL_H
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <SDL3/SDL_render.h>
|
||||
#include <SDL3/SDL_video.h>
|
||||
|
||||
#include "coords.h"
|
||||
|
||||
SDL_Window *
|
||||
sc_sdl_create_window(const char *title, int64_t x, int64_t y, int64_t width,
|
||||
int64_t height, int64_t flags);
|
||||
|
||||
struct sc_size
|
||||
sc_sdl_get_window_size(SDL_Window *window);
|
||||
|
||||
struct sc_size
|
||||
sc_sdl_get_window_size_in_pixels(SDL_Window *window);
|
||||
|
||||
void
|
||||
sc_sdl_set_window_size(SDL_Window *window, struct sc_size size);
|
||||
|
||||
struct sc_point
|
||||
sc_sdl_get_window_position(SDL_Window *window);
|
||||
|
||||
void
|
||||
sc_sdl_set_window_position(SDL_Window *window, struct sc_point point);
|
||||
|
||||
void
|
||||
sc_sdl_show_window(SDL_Window *window);
|
||||
|
||||
void
|
||||
sc_sdl_hide_window(SDL_Window *window);
|
||||
|
||||
struct sc_size
|
||||
sc_sdl_get_render_output_size(SDL_Renderer *renderer);
|
||||
|
||||
bool
|
||||
sc_sdl_render_clear(SDL_Renderer *renderer);
|
||||
|
||||
void
|
||||
sc_sdl_render_present(SDL_Renderer *renderer);
|
||||
|
||||
#endif
|
||||
@@ -1,33 +0,0 @@
|
||||
#include "window.h"
|
||||
|
||||
SDL_Window *
|
||||
sc_create_sdl_window(const char *title, int64_t x, int64_t y, int64_t width,
|
||||
int64_t height, int64_t flags) {
|
||||
SDL_Window *window = NULL;
|
||||
|
||||
SDL_PropertiesID props = SDL_CreateProperties();
|
||||
if (!props) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool ok =
|
||||
SDL_SetStringProperty(props, SDL_PROP_WINDOW_CREATE_TITLE_STRING,
|
||||
title);
|
||||
ok &= SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_X_NUMBER, x);
|
||||
ok &= SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_Y_NUMBER, y);
|
||||
ok &= SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER,
|
||||
width);
|
||||
ok &= SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER,
|
||||
height);
|
||||
ok &= SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_FLAGS_NUMBER,
|
||||
flags);
|
||||
|
||||
if (!ok) {
|
||||
SDL_DestroyProperties(props);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
window = SDL_CreateWindowWithProperties(props);
|
||||
SDL_DestroyProperties(props);
|
||||
return window;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
#ifndef SC_WINDOW_H
|
||||
#define SC_WINDOW_H
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <SDL3/SDL_video.h>
|
||||
|
||||
SDL_Window *
|
||||
sc_create_sdl_window(const char *title, int64_t x, int64_t y, int64_t width,
|
||||
int64_t height, int64_t flags);
|
||||
|
||||
#endif
|
||||
@@ -411,6 +411,26 @@ static void test_serialize_open_hard_keyboard(void) {
|
||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||
}
|
||||
|
||||
static void test_serialize_start_app(void) {
|
||||
struct sc_control_msg msg = {
|
||||
.type = SC_CONTROL_MSG_TYPE_START_APP,
|
||||
.start_app = {
|
||||
.name = "firefox",
|
||||
},
|
||||
};
|
||||
|
||||
uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
|
||||
size_t size = sc_control_msg_serialize(&msg, buf);
|
||||
assert(size == 9);
|
||||
|
||||
const uint8_t expected[] = {
|
||||
SC_CONTROL_MSG_TYPE_START_APP,
|
||||
7, // length
|
||||
'f', 'i', 'r', 'e', 'f', 'o', 'x', // app name
|
||||
};
|
||||
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,
|
||||
@@ -426,6 +446,55 @@ static void test_serialize_reset_video(void) {
|
||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||
}
|
||||
|
||||
static void test_serialize_camera_set_torch(void) {
|
||||
struct sc_control_msg msg = {
|
||||
.type = SC_CONTROL_MSG_TYPE_CAMERA_SET_TORCH,
|
||||
.camera_set_torch = {
|
||||
.on = true,
|
||||
},
|
||||
};
|
||||
|
||||
uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
|
||||
size_t size = sc_control_msg_serialize(&msg, buf);
|
||||
assert(size == 2);
|
||||
|
||||
const uint8_t expected[] = {
|
||||
SC_CONTROL_MSG_TYPE_CAMERA_SET_TORCH,
|
||||
0x01, // true
|
||||
};
|
||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||
}
|
||||
|
||||
static void test_serialize_camera_zoom_in(void) {
|
||||
struct sc_control_msg msg = {
|
||||
.type = SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_IN,
|
||||
};
|
||||
|
||||
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_CAMERA_ZOOM_IN,
|
||||
};
|
||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||
}
|
||||
|
||||
static void test_serialize_camera_zoom_out(void) {
|
||||
struct sc_control_msg msg = {
|
||||
.type = SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_OUT,
|
||||
};
|
||||
|
||||
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_CAMERA_ZOOM_OUT,
|
||||
};
|
||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
(void) argc;
|
||||
(void) argv;
|
||||
@@ -448,6 +517,10 @@ int main(int argc, char *argv[]) {
|
||||
test_serialize_uhid_input();
|
||||
test_serialize_uhid_destroy();
|
||||
test_serialize_open_hard_keyboard();
|
||||
test_serialize_start_app();
|
||||
test_serialize_reset_video();
|
||||
test_serialize_camera_set_torch();
|
||||
test_serialize_camera_zoom_in();
|
||||
test_serialize_camera_zoom_out();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.7.1'
|
||||
classpath 'com.android.tools.build:gradle:8.13.0'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
||||
24
doc/build.md
24
doc/build.md
@@ -30,13 +30,13 @@ the following files to a directory accessible from your `PATH`:
|
||||
|
||||
It is also available in scrcpy releases.
|
||||
|
||||
The client requires [FFmpeg] and [LibSDL2]. Just follow the instructions.
|
||||
The client requires [FFmpeg] and [SDL]. Just follow the instructions.
|
||||
|
||||
[adb]: https://developer.android.com/studio/command-line/adb.html
|
||||
[platform-tools]: https://developer.android.com/studio/releases/platform-tools.html
|
||||
[platform-tools-windows]: https://dl.google.com/android/repository/platform-tools-latest-windows.zip
|
||||
[ffmpeg]: https://en.wikipedia.org/wiki/FFmpeg
|
||||
[LibSDL2]: https://en.wikipedia.org/wiki/Simple_DirectMedia_Layer
|
||||
[SDL]: https://en.wikipedia.org/wiki/Simple_DirectMedia_Layer
|
||||
|
||||
|
||||
|
||||
@@ -50,10 +50,10 @@ Install the required packages from your package manager.
|
||||
|
||||
```bash
|
||||
# runtime dependencies
|
||||
sudo apt install ffmpeg libsdl2-2.0-0 adb libusb-1.0-0
|
||||
sudo apt install ffmpeg libsdl3-0 adb libusb-1.0-0
|
||||
|
||||
# client build dependencies
|
||||
sudo apt install gcc git pkg-config meson ninja-build libsdl2-dev \
|
||||
sudo apt install gcc git pkg-config meson ninja-build libsdl3-dev \
|
||||
libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \
|
||||
libswresample-dev libusb-1.0-0-dev
|
||||
|
||||
@@ -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 SDL3-devel ffms2-devel libusb1-devel libavdevice-free-devel meson gcc make
|
||||
|
||||
# server build dependencies
|
||||
sudo dnf install java-devel
|
||||
@@ -121,7 +121,7 @@ install the required packages:
|
||||
|
||||
```bash
|
||||
# runtime dependencies
|
||||
pacman -S mingw-w64-x86_64-SDL2 \
|
||||
pacman -S mingw-w64-x86_64-sdl3 \
|
||||
mingw-w64-x86_64-ffmpeg \
|
||||
mingw-w64-x86_64-libusb
|
||||
|
||||
@@ -136,7 +136,7 @@ For a 32 bits version, replace `x86_64` by `i686`:
|
||||
|
||||
```bash
|
||||
# runtime dependencies
|
||||
pacman -S mingw-w64-i686-SDL2 \
|
||||
pacman -S mingw-w64-i686-sdl3 \
|
||||
mingw-w64-i686-ffmpeg \
|
||||
mingw-w64-i686-libusb
|
||||
|
||||
@@ -162,7 +162,7 @@ Install the packages with [Homebrew]:
|
||||
|
||||
```bash
|
||||
# runtime dependencies
|
||||
brew install sdl2 ffmpeg libusb
|
||||
brew install sdl3 ffmpeg libusb
|
||||
|
||||
# client build dependencies
|
||||
brew install pkg-config meson
|
||||
@@ -233,10 +233,10 @@ install` must be run as root)._
|
||||
|
||||
#### Option 2: Use prebuilt server
|
||||
|
||||
- [`scrcpy-server-v3.3.3`][direct-scrcpy-server]
|
||||
<sub>SHA-256: `7e70323ba7f259649dd4acce97ac4fefbae8102b2c6d91e2e7be613fd5354be0`</sub>
|
||||
- [`scrcpy-server-v3.3.4`][direct-scrcpy-server]
|
||||
<sub>SHA-256: `8588238c9a5a00aa542906b6ec7e6d5541d9ffb9b5d0f6e1bc0e365e2303079e`</sub>
|
||||
|
||||
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.3/scrcpy-server-v3.3.3
|
||||
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-server-v3.3.4
|
||||
|
||||
Download the prebuilt server somewhere, and specify its path during the Meson
|
||||
configuration:
|
||||
@@ -271,7 +271,7 @@ This installs several files:
|
||||
- `/usr/local/bin/scrcpy` (main app)
|
||||
- `/usr/local/share/scrcpy/scrcpy-server` (server to push to the device)
|
||||
- `/usr/local/share/man/man1/scrcpy.1` (manpage)
|
||||
- `/usr/local/share/icons/hicolor/256x256/apps/icon.png` (app icon)
|
||||
- `/usr/local/share/icons/hicolor/256x256/apps/scrcpy.png` (app icon)
|
||||
- `/usr/local/share/zsh/site-functions/_scrcpy` (zsh completion)
|
||||
- `/usr/local/share/bash-completion/completions/scrcpy` (bash completion)
|
||||
|
||||
|
||||
@@ -165,6 +165,30 @@ scrcpy --video-source=camera --camera-facing=back --camera-ar=16:9 --camera-high
|
||||
[brace expansion]: https://www.gnu.org/software/bash/manual/html_node/Brace-Expansion.html
|
||||
|
||||
|
||||
## Torch
|
||||
|
||||
The camera torch can be turned on at startup by `--camera-torch`:
|
||||
|
||||
```
|
||||
scrcpy --video-source=camera --camera-torch
|
||||
```
|
||||
|
||||
It can also be turned on and off dynamically with <kbd>MOD</kbd>+<kbd>t</kbd>
|
||||
and <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>t</kbd>, respectively.
|
||||
|
||||
|
||||
## Zoom
|
||||
|
||||
The camera zoom can be set with `--camera-zoom=`:
|
||||
|
||||
```bash
|
||||
scrcpy --video-source=camera --camera-zoom=1.5
|
||||
```
|
||||
|
||||
It can also be adjusted dynamically using <kbd>MOD</kbd>+<kbd>↑</kbd> _(up)_ and
|
||||
<kbd>MOD</kbd>+<kbd>↓</kbd> _(down)_.
|
||||
|
||||
|
||||
## Webcam
|
||||
|
||||
Combined with the [V4L2](v4l2.md) feature on Linux, the Android device camera
|
||||
|
||||
10
doc/linux.md
10
doc/linux.md
@@ -6,11 +6,11 @@
|
||||
|
||||
Download a static build of the [latest release]:
|
||||
|
||||
- [`scrcpy-linux-x86_64-v3.3.3.tar.gz`][direct-linux-x86_64] (x86_64)
|
||||
<sub>SHA-256: `9b30e813e8191329ba8025dc80cb0f198fb0a318960a3b5c15395cf675c9c638`</sub>
|
||||
- [`scrcpy-linux-x86_64-v3.3.4.tar.gz`][direct-linux-x86_64] (x86_64)
|
||||
<sub>SHA-256: `0305d98c06178c67e12427bbf340c436d0d58c9e2a39bf9ffbbf8f54d7ef95a5`</sub>
|
||||
|
||||
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
|
||||
[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.3/scrcpy-linux-x86_64-v3.3.3.tar.gz
|
||||
[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-linux-x86_64-v3.3.4.tar.gz
|
||||
|
||||
and extract it.
|
||||
|
||||
@@ -39,8 +39,8 @@ First, you need to install the required packages:
|
||||
|
||||
```bash
|
||||
# for Debian/Ubuntu
|
||||
sudo apt install ffmpeg libsdl2-2.0-0 adb wget \
|
||||
gcc git pkg-config meson ninja-build libsdl2-dev \
|
||||
sudo apt install ffmpeg libsdl3-0 adb wget \
|
||||
gcc git pkg-config meson ninja-build libsdl3-dev \
|
||||
libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \
|
||||
libswresample-dev libusb-1.0-0 libusb-1.0-0-dev
|
||||
```
|
||||
|
||||
13
doc/macos.md
13
doc/macos.md
@@ -6,15 +6,14 @@
|
||||
|
||||
Download a static build of the [latest release]:
|
||||
|
||||
- [`scrcpy-macos-aarch64-v3.3.3.tar.gz`][direct-macos-aarch64] (aarch64)
|
||||
<sub>SHA-256: `b93299468f19ae89ac70f7c1453914c41f1f2bcd31f6ab530038da885c19581f`</sub>
|
||||
|
||||
- [`scrcpy-macos-x86_64-v3.3.3.tar.gz`][direct-macos-x86_64] (x86_64)
|
||||
<sub>SHA-256: `c767fc1d41e4ae26e40558656570962f474739924fd22ee023d8754889ee4366`</sub>
|
||||
- [`scrcpy-macos-aarch64-v3.3.4.tar.gz`][direct-macos-aarch64] (aarch64)
|
||||
<sub>SHA-256: `8fef43520405dd523c74e1530ac68febcc5a405ea89712c874936675da8513dd`</sub>
|
||||
- [`scrcpy-macos-x86_64-v3.3.4.tar.gz`][direct-macos-x86_64] (x86_64)
|
||||
<sub>SHA-256: `cf9b3453a33279b6009dfb256b1a84c374bd4c30a71edd74bacab28d72a5d929`</sub>
|
||||
|
||||
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
|
||||
[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.3/scrcpy-macos-aarch64-v3.3.3.tar.gz
|
||||
[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.3/scrcpy-macos-x86_64-v3.3.3.tar.gz
|
||||
[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-macos-aarch64-v3.3.4.tar.gz
|
||||
[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-macos-x86_64-v3.3.4.tar.gz
|
||||
|
||||
and extract it.
|
||||
|
||||
|
||||
@@ -58,6 +58,10 @@ _<kbd>[Super]</kbd> is typically the <kbd>Windows</kbd> or <kbd>Cmd</kbd> key._
|
||||
| Tilt horizontally (slide with 2 fingers) | <kbd>Ctrl</kbd>+<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)
|
||||
| Turn on the camera torch (camera mode only) | <kbd>MOD</kbd>+<kbd>t</kbd>
|
||||
| Turn off the camera torch (camera mode only)| <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>t</kbd>
|
||||
| Zoom camera in (camera mode only) | <kbd>MOD</kbd>+<kbd>↑</kbd> _(up)_
|
||||
| Zoom camera out (camera mode only) | <kbd>MOD</kbd>+<kbd>↓</kbd> _(down)_
|
||||
|
||||
_¹Double-click on black borders to remove them._
|
||||
_²Right-click turns the screen on if it was off, presses BACK otherwise._
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
|
||||
Download the [latest release]:
|
||||
|
||||
- [`scrcpy-win64-v3.3.3.zip`][direct-win64] (64-bit)
|
||||
<sub>SHA-256: `4b458d33d0436688c69875cd267cae6fa8be08aa3c17772edf3a940a3dc4b17e`</sub>
|
||||
- [`scrcpy-win32-v3.3.3.zip`][direct-win32] (32-bit)
|
||||
<sub>SHA-256: `e3d43e21c0bd6e070381c390c1e4cccd48a1e71ae73a8c217e6e6b8506598c79`</sub>
|
||||
- [`scrcpy-win64-v3.3.4.zip`][direct-win64] (64-bit)
|
||||
<sub>SHA-256: `d8a155b7c180b7ca4cdadd40712b8750b63f3aab48cb5b8a2a39ac2d0d4c5d38`</sub>
|
||||
- [`scrcpy-win32-v3.3.4.zip`][direct-win32] (32-bit)
|
||||
<sub>SHA-256: `393f7d5379dabd8aacc41184755c3d0df975cd2861353cb7a8d50e0835e2eb72`</sub>
|
||||
|
||||
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
|
||||
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.3/scrcpy-win64-v3.3.3.zip
|
||||
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.3/scrcpy-win32-v3.3.3.zip
|
||||
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-win64-v3.3.4.zip
|
||||
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-win32-v3.3.4.zip
|
||||
|
||||
and extract it.
|
||||
|
||||
|
||||
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,7 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||
# https://gradle.org/release-checksums/
|
||||
distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab
|
||||
distributionSha256Sum=bd71102213493060956ec229d946beee57158dbd89d0e62b91bca0fa2c5f3531
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
set -e
|
||||
|
||||
BUILDDIR=build-auto
|
||||
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.3.3/scrcpy-server-v3.3.3
|
||||
PREBUILT_SERVER_SHA256=7e70323ba7f259649dd4acce97ac4fefbae8102b2c6d91e2e7be613fd5354be0
|
||||
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-server-v3.3.4
|
||||
PREBUILT_SERVER_SHA256=8588238c9a5a00aa542906b6ec7e6d5541d9ffb9b5d0f6e1bc0e365e2303079e
|
||||
|
||||
echo "[scrcpy] Downloading prebuilt server..."
|
||||
wget "$PREBUILT_SERVER_URL" -O scrcpy-server
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
project('scrcpy', 'c',
|
||||
version: '3.3.3',
|
||||
version: '3.3.4',
|
||||
meson_version: '>= 0.49',
|
||||
default_options: [
|
||||
'c_std=c11',
|
||||
|
||||
@@ -38,6 +38,6 @@ 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/data/scrcpy.png "$LINUX_BUILD_DIR/dist/"
|
||||
cp app/scrcpy.1 "$LINUX_BUILD_DIR/dist/"
|
||||
cp -r "$ADB_INSTALL_DIR"/. "$LINUX_BUILD_DIR/dist/"
|
||||
|
||||
@@ -38,6 +38,6 @@ 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/data/scrcpy.png "$MACOS_BUILD_DIR/dist/"
|
||||
cp app/scrcpy.1 "$MACOS_BUILD_DIR/dist/"
|
||||
cp -r "$ADB_INSTALL_DIR"/. "$MACOS_BUILD_DIR/dist/"
|
||||
|
||||
@@ -47,7 +47,7 @@ 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/scrcpy.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/"
|
||||
|
||||
2
run
2
run
@@ -20,6 +20,6 @@ then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRCPY_ICON_PATH="app/data/icon.png" \
|
||||
SCRCPY_ICON_DIR="app/data" \
|
||||
SCRCPY_SERVER_PATH="$BUILDDIR/server/scrcpy-server" \
|
||||
"$BUILDDIR/app/scrcpy" "$@"
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
namespace 'com.genymobile.scrcpy'
|
||||
compileSdk 35
|
||||
namespace = 'com.genymobile.scrcpy'
|
||||
compileSdk 36
|
||||
defaultConfig {
|
||||
applicationId "com.genymobile.scrcpy"
|
||||
applicationId = "com.genymobile.scrcpy"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 35
|
||||
versionCode 30303
|
||||
versionName "3.3.3"
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
targetSdkVersion 36
|
||||
versionCode 30304
|
||||
versionName "3.3.4"
|
||||
testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
@@ -18,8 +18,11 @@ android {
|
||||
}
|
||||
}
|
||||
buildFeatures {
|
||||
buildConfig true
|
||||
aidl true
|
||||
buildConfig = true
|
||||
aidl = true
|
||||
}
|
||||
lint {
|
||||
disable 'UseRequiresApi'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
set -e
|
||||
|
||||
SCRCPY_DEBUG=false
|
||||
SCRCPY_VERSION_NAME=3.3.3
|
||||
SCRCPY_VERSION_NAME=3.3.4
|
||||
|
||||
PLATFORM=${ANDROID_PLATFORM:-35}
|
||||
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.0.0}
|
||||
PLATFORM=${ANDROID_PLATFORM:-36}
|
||||
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-36.0.0}
|
||||
PLATFORM_TOOLS="$ANDROID_HOME/platforms/android-$PLATFORM"
|
||||
BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS"
|
||||
|
||||
@@ -86,7 +86,7 @@ javac -encoding UTF-8 -bootclasspath "$ANDROID_JAR" \
|
||||
echo "Dexing..."
|
||||
cd "$CLASSES_DIR"
|
||||
|
||||
if [[ $PLATFORM -lt 31 ]]
|
||||
if [[ "${PLATFORM%%.*}" -lt 31 ]]
|
||||
then
|
||||
# use dx
|
||||
"$BUILD_TOOLS_DIR/dx" --dex --output "$BUILD_DIR/classes.dex" \
|
||||
|
||||
@@ -109,8 +109,10 @@ public final class FakeContext extends ContextWrapper {
|
||||
}
|
||||
|
||||
// "semclipboard" is a Samsung-internal service
|
||||
// See <https://github.com/Genymobile/scrcpy/issues/6224>
|
||||
if (Context.CLIPBOARD_SERVICE.equals(name) || "semclipboard".equals(name)) {
|
||||
// See:
|
||||
// - <https://github.com/Genymobile/scrcpy/issues/6224>
|
||||
// - <https://github.com/Genymobile/scrcpy/issues/6523>
|
||||
if (Context.CLIPBOARD_SERVICE.equals(name) || "semclipboard".equals(name) || Context.ACTIVITY_SERVICE.equals(name)) {
|
||||
try {
|
||||
Field field = service.getClass().getDeclaredField("mContext");
|
||||
field.setAccessible(true);
|
||||
|
||||
@@ -44,8 +44,10 @@ public class Options {
|
||||
private Size cameraSize;
|
||||
private CameraFacing cameraFacing;
|
||||
private CameraAspectRatio cameraAspectRatio;
|
||||
private float cameraZoom = 1;
|
||||
private int cameraFps;
|
||||
private boolean cameraHighSpeed;
|
||||
private boolean cameraTorch;
|
||||
private boolean showTouches;
|
||||
private boolean stayAwake;
|
||||
private int screenOffTimeout = -1;
|
||||
@@ -168,6 +170,10 @@ public class Options {
|
||||
return cameraAspectRatio;
|
||||
}
|
||||
|
||||
public float getCameraZoom() {
|
||||
return cameraZoom;
|
||||
}
|
||||
|
||||
public int getCameraFps() {
|
||||
return cameraFps;
|
||||
}
|
||||
@@ -176,6 +182,10 @@ public class Options {
|
||||
return cameraHighSpeed;
|
||||
}
|
||||
|
||||
public boolean getCameraTorch() {
|
||||
return cameraTorch;
|
||||
}
|
||||
|
||||
public boolean getShowTouches() {
|
||||
return showTouches;
|
||||
}
|
||||
@@ -414,6 +424,7 @@ public class Options {
|
||||
if (!value.isEmpty()) {
|
||||
options.audioEncoder = value;
|
||||
}
|
||||
break;
|
||||
case "power_off_on_close":
|
||||
options.powerOffScreenOnClose = Boolean.parseBoolean(value);
|
||||
break;
|
||||
@@ -468,12 +479,20 @@ public class Options {
|
||||
options.cameraAspectRatio = parseCameraAspectRatio(value);
|
||||
}
|
||||
break;
|
||||
case "camera_zoom":
|
||||
if (!value.isEmpty()) {
|
||||
options.cameraZoom = Float.parseFloat(value);
|
||||
}
|
||||
break;
|
||||
case "camera_fps":
|
||||
options.cameraFps = Integer.parseInt(value);
|
||||
break;
|
||||
case "camera_high_speed":
|
||||
options.cameraHighSpeed = Boolean.parseBoolean(value);
|
||||
break;
|
||||
case "camera_torch":
|
||||
options.cameraTorch = Boolean.parseBoolean(value);
|
||||
break;
|
||||
case "new_display":
|
||||
options.newDisplay = parseNewDisplay(value);
|
||||
break;
|
||||
|
||||
@@ -27,6 +27,7 @@ import com.genymobile.scrcpy.video.VideoSource;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.os.Build;
|
||||
import android.os.Looper;
|
||||
import android.system.Os;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -225,10 +226,16 @@ public final class Server {
|
||||
}
|
||||
|
||||
private static void internalMain(String... args) throws Exception {
|
||||
Thread.UncaughtExceptionHandler defaultHandler = Thread.getDefaultUncaughtExceptionHandler();
|
||||
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
|
||||
Ln.e("Exception on thread " + t, e);
|
||||
if (defaultHandler != null) {
|
||||
defaultHandler.uncaughtException(t, e);
|
||||
}
|
||||
});
|
||||
|
||||
dropRootPrivileges();
|
||||
|
||||
prepareMainLooper();
|
||||
|
||||
Options options = Options.parse(args);
|
||||
@@ -269,4 +276,17 @@ public final class Server {
|
||||
// Do not print stack trace, a user-friendly error-message has already been logged
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private static void dropRootPrivileges() {
|
||||
try {
|
||||
if (Os.getuid() == 0) {
|
||||
// Copy-paste does not work with root user
|
||||
// <https://github.com/Genymobile/scrcpy/issues/6224>
|
||||
Os.setuid(2000);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Ln.w("Cannot set UID", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ import com.genymobile.scrcpy.util.Ln;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Application;
|
||||
import android.app.Instrumentation;
|
||||
import android.content.AttributionSource;
|
||||
import android.content.Context;
|
||||
import android.content.ContextWrapper;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.media.AudioAttributes;
|
||||
import android.media.AudioManager;
|
||||
@@ -103,10 +103,7 @@ public final class Workarounds {
|
||||
|
||||
private static void fillAppContext() {
|
||||
try {
|
||||
Application app = new Application();
|
||||
Field baseField = ContextWrapper.class.getDeclaredField("mBase");
|
||||
baseField.setAccessible(true);
|
||||
baseField.set(app, FakeContext.get());
|
||||
Application app = Instrumentation.newApplication(Application.class, FakeContext.get());
|
||||
|
||||
// activityThread.mInitialApplication = app;
|
||||
Field mInitialApplicationField = ACTIVITY_THREAD_CLASS.getDeclaredField("mInitialApplication");
|
||||
|
||||
@@ -25,6 +25,9 @@ public final class ControlMessage {
|
||||
public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 15;
|
||||
public static final int TYPE_START_APP = 16;
|
||||
public static final int TYPE_RESET_VIDEO = 17;
|
||||
public static final int TYPE_CAMERA_SET_TORCH = 18;
|
||||
public static final int TYPE_CAMERA_ZOOM_IN = 19;
|
||||
public static final int TYPE_CAMERA_ZOOM_OUT = 20;
|
||||
|
||||
public static final long SEQUENCE_INVALID = 0;
|
||||
|
||||
@@ -166,6 +169,13 @@ public final class ControlMessage {
|
||||
return msg;
|
||||
}
|
||||
|
||||
public static ControlMessage createCameraSetTorch(boolean on) {
|
||||
ControlMessage msg = new ControlMessage();
|
||||
msg.type = TYPE_CAMERA_SET_TORCH;
|
||||
msg.on = on;
|
||||
return msg;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@@ -47,6 +47,8 @@ public class ControlMessageReader {
|
||||
case ControlMessage.TYPE_ROTATE_DEVICE:
|
||||
case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
|
||||
case ControlMessage.TYPE_RESET_VIDEO:
|
||||
case ControlMessage.TYPE_CAMERA_ZOOM_IN:
|
||||
case ControlMessage.TYPE_CAMERA_ZOOM_OUT:
|
||||
return ControlMessage.createEmpty(type);
|
||||
case ControlMessage.TYPE_UHID_CREATE:
|
||||
return parseUhidCreate();
|
||||
@@ -56,6 +58,8 @@ public class ControlMessageReader {
|
||||
return parseUhidDestroy();
|
||||
case ControlMessage.TYPE_START_APP:
|
||||
return parseStartApp();
|
||||
case ControlMessage.TYPE_CAMERA_SET_TORCH:
|
||||
return parseCameraSetTorch();
|
||||
default:
|
||||
throw new ControlProtocolException("Unknown event type: " + type);
|
||||
}
|
||||
@@ -166,6 +170,11 @@ public class ControlMessageReader {
|
||||
return ControlMessage.createStartApp(name);
|
||||
}
|
||||
|
||||
private ControlMessage parseCameraSetTorch() throws IOException {
|
||||
boolean on = dis.readBoolean();
|
||||
return ControlMessage.createCameraSetTorch(on);
|
||||
}
|
||||
|
||||
private Position parsePosition() throws IOException {
|
||||
int x = dis.readInt();
|
||||
int y = dis.readInt();
|
||||
|
||||
@@ -12,7 +12,9 @@ import com.genymobile.scrcpy.device.Position;
|
||||
import com.genymobile.scrcpy.device.Size;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
import com.genymobile.scrcpy.util.LogUtils;
|
||||
import com.genymobile.scrcpy.video.CameraCapture;
|
||||
import com.genymobile.scrcpy.video.SurfaceCapture;
|
||||
import com.genymobile.scrcpy.video.VideoSource;
|
||||
import com.genymobile.scrcpy.video.VirtualDisplayListener;
|
||||
import com.genymobile.scrcpy.wrappers.ClipboardManager;
|
||||
import com.genymobile.scrcpy.wrappers.InputManager;
|
||||
@@ -75,6 +77,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
|
||||
private UhidManager uhidManager;
|
||||
|
||||
private final boolean camera;
|
||||
private final int displayId;
|
||||
private final boolean supportsInputEvents;
|
||||
private final ControlChannel controlChannel;
|
||||
@@ -97,13 +100,26 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
|
||||
private boolean keepDisplayPowerOff;
|
||||
|
||||
// Used for resetting video encoding on RESET_VIDEO message
|
||||
// Used for resetting video encoding on RESET_VIDEO message or for sending camera controls
|
||||
private SurfaceCapture surfaceCapture;
|
||||
|
||||
public Controller(ControlChannel controlChannel, CleanUp cleanUp, Options options) {
|
||||
this.displayId = options.getDisplayId();
|
||||
this.camera = options.getVideoSource() == VideoSource.CAMERA;
|
||||
this.controlChannel = controlChannel;
|
||||
this.cleanUp = cleanUp;
|
||||
|
||||
if (this.camera) {
|
||||
// Unused for camera
|
||||
this.displayId = Device.DISPLAY_ID_NONE;
|
||||
this.supportsInputEvents = false;
|
||||
this.sender = null;
|
||||
this.clipboardAutosync = false;
|
||||
this.powerOn = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.displayId = options.getDisplayId();
|
||||
|
||||
this.clipboardAutosync = options.getClipboardAutosync();
|
||||
this.powerOn = options.getPowerOn();
|
||||
initPointers();
|
||||
@@ -201,7 +217,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
|
||||
private void control() throws IOException {
|
||||
// on start, power on the device
|
||||
if (powerOn && displayId == 0 && !Device.isScreenOn(displayId)) {
|
||||
if (!camera && powerOn && displayId == 0 && !Device.isScreenOn(displayId)) {
|
||||
Device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC);
|
||||
|
||||
// dirty hack
|
||||
@@ -236,7 +252,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
}
|
||||
}, "control-recv");
|
||||
thread.start();
|
||||
sender.start();
|
||||
if (sender != null) {
|
||||
sender.start();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -244,7 +262,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
if (thread != null) {
|
||||
thread.interrupt();
|
||||
}
|
||||
sender.stop();
|
||||
if (sender != null) {
|
||||
sender.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -252,90 +272,122 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
if (thread != null) {
|
||||
thread.join();
|
||||
}
|
||||
sender.join();
|
||||
if (sender != null) {
|
||||
sender.join();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean handleEvent() throws IOException {
|
||||
ControlMessage msg;
|
||||
try {
|
||||
msg = controlChannel.recv();
|
||||
} catch (ControlProtocolException e) {
|
||||
Ln.e("Control protocol error", e);
|
||||
return false;
|
||||
} catch (IOException e) {
|
||||
// this is expected on close
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (msg.getType()) {
|
||||
case ControlMessage.TYPE_INJECT_KEYCODE:
|
||||
if (supportsInputEvents) {
|
||||
injectKeycode(msg.getAction(), msg.getKeycode(), msg.getRepeat(), msg.getMetaState());
|
||||
}
|
||||
break;
|
||||
case ControlMessage.TYPE_INJECT_TEXT:
|
||||
if (supportsInputEvents) {
|
||||
injectText(msg.getText());
|
||||
}
|
||||
break;
|
||||
case ControlMessage.TYPE_INJECT_TOUCH_EVENT:
|
||||
if (supportsInputEvents) {
|
||||
injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getActionButton(), msg.getButtons());
|
||||
}
|
||||
break;
|
||||
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
|
||||
if (supportsInputEvents) {
|
||||
injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll(), msg.getButtons());
|
||||
}
|
||||
break;
|
||||
case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
|
||||
if (supportsInputEvents) {
|
||||
pressBackOrTurnScreenOn(msg.getAction());
|
||||
}
|
||||
break;
|
||||
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
|
||||
Device.expandNotificationPanel();
|
||||
break;
|
||||
case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL:
|
||||
Device.expandSettingsPanel();
|
||||
break;
|
||||
case ControlMessage.TYPE_COLLAPSE_PANELS:
|
||||
Device.collapsePanels();
|
||||
break;
|
||||
case ControlMessage.TYPE_GET_CLIPBOARD:
|
||||
getClipboard(msg.getCopyKey());
|
||||
break;
|
||||
case ControlMessage.TYPE_SET_CLIPBOARD:
|
||||
setClipboard(msg.getText(), msg.getPaste(), msg.getSequence());
|
||||
break;
|
||||
case ControlMessage.TYPE_SET_DISPLAY_POWER:
|
||||
if (supportsInputEvents) {
|
||||
setDisplayPower(msg.getOn());
|
||||
}
|
||||
break;
|
||||
case ControlMessage.TYPE_ROTATE_DEVICE:
|
||||
Device.rotateDevice(getActionDisplayId());
|
||||
break;
|
||||
case ControlMessage.TYPE_UHID_CREATE:
|
||||
getUhidManager().open(msg.getId(), msg.getVendorId(), msg.getProductId(), msg.getText(), msg.getData());
|
||||
break;
|
||||
case ControlMessage.TYPE_UHID_INPUT:
|
||||
getUhidManager().writeInput(msg.getId(), msg.getData());
|
||||
break;
|
||||
case ControlMessage.TYPE_UHID_DESTROY:
|
||||
getUhidManager().close(msg.getId());
|
||||
break;
|
||||
case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
|
||||
openHardKeyboardSettings();
|
||||
break;
|
||||
case ControlMessage.TYPE_START_APP:
|
||||
startAppAsync(msg.getText());
|
||||
break;
|
||||
int type = msg.getType();
|
||||
|
||||
// Events for all sources (display or camera)
|
||||
switch (type) {
|
||||
case ControlMessage.TYPE_RESET_VIDEO:
|
||||
resetVideo();
|
||||
break;
|
||||
return true;
|
||||
default:
|
||||
// do nothing
|
||||
// fall through
|
||||
}
|
||||
|
||||
return true;
|
||||
if (!camera) {
|
||||
switch (type) {
|
||||
case ControlMessage.TYPE_INJECT_KEYCODE:
|
||||
if (supportsInputEvents) {
|
||||
injectKeycode(msg.getAction(), msg.getKeycode(), msg.getRepeat(), msg.getMetaState());
|
||||
}
|
||||
return true;
|
||||
case ControlMessage.TYPE_INJECT_TEXT:
|
||||
if (supportsInputEvents) {
|
||||
injectText(msg.getText());
|
||||
}
|
||||
return true;
|
||||
case ControlMessage.TYPE_INJECT_TOUCH_EVENT:
|
||||
if (supportsInputEvents) {
|
||||
injectTouch(
|
||||
msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getActionButton(), msg.getButtons());
|
||||
}
|
||||
return true;
|
||||
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
|
||||
if (supportsInputEvents) {
|
||||
injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll(), msg.getButtons());
|
||||
}
|
||||
return true;
|
||||
case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
|
||||
if (supportsInputEvents) {
|
||||
pressBackOrTurnScreenOn(msg.getAction());
|
||||
}
|
||||
return true;
|
||||
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
|
||||
Device.expandNotificationPanel();
|
||||
return true;
|
||||
case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL:
|
||||
Device.expandSettingsPanel();
|
||||
return true;
|
||||
case ControlMessage.TYPE_COLLAPSE_PANELS:
|
||||
Device.collapsePanels();
|
||||
return true;
|
||||
case ControlMessage.TYPE_GET_CLIPBOARD:
|
||||
getClipboard(msg.getCopyKey());
|
||||
return true;
|
||||
case ControlMessage.TYPE_SET_CLIPBOARD:
|
||||
setClipboard(msg.getText(), msg.getPaste(), msg.getSequence());
|
||||
return true;
|
||||
case ControlMessage.TYPE_SET_DISPLAY_POWER:
|
||||
if (supportsInputEvents) {
|
||||
setDisplayPower(msg.getOn());
|
||||
}
|
||||
return true;
|
||||
case ControlMessage.TYPE_ROTATE_DEVICE:
|
||||
Device.rotateDevice(getActionDisplayId());
|
||||
return true;
|
||||
case ControlMessage.TYPE_UHID_CREATE:
|
||||
getUhidManager().open(msg.getId(), msg.getVendorId(), msg.getProductId(), msg.getText(), msg.getData());
|
||||
return true;
|
||||
case ControlMessage.TYPE_UHID_INPUT:
|
||||
getUhidManager().writeInput(msg.getId(), msg.getData());
|
||||
return true;
|
||||
case ControlMessage.TYPE_UHID_DESTROY:
|
||||
getUhidManager().close(msg.getId());
|
||||
return true;
|
||||
case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
|
||||
openHardKeyboardSettings();
|
||||
return true;
|
||||
case ControlMessage.TYPE_START_APP:
|
||||
startAppAsync(msg.getText());
|
||||
return true;
|
||||
default:
|
||||
// fall through
|
||||
}
|
||||
} else {
|
||||
assert surfaceCapture instanceof CameraCapture;
|
||||
CameraCapture cameraCapture = (CameraCapture) surfaceCapture;
|
||||
switch (type) {
|
||||
case ControlMessage.TYPE_CAMERA_SET_TORCH:
|
||||
cameraCapture.setTorchEnabled(msg.getOn());
|
||||
return true;
|
||||
case ControlMessage.TYPE_CAMERA_ZOOM_IN:
|
||||
cameraCapture.zoomIn();
|
||||
return true;
|
||||
case ControlMessage.TYPE_CAMERA_ZOOM_OUT:
|
||||
cameraCapture.zoomOut();
|
||||
return true;
|
||||
default:
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
|
||||
throw new AssertionError("Unexpected message type: " + type);
|
||||
}
|
||||
|
||||
private boolean injectKeycode(int action, int keycode, int repeat, int metaState) {
|
||||
@@ -751,7 +803,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
private void resetVideo() {
|
||||
if (surfaceCapture != null) {
|
||||
Ln.i("Video capture reset");
|
||||
surfaceCapture.requestInvalidate();
|
||||
surfaceCapture.invalidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.genymobile.scrcpy.opengl;
|
||||
|
||||
import com.genymobile.scrcpy.device.Size;
|
||||
import com.genymobile.scrcpy.util.Threads;
|
||||
|
||||
import android.graphics.SurfaceTexture;
|
||||
import android.opengl.EGL14;
|
||||
@@ -15,6 +16,7 @@ import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.view.Surface;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.Semaphore;
|
||||
|
||||
public final class OpenGLRunner {
|
||||
@@ -80,31 +82,17 @@ public final class OpenGLRunner {
|
||||
public Surface start(Size inputSize, Size outputSize, Surface outputSurface) throws OpenGLException {
|
||||
initOnce();
|
||||
|
||||
// Simulate CompletableFuture, but working for all Android versions
|
||||
final Semaphore sem = new Semaphore(0);
|
||||
Throwable[] throwableRef = new Throwable[1];
|
||||
|
||||
// The whole OpenGL execution must be performed on a Handler, so that SurfaceTexture.setOnFrameAvailableListener() works correctly.
|
||||
// See <https://github.com/Genymobile/scrcpy/issues/5444>
|
||||
handler.post(() -> {
|
||||
try {
|
||||
run(inputSize, outputSize, outputSurface);
|
||||
} catch (Throwable throwable) {
|
||||
throwableRef[0] = throwable;
|
||||
} finally {
|
||||
sem.release();
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
sem.acquire();
|
||||
} catch (InterruptedException e) {
|
||||
// Behave as if this method call was synchronous
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
|
||||
Throwable throwable = throwableRef[0];
|
||||
if (throwable != null) {
|
||||
Threads.executeSynchronouslyOn(handler, new Callable<Void>() {
|
||||
@Override
|
||||
public Void call() throws Exception {
|
||||
run(inputSize, outputSize, outputSurface);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
} catch (Throwable throwable) {
|
||||
if (throwable instanceof OpenGLException) {
|
||||
throw (OpenGLException) throwable;
|
||||
}
|
||||
|
||||
@@ -74,9 +74,11 @@ public final class Ln {
|
||||
public static void w(String message, Throwable throwable) {
|
||||
if (isEnabled(Level.WARN)) {
|
||||
Log.w(TAG, message, throwable);
|
||||
CONSOLE_ERR.print(PREFIX + "WARN: " + message + '\n');
|
||||
if (throwable != null) {
|
||||
throwable.printStackTrace(CONSOLE_ERR);
|
||||
synchronized (CONSOLE_ERR) {
|
||||
CONSOLE_ERR.print(PREFIX + "WARN: " + message + '\n');
|
||||
if (throwable != null) {
|
||||
throwable.printStackTrace(CONSOLE_ERR);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,9 +90,11 @@ public final class Ln {
|
||||
public static void e(String message, Throwable throwable) {
|
||||
if (isEnabled(Level.ERROR)) {
|
||||
Log.e(TAG, message, throwable);
|
||||
CONSOLE_ERR.print(PREFIX + "ERROR: " + message + '\n');
|
||||
if (throwable != null) {
|
||||
throwable.printStackTrace(CONSOLE_ERR);
|
||||
synchronized (CONSOLE_ERR) {
|
||||
CONSOLE_ERR.print(PREFIX + "ERROR: " + message + '\n');
|
||||
if (throwable != null) {
|
||||
throwable.printStackTrace(CONSOLE_ERR);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import android.media.MediaCodecList;
|
||||
import android.os.Build;
|
||||
import android.util.Range;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
@@ -164,7 +165,7 @@ public final class LogUtils {
|
||||
// Capture frame rates for low-FPS mode are the same for every resolution
|
||||
Range<Integer>[] lowFpsRanges = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES);
|
||||
if (lowFpsRanges != null) {
|
||||
SortedSet<Integer> uniqueLowFps = getUniqueSet(lowFpsRanges);
|
||||
String uniqueLowFps = getFormattedUniqueSet(lowFpsRanges);
|
||||
builder.append(", fps=").append(uniqueLowFps);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@@ -172,6 +173,18 @@ public final class LogUtils {
|
||||
Ln.w("Could not get available frame rates for camera " + id, e);
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= AndroidVersions.API_30_ANDROID_11) {
|
||||
try {
|
||||
Range<Float> zoomRange = characteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE);
|
||||
if (zoomRange != null) {
|
||||
String zoom = getFormattedZoomRange(zoomRange);
|
||||
builder.append(", zoom-range=").append(zoom);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Ln.w("Could not get available zoom ranges for camera " + id, e);
|
||||
}
|
||||
}
|
||||
|
||||
builder.append(')');
|
||||
|
||||
if (includeSizes) {
|
||||
@@ -191,7 +204,7 @@ public final class LogUtils {
|
||||
builder.append("\n High speed capture (--camera-high-speed):");
|
||||
for (android.util.Size size : highSpeedSizes) {
|
||||
Range<Integer>[] highFpsRanges = configs.getHighSpeedVideoFpsRanges();
|
||||
SortedSet<Integer> uniqueHighFps = getUniqueSet(highFpsRanges);
|
||||
String uniqueHighFps = getFormattedUniqueSet(highFpsRanges);
|
||||
builder.append("\n - ").append(size.getWidth()).append("x").append(size.getHeight());
|
||||
builder.append(" (fps=").append(uniqueHighFps).append(')');
|
||||
}
|
||||
@@ -205,14 +218,31 @@ public final class LogUtils {
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static SortedSet<Integer> getUniqueSet(Range<Integer>[] ranges) {
|
||||
private static String getFormattedUniqueSet(Range<Integer>[] ranges) {
|
||||
SortedSet<Integer> set = new TreeSet<>();
|
||||
for (Range<Integer> range : ranges) {
|
||||
set.add(range.getUpper());
|
||||
}
|
||||
return set;
|
||||
|
||||
StringBuilder builder = new StringBuilder("{");
|
||||
boolean first = true;
|
||||
for (Integer i : set) {
|
||||
if (!first) {
|
||||
builder.append(", ");
|
||||
} else {
|
||||
first = false;
|
||||
}
|
||||
builder.append(i);
|
||||
}
|
||||
builder.append("}");
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static String getFormattedZoomRange(Range<Float> range) {
|
||||
DecimalFormat format = new DecimalFormat("#.##");
|
||||
return "[" + format.format(range.getLower()) + ", " + format.format(range.getUpper()) + "]";
|
||||
}
|
||||
|
||||
public static String buildAppListMessage() {
|
||||
List<DeviceApp> apps = Device.listApps();
|
||||
|
||||
43
server/src/main/java/com/genymobile/scrcpy/util/Threads.java
Normal file
43
server/src/main/java/com/genymobile/scrcpy/util/Threads.java
Normal file
@@ -0,0 +1,43 @@
|
||||
package com.genymobile.scrcpy.util;
|
||||
|
||||
import android.os.Handler;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.Semaphore;
|
||||
|
||||
public final class Threads {
|
||||
private Threads() {
|
||||
// not instantiable
|
||||
}
|
||||
|
||||
public static <T> T executeSynchronouslyOn(Handler handler, Callable<T> callable) throws Throwable {
|
||||
// Simulate CompletableFuture, but working for all Android versions
|
||||
final Semaphore sem = new Semaphore(0);
|
||||
@SuppressWarnings("unchecked")
|
||||
T[] resultRef = (T[]) new Object[1];
|
||||
Throwable[] throwableRef = new Throwable[1];
|
||||
|
||||
handler.post(() -> {
|
||||
try {
|
||||
resultRef[0] = callable.call();
|
||||
} catch (Throwable throwable) {
|
||||
throwableRef[0] = throwable;
|
||||
} finally {
|
||||
sem.release();
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
sem.acquire();
|
||||
} catch (InterruptedException e) {
|
||||
// Behave as if this method call was synchronous
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
|
||||
if (throwableRef[0] != null) {
|
||||
throw throwableRef[0];
|
||||
}
|
||||
|
||||
return resultRef[0];
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ import android.view.Surface;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -53,6 +54,8 @@ public class CameraCapture extends SurfaceCapture {
|
||||
0, 1, 0, 1, // column 4
|
||||
};
|
||||
|
||||
private static final float ZOOM_FACTOR = 1 + 1 / 16f;
|
||||
|
||||
private final String explicitCameraId;
|
||||
private final CameraFacing cameraFacing;
|
||||
private final Size explicitSize;
|
||||
@@ -63,10 +66,13 @@ public class CameraCapture extends SurfaceCapture {
|
||||
private final Rect crop;
|
||||
private final Orientation captureOrientation;
|
||||
private final float angle;
|
||||
private final boolean initialTorch;
|
||||
private float zoom;
|
||||
|
||||
private String cameraId;
|
||||
private Size captureSize;
|
||||
private Size videoSize; // after OpenGL transforms
|
||||
private Range<Float> zoomRange;
|
||||
|
||||
private AffineMatrix transform;
|
||||
private OpenGLRunner glRunner;
|
||||
@@ -78,6 +84,11 @@ public class CameraCapture extends SurfaceCapture {
|
||||
|
||||
private final AtomicBoolean disconnected = new AtomicBoolean();
|
||||
|
||||
// The following fields must be accessed only from the camera thread
|
||||
private boolean started;
|
||||
private CaptureRequest.Builder requestBuilder;
|
||||
private CameraCaptureSession currentSession;
|
||||
|
||||
public CameraCapture(Options options) {
|
||||
this.explicitCameraId = options.getCameraId();
|
||||
this.cameraFacing = options.getCameraFacing();
|
||||
@@ -90,6 +101,8 @@ public class CameraCapture extends SurfaceCapture {
|
||||
this.captureOrientation = options.getCaptureOrientation();
|
||||
assert captureOrientation != null;
|
||||
this.angle = options.getAngle();
|
||||
this.initialTorch = options.getCameraTorch();
|
||||
this.zoom = options.getCameraZoom();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -250,6 +263,7 @@ public class CameraCapture extends SurfaceCapture {
|
||||
return ratio.getAspectRatio();
|
||||
}
|
||||
|
||||
@TargetApi(AndroidVersions.API_30_ANDROID_11)
|
||||
@Override
|
||||
public void start(Surface surface) throws IOException {
|
||||
if (transform != null) {
|
||||
@@ -261,11 +275,68 @@ public class CameraCapture extends SurfaceCapture {
|
||||
surface = glRunner.start(captureSize, videoSize, surface);
|
||||
}
|
||||
|
||||
cameraHandler.post(() -> {
|
||||
assertCameraThread();
|
||||
started = true;
|
||||
});
|
||||
|
||||
Surface captureSurface = surface;
|
||||
OutputConfiguration outputConfig = new OutputConfiguration(captureSurface);
|
||||
List<OutputConfiguration> outputs = Collections.singletonList(outputConfig);
|
||||
int sessionType = highSpeed ? SessionConfiguration.SESSION_HIGH_SPEED : SessionConfiguration.SESSION_REGULAR;
|
||||
SessionConfiguration sessionConfig = new SessionConfiguration(sessionType, outputs, cameraExecutor, new CameraCaptureSession.StateCallback() {
|
||||
@Override
|
||||
public void onConfigured(CameraCaptureSession session) {
|
||||
assertCameraThread();
|
||||
if (!started) {
|
||||
// Stopped on the encoder thread between the call to start() and this callback
|
||||
return;
|
||||
}
|
||||
|
||||
CameraManager cameraManager = ServiceManager.getCameraManager();
|
||||
try {
|
||||
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId);
|
||||
zoomRange = characteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE);
|
||||
} catch (CameraAccessException e) {
|
||||
Ln.w("Could not get camera characteristics");
|
||||
}
|
||||
|
||||
try {
|
||||
requestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
|
||||
requestBuilder.addTarget(captureSurface);
|
||||
|
||||
if (fps > 0) {
|
||||
requestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, new Range<>(fps, fps));
|
||||
}
|
||||
if (initialTorch) {
|
||||
Ln.i("Turn camera torch on");
|
||||
requestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH);
|
||||
}
|
||||
if (zoom != 1) {
|
||||
zoom = clampZoom(zoom);
|
||||
Ln.i("Set camera zoom: " + zoom);
|
||||
requestBuilder.set(CaptureRequest.CONTROL_ZOOM_RATIO, zoom);
|
||||
}
|
||||
|
||||
CaptureRequest request = requestBuilder.build();
|
||||
setRepeatingRequest(session, request);
|
||||
currentSession = session;
|
||||
} catch (CameraAccessException e) {
|
||||
Ln.e("Camera error", e);
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigureFailed(CameraCaptureSession session) {
|
||||
Ln.e("Camera configuration error");
|
||||
invalidate();
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
CameraCaptureSession session = createCaptureSession(cameraDevice, surface);
|
||||
CaptureRequest request = createCaptureRequest(surface);
|
||||
setRepeatingRequest(session, request);
|
||||
} catch (CameraAccessException | InterruptedException e) {
|
||||
cameraDevice.createCaptureSession(sessionConfig);
|
||||
} catch (CameraAccessException e) {
|
||||
stop();
|
||||
throw new IOException(e);
|
||||
}
|
||||
@@ -273,6 +344,13 @@ public class CameraCapture extends SurfaceCapture {
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
cameraHandler.post(() -> {
|
||||
assertCameraThread();
|
||||
currentSession = null;
|
||||
requestBuilder = null;
|
||||
started = false;
|
||||
});
|
||||
|
||||
if (glRunner != null) {
|
||||
glRunner.stopAndRelease();
|
||||
glRunner = null;
|
||||
@@ -353,46 +431,7 @@ public class CameraCapture extends SurfaceCapture {
|
||||
}
|
||||
|
||||
@TargetApi(AndroidVersions.API_31_ANDROID_12)
|
||||
private CameraCaptureSession createCaptureSession(CameraDevice camera, Surface surface) throws CameraAccessException, InterruptedException {
|
||||
CompletableFuture<CameraCaptureSession> future = new CompletableFuture<>();
|
||||
OutputConfiguration outputConfig = new OutputConfiguration(surface);
|
||||
List<OutputConfiguration> outputs = Arrays.asList(outputConfig);
|
||||
|
||||
int sessionType = highSpeed ? SessionConfiguration.SESSION_HIGH_SPEED : SessionConfiguration.SESSION_REGULAR;
|
||||
SessionConfiguration sessionConfig = new SessionConfiguration(sessionType, outputs, cameraExecutor, new CameraCaptureSession.StateCallback() {
|
||||
@Override
|
||||
public void onConfigured(CameraCaptureSession session) {
|
||||
future.complete(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigureFailed(CameraCaptureSession session) {
|
||||
future.completeExceptionally(new CameraAccessException(CameraAccessException.CAMERA_ERROR));
|
||||
}
|
||||
});
|
||||
|
||||
camera.createCaptureSession(sessionConfig);
|
||||
|
||||
try {
|
||||
return future.get();
|
||||
} catch (ExecutionException e) {
|
||||
throw (CameraAccessException) e.getCause();
|
||||
}
|
||||
}
|
||||
|
||||
private CaptureRequest createCaptureRequest(Surface surface) throws CameraAccessException {
|
||||
CaptureRequest.Builder requestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
|
||||
requestBuilder.addTarget(surface);
|
||||
|
||||
if (fps > 0) {
|
||||
requestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, new Range<>(fps, fps));
|
||||
}
|
||||
|
||||
return requestBuilder.build();
|
||||
}
|
||||
|
||||
@TargetApi(AndroidVersions.API_31_ANDROID_12)
|
||||
private void setRepeatingRequest(CameraCaptureSession session, CaptureRequest request) throws CameraAccessException, InterruptedException {
|
||||
private void setRepeatingRequest(CameraCaptureSession session, CaptureRequest request) throws CameraAccessException {
|
||||
CameraCaptureSession.CaptureCallback callback = new CameraCaptureSession.CaptureCallback() {
|
||||
@Override
|
||||
public void onCaptureStarted(CameraCaptureSession session, CaptureRequest request, long timestamp, long frameNumber) {
|
||||
@@ -419,8 +458,63 @@ public class CameraCapture extends SurfaceCapture {
|
||||
return disconnected.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestInvalidate() {
|
||||
// do nothing (the user could not request a reset anyway for now, since there is no controller for camera mirroring)
|
||||
public void setTorchEnabled(boolean enabled) {
|
||||
cameraHandler.post(() -> {
|
||||
assertCameraThread();
|
||||
if (currentSession != null && requestBuilder != null) {
|
||||
try {
|
||||
Ln.i("Turn camera torch " + (enabled ? "on" : "off"));
|
||||
requestBuilder.set(CaptureRequest.FLASH_MODE, enabled ? CaptureRequest.FLASH_MODE_TORCH : CaptureRequest.FLASH_MODE_OFF);
|
||||
CaptureRequest request = requestBuilder.build();
|
||||
setRepeatingRequest(currentSession, request);
|
||||
} catch (CameraAccessException e) {
|
||||
Ln.e("Camera error", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@TargetApi(AndroidVersions.API_30_ANDROID_11)
|
||||
private void zoom(boolean in) {
|
||||
cameraHandler.post(() -> {
|
||||
assertCameraThread();
|
||||
if (currentSession != null && requestBuilder != null) {
|
||||
// Always align to log values
|
||||
double z = Math.round(Math.log(zoom) / Math.log(ZOOM_FACTOR));
|
||||
double dir = in ? 1 : -1;
|
||||
zoom = (float) Math.pow(ZOOM_FACTOR, z + dir);
|
||||
|
||||
try {
|
||||
zoom = clampZoom(zoom);
|
||||
Ln.i("Set camera zoom: " + zoom);
|
||||
requestBuilder.set(CaptureRequest.CONTROL_ZOOM_RATIO, zoom);
|
||||
CaptureRequest request = requestBuilder.build();
|
||||
setRepeatingRequest(currentSession, request);
|
||||
} catch (CameraAccessException e) {
|
||||
Ln.e("Camera error", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void zoomIn() {
|
||||
zoom(true);
|
||||
}
|
||||
|
||||
public void zoomOut() {
|
||||
zoom(false);
|
||||
}
|
||||
|
||||
private float clampZoom(float value) {
|
||||
assertCameraThread();
|
||||
if (zoomRange == null) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return zoomRange.clamp(value);
|
||||
}
|
||||
|
||||
private void assertCameraThread() {
|
||||
assert Thread.currentThread() == cameraThread;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,9 +261,4 @@ public class NewDisplayCapture extends SurfaceCapture {
|
||||
int num = size.getMax();
|
||||
return initialDpi * num / den;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestInvalidate() {
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,9 +211,4 @@ public class ScreenCapture extends SurfaceCapture {
|
||||
SurfaceControl.closeTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestInvalidate() {
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,9 +19,9 @@ public abstract class SurfaceCapture {
|
||||
private CaptureListener listener;
|
||||
|
||||
/**
|
||||
* Notify the listener that the capture has been invalidated (for example, because its size changed).
|
||||
* Notify the listener that the capture has been invalidated (for example, because its size changed, or due to a manual user request).
|
||||
*/
|
||||
protected void invalidate() {
|
||||
public void invalidate() {
|
||||
listener.onInvalidated();
|
||||
}
|
||||
|
||||
@@ -86,11 +86,4 @@ public abstract class SurfaceCapture {
|
||||
public boolean isClosed() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually request to invalidate (typically a user request).
|
||||
* <p>
|
||||
* The capture implementation is free to ignore the request and do nothing.
|
||||
*/
|
||||
public abstract void requestInvalidate();
|
||||
}
|
||||
|
||||
@@ -139,7 +139,13 @@ public final class DisplayManager {
|
||||
int layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo);
|
||||
int flags = cls.getDeclaredField("flags").getInt(displayInfo);
|
||||
int dpi = cls.getDeclaredField("logicalDensityDpi").getInt(displayInfo);
|
||||
String uniqueId = (String) cls.getDeclaredField("uniqueId").get(displayInfo);
|
||||
String uniqueId;
|
||||
try {
|
||||
uniqueId = (String) cls.getDeclaredField("uniqueId").get(displayInfo);
|
||||
} catch (NoSuchFieldException e) {
|
||||
// This field might not exist: <https://github.com/Genymobile/scrcpy/issues/6461>
|
||||
uniqueId = null;
|
||||
}
|
||||
return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, dpi, uniqueId);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new AssertionError(e);
|
||||
|
||||
@@ -422,6 +422,56 @@ public class ControlMessageReaderTest {
|
||||
Assert.assertEquals(-1, bis.read()); // EOS
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseCameraSetTorch() throws IOException {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
dos.writeByte(ControlMessage.TYPE_CAMERA_SET_TORCH);
|
||||
dos.writeBoolean(true);
|
||||
byte[] packet = bos.toByteArray();
|
||||
|
||||
ByteArrayInputStream bis = new ByteArrayInputStream(packet);
|
||||
ControlMessageReader reader = new ControlMessageReader(bis);
|
||||
|
||||
ControlMessage event = reader.read();
|
||||
Assert.assertEquals(ControlMessage.TYPE_CAMERA_SET_TORCH, event.getType());
|
||||
Assert.assertTrue(event.getOn());
|
||||
|
||||
Assert.assertEquals(-1, bis.read()); // EOS
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseCameraZoomIn() throws IOException {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
dos.writeByte(ControlMessage.TYPE_CAMERA_ZOOM_IN);
|
||||
byte[] packet = bos.toByteArray();
|
||||
|
||||
ByteArrayInputStream bis = new ByteArrayInputStream(packet);
|
||||
ControlMessageReader reader = new ControlMessageReader(bis);
|
||||
|
||||
ControlMessage event = reader.read();
|
||||
Assert.assertEquals(ControlMessage.TYPE_CAMERA_ZOOM_IN, event.getType());
|
||||
|
||||
Assert.assertEquals(-1, bis.read()); // EOS
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseCameraZoomOut() throws IOException {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
dos.writeByte(ControlMessage.TYPE_CAMERA_ZOOM_OUT);
|
||||
byte[] packet = bos.toByteArray();
|
||||
|
||||
ByteArrayInputStream bis = new ByteArrayInputStream(packet);
|
||||
ControlMessageReader reader = new ControlMessageReader(bis);
|
||||
|
||||
ControlMessage event = reader.read();
|
||||
Assert.assertEquals(ControlMessage.TYPE_CAMERA_ZOOM_OUT, event.getType());
|
||||
|
||||
Assert.assertEquals(-1, bis.read()); // EOS
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultiEvents() throws IOException {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
|
||||
Reference in New Issue
Block a user