Compare commits

..

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

69 changed files with 648 additions and 652 deletions

View file

@ -84,7 +84,7 @@ jobs:
run: release/test_client.sh run: release/test_client.sh
build-linux-x86_64: build-linux-x86_64:
runs-on: ubuntu-22.04 runs-on: ubuntu-20.04
steps: steps:
- name: Check architecture - name: Check architecture
run: | run: |
@ -202,7 +202,8 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
brew install meson nasm libiconv zlib automake autoconf libtool brew install meson ninja nasm libiconv zlib automake autoconf \
libtool
- name: Build - name: Build
env: env:
@ -229,7 +230,7 @@ jobs:
path: release/work/build-macos-aarch64/dist-tar/ path: release/work/build-macos-aarch64/dist-tar/
build-macos-x86_64: build-macos-x86_64:
runs-on: macos-15-intel runs-on: macos-13
steps: steps:
- name: Check architecture - name: Check architecture
run: | run: |
@ -244,7 +245,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install dependencies - name: Install dependencies
run: brew install meson nasm libiconv zlib automake run: brew install meson ninja nasm libiconv zlib automake
# autoconf and libtool are already installed on macos-13 # autoconf and libtool are already installed on macos-13
- name: Build - name: Build

10
FAQ.md
View file

@ -141,13 +141,12 @@ On Windows, if `scrcpy --otg` (or `--keyboard=aoa`/`--mouse=aoa`) results in:
(or if only unrelated USB devices are detected), there might be drivers issues. (or if only unrelated USB devices are detected), there might be drivers issues.
Please read [#3654], in particular [this comment][#3654-comment1], [the next Please read [#3654], in particular [this comment][#3654-comment1] and [the next
one][#3654-comment2] and [this one][#3654-comment3]. one][#3654-comment2].
[#3654]: https://github.com/Genymobile/scrcpy/issues/3654 [#3654]: https://github.com/Genymobile/scrcpy/issues/3654
[#3654-comment1]: https://github.com/Genymobile/scrcpy/issues/3654#issuecomment-1369278232 [#3654-comment1]: https://github.com/Genymobile/scrcpy/issues/3654#issuecomment-1369278232
[#3654-comment2]: https://github.com/Genymobile/scrcpy/issues/3654#issuecomment-1369295011 [#3654-comment2]: https://github.com/Genymobile/scrcpy/issues/3654#issuecomment-1369295011
[#3654-comment3]: https://github.com/Genymobile/scrcpy/issues/3654#issuecomment-2613219725
## Control issues ## Control issues
@ -167,13 +166,14 @@ Rebooting the device is necessary once this option is set.
### Special characters do not work ### Special characters do not work
The default text injection method is limited to ASCII characters. A trick allows The default text injection method is [limited to ASCII characters][text-input].
to also inject some [accented characters][accented-characters], A trick allows to also inject some [accented characters][accented-characters],
but that's all. See [#37]. but that's all. See [#37].
To avoid the problem, [change the keyboard mode to simulate a physical To avoid the problem, [change the keyboard mode to simulate a physical
keyboard][hid]. keyboard][hid].
[text-input]: https://github.com/Genymobile/scrcpy/issues?q=is%3Aopen+is%3Aissue+label%3Aunicode
[accented-characters]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-accented-characters [accented-characters]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-accented-characters
[#37]: https://github.com/Genymobile/scrcpy/issues/37 [#37]: https://github.com/Genymobile/scrcpy/issues/37
[hid]: doc/keyboard.md#physical-keyboard-simulation [hid]: doc/keyboard.md#physical-keyboard-simulation

View file

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

View file

@ -2,7 +2,7 @@
source for the project. Do not download releases from random websites, even if source for the project. Do not download releases from random websites, even if
their name contains `scrcpy`.** their name contains `scrcpy`.**
# scrcpy (v3.3.4) # scrcpy (v3.2)
<img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" /> <img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />
@ -58,7 +58,7 @@ Make sure you [enabled USB debugging][enable-adb] on your device(s).
On some devices (especially Xiaomi), you might get the following error: On some devices (especially Xiaomi), you might get the following error:
``` ```
Injecting input events requires the caller (or the source of the instrumentation, if any) to have the INJECT_EVENTS permission. java.lang.SecurityException: Injecting input events requires the caller (or the source of the instrumentation, if any) to have the INJECT_EVENTS permission.
``` ```
In that case, you need to enable [an additional option][control] `USB debugging In that case, you need to enable [an additional option][control] `USB debugging
@ -207,10 +207,10 @@ work][donate]:
[donate]: https://blog.rom1v.com/about/#support-my-open-source-work [donate]: https://blog.rom1v.com/about/#support-my-open-source-work
## License ## Licence
Copyright (C) 2018 Genymobile Copyright (C) 2018 Genymobile
Copyright (C) 2018-2026 Romain Vimont Copyright (C) 2018-2025 Romain Vimont
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View file

@ -205,7 +205,6 @@ _scrcpy() {
|-p|--port \ |-p|--port \
|--push-target \ |--push-target \
|--rotation \ |--rotation \
|--screen-off-timeout \
|--tunnel-host \ |--tunnel-host \
|--tunnel-port \ |--tunnel-port \
|--v4l2-buffer \ |--v4l2-buffer \

View file

@ -1,4 +1,4 @@
#compdef scrcpy scrcpy.exe #compdef -N scrcpy -N scrcpy.exe
# #
# name: scrcpy # name: scrcpy
# auth: hltdev [hltdev8642@gmail.com] # auth: hltdev [hltdev8642@gmail.com]
@ -11,7 +11,7 @@ arguments=(
'--always-on-top[Make scrcpy window always on top \(above other windows\)]' '--always-on-top[Make scrcpy window always on top \(above other windows\)]'
'--angle=[Rotate the video content by a custom angle, in degrees]' '--angle=[Rotate the video content by a custom angle, in degrees]'
'--audio-bit-rate=[Encode the audio at the given bit-rate]' '--audio-bit-rate=[Encode the audio at the given bit-rate]'
'--audio-buffer=[Configure the audio buffering delay \(in milliseconds\)]' '--audio-buffer=[Configure the audio buffering delay (in milliseconds)]'
'--audio-codec=[Select the audio codec]:codec:(opus aac flac raw)' '--audio-codec=[Select the audio codec]:codec:(opus aac flac raw)'
'--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]' '--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]'
'--audio-dup=[Duplicate audio]' '--audio-dup=[Duplicate audio]'
@ -35,10 +35,10 @@ arguments=(
{-e,--select-tcpip}'[Use TCP/IP device]' {-e,--select-tcpip}'[Use TCP/IP device]'
{-f,--fullscreen}'[Start in fullscreen]' {-f,--fullscreen}'[Start in fullscreen]'
'--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]' '--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]'
'-G[Use UHID/AOA gamepad \(same as --gamepad=uhid or --gamepad=aoa, depending on OTG mode\)]' '-G[Use UHID/AOA gamepad (same as --gamepad=uhid or --gamepad=aoa, depending on OTG mode)]'
'--gamepad=[Set the gamepad input mode]:mode:(disabled uhid aoa)' '--gamepad=[Set the gamepad input mode]:mode:(disabled uhid aoa)'
{-h,--help}'[Print the help]' {-h,--help}'[Print the help]'
'-K[Use UHID/AOA keyboard \(same as --keyboard=uhid or --keyboard=aoa, depending on OTG mode\)]' '-K[Use UHID/AOA keyboard (same as --keyboard=uhid or --keyboard=aoa, depending on OTG mode)]'
'--keyboard=[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)' '--keyboard=[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)'
'--kill-adb-on-close[Kill adb when scrcpy terminates]' '--kill-adb-on-close[Kill adb when scrcpy terminates]'
'--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]' '--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]'
@ -48,7 +48,7 @@ arguments=(
'--list-displays[List displays available on the device]' '--list-displays[List displays available on the device]'
'--list-encoders[List video and audio encoders available on the device]' '--list-encoders[List video and audio encoders available on the device]'
{-m,--max-size=}'[Limit both the width and height of the video to value]' {-m,--max-size=}'[Limit both the width and height of the video to value]'
'-M[Use UHID/AOA mouse \(same as --mouse=uhid or --mouse=aoa, depending on OTG mode\)]' '-M[Use UHID/AOA mouse (same as --mouse=uhid or --mouse=aoa, depending on OTG mode)]'
'--max-fps=[Limit the frame rate of screen capture]' '--max-fps=[Limit the frame rate of screen capture]'
'--mouse=[Set the mouse input mode]:mode:(disabled sdk uhid aoa)' '--mouse=[Set the mouse input mode]:mode:(disabled sdk uhid aoa)'
'--mouse-bind=[Configure bindings of secondary clicks]' '--mouse-bind=[Configure bindings of secondary clicks]'

View file

@ -1,13 +1,13 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -ex set -ex
. $(dirname ${BASH_SOURCE[0]})/_init "$@" DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
VERSION=36.0.0 VERSION=35.0.2
URL="https://dl.google.com/android/repository/platform-tools_r$VERSION-linux.zip" FILENAME=platform-tools_r$VERSION-linux.zip
SHA256SUM=0ead642c943ffe79701fccca8f5f1c69c4ce4f43df2eefee553f6ccb27cbfbe8 PROJECT_DIR=platform-tools-$VERSION-linux
SHA256SUM=acfdcccb123a8718c46c46c059b2f621140194e5ec1ac9d81715be3d6ab6cd0a
PROJECT_DIR="platform-tools-$VERSION-linux"
FILENAME="$PROJECT_DIR.zip"
cd "$SOURCES_DIR" cd "$SOURCES_DIR"
@ -15,7 +15,7 @@ if [[ -d "$PROJECT_DIR" ]]
then then
echo "$PWD/$PROJECT_DIR" found echo "$PWD/$PROJECT_DIR" found
else else
get_file "$URL" "$FILENAME" "$SHA256SUM" get_file "https://dl.google.com/android/repository/$FILENAME" "$FILENAME" "$SHA256SUM"
mkdir -p "$PROJECT_DIR" mkdir -p "$PROJECT_DIR"
cd "$PROJECT_DIR" cd "$PROJECT_DIR"
ZIP_PREFIX=platform-tools ZIP_PREFIX=platform-tools

View file

@ -1,13 +1,13 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -ex set -ex
. $(dirname ${BASH_SOURCE[0]})/_init "$@" DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
VERSION=36.0.0 VERSION=35.0.2
URL="https://dl.google.com/android/repository/platform-tools_r$VERSION-darwin.zip" FILENAME=platform-tools_r$VERSION-darwin.zip
SHA256SUM=d3e9fa1df3345cf728586908426615a60863d2632f73f1ce14f0f1349ef000fd PROJECT_DIR=platform-tools-$VERSION-darwin
SHA256SUM=1820078db90bf21628d257ff052528af1c61bb48f754b3555648f5652fa35d78
PROJECT_DIR="platform-tools-$VERSION-darwin"
FILENAME="$PROJECT_DIR.zip"
cd "$SOURCES_DIR" cd "$SOURCES_DIR"
@ -15,7 +15,7 @@ if [[ -d "$PROJECT_DIR" ]]
then then
echo "$PWD/$PROJECT_DIR" found echo "$PWD/$PROJECT_DIR" found
else else
get_file "$URL" "$FILENAME" "$SHA256SUM" get_file "https://dl.google.com/android/repository/$FILENAME" "$FILENAME" "$SHA256SUM"
mkdir -p "$PROJECT_DIR" mkdir -p "$PROJECT_DIR"
cd "$PROJECT_DIR" cd "$PROJECT_DIR"
ZIP_PREFIX=platform-tools ZIP_PREFIX=platform-tools

View file

@ -1,13 +1,13 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -ex set -ex
. $(dirname ${BASH_SOURCE[0]})/_init "$@" DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
VERSION=36.0.0 VERSION=35.0.2
URL="https://dl.google.com/android/repository/platform-tools_r$VERSION-win.zip" FILENAME=platform-tools_r$VERSION-win.zip
SHA256SUM=12c2841f354e92a0eb2fd7bf6f0f9bf8538abce7bd6b060ac8349d6f6a61107c PROJECT_DIR=platform-tools-$VERSION-windows
SHA256SUM=2975a3eac0b19182748d64195375ad056986561d994fffbdc64332a516300bb9
PROJECT_DIR="platform-tools-$VERSION-windows"
FILENAME="$PROJECT_DIR.zip"
cd "$SOURCES_DIR" cd "$SOURCES_DIR"
@ -15,7 +15,7 @@ if [[ -d "$PROJECT_DIR" ]]
then then
echo "$PWD/$PROJECT_DIR" found echo "$PWD/$PROJECT_DIR" found
else else
get_file "$URL" "$FILENAME" "$SHA256SUM" get_file "https://dl.google.com/android/repository/$FILENAME" "$FILENAME" "$SHA256SUM"
mkdir -p "$PROJECT_DIR" mkdir -p "$PROJECT_DIR"
cd "$PROJECT_DIR" cd "$PROJECT_DIR"
ZIP_PREFIX=platform-tools ZIP_PREFIX=platform-tools

View file

@ -1,9 +1,10 @@
#!/usr/bin/env bash
# This file is intended to be sourced by other scripts, not executed # This file is intended to be sourced by other scripts, not executed
process_args() { process_args() {
if [[ $# != 3 ]] if [[ $# != 3 ]]
then then
# <host>: linux, macos, win32 or win64 # <host>: win32 or win64
# <build_type>: native or cross # <build_type>: native or cross
# <link_type>: static or shared # <link_type>: static or shared
echo "Syntax: $0 <host> <build_type> <link_type>" >&2 echo "Syntax: $0 <host> <build_type> <link_type>" >&2
@ -11,8 +12,8 @@ process_args() {
fi fi
HOST="$1" HOST="$1"
BUILD_TYPE="$2" BUILD_TYPE="$2" # native or cross
LINK_TYPE="$3" LINK_TYPE="$3" # static or shared
DIRNAME="$HOST-$BUILD_TYPE-$LINK_TYPE" DIRNAME="$HOST-$BUILD_TYPE-$LINK_TYPE"
if [[ "$BUILD_TYPE" != native && "$BUILD_TYPE" != cross ]] if [[ "$BUILD_TYPE" != native && "$BUILD_TYPE" != cross ]]

View file

@ -1,22 +1,22 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -ex set -ex
. $(dirname ${BASH_SOURCE[0]})/_init DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
process_args "$@" process_args "$@"
VERSION=1.5.0 VERSION=1.5.0
URL="https://code.videolan.org/videolan/dav1d/-/archive/$VERSION/dav1d-$VERSION.tar.gz" FILENAME=dav1d-$VERSION.tar.gz
PROJECT_DIR=dav1d-$VERSION
SHA256SUM=78b15d9954b513ea92d27f39362535ded2243e1b0924fde39f37a31ebed5f76b SHA256SUM=78b15d9954b513ea92d27f39362535ded2243e1b0924fde39f37a31ebed5f76b
PROJECT_DIR="dav1d-$VERSION"
FILENAME="$PROJECT_DIR.tar.gz"
cd "$SOURCES_DIR" cd "$SOURCES_DIR"
if [[ -d "$PROJECT_DIR" ]] if [[ -d "$PROJECT_DIR" ]]
then then
echo "$PWD/$PROJECT_DIR" found echo "$PWD/$PROJECT_DIR" found
else else
get_file "$URL" "$FILENAME" "$SHA256SUM" get_file "https://code.videolan.org/videolan/dav1d/-/archive/$VERSION/$FILENAME" "$FILENAME" "$SHA256SUM"
tar xf "$FILENAME" # First level directory is "$PROJECT_DIR" tar xf "$FILENAME" # First level directory is "$PROJECT_DIR"
fi fi

View file

@ -1,22 +1,22 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -ex set -ex
. $(dirname ${BASH_SOURCE[0]})/_init DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
process_args "$@" process_args "$@"
VERSION=7.1.1 VERSION=7.1.1
URL="https://ffmpeg.org/releases/ffmpeg-$VERSION.tar.xz" FILENAME=ffmpeg-$VERSION.tar.xz
PROJECT_DIR=ffmpeg-$VERSION
SHA256SUM=733984395e0dbbe5c046abda2dc49a5544e7e0e1e2366bba849222ae9e3a03b1 SHA256SUM=733984395e0dbbe5c046abda2dc49a5544e7e0e1e2366bba849222ae9e3a03b1
PROJECT_DIR="ffmpeg-$VERSION"
FILENAME="$PROJECT_DIR.tar.xz"
cd "$SOURCES_DIR" cd "$SOURCES_DIR"
if [[ -d "$PROJECT_DIR" ]] if [[ -d "$PROJECT_DIR" ]]
then then
echo "$PWD/$PROJECT_DIR" found echo "$PWD/$PROJECT_DIR" found
else else
get_file "$URL" "$FILENAME" "$SHA256SUM" get_file "https://ffmpeg.org/releases/$FILENAME" "$FILENAME" "$SHA256SUM"
tar xf "$FILENAME" # First level directory is "$PROJECT_DIR" tar xf "$FILENAME" # First level directory is "$PROJECT_DIR"
fi fi

View file

@ -1,14 +1,14 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -ex set -ex
. $(dirname ${BASH_SOURCE[0]})/_init DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
process_args "$@" process_args "$@"
VERSION=1.0.29 VERSION=1.0.28
URL="https://github.com/libusb/libusb/archive/refs/tags/v$VERSION.tar.gz" FILENAME=libusb-$VERSION.tar.gz
SHA256SUM=7c2dd39c0b2589236e48c93247c986ae272e27570942b4163cb00a060fcf1b74 PROJECT_DIR=libusb-$VERSION
SHA256SUM=378b3709a405065f8f9fb9f35e82d666defde4d342c2a1b181a9ac134d23c6fe
PROJECT_DIR="libusb-$VERSION"
FILENAME="$PROJECT_DIR.tar.gz"
cd "$SOURCES_DIR" cd "$SOURCES_DIR"
@ -16,7 +16,7 @@ if [[ -d "$PROJECT_DIR" ]]
then then
echo "$PWD/$PROJECT_DIR" found echo "$PWD/$PROJECT_DIR" found
else else
get_file "$URL" "$FILENAME" "$SHA256SUM" get_file "https://github.com/libusb/libusb/archive/refs/tags/v$VERSION.tar.gz" "$FILENAME" "$SHA256SUM"
tar xf "$FILENAME" # First level directory is "$PROJECT_DIR" tar xf "$FILENAME" # First level directory is "$PROJECT_DIR"
fi fi

View file

@ -0,0 +1,33 @@
From 6be87ceb33a9aad3bf5204bb13b3a5e8b498fd26 Mon Sep 17 00:00:00 2001
From: Neal Gompa <neal@gompa.dev>
Date: Mon, 10 Feb 2025 05:00:56 -0500
Subject: [PATCH] pipewire: Ensure that the correct struct is used for
enumeration APIs
PipeWire now requires the correct struct type is used, otherwise
it will fail to compile.
Reference: https://gitlab.freedesktop.org/pipewire/pipewire/-/commit/188d920733f0791413d3386e5536ee7377f71b2f
Fixes: https://github.com/libsdl-org/SDL/issues/12224
(cherry picked from commit d35bef64e913dd7d5dd3153a4b61f10ef837dad6)
---
src/audio/pipewire/SDL_pipewire.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/audio/pipewire/SDL_pipewire.c b/src/audio/pipewire/SDL_pipewire.c
index 889e05decb..5d1bfc28de 100644
--- a/src/audio/pipewire/SDL_pipewire.c
+++ b/src/audio/pipewire/SDL_pipewire.c
@@ -590,7 +590,7 @@ static void node_event_info(void *object, const struct pw_node_info *info)
/* Need to parse the parameters to get the sample rate */
for (i = 0; i < info->n_params; ++i) {
- pw_node_enum_params(node->proxy, 0, info->params[i].id, 0, 0, NULL);
+ pw_node_enum_params((struct pw_node*)node->proxy, 0, info->params[i].id, 0, 0, NULL);
}
hotplug_core_sync(node);
--
2.49.0

View file

@ -1,14 +1,14 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -ex set -ex
. $(dirname ${BASH_SOURCE[0]})/_init DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
process_args "$@" process_args "$@"
VERSION=2.32.8 VERSION=2.32.2
URL="https://github.com/libsdl-org/SDL/archive/refs/tags/release-$VERSION.tar.gz" FILENAME=SDL-$VERSION.tar.gz
SHA256SUM=dd35e05644ae527848d02433bec24dd0ea65db59faecf1a0e5d1880c533dac2c PROJECT_DIR=SDL-release-$VERSION
SHA256SUM=f2c7297ae7b3d3910a8b131e1e2a558fdd6d1a4443d5e345374d45cadfcb05a4
PROJECT_DIR="sdl-$VERSION"
FILENAME="$PROJECT_DIR.tar.gz"
cd "$SOURCES_DIR" cd "$SOURCES_DIR"
@ -16,9 +16,9 @@ if [[ -d "$PROJECT_DIR" ]]
then then
echo "$PWD/$PROJECT_DIR" found echo "$PWD/$PROJECT_DIR" found
else else
get_file "$URL" "$FILENAME" "$SHA256SUM" get_file "https://github.com/libsdl-org/SDL/archive/refs/tags/release-$VERSION.tar.gz" "$FILENAME" "$SHA256SUM"
tar xf "$FILENAME" # First level directory is "SDL-release-$VERSION" tar xf "$FILENAME" # First level directory is "$PROJECT_DIR"
mv "SDL-release-$VERSION" "$PROJECT_DIR" patch -d "$PROJECT_DIR" -p1 < "$PATCHES_DIR"/SDL-pipewire-Ensure-that-the-correct-struct-is-used-for-.patch
fi fi
mkdir -p "$BUILD_DIR/$PROJECT_DIR" mkdir -p "$BUILD_DIR/$PROJECT_DIR"
@ -29,7 +29,7 @@ export CXXFLAGS="$CFLAGS"
if [[ -d "$DIRNAME" ]] if [[ -d "$DIRNAME" ]]
then then
echo "'$PWD/$DIRNAME' already exists, not reconfigured" echo "'$PWD/$HDIRNAME' already exists, not reconfigured"
cd "$DIRNAME" cd "$DIRNAME"
else else
mkdir "$DIRNAME" mkdir "$DIRNAME"

View file

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

View file

@ -510,10 +510,6 @@ The device serial number. Mandatory only if several devices are connected to adb
.B \-S, \-\-turn\-screen\-off .B \-S, \-\-turn\-screen\-off
Turn the device screen off immediately. Turn the device screen off immediately.
.TP
.B "\-\-screen\-off\-timeout " seconds
Set the screen off timeout while scrcpy is running (restore the initial value on exit).
.TP .TP
.BI "\-\-shortcut\-mod " key\fR[+...]][,...] .BI "\-\-shortcut\-mod " key\fR[+...]][,...]
Specify the modifiers to use for scrcpy shortcuts. Possible keys are "lctrl", "rctrl", "lalt", "ralt", "lsuper" and "rsuper". Specify the modifiers to use for scrcpy shortcuts. Possible keys are "lctrl", "rctrl", "lalt", "ralt", "lsuper" and "rsuper".
@ -852,7 +848,7 @@ Report bugs to <https://github.com/Genymobile/scrcpy/issues>.
.SH COPYRIGHT .SH COPYRIGHT
Copyright \(co 2018 Genymobile <https://www.genymobile.com> Copyright \(co 2018 Genymobile <https://www.genymobile.com>
Copyright \(co 2018\-2026 Romain Vimont <rom@rom1v.com> Copyright \(co 2018\-2025 Romain Vimont <rom@rom1v.com>
Licensed under the Apache License, Version 2.0. Licensed under the Apache License, Version 2.0.

View file

@ -103,14 +103,14 @@ argv_to_string(const char *const *argv, char *buf, size_t bufsize) {
static void static void
show_adb_installation_msg(void) { show_adb_installation_msg(void) {
#ifndef _WIN32 #ifndef __WINDOWS__
static const struct { static const struct {
const char *binary; const char *binary;
const char *command; const char *command;
} pkg_managers[] = { } pkg_managers[] = {
{"apt", "apt install adb"}, {"apt", "apt install adb"},
{"apt-get", "apt-get install adb"}, {"apt-get", "apt-get install adb"},
{"brew", "brew install --cask android-platform-tools"}, {"brew", "brew cask install android-platform-tools"},
{"dnf", "dnf install android-tools"}, {"dnf", "dnf install android-tools"},
{"emerge", "emerge dev-util/android-tools"}, {"emerge", "emerge dev-util/android-tools"},
{"pacman", "pacman -S android-tools"}, {"pacman", "pacman -S android-tools"},
@ -331,7 +331,7 @@ sc_adb_reverse_remove(struct sc_intr *intr, const char *serial,
bool bool
sc_adb_push(struct sc_intr *intr, const char *serial, const char *local, sc_adb_push(struct sc_intr *intr, const char *serial, const char *local,
const char *remote, unsigned flags) { const char *remote, unsigned flags) {
#ifdef _WIN32 #ifdef __WINDOWS__
// Windows will parse the string, so the paths must be quoted // Windows will parse the string, so the paths must be quoted
// (see sys/win/command.c) // (see sys/win/command.c)
local = sc_str_quote(local); local = sc_str_quote(local);
@ -351,7 +351,7 @@ sc_adb_push(struct sc_intr *intr, const char *serial, const char *local,
sc_pid pid = sc_adb_execute(argv, flags); sc_pid pid = sc_adb_execute(argv, flags);
#ifdef _WIN32 #ifdef __WINDOWS__
free((void *) remote); free((void *) remote);
free((void *) local); free((void *) local);
#endif #endif
@ -362,7 +362,7 @@ sc_adb_push(struct sc_intr *intr, const char *serial, const char *local,
bool bool
sc_adb_install(struct sc_intr *intr, const char *serial, const char *local, sc_adb_install(struct sc_intr *intr, const char *serial, const char *local,
unsigned flags) { unsigned flags) {
#ifdef _WIN32 #ifdef __WINDOWS__
// Windows will parse the string, so the local name must be quoted // Windows will parse the string, so the local name must be quoted
// (see sys/win/command.c) // (see sys/win/command.c)
local = sc_str_quote(local); local = sc_str_quote(local);
@ -377,7 +377,7 @@ sc_adb_install(struct sc_intr *intr, const char *serial, const char *local,
sc_pid pid = sc_adb_execute(argv, flags); sc_pid pid = sc_adb_execute(argv, flags);
#ifdef _WIN32 #ifdef __WINDOWS__
free((void *) local); free((void *) local);
#endif #endif

View file

@ -75,14 +75,6 @@
# define SCRCPY_SDL_HAS_THREAD_PRIORITY_TIME_CRITICAL # define SCRCPY_SDL_HAS_THREAD_PRIORITY_TIME_CRITICAL
#endif #endif
#if SDL_VERSION_ATLEAST(2, 0, 18)
# define SCRCPY_SDL_HAS_HINT_APP_NAME
#endif
#if SDL_VERSION_ATLEAST(2, 0, 14)
# define SCRCPY_SDL_HAS_HINT_AUDIO_DEVICE_APP_NAME
#endif
#ifndef HAVE_STRDUP #ifndef HAVE_STRDUP
char *strdup(const char *s); char *strdup(const char *s);
#endif #endif

View file

@ -127,14 +127,10 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) {
return 32; return 32;
case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT: case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT:
write_position(&buf[1], &msg->inject_scroll_event.position); write_position(&buf[1], &msg->inject_scroll_event.position);
// Accept values in the range [-16, 16]. int16_t hscroll =
// Normalize to [-1, 1] in order to use sc_float_to_i16fp(). sc_float_to_i16fp(msg->inject_scroll_event.hscroll);
float hscroll_norm = msg->inject_scroll_event.hscroll / 16; int16_t vscroll =
hscroll_norm = CLAMP(hscroll_norm, -1, 1); sc_float_to_i16fp(msg->inject_scroll_event.vscroll);
float vscroll_norm = msg->inject_scroll_event.vscroll / 16;
vscroll_norm = CLAMP(vscroll_norm, -1, 1);
int16_t hscroll = sc_float_to_i16fp(hscroll_norm);
int16_t vscroll = sc_float_to_i16fp(vscroll_norm);
sc_write16be(&buf[13], (uint16_t) hscroll); sc_write16be(&buf[13], (uint16_t) hscroll);
sc_write16be(&buf[15], (uint16_t) vscroll); sc_write16be(&buf[15], (uint16_t) vscroll);
sc_write32be(&buf[17], msg->inject_scroll_event.buttons); sc_write32be(&buf[17], msg->inject_scroll_event.buttons);

View file

@ -53,7 +53,7 @@ sc_device_msg_deserialize(const uint8_t *buf, size_t len,
} }
uint16_t id = sc_read16be(&buf[1]); uint16_t id = sc_read16be(&buf[1]);
size_t size = sc_read16be(&buf[3]); size_t size = sc_read16be(&buf[3]);
if (size > len - 5) { if (size < len - 5) {
return 0; // not available return 0; // not available
} }
uint8_t *data = malloc(size); uint8_t *data = malloc(size);

View file

@ -170,7 +170,6 @@ sc_display_set_pending_frame(struct sc_display *display, const AVFrame *frame) {
} }
} }
av_frame_unref(display->pending.frame);
int r = av_frame_ref(display->pending.frame, frame); int r = av_frame_ref(display->pending.frame, frame);
if (r) { if (r) {
LOGE("Could not ref frame: %d", r); LOGE("Could not ref frame: %d", r);
@ -182,11 +181,6 @@ sc_display_set_pending_frame(struct sc_display *display, const AVFrame *frame) {
return true; return true;
} }
// Forward declaration
static bool
sc_display_update_texture_internal(struct sc_display *display,
const AVFrame *frame);
static bool static bool
sc_display_apply_pending(struct sc_display *display) { sc_display_apply_pending(struct sc_display *display) {
if (display->pending.flags & SC_DISPLAY_PENDING_FLAG_SIZE) { if (display->pending.flags & SC_DISPLAY_PENDING_FLAG_SIZE) {
@ -202,8 +196,7 @@ sc_display_apply_pending(struct sc_display *display) {
if (display->pending.flags & SC_DISPLAY_PENDING_FLAG_FRAME) { if (display->pending.flags & SC_DISPLAY_PENDING_FLAG_FRAME) {
assert(display->pending.frame); assert(display->pending.frame);
bool ok = sc_display_update_texture_internal(display, bool ok = sc_display_update_texture(display, display->pending.frame);
display->pending.frame);
if (!ok) { if (!ok) {
return false; return false;
} }

View file

@ -22,7 +22,7 @@ struct sc_display {
struct sc_opengl gl; struct sc_opengl gl;
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE #ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
SDL_GLContext gl_context; SDL_GLContext *gl_context;
#endif #endif
bool mipmaps; bool mipmaps;

View file

@ -3,8 +3,8 @@
#include <stdint.h> #include <stdint.h>
// 1 byte for buttons + padding, 1 byte for X position, 1 byte for Y position, // 1 byte for buttons + padding, 1 byte for X position, 1 byte for Y position,
// 1 byte for wheel motion, 1 byte for hozizontal scrolling // 1 byte for wheel motion
#define SC_HID_MOUSE_INPUT_SIZE 5 #define SC_HID_MOUSE_INPUT_SIZE 4
/** /**
* Mouse descriptor from the specification: * Mouse descriptor from the specification:
@ -75,21 +75,6 @@ static const uint8_t SC_HID_MOUSE_REPORT_DESC[] = {
// Input (Data, Variable, Relative): 3 position bytes (X, Y, Wheel) // Input (Data, Variable, Relative): 3 position bytes (X, Y, Wheel)
0x81, 0x06, 0x81, 0x06,
// Usage Page (Consumer Page)
0x05, 0x0C,
// Usage(AC Pan)
0x0A, 0x38, 0x02,
// Logical Minimum (-127)
0x15, 0x81,
// Logical Maximum (127)
0x25, 0x7F,
// Report Size (8)
0x75, 0x08,
// Report Count (1)
0x95, 0x01,
// Input (Data, Variable, Relative): 1 byte (AC Pan)
0x81, 0x06,
// End Collection // End Collection
0xC0, 0xC0,
@ -175,8 +160,7 @@ sc_hid_mouse_generate_input_from_motion(struct sc_hid_input *hid_input,
data[0] = sc_hid_buttons_from_buttons_state(event->buttons_state); data[0] = sc_hid_buttons_from_buttons_state(event->buttons_state);
data[1] = CLAMP(event->xrel, -127, 127); data[1] = CLAMP(event->xrel, -127, 127);
data[2] = CLAMP(event->yrel, -127, 127); data[2] = CLAMP(event->yrel, -127, 127);
data[3] = 0; // no vertical scrolling data[3] = 0; // wheel coordinates only used for scrolling
data[4] = 0; // no horizontal scrolling
} }
void void
@ -188,27 +172,22 @@ sc_hid_mouse_generate_input_from_click(struct sc_hid_input *hid_input,
data[0] = sc_hid_buttons_from_buttons_state(event->buttons_state); data[0] = sc_hid_buttons_from_buttons_state(event->buttons_state);
data[1] = 0; // no x motion data[1] = 0; // no x motion
data[2] = 0; // no y motion data[2] = 0; // no y motion
data[3] = 0; // no vertical scrolling data[3] = 0; // wheel coordinates only used for scrolling
data[4] = 0; // no horizontal scrolling
} }
bool void
sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input, sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input,
const struct sc_mouse_scroll_event *event) { const struct sc_mouse_scroll_event *event) {
if (!event->vscroll_int && !event->hscroll_int) {
// Need a full integral value for HID
return false;
}
sc_hid_mouse_input_init(hid_input); sc_hid_mouse_input_init(hid_input);
uint8_t *data = hid_input->data; uint8_t *data = hid_input->data;
data[0] = 0; // buttons state irrelevant (and unknown) data[0] = 0; // buttons state irrelevant (and unknown)
data[1] = 0; // no x motion data[1] = 0; // no x motion
data[2] = 0; // no y motion data[2] = 0; // no y motion
data[3] = CLAMP(event->vscroll_int, -127, 127); // In practice, vscroll is always -1, 0 or 1, but in theory other values
data[4] = CLAMP(event->hscroll_int, -127, 127); // are possible
return true; data[3] = CLAMP(event->vscroll, -127, 127);
// Horizontal scrolling ignored
} }
void sc_hid_mouse_generate_open(struct sc_hid_open *hid_open) { void sc_hid_mouse_generate_open(struct sc_hid_open *hid_open) {

View file

@ -22,7 +22,7 @@ void
sc_hid_mouse_generate_input_from_click(struct sc_hid_input *hid_input, sc_hid_mouse_generate_input_from_click(struct sc_hid_input *hid_input,
const struct sc_mouse_click_event *event); const struct sc_mouse_click_event *event);
bool void
sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input, sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input,
const struct sc_mouse_scroll_event *event); const struct sc_mouse_scroll_event *event);

View file

@ -393,8 +393,6 @@ struct sc_mouse_scroll_event {
struct sc_position position; struct sc_position position;
float hscroll; float hscroll;
float vscroll; float vscroll;
int32_t hscroll_int;
int32_t vscroll_int;
uint8_t buttons_state; // bitwise-OR of sc_mouse_button values uint8_t buttons_state; // bitwise-OR of sc_mouse_button values
}; };

View file

@ -897,14 +897,12 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im,
struct sc_mouse_scroll_event evt = { struct sc_mouse_scroll_event evt = {
.position = sc_input_manager_get_position(im, mouse_x, mouse_y), .position = sc_input_manager_get_position(im, mouse_x, mouse_y),
#if SDL_VERSION_ATLEAST(2, 0, 18) #if SDL_VERSION_ATLEAST(2, 0, 18)
.hscroll = event->preciseX, .hscroll = CLAMP(event->preciseX, -1.0f, 1.0f),
.vscroll = event->preciseY, .vscroll = CLAMP(event->preciseY, -1.0f, 1.0f),
#else #else
.hscroll = event->x, .hscroll = CLAMP(event->x, -1, 1),
.vscroll = event->y, .vscroll = CLAMP(event->y, -1, 1),
#endif #endif
.hscroll_int = event->x,
.vscroll_int = event->y,
.buttons_state = im->mouse_buttons_state, .buttons_state = im->mouse_buttons_state,
}; };

View file

@ -1,7 +1,6 @@
#include "common.h" #include "common.h"
#include <stdbool.h> #include <stdbool.h>
#include <stdio.h>
#ifdef HAVE_V4L2 #ifdef HAVE_V4L2
# include <libavdevice/avdevice.h> # include <libavdevice/avdevice.h>
#endif #endif

View file

@ -93,7 +93,7 @@ struct scrcpy {
#ifdef _WIN32 #ifdef _WIN32
static BOOL WINAPI windows_ctrl_handler(DWORD ctrl_type) { static BOOL WINAPI windows_ctrl_handler(DWORD ctrl_type) {
if (ctrl_type == CTRL_C_EVENT || ctrl_type == CTRL_BREAK_EVENT) { if (ctrl_type == CTRL_C_EVENT) {
sc_push_event(SDL_QUIT); sc_push_event(SDL_QUIT);
return TRUE; return TRUE;
} }
@ -107,17 +107,6 @@ sdl_set_hints(const char *render_driver) {
LOGW("Could not set render driver"); LOGW("Could not set render driver");
} }
// App name used in various contexts (such as PulseAudio)
#if defined(SCRCPY_SDL_HAS_HINT_APP_NAME)
if (!SDL_SetHint(SDL_HINT_APP_NAME, "scrcpy")) {
LOGW("Could not set app name");
}
#elif defined(SCRCPY_SDL_HAS_HINT_AUDIO_DEVICE_APP_NAME)
if (!SDL_SetHint(SDL_HINT_AUDIO_DEVICE_APP_NAME, "scrcpy")) {
LOGW("Could not set audio device app name");
}
#endif
// Linear filtering // Linear filtering
if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1")) { if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1")) {
LOGW("Could not enable linear filtering"); LOGW("Could not enable linear filtering");
@ -176,7 +165,7 @@ sdl_configure(bool video_playback, bool disable_screensaver) {
} }
static enum scrcpy_exit_code static enum scrcpy_exit_code
event_loop(struct scrcpy *s, bool has_screen) { event_loop(struct scrcpy *s) {
SDL_Event event; SDL_Event event;
while (SDL_WaitEvent(&event)) { while (SDL_WaitEvent(&event)) {
switch (event.type) { switch (event.type) {
@ -208,7 +197,7 @@ event_loop(struct scrcpy *s, bool has_screen) {
break; break;
} }
default: default:
if (has_screen && !sc_screen_handle_event(&s->screen, &event)) { if (!sc_screen_handle_event(&s->screen, &event)) {
return SCRCPY_EXIT_FAILURE; return SCRCPY_EXIT_FAILURE;
} }
break; break;
@ -944,7 +933,7 @@ aoa_complete:
} }
} }
ret = event_loop(s, options->window); ret = event_loop(s);
terminate_event_loop(); terminate_event_loop();
LOGD("quit..."); LOGD("quit...");

View file

@ -225,7 +225,7 @@ sc_screen_render_novideo(struct sc_screen *screen) {
(void) res; // any error already logged (void) res; // any error already logged
} }
#if defined(__APPLE__) || defined(_WIN32) #if defined(__APPLE__) || defined(__WINDOWS__)
# define CONTINUOUS_RESIZING_WORKAROUND # define CONTINUOUS_RESIZING_WORKAROUND
#endif #endif
@ -409,7 +409,7 @@ sc_screen_init(struct sc_screen *screen,
} else { } else {
// without video, the icon is used as window content, it must be present // without video, the icon is used as window content, it must be present
LOGE("Could not load icon"); LOGE("Could not load icon");
goto error_destroy_window; goto error_destroy_fps_counter;
} }
SDL_Surface *icon_novideo = params->video ? NULL : icon; SDL_Surface *icon_novideo = params->video ? NULL : icon;

View file

@ -55,9 +55,7 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp,
struct sc_mouse_uhid *mouse = DOWNCAST(mp); struct sc_mouse_uhid *mouse = DOWNCAST(mp);
struct sc_hid_input hid_input; struct sc_hid_input hid_input;
if (!sc_hid_mouse_generate_input_from_scroll(&hid_input, event)) { sc_hid_mouse_generate_input_from_scroll(&hid_input, event);
return;
}
sc_mouse_uhid_send_input(mouse, &hid_input, "mouse scroll"); sc_mouse_uhid_send_input(mouse, &hid_input, "mouse scroll");
} }

View file

@ -42,9 +42,7 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp,
struct sc_mouse_aoa *mouse = DOWNCAST(mp); struct sc_mouse_aoa *mouse = DOWNCAST(mp);
struct sc_hid_input hid_input; struct sc_hid_input hid_input;
if (!sc_hid_mouse_generate_input_from_scroll(&hid_input, event)) { sc_hid_mouse_generate_input_from_scroll(&hid_input, event);
return;
}
if (!sc_aoa_push_input(mouse->aoa, &hid_input)) { if (!sc_aoa_push_input(mouse->aoa, &hid_input)) {
LOGW("Could not push AOA HID input (mouse scroll)"); LOGW("Could not push AOA HID input (mouse scroll)");

View file

@ -164,15 +164,8 @@ sc_screen_otg_process_mouse_wheel(struct sc_screen_otg *screen,
struct sc_mouse_scroll_event evt = { struct sc_mouse_scroll_event evt = {
// .position not used for HID events // .position not used for HID events
#if SDL_VERSION_ATLEAST(2, 0, 18)
.hscroll = event->preciseX,
.vscroll = event->preciseY,
#else
.hscroll = event->x, .hscroll = event->x,
.vscroll = event->y, .vscroll = event->y,
#endif
.hscroll_int = event->x,
.vscroll_int = event->y,
.buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state), .buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state),
}; };

View file

@ -2,7 +2,6 @@
#include <assert.h> #include <assert.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h>
#ifdef _WIN32 #ifdef _WIN32
# include <ws2tcpip.h> # include <ws2tcpip.h>

View file

@ -191,8 +191,7 @@ sc_vecdeque_reallocdata_(void *ptr, size_t newcap, size_t item_size,
size_t right_len = MIN(size, oldcap - oldorigin); size_t right_len = MIN(size, oldcap - oldorigin);
assert(right_len); assert(right_len);
memcpy(newptr, (char *) ptr + (oldorigin * item_size), memcpy(newptr, (char *) ptr + (oldorigin * item_size), right_len * item_size);
right_len * item_size);
if (size > right_len) { if (size > right_len) {
memcpy((char *) newptr + (right_len * item_size), ptr, memcpy((char *) newptr + (right_len * item_size), ptr,

View file

@ -127,8 +127,8 @@ static void test_serialize_inject_scroll_event(void) {
.height = 1920, .height = 1920,
}, },
}, },
.hscroll = 16, .hscroll = 1,
.vscroll = -16, .vscroll = -1,
.buttons = 1, .buttons = 1,
}, },
}; };
@ -141,8 +141,8 @@ static void test_serialize_inject_scroll_event(void) {
SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT,
0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026
0x04, 0x38, 0x07, 0x80, // 1080 1920 0x04, 0x38, 0x07, 0x80, // 1080 1920
0x7F, 0xFF, // 16 (float encoded as i16 in the range [-16, 16]) 0x7F, 0xFF, // 1 (float encoded as i16)
0x80, 0x00, // -16 (float encoded as i16 in the range [-16, 16]) 0x80, 0x00, // -1 (float encoded as i16)
0x00, 0x00, 0x00, 0x01, // 1 0x00, 0x00, 0x00, 0x01, // 1
}; };
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
@ -411,26 +411,6 @@ static void test_serialize_open_hard_keyboard(void) {
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
} }
static void test_serialize_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) { static void test_serialize_reset_video(void) {
struct sc_control_msg msg = { struct sc_control_msg msg = {
.type = SC_CONTROL_MSG_TYPE_RESET_VIDEO, .type = SC_CONTROL_MSG_TYPE_RESET_VIDEO,
@ -468,7 +448,6 @@ int main(int argc, char *argv[]) {
test_serialize_uhid_input(); test_serialize_uhid_input();
test_serialize_uhid_destroy(); test_serialize_uhid_destroy();
test_serialize_open_hard_keyboard(); test_serialize_open_hard_keyboard();
test_serialize_start_app();
test_serialize_reset_video(); test_serialize_reset_video();
return 0; return 0;
} }

View file

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

View file

@ -154,11 +154,7 @@ install it manually and make it available from the `PATH`:
export PATH="$JAVA_HOME/bin:$PATH" export PATH="$JAVA_HOME/bin:$PATH"
``` ```
When following the rest of the build instructions below, make sure you use the ### Mac OS
MinGW terminal within MSYS2.
### macOS
Install the packages with [Homebrew]: Install the packages with [Homebrew]:
@ -176,7 +172,8 @@ Additionally, if you want to build the server, install Java 17 from Caskroom, an
make it available from the `PATH`: make it available from the `PATH`:
```bash ```bash
brew install openjdk@17 brew tap homebrew/cask-versions
brew install adoptopenjdk/openjdk/adoptopenjdk17
export JAVA_HOME="$(/usr/libexec/java_home --version 1.17)" export JAVA_HOME="$(/usr/libexec/java_home --version 1.17)"
export PATH="$JAVA_HOME/bin:$PATH" export PATH="$JAVA_HOME/bin:$PATH"
``` ```
@ -236,10 +233,10 @@ install` must be run as root)._
#### Option 2: Use prebuilt server #### Option 2: Use prebuilt server
- [`scrcpy-server-v3.3.4`][direct-scrcpy-server] - [`scrcpy-server-v3.2`][direct-scrcpy-server]
<sub>SHA-256: `8588238c9a5a00aa542906b6ec7e6d5541d9ffb9b5d0f6e1bc0e365e2303079e`</sub> <sub>SHA-256: `b920e0ea01936bf2482f4ba2fa985c22c13c621999e3d33b45baa5acfc1ea3d0`</sub>
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-server-v3.3.4 [direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-server-v3.2
Download the prebuilt server somewhere, and specify its path during the Meson Download the prebuilt server somewhere, and specify its path during the Meson
configuration: configuration:
@ -274,7 +271,7 @@ This installs several files:
- `/usr/local/bin/scrcpy` (main app) - `/usr/local/bin/scrcpy` (main app)
- `/usr/local/share/scrcpy/scrcpy-server` (server to push to the device) - `/usr/local/share/scrcpy/scrcpy-server` (server to push to the device)
- `/usr/local/share/man/man1/scrcpy.1` (manpage) - `/usr/local/share/man/man1/scrcpy.1` (manpage)
- `/usr/local/share/icons/hicolor/256x256/apps/scrcpy.png` (app icon) - `/usr/local/share/icons/hicolor/256x256/apps/icon.png` (app icon)
- `/usr/local/share/zsh/site-functions/_scrcpy` (zsh completion) - `/usr/local/share/zsh/site-functions/_scrcpy` (zsh completion)
- `/usr/local/share/bash-completion/completions/scrcpy` (bash completion) - `/usr/local/share/bash-completion/completions/scrcpy` (bash completion)

View file

@ -409,8 +409,8 @@ with any client which uses the same protocol.
For simplicity, some [server-specific options] have been added to produce raw For simplicity, some [server-specific options] have been added to produce raw
streams easily: streams easily:
- `send_device_meta=false`: disable the device metadata (in practice, the - `send_device_meta=false`: disable the device metata (in practice, the device
device name) sent on the _first_ socket name) sent on the _first_ socket
- `send_frame_meta=false`: disable the 12-byte header for each packet - `send_frame_meta=false`: disable the 12-byte header for each packet
- `send_dummy_byte`: disable the dummy byte sent on forward connections - `send_dummy_byte`: disable the dummy byte sent on forward connections
- `send_codec_meta`: disable the codec information (and initial device size for - `send_codec_meta`: disable the codec information (and initial device size for

View file

@ -6,11 +6,11 @@
Download a static build of the [latest release]: Download a static build of the [latest release]:
- [`scrcpy-linux-x86_64-v3.3.4.tar.gz`][direct-linux-x86_64] (x86_64) - [`scrcpy-linux-x86_64-v3.2.tar.gz`][direct-linux-x86_64] (x86_64)
<sub>SHA-256: `0305d98c06178c67e12427bbf340c436d0d58c9e2a39bf9ffbbf8f54d7ef95a5`</sub> <sub>SHA-256: `df6cf000447428fcde322022848d655ff0211d98688d0f17cbbf21be9c1272be`</sub>
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest [latest release]: https://github.com/Genymobile/scrcpy/releases/latest
[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-linux-x86_64-v3.3.4.tar.gz [direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-linux-x86_64-v3.2.tar.gz
and extract it. and extract it.
@ -27,7 +27,7 @@ Scrcpy is packaged in several distributions and package managers:
- Arch Linux: `pacman -S scrcpy` - Arch Linux: `pacman -S scrcpy`
- Fedora: `dnf copr enable zeno/scrcpy && dnf install scrcpy` - Fedora: `dnf copr enable zeno/scrcpy && dnf install scrcpy`
- Gentoo: `emerge scrcpy` - Gentoo: `emerge scrcpy`
- Snap: ~~`snap install scrcpy`~~ _(obsolete version)_ - Snap: `snap install scrcpy`
- … (see [repology](https://repology.org/project/scrcpy/versions)) - … (see [repology](https://repology.org/project/scrcpy/versions))

View file

@ -6,14 +6,15 @@
Download a static build of the [latest release]: Download a static build of the [latest release]:
- [`scrcpy-macos-aarch64-v3.3.4.tar.gz`][direct-macos-aarch64] (aarch64) - [`scrcpy-macos-aarch64-v3.2.tar.gz`][direct-macos-aarch64] (aarch64)
<sub>SHA-256: `8fef43520405dd523c74e1530ac68febcc5a405ea89712c874936675da8513dd`</sub> <sub>SHA-256: `f6d1f3c5f74d4d46f5080baa5b56b69f5edbf698d47e0cf4e2a1fd5058f9507b`</sub>
- [`scrcpy-macos-x86_64-v3.3.4.tar.gz`][direct-macos-x86_64] (x86_64)
<sub>SHA-256: `cf9b3453a33279b6009dfb256b1a84c374bd4c30a71edd74bacab28d72a5d929`</sub> - [`scrcpy-macos-x86_64-v3.2.tar.gz`][direct-macos-x86_64] (x86_64)
<sub>SHA-256: `e337d5cf0ba4e1281699c338ce5f104aee96eb7b2893dc851399b6643eb4044e`</sub>
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest [latest release]: https://github.com/Genymobile/scrcpy/releases/latest
[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-macos-aarch64-v3.3.4.tar.gz [direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-macos-aarch64-v3.2.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 [direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-macos-x86_64-v3.2.tar.gz
and extract it. and extract it.

View file

@ -24,7 +24,7 @@ to create several devices or devices with specific IDs).
If you encounter problems detecting your device with Chrome/WebRTC, you can try If you encounter problems detecting your device with Chrome/WebRTC, you can try
`exclusive_caps` mode: `exclusive_caps` mode:
```bash ```
sudo modprobe v4l2loopback exclusive_caps=1 sudo modprobe v4l2loopback exclusive_caps=1
``` ```
@ -38,13 +38,6 @@ v4l2-ctl --list-devices
ls /dev/video* ls /dev/video*
``` ```
If a loopback device was not created automatically, you can make a new one:
```bash
# requires v4l2loopback-utils package
sudo v4l2loopback-ctl add
```
To start `scrcpy` using a v4l2 sink: To start `scrcpy` using a v4l2 sink:
```bash ```bash

View file

@ -6,14 +6,14 @@
Download the [latest release]: Download the [latest release]:
- [`scrcpy-win64-v3.3.4.zip`][direct-win64] (64-bit) - [`scrcpy-win64-v3.2.zip`][direct-win64] (64-bit)
<sub>SHA-256: `d8a155b7c180b7ca4cdadd40712b8750b63f3aab48cb5b8a2a39ac2d0d4c5d38`</sub> <sub>SHA-256: `eaa27133e0520979873ba57ad651560a4cc2618373bd05450b23a84d32beafd0`</sub>
- [`scrcpy-win32-v3.3.4.zip`][direct-win32] (32-bit) - [`scrcpy-win32-v3.2.zip`][direct-win32] (32-bit)
<sub>SHA-256: `393f7d5379dabd8aacc41184755c3d0df975cd2861353cb7a8d50e0835e2eb72`</sub> <sub>SHA-256: `4a3407d7f0c2c8a03e22a12cf0b5e1e585a5056fe23c8e5cf3252207c6fa8357`</sub>
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest [latest release]: https://github.com/Genymobile/scrcpy/releases/latest
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-win64-v3.3.4.zip [direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-win64-v3.2.zip
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-win32-v3.3.4.zip [direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-win32-v3.2.zip
and extract it. and extract it.

View file

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

View file

@ -2,8 +2,8 @@
set -e set -e
BUILDDIR=build-auto BUILDDIR=build-auto
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-server-v3.3.4 PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-server-v3.2
PREBUILT_SERVER_SHA256=8588238c9a5a00aa542906b6ec7e6d5541d9ffb9b5d0f6e1bc0e365e2303079e PREBUILT_SERVER_SHA256=b920e0ea01936bf2482f4ba2fa985c22c13c621999e3d33b45baa5acfc1ea3d0
echo "[scrcpy] Downloading prebuilt server..." echo "[scrcpy] Downloading prebuilt server..."
wget "$PREBUILT_SERVER_URL" -O scrcpy-server wget "$PREBUILT_SERVER_URL" -O scrcpy-server

View file

@ -1,5 +1,5 @@
project('scrcpy', 'c', project('scrcpy', 'c',
version: '3.3.4', version: '3.2',
meson_version: '>= 0.49', meson_version: '>= 0.49',
default_options: [ default_options: [
'c_std=c11', 'c_std=c11',

View file

@ -1,15 +1,15 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
android { android {
namespace = 'com.genymobile.scrcpy' namespace 'com.genymobile.scrcpy'
compileSdk 36 compileSdk 35
defaultConfig { defaultConfig {
applicationId = "com.genymobile.scrcpy" applicationId "com.genymobile.scrcpy"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 36 targetSdkVersion 35
versionCode 30304 versionCode 30200
versionName "3.3.4" versionName "3.2"
testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
} }
buildTypes { buildTypes {
release { release {
@ -18,11 +18,8 @@ android {
} }
} }
buildFeatures { buildFeatures {
buildConfig = true buildConfig true
aidl = true aidl true
}
lint {
disable 'UseRequiresApi'
} }
} }

View file

@ -12,10 +12,10 @@
set -e set -e
SCRCPY_DEBUG=false SCRCPY_DEBUG=false
SCRCPY_VERSION_NAME=3.3.4 SCRCPY_VERSION_NAME=3.2
PLATFORM=${ANDROID_PLATFORM:-36} PLATFORM=${ANDROID_PLATFORM:-35}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-36.0.0} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.0.0}
PLATFORM_TOOLS="$ANDROID_HOME/platforms/android-$PLATFORM" PLATFORM_TOOLS="$ANDROID_HOME/platforms/android-$PLATFORM"
BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS" BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS"
@ -86,7 +86,7 @@ javac -encoding UTF-8 -bootclasspath "$ANDROID_JAR" \
echo "Dexing..." echo "Dexing..."
cd "$CLASSES_DIR" cd "$CLASSES_DIR"
if [[ "${PLATFORM%%.*}" -lt 31 ]] if [[ $PLATFORM -lt 31 ]]
then then
# use dx # use dx
"$BUILD_TOOLS_DIR/dx" --dex --output "$BUILD_DIR/classes.dex" \ "$BUILD_TOOLS_DIR/dx" --dex --output "$BUILD_DIR/classes.dex" \

View file

@ -48,4 +48,19 @@ oneway interface IDisplayWindowListener {
* Called when a display is removed from the hierarchy. * Called when a display is removed from the hierarchy.
*/ */
void onDisplayRemoved(int displayId); void onDisplayRemoved(int displayId);
/**
* Called when fixed rotation is started on a display.
*/
void onFixedRotationStarted(int displayId, int newRotation);
/**
* Called when the previous fixed rotation on a display is finished.
*/
void onFixedRotationFinished(int displayId);
/**
* Called when the keep clear ares on a display have changed.
*/
void onKeepClearAreasChanged(int displayId, in List<Rect> restricted, in List<Rect> unrestricted);
} }

View file

@ -7,7 +7,6 @@ import com.genymobile.scrcpy.util.SettingsException;
import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.os.BatteryManager; import android.os.BatteryManager;
import android.os.Looper;
import android.system.ErrnoException; import android.system.ErrnoException;
import android.system.Os; import android.system.Os;
@ -180,11 +179,6 @@ public final class CleanUp {
} }
} }
@SuppressWarnings("deprecation")
private static void prepareMainLooper() {
Looper.prepareMainLooper();
}
public static void main(String... args) { public static void main(String... args) {
try { try {
// Start a new session to avoid being terminated along with the server process on some devices // Start a new session to avoid being terminated along with the server process on some devices
@ -194,10 +188,6 @@ public final class CleanUp {
} }
unlinkSelf(); unlinkSelf();
// Needed for workarounds
prepareMainLooper();
Workarounds.apply();
int displayId = Integer.parseInt(args[0]); int displayId = Integer.parseInt(args[0]);
int restoreStayOn = Integer.parseInt(args[1]); int restoreStayOn = Integer.parseInt(args[1]);
boolean disableShowTouches = Boolean.parseBoolean(args[2]); boolean disableShowTouches = Boolean.parseBoolean(args[2]);

View file

@ -2,7 +2,6 @@ package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.annotation.SuppressLint;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.AttributionSource; import android.content.AttributionSource;
import android.content.ContentResolver; import android.content.ContentResolver;
@ -12,8 +11,6 @@ import android.content.IContentProvider;
import android.os.Binder; import android.os.Binder;
import android.os.Process; import android.os.Process;
import java.lang.reflect.Field;
public final class FakeContext extends ContextWrapper { public final class FakeContext extends ContextWrapper {
public static final String PACKAGE_NAME = "com.android.shell"; public static final String PACKAGE_NAME = "com.android.shell";
@ -75,7 +72,7 @@ public final class FakeContext extends ContextWrapper {
@Override @Override
public AttributionSource getAttributionSource() { public AttributionSource getAttributionSource() {
AttributionSource.Builder builder = new AttributionSource.Builder(Process.SHELL_UID); AttributionSource.Builder builder = new AttributionSource.Builder(Process.SHELL_UID);
builder.setPackageName(PACKAGE_NAME); builder.setPackageName("shell");
return builder.build(); return builder.build();
} }
@ -90,38 +87,8 @@ public final class FakeContext extends ContextWrapper {
return this; return this;
} }
@Override
public Context createPackageContext(String packageName, int flags) {
return this;
}
@Override @Override
public ContentResolver getContentResolver() { public ContentResolver getContentResolver() {
return contentResolver; return contentResolver;
} }
@SuppressLint("SoonBlockedPrivateApi")
@Override
public Object getSystemService(String name) {
Object service = super.getSystemService(name);
if (service == null) {
return null;
}
// "semclipboard" is a Samsung-internal service
// 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);
field.set(service, this);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
return service;
}
} }

View file

@ -24,13 +24,10 @@ import com.genymobile.scrcpy.video.SurfaceCapture;
import com.genymobile.scrcpy.video.SurfaceEncoder; import com.genymobile.scrcpy.video.SurfaceEncoder;
import com.genymobile.scrcpy.video.VideoSource; import com.genymobile.scrcpy.video.VideoSource;
import android.annotation.SuppressLint;
import android.os.Build; import android.os.Build;
import android.os.Looper;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -58,7 +55,17 @@ public final class Server {
this.fatalError = true; this.fatalError = true;
} }
if (running == 0 || this.fatalError) { if (running == 0 || this.fatalError) {
Looper.getMainLooper().quitSafely(); notify();
}
}
synchronized void await() {
try {
while (running > 0 && !fatalError) {
wait();
}
} catch (InterruptedException e) {
// ignore
} }
} }
} }
@ -165,7 +172,7 @@ public final class Server {
}); });
} }
Looper.loop(); // interrupted by the Completion implementation completion.await();
} finally { } finally {
if (cleanUp != null) { if (cleanUp != null) {
cleanUp.interrupt(); cleanUp.interrupt();
@ -194,21 +201,6 @@ public final class Server {
} }
} }
private static void prepareMainLooper() {
// Like Looper.prepareMainLooper(), but with quitAllowed set to true
Looper.prepare();
synchronized (Looper.class) {
try {
@SuppressLint("DiscouragedPrivateApi")
Field field = Looper.class.getDeclaredField("sMainLooper");
field.setAccessible(true);
field.set(null, Looper.myLooper());
} catch (ReflectiveOperationException e) {
throw new AssertionError(e);
}
}
}
public static void main(String... args) { public static void main(String... args) {
int status = 0; int status = 0;
try { try {
@ -225,16 +217,10 @@ public final class Server {
} }
private static void internalMain(String... args) throws Exception { private static void internalMain(String... args) throws Exception {
Thread.UncaughtExceptionHandler defaultHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler((t, e) -> { Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
Ln.e("Exception on thread " + t, e); Ln.e("Exception on thread " + t, e);
if (defaultHandler != null) {
defaultHandler.uncaughtException(t, e);
}
}); });
prepareMainLooper();
Options options = Options.parse(args); Options options = Options.parse(args);
Ln.disableSystemStreams(); Ln.disableSystemStreams();

View file

@ -6,9 +6,9 @@ import com.genymobile.scrcpy.util.Ln;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.app.Application; import android.app.Application;
import android.app.Instrumentation;
import android.content.AttributionSource; import android.content.AttributionSource;
import android.content.Context; import android.content.Context;
import android.content.ContextWrapper;
import android.content.pm.ApplicationInfo; import android.content.pm.ApplicationInfo;
import android.media.AudioAttributes; import android.media.AudioAttributes;
import android.media.AudioManager; import android.media.AudioManager;
@ -29,6 +29,8 @@ public final class Workarounds {
private static final Object ACTIVITY_THREAD; private static final Object ACTIVITY_THREAD;
static { static {
prepareMainLooper();
try { try {
// ActivityThread activityThread = new ActivityThread(); // ActivityThread activityThread = new ActivityThread();
ACTIVITY_THREAD_CLASS = Class.forName("android.app.ActivityThread"); ACTIVITY_THREAD_CLASS = Class.forName("android.app.ActivityThread");
@ -75,6 +77,19 @@ public final class Workarounds {
fillAppContext(); fillAppContext();
} }
@SuppressWarnings("deprecation")
private static void prepareMainLooper() {
// Some devices internally create a Handler when creating an input Surface, causing an exception:
// "Can't create handler inside thread that has not called Looper.prepare()"
// <https://github.com/Genymobile/scrcpy/issues/240>
//
// Use Looper.prepareMainLooper() instead of Looper.prepare() to avoid a NullPointerException:
// "Attempt to read from field 'android.os.MessageQueue android.os.Looper.mQueue'
// on a null object reference"
// <https://github.com/Genymobile/scrcpy/issues/921>
Looper.prepareMainLooper();
}
private static void fillAppInfo() { private static void fillAppInfo() {
try { try {
// ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData(); // ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData();
@ -103,7 +118,10 @@ public final class Workarounds {
private static void fillAppContext() { private static void fillAppContext() {
try { try {
Application app = Instrumentation.newApplication(Application.class, FakeContext.get()); Application app = new Application();
Field baseField = ContextWrapper.class.getDeclaredField("mBase");
baseField.setAccessible(true);
baseField.set(app, FakeContext.get());
// activityThread.mInitialApplication = app; // activityThread.mInitialApplication = app;
Field mInitialApplicationField = ACTIVITY_THREAD_CLASS.getDeclaredField("mInitialApplication"); Field mInitialApplicationField = ACTIVITY_THREAD_CLASS.getDeclaredField("mInitialApplication");

View file

@ -112,9 +112,8 @@ public class ControlMessageReader {
private ControlMessage parseInjectScrollEvent() throws IOException { private ControlMessage parseInjectScrollEvent() throws IOException {
Position position = parsePosition(); Position position = parsePosition();
// Binary.i16FixedPointToFloat() decodes values assuming the full range is [-1, 1], but the actual range is [-16, 16]. float hScroll = Binary.i16FixedPointToFloat(dis.readShort());
float hScroll = Binary.i16FixedPointToFloat(dis.readShort()) * 16; float vScroll = Binary.i16FixedPointToFloat(dis.readShort());
float vScroll = Binary.i16FixedPointToFloat(dis.readShort()) * 16;
int buttons = dis.readInt(); int buttons = dis.readInt();
return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll, buttons); return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll, buttons);
} }

View file

@ -6,7 +6,6 @@ import com.genymobile.scrcpy.CleanUp;
import com.genymobile.scrcpy.Options; import com.genymobile.scrcpy.Options;
import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.Device;
import com.genymobile.scrcpy.device.DeviceApp; import com.genymobile.scrcpy.device.DeviceApp;
import com.genymobile.scrcpy.device.DisplayInfo;
import com.genymobile.scrcpy.device.Point; import com.genymobile.scrcpy.device.Point;
import com.genymobile.scrcpy.device.Position; import com.genymobile.scrcpy.device.Position;
import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.device.Size;
@ -18,6 +17,7 @@ import com.genymobile.scrcpy.wrappers.ClipboardManager;
import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.InputManager;
import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.content.IOnPrimaryClipChangedListener;
import android.content.Intent; import android.content.Intent;
import android.os.Build; import android.os.Build;
import android.os.SystemClock; import android.os.SystemClock;
@ -114,12 +114,13 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
Ln.w("Input events are not supported for secondary displays before Android 10"); Ln.w("Input events are not supported for secondary displays before Android 10");
} }
// Make sure the clipboard manager is always created from the main thread (even if clipboardAutosync is disabled)
ClipboardManager clipboardManager = ServiceManager.getClipboardManager();
if (clipboardAutosync) { if (clipboardAutosync) {
// If control and autosync are enabled, synchronize Android clipboard to the computer automatically // If control and autosync are enabled, synchronize Android clipboard to the computer automatically
ClipboardManager clipboardManager = ServiceManager.getClipboardManager();
if (clipboardManager != null) { if (clipboardManager != null) {
clipboardManager.addPrimaryClipChangedListener(() -> { clipboardManager.addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() {
@Override
public void dispatchPrimaryClipChanged() {
if (isSettingClipboard.get()) { if (isSettingClipboard.get()) {
// This is a notification for the change we are currently applying, ignore it // This is a notification for the change we are currently applying, ignore it
return; return;
@ -129,6 +130,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
DeviceMessage msg = DeviceMessage.createClipboard(text); DeviceMessage msg = DeviceMessage.createClipboard(text);
sender.send(msg); sender.send(msg);
} }
}
}); });
} else { } else {
Ln.w("No clipboard manager, copy-paste between device and computer will not work"); Ln.w("No clipboard manager, copy-paste between device and computer will not work");
@ -154,34 +156,8 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
private UhidManager getUhidManager() { private UhidManager getUhidManager() {
if (uhidManager == null) { if (uhidManager == null) {
int uhidDisplayId = displayId; uhidManager = new UhidManager(sender);
if (Build.VERSION.SDK_INT >= AndroidVersions.API_35_ANDROID_15) {
if (displayId == Device.DISPLAY_ID_NONE) {
// Mirroring a new virtual display id (using --new-display-id feature) on Android >= 15, where the UHID mouse pointer can be
// associated to the virtual display
try {
// Wait for at most 1 second until a virtual display id is known
DisplayData data = waitDisplayData(1000);
if (data != null) {
uhidDisplayId = data.virtualDisplayId;
} }
} catch (InterruptedException e) {
// do nothing
}
}
}
String displayUniqueId = null;
if (uhidDisplayId > 0) {
// Ignore Device.DISPLAY_ID_NONE and 0 (main display)
DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(uhidDisplayId);
if (displayInfo != null) {
displayUniqueId = displayInfo.getUniqueId();
}
}
uhidManager = new UhidManager(sender, displayUniqueId);
}
return uhidManager; return uhidManager;
} }
@ -723,9 +699,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
if (timeout < 0) { if (timeout < 0) {
return null; return null;
} }
if (timeout > 0) {
displayDataAvailable.wait(timeout); displayDataAvailable.wait(timeout);
}
data = displayData.get(); data = displayData.get();
} }

View file

@ -3,7 +3,6 @@ package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.StringUtils; import com.genymobile.scrcpy.util.StringUtils;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.os.Build; import android.os.Build;
import android.os.HandlerThread; import android.os.HandlerThread;
@ -32,20 +31,14 @@ public final class UhidManager {
private static final int SIZE_OF_UHID_EVENT = 4380; // sizeof(struct uhid_event) private static final int SIZE_OF_UHID_EVENT = 4380; // sizeof(struct uhid_event)
// Must be unique across the system
private static final String INPUT_PORT = "scrcpy:" + Os.getpid();
private final String displayUniqueId;
private final ArrayMap<Integer, FileDescriptor> fds = new ArrayMap<>(); private final ArrayMap<Integer, FileDescriptor> fds = new ArrayMap<>();
private final ByteBuffer buffer = ByteBuffer.allocate(SIZE_OF_UHID_EVENT).order(ByteOrder.nativeOrder()); private final ByteBuffer buffer = ByteBuffer.allocate(SIZE_OF_UHID_EVENT).order(ByteOrder.nativeOrder());
private final DeviceMessageSender sender; private final DeviceMessageSender sender;
private final MessageQueue queue; private final MessageQueue queue;
public UhidManager(DeviceMessageSender sender, String displayUniqueId) { public UhidManager(DeviceMessageSender sender) {
this.sender = sender; this.sender = sender;
this.displayUniqueId = displayUniqueId;
if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0) { if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0) {
HandlerThread thread = new HandlerThread("UHidManager"); HandlerThread thread = new HandlerThread("UHidManager");
thread.start(); thread.start();
@ -59,22 +52,15 @@ public final class UhidManager {
try { try {
FileDescriptor fd = Os.open("/dev/uhid", OsConstants.O_RDWR, 0); FileDescriptor fd = Os.open("/dev/uhid", OsConstants.O_RDWR, 0);
try { try {
// First UHID device added
boolean firstDevice = fds.isEmpty();
FileDescriptor old = fds.put(id, fd); FileDescriptor old = fds.put(id, fd);
if (old != null) { if (old != null) {
Ln.w("Duplicate UHID id: " + id); Ln.w("Duplicate UHID id: " + id);
close(old); close(old);
} }
String phys = mustUseInputPort() ? INPUT_PORT : null; byte[] req = buildUhidCreate2Req(vendorId, productId, name, reportDesc);
byte[] req = buildUhidCreate2Req(vendorId, productId, name, reportDesc, phys);
Os.write(fd, req, 0, req.length); Os.write(fd, req, 0, req.length);
if (firstDevice) {
addUniqueIdAssociation();
}
registerUhidListener(id, fd); registerUhidListener(id, fd);
} catch (Exception e) { } catch (Exception e) {
close(fd); close(fd);
@ -162,7 +148,7 @@ public final class UhidManager {
} }
} }
private static byte[] buildUhidCreate2Req(int vendorId, int productId, String name, byte[] reportDesc, String phys) { private static byte[] buildUhidCreate2Req(int vendorId, int productId, String name, byte[] reportDesc) {
/* /*
* struct uhid_event { * struct uhid_event {
* uint32_t type; * uint32_t type;
@ -184,23 +170,17 @@ public final class UhidManager {
* } __attribute__((__packed__)); * } __attribute__((__packed__));
*/ */
byte[] empty = new byte[256];
ByteBuffer buf = ByteBuffer.allocate(280 + reportDesc.length).order(ByteOrder.nativeOrder()); ByteBuffer buf = ByteBuffer.allocate(280 + reportDesc.length).order(ByteOrder.nativeOrder());
buf.putInt(UHID_CREATE2); buf.putInt(UHID_CREATE2);
String actualName = name.isEmpty() ? "scrcpy" : name; String actualName = name.isEmpty() ? "scrcpy" : name;
byte[] nameBytes = actualName.getBytes(StandardCharsets.UTF_8); byte[] utf8Name = actualName.getBytes(StandardCharsets.UTF_8);
int nameLen = StringUtils.getUtf8TruncationIndex(nameBytes, 127); int len = StringUtils.getUtf8TruncationIndex(utf8Name, 127);
assert nameLen <= 127; assert len <= 127;
buf.put(nameBytes, 0, nameLen); buf.put(utf8Name, 0, len);
buf.put(empty, 0, 256 - len);
if (phys != null) {
buf.position(4 + 128);
byte[] physBytes = phys.getBytes(StandardCharsets.US_ASCII);
assert physBytes.length <= 63;
buf.put(physBytes);
}
buf.position(4 + 256);
buf.putShort((short) reportDesc.length); buf.putShort((short) reportDesc.length);
buf.putShort(BUS_VIRTUAL); buf.putShort(BUS_VIRTUAL);
buf.putInt(vendorId); buf.putInt(vendorId);
@ -239,26 +219,15 @@ public final class UhidManager {
if (fd != null) { if (fd != null) {
unregisterUhidListener(fd); unregisterUhidListener(fd);
close(fd); close(fd);
if (fds.isEmpty()) {
// Last UHID device removed
removeUniqueIdAssociation();
}
} else { } else {
Ln.w("Closing unknown UHID device: " + id); Ln.w("Closing unknown UHID device: " + id);
} }
} }
public void closeAll() { public void closeAll() {
if (fds.isEmpty()) {
return;
}
for (FileDescriptor fd : fds.values()) { for (FileDescriptor fd : fds.values()) {
close(fd); close(fd);
} }
removeUniqueIdAssociation();
} }
private static void close(FileDescriptor fd) { private static void close(FileDescriptor fd) {
@ -268,20 +237,4 @@ public final class UhidManager {
Ln.e("Failed to close uhid: " + e.getMessage()); Ln.e("Failed to close uhid: " + e.getMessage());
} }
} }
private boolean mustUseInputPort() {
return Build.VERSION.SDK_INT >= AndroidVersions.API_35_ANDROID_15 && displayUniqueId != null;
}
private void addUniqueIdAssociation() {
if (mustUseInputPort()) {
ServiceManager.getInputManager().addUniqueIdAssociationByPort(INPUT_PORT, displayUniqueId);
}
}
private void removeUniqueIdAssociation() {
if (mustUseInputPort()) {
ServiceManager.getInputManager().removeUniqueIdAssociationByPort(INPUT_PORT);
}
}
} }

View file

@ -7,18 +7,16 @@ public final class DisplayInfo {
private final int layerStack; private final int layerStack;
private final int flags; private final int flags;
private final int dpi; private final int dpi;
private final String uniqueId;
public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 0x00000001; public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 0x00000001;
public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags, int dpi, String uniqueId) { public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags, int dpi) {
this.displayId = displayId; this.displayId = displayId;
this.size = size; this.size = size;
this.rotation = rotation; this.rotation = rotation;
this.layerStack = layerStack; this.layerStack = layerStack;
this.flags = flags; this.flags = flags;
this.dpi = dpi; this.dpi = dpi;
this.uniqueId = uniqueId;
} }
public int getDisplayId() { public int getDisplayId() {
@ -44,8 +42,5 @@ public final class DisplayInfo {
public int getDpi() { public int getDpi() {
return dpi; return dpi;
} }
public String getUniqueId() {
return uniqueId;
}
} }

View file

@ -32,11 +32,9 @@ public enum Orientation {
throw new IllegalArgumentException("Unknown orientation: " + name); throw new IllegalArgumentException("Unknown orientation: " + name);
} }
public static Orientation fromRotation(int ccwRotation) { public static Orientation fromRotation(int rotation) {
assert ccwRotation >= 0 && ccwRotation < 4; assert rotation >= 0 && rotation < 4;
// Display rotation is expressed counter-clockwise, orientation is expressed clockwise return values()[rotation];
int cwRotation = (4 - ccwRotation) % 4;
return values()[cwRotation];
} }
public boolean isFlipped() { public boolean isFlipped() {

View file

@ -1,7 +1,6 @@
package com.genymobile.scrcpy.opengl; package com.genymobile.scrcpy.opengl;
import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.util.Threads;
import android.graphics.SurfaceTexture; import android.graphics.SurfaceTexture;
import android.opengl.EGL14; import android.opengl.EGL14;
@ -16,7 +15,6 @@ import android.os.Handler;
import android.os.HandlerThread; import android.os.HandlerThread;
import android.view.Surface; import android.view.Surface;
import java.util.concurrent.Callable;
import java.util.concurrent.Semaphore; import java.util.concurrent.Semaphore;
public final class OpenGLRunner { public final class OpenGLRunner {
@ -82,17 +80,31 @@ public final class OpenGLRunner {
public Surface start(Size inputSize, Size outputSize, Surface outputSurface) throws OpenGLException { public Surface start(Size inputSize, Size outputSize, Surface outputSurface) throws OpenGLException {
initOnce(); 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. // The whole OpenGL execution must be performed on a Handler, so that SurfaceTexture.setOnFrameAvailableListener() works correctly.
// See <https://github.com/Genymobile/scrcpy/issues/5444> // See <https://github.com/Genymobile/scrcpy/issues/5444>
handler.post(() -> {
try { try {
Threads.executeSynchronouslyOn(handler, new Callable<Void>() {
@Override
public Void call() throws Exception {
run(inputSize, outputSize, outputSurface); run(inputSize, outputSize, outputSurface);
return null; } catch (Throwable throwable) {
throwableRef[0] = throwable;
} finally {
sem.release();
} }
}); });
} catch (Throwable throwable) {
try {
sem.acquire();
} catch (InterruptedException e) {
// Behave as if this method call was synchronous
Thread.currentThread().interrupt();
}
Throwable throwable = throwableRef[0];
if (throwable != null) {
if (throwable instanceof OpenGLException) { if (throwable instanceof OpenGLException) {
throw (OpenGLException) throwable; throw (OpenGLException) throwable;
} }

View file

@ -74,14 +74,12 @@ public final class Ln {
public static void w(String message, Throwable throwable) { public static void w(String message, Throwable throwable) {
if (isEnabled(Level.WARN)) { if (isEnabled(Level.WARN)) {
Log.w(TAG, message, throwable); Log.w(TAG, message, throwable);
synchronized (CONSOLE_ERR) {
CONSOLE_ERR.print(PREFIX + "WARN: " + message + '\n'); CONSOLE_ERR.print(PREFIX + "WARN: " + message + '\n');
if (throwable != null) { if (throwable != null) {
throwable.printStackTrace(CONSOLE_ERR); throwable.printStackTrace(CONSOLE_ERR);
} }
} }
} }
}
public static void w(String message) { public static void w(String message) {
w(message, null); w(message, null);
@ -90,14 +88,12 @@ public final class Ln {
public static void e(String message, Throwable throwable) { public static void e(String message, Throwable throwable) {
if (isEnabled(Level.ERROR)) { if (isEnabled(Level.ERROR)) {
Log.e(TAG, message, throwable); Log.e(TAG, message, throwable);
synchronized (CONSOLE_ERR) {
CONSOLE_ERR.print(PREFIX + "ERROR: " + message + '\n'); CONSOLE_ERR.print(PREFIX + "ERROR: " + message + '\n');
if (throwable != null) { if (throwable != null) {
throwable.printStackTrace(CONSOLE_ERR); throwable.printStackTrace(CONSOLE_ERR);
} }
} }
} }
}
public static void e(String message) { public static void e(String message) {
e(message, null); e(message, null);

View file

@ -1,8 +1,13 @@
package com.genymobile.scrcpy.util; package com.genymobile.scrcpy.util;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.wrappers.ContentProvider; import com.genymobile.scrcpy.wrappers.ContentProvider;
import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.os.Build;
import java.io.IOException;
public final class Settings { public final class Settings {
public static final String TABLE_SYSTEM = ContentProvider.TABLE_SYSTEM; public static final String TABLE_SYSTEM = ContentProvider.TABLE_SYSTEM;
@ -13,26 +18,66 @@ public final class Settings {
/* not instantiable */ /* not instantiable */
} }
private static void execSettingsPut(String table, String key, String value) throws SettingsException {
try {
Command.exec("settings", "put", table, key, value);
} catch (IOException | InterruptedException e) {
throw new SettingsException("put", table, key, value, e);
}
}
private static String execSettingsGet(String table, String key) throws SettingsException {
try {
return Command.execReadLine("settings", "get", table, key);
} catch (IOException | InterruptedException e) {
throw new SettingsException("get", table, key, null, e);
}
}
public static String getValue(String table, String key) throws SettingsException { public static String getValue(String table, String key) throws SettingsException {
if (Build.VERSION.SDK_INT <= AndroidVersions.API_30_ANDROID_11) {
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) {
return provider.getValue(table, key); return provider.getValue(table, key);
} catch (SettingsException e) {
Ln.w("Could not get settings value via ContentProvider, fallback to settings process", e);
} }
} }
return execSettingsGet(table, key);
}
public static void putValue(String table, String key, String value) throws SettingsException { public static void putValue(String table, String key, String value) throws SettingsException {
if (Build.VERSION.SDK_INT <= AndroidVersions.API_30_ANDROID_11) {
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) {
provider.putValue(table, key, value); provider.putValue(table, key, value);
} catch (SettingsException e) {
Ln.w("Could not put settings value via ContentProvider, fallback to settings process", e);
}
} }
execSettingsPut(table, key, value);
} }
public static String getAndPutValue(String table, String key, String value) throws SettingsException { public static String getAndPutValue(String table, String key, String value) throws SettingsException {
if (Build.VERSION.SDK_INT <= AndroidVersions.API_30_ANDROID_11) {
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) {
String oldValue = provider.getValue(table, key); String oldValue = provider.getValue(table, key);
if (!value.equals(oldValue)) { if (!value.equals(oldValue)) {
provider.putValue(table, key, value); provider.putValue(table, key, value);
} }
return oldValue; return oldValue;
} catch (SettingsException e) {
Ln.w("Could not get and put settings value via ContentProvider, fallback to settings process", e);
} }
} }
String oldValue = getValue(table, key);
if (!value.equals(oldValue)) {
putValue(table, key, value);
}
return oldValue;
}
} }

View file

@ -1,43 +0,0 @@
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];
}
}

View file

@ -25,7 +25,6 @@ public class NewDisplayCapture extends SurfaceCapture {
// Internal fields copied from android.hardware.display.DisplayManager // Internal fields copied from android.hardware.display.DisplayManager
private static final int VIRTUAL_DISPLAY_FLAG_PUBLIC = android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC; private static final int VIRTUAL_DISPLAY_FLAG_PUBLIC = android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC;
private static final int VIRTUAL_DISPLAY_FLAG_PRESENTATION = android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION;
private static final int VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY = android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY; private static final int VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY = android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
private static final int VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH = 1 << 6; private static final int VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH = 1 << 6;
private static final int VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT = 1 << 7; private static final int VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT = 1 << 7;
@ -170,7 +169,6 @@ public class NewDisplayCapture extends SurfaceCapture {
int virtualDisplayId; int virtualDisplayId;
try { try {
int flags = VIRTUAL_DISPLAY_FLAG_PUBLIC int flags = VIRTUAL_DISPLAY_FLAG_PUBLIC
| VIRTUAL_DISPLAY_FLAG_PRESENTATION
| VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY | VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
| VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH | VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH
| VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT; | VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT;

View file

@ -1,43 +1,270 @@
package com.genymobile.scrcpy.wrappers; package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.util.Ln;
import android.content.ClipData; import android.content.ClipData;
import android.content.Context; import android.content.IOnPrimaryClipChangedListener;
import android.os.Build;
import android.os.IInterface;
import java.lang.reflect.Method;
public final class ClipboardManager { public final class ClipboardManager {
private final android.content.ClipboardManager manager; private final IInterface manager;
private Method getPrimaryClipMethod;
private Method setPrimaryClipMethod;
private Method addPrimaryClipChangedListener;
private int getMethodVersion;
private int setMethodVersion;
private int addListenerMethodVersion;
static ClipboardManager create() { static ClipboardManager create() {
android.content.ClipboardManager manager = (android.content.ClipboardManager) FakeContext.get().getSystemService(Context.CLIPBOARD_SERVICE); IInterface clipboard = ServiceManager.getService("clipboard", "android.content.IClipboard");
if (manager == null) { if (clipboard == null) {
// Some devices have no clipboard manager // Some devices have no clipboard manager
// <https://github.com/Genymobile/scrcpy/issues/1440> // <https://github.com/Genymobile/scrcpy/issues/1440>
// <https://github.com/Genymobile/scrcpy/issues/1556> // <https://github.com/Genymobile/scrcpy/issues/1556>
return null; return null;
} }
return new ClipboardManager(manager); return new ClipboardManager(clipboard);
} }
private ClipboardManager(android.content.ClipboardManager manager) { private ClipboardManager(IInterface manager) {
this.manager = manager; this.manager = manager;
} }
private Method getGetPrimaryClipMethod() throws NoSuchMethodException {
if (getPrimaryClipMethod == null) {
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class);
return getPrimaryClipMethod;
}
try {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class);
getMethodVersion = 0;
return getPrimaryClipMethod;
} catch (NoSuchMethodException e) {
// fall-through
}
try {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class);
getMethodVersion = 1;
return getPrimaryClipMethod;
} catch (NoSuchMethodException e) {
// fall-through
}
try {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class);
getMethodVersion = 2;
return getPrimaryClipMethod;
} catch (NoSuchMethodException e) {
// fall-through
}
try {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class, String.class);
getMethodVersion = 3;
return getPrimaryClipMethod;
} catch (NoSuchMethodException e) {
// fall-through
}
try {
getPrimaryClipMethod = manager.getClass()
.getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, boolean.class);
getMethodVersion = 4;
return getPrimaryClipMethod;
} catch (NoSuchMethodException e) {
// fall-through
}
try {
getPrimaryClipMethod = manager.getClass()
.getMethod("getPrimaryClip", String.class, String.class, String.class, String.class, int.class, int.class, boolean.class);
getMethodVersion = 5;
return getPrimaryClipMethod;
} catch (NoSuchMethodException e) {
// fall-through
}
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, String.class);
getMethodVersion = 6;
}
return getPrimaryClipMethod;
}
private Method getSetPrimaryClipMethod() throws NoSuchMethodException {
if (setPrimaryClipMethod == null) {
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class);
return setPrimaryClipMethod;
}
try {
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class);
setMethodVersion = 0;
return setPrimaryClipMethod;
} catch (NoSuchMethodException e1) {
// fall-through
}
try {
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class);
setMethodVersion = 1;
return setPrimaryClipMethod;
} catch (NoSuchMethodException e2) {
// fall-through
}
try {
setPrimaryClipMethod = manager.getClass()
.getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class);
setMethodVersion = 2;
return setPrimaryClipMethod;
} catch (NoSuchMethodException e3) {
// fall-through
}
setPrimaryClipMethod = manager.getClass()
.getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class, boolean.class);
setMethodVersion = 3;
}
return setPrimaryClipMethod;
}
private static ClipData getPrimaryClip(Method method, int methodVersion, IInterface manager) throws ReflectiveOperationException {
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME);
}
switch (methodVersion) {
case 0:
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
case 1:
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
case 2:
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0);
case 3:
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID, null);
case 4:
// The last boolean parameter is "userOperate"
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, true);
case 5:
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, null, null, FakeContext.ROOT_UID, 0, true);
default:
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, null);
}
}
private static void setPrimaryClip(Method method, int methodVersion, IInterface manager, ClipData clipData) throws ReflectiveOperationException {
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME);
return;
}
switch (methodVersion) {
case 0:
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
break;
case 1:
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
break;
case 2:
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0);
break;
default:
// The last boolean parameter is "userOperate"
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, true);
}
}
public CharSequence getText() { public CharSequence getText() {
ClipData clipData = manager.getPrimaryClip(); try {
Method method = getGetPrimaryClipMethod();
ClipData clipData = getPrimaryClip(method, getMethodVersion, manager);
if (clipData == null || clipData.getItemCount() == 0) { if (clipData == null || clipData.getItemCount() == 0) {
return null; return null;
} }
return clipData.getItemAt(0).getText(); return clipData.getItemAt(0).getText();
} catch (ReflectiveOperationException e) {
Ln.e("Could not invoke method", e);
return null;
}
} }
public boolean setText(CharSequence text) { public boolean setText(CharSequence text) {
try {
Method method = getSetPrimaryClipMethod();
ClipData clipData = ClipData.newPlainText(null, text); ClipData clipData = ClipData.newPlainText(null, text);
manager.setPrimaryClip(clipData); setPrimaryClip(method, setMethodVersion, manager, clipData);
return true; return true;
} catch (ReflectiveOperationException e) {
Ln.e("Could not invoke method", e);
return false;
}
} }
public void addPrimaryClipChangedListener(android.content.ClipboardManager.OnPrimaryClipChangedListener listener) { private static void addPrimaryClipChangedListener(Method method, int methodVersion, IInterface manager, IOnPrimaryClipChangedListener listener)
manager.addPrimaryClipChangedListener(listener); throws ReflectiveOperationException {
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
method.invoke(manager, listener, FakeContext.PACKAGE_NAME);
return;
}
switch (methodVersion) {
case 0:
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
break;
case 1:
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
break;
default:
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0);
break;
}
}
private Method getAddPrimaryClipChangedListener() throws NoSuchMethodException {
if (addPrimaryClipChangedListener == null) {
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
addPrimaryClipChangedListener = manager.getClass()
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class);
} else {
try {
addPrimaryClipChangedListener = manager.getClass()
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class);
addListenerMethodVersion = 0;
} catch (NoSuchMethodException e1) {
try {
addPrimaryClipChangedListener = manager.getClass()
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class,
int.class);
addListenerMethodVersion = 1;
} catch (NoSuchMethodException e2) {
addPrimaryClipChangedListener = manager.getClass()
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class,
int.class, int.class);
addListenerMethodVersion = 2;
}
}
}
}
return addPrimaryClipChangedListener;
}
public boolean addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) {
try {
Method method = getAddPrimaryClipChangedListener();
addPrimaryClipChangedListener(method, addListenerMethodVersion, manager, listener);
return true;
} catch (ReflectiveOperationException e) {
Ln.e("Could not invoke method", e);
return false;
}
} }
} }

View file

@ -46,7 +46,6 @@ public final class DisplayManager {
} }
private final Object manager; // instance of hidden class android.hardware.display.DisplayManagerGlobal private final Object manager; // instance of hidden class android.hardware.display.DisplayManagerGlobal
private Method getDisplayInfoMethod;
private Method createVirtualDisplayMethod; private Method createVirtualDisplayMethod;
private Method requestDisplayPowerMethod; private Method requestDisplayPowerMethod;
@ -82,7 +81,7 @@ public final class DisplayManager {
int density = Integer.parseInt(m.group(5)); int density = Integer.parseInt(m.group(5));
int layerStack = Integer.parseInt(m.group(6)); int layerStack = Integer.parseInt(m.group(6));
return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, density, null); return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, density);
} }
private static DisplayInfo getDisplayInfoFromDumpsysDisplay(int displayId) { private static DisplayInfo getDisplayInfoFromDumpsysDisplay(int displayId) {
@ -96,12 +95,12 @@ public final class DisplayManager {
} }
private static int parseDisplayFlags(String text) { private static int parseDisplayFlags(String text) {
Pattern regex = Pattern.compile("FLAG_[A-Z_]+");
if (text == null) { if (text == null) {
return 0; return 0;
} }
int flags = 0; int flags = 0;
Pattern regex = Pattern.compile("FLAG_[A-Z_]+");
Matcher m = regex.matcher(text); Matcher m = regex.matcher(text);
while (m.find()) { while (m.find()) {
String flagString = m.group(); String flagString = m.group();
@ -115,18 +114,9 @@ public final class DisplayManager {
return flags; return flags;
} }
// getDisplayInfo() may be used from both the Controller thread and the video (main) thread
private synchronized Method getGetDisplayInfoMethod() throws NoSuchMethodException {
if (getDisplayInfoMethod == null) {
getDisplayInfoMethod = manager.getClass().getMethod("getDisplayInfo", int.class);
}
return getDisplayInfoMethod;
}
public DisplayInfo getDisplayInfo(int displayId) { public DisplayInfo getDisplayInfo(int displayId) {
try { try {
Method method = getGetDisplayInfoMethod(); Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, displayId);
Object displayInfo = method.invoke(manager, displayId);
if (displayInfo == null) { if (displayInfo == null) {
// fallback when displayInfo is null // fallback when displayInfo is null
return getDisplayInfoFromDumpsysDisplay(displayId); return getDisplayInfoFromDumpsysDisplay(displayId);
@ -139,14 +129,7 @@ public final class DisplayManager {
int layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo); int layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo);
int flags = cls.getDeclaredField("flags").getInt(displayInfo); int flags = cls.getDeclaredField("flags").getInt(displayInfo);
int dpi = cls.getDeclaredField("logicalDensityDpi").getInt(displayInfo); int dpi = cls.getDeclaredField("logicalDensityDpi").getInt(displayInfo);
String uniqueId; return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, dpi);
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) { } catch (ReflectiveOperationException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }

View file

@ -1,12 +1,11 @@
package com.genymobile.scrcpy.wrappers; package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.util.Ln;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.os.Parcel; import android.graphics.Rect;
import android.os.RemoteException;
import android.view.IDisplayWindowListener; import android.view.IDisplayWindowListener;
import java.util.List;
public class DisplayWindowListener extends IDisplayWindowListener.Stub { public class DisplayWindowListener extends IDisplayWindowListener.Stub {
@Override @Override
public void onDisplayAdded(int displayId) { public void onDisplayAdded(int displayId) {
@ -24,14 +23,17 @@ public class DisplayWindowListener extends IDisplayWindowListener.Stub {
} }
@Override @Override
public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { public void onFixedRotationStarted(int displayId, int newRotation) {
try { // empty default implementation
return super.onTransact(code, data, reply, flags);
} catch (AbstractMethodError e) {
Ln.v("Ignoring AbstractMethodError: " + e.getMessage());
// Ignore unknown methods, write default response to reply parcel
reply.writeNoException();
return true;
} }
@Override
public void onFixedRotationFinished(int displayId) {
// empty default implementation
}
@Override
public void onKeepClearAreasChanged(int displayId, List<Rect> restricted, List<Rect> unrestricted) {
// empty default implementation
} }
} }

View file

@ -1,15 +1,11 @@
package com.genymobile.scrcpy.wrappers; package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.Ln;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.view.InputEvent; import android.view.InputEvent;
import android.view.MotionEvent; import android.view.MotionEvent;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
@SuppressLint("PrivateApi,DiscouragedPrivateApi") @SuppressLint("PrivateApi,DiscouragedPrivateApi")
@ -19,28 +15,39 @@ public final class InputManager {
public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT = 1; public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT = 1;
public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2; public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2;
private final android.hardware.input.InputManager manager; private final Object manager;
private long lastPermissionLogDate; private Method injectInputEventMethod;
private static Method injectInputEventMethod;
private static Method setDisplayIdMethod; private static Method setDisplayIdMethod;
private static Method setActionButtonMethod; private static Method setActionButtonMethod;
private static Method addUniqueIdAssociationByPortMethod;
private static Method removeUniqueIdAssociationByPortMethod;
static InputManager create() { static InputManager create() {
android.hardware.input.InputManager manager = (android.hardware.input.InputManager) FakeContext.get() try {
.getSystemService(FakeContext.INPUT_SERVICE); Class<?> inputManagerClass = getInputManagerClass();
return new InputManager(manager); Method getInstanceMethod = inputManagerClass.getDeclaredMethod("getInstance");
Object im = getInstanceMethod.invoke(null);
return new InputManager(im);
} catch (ReflectiveOperationException e) {
throw new AssertionError(e);
}
} }
private InputManager(android.hardware.input.InputManager manager) { private static Class<?> getInputManagerClass() {
try {
// Parts of the InputManager class have been moved to a new InputManagerGlobal class in Android 14 preview
return Class.forName("android.hardware.input.InputManagerGlobal");
} catch (ClassNotFoundException e) {
return android.hardware.input.InputManager.class;
}
}
private InputManager(Object manager) {
this.manager = manager; this.manager = manager;
} }
private static Method getInjectInputEventMethod() throws NoSuchMethodException { private Method getInjectInputEventMethod() throws NoSuchMethodException {
if (injectInputEventMethod == null) { if (injectInputEventMethod == null) {
injectInputEventMethod = android.hardware.input.InputManager.class.getMethod("injectInputEvent", InputEvent.class, int.class); injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class);
} }
return injectInputEventMethod; return injectInputEventMethod;
} }
@ -50,23 +57,6 @@ public final class InputManager {
Method method = getInjectInputEventMethod(); Method method = getInjectInputEventMethod();
return (boolean) method.invoke(manager, inputEvent, mode); return (boolean) method.invoke(manager, inputEvent, mode);
} catch (ReflectiveOperationException e) { } catch (ReflectiveOperationException e) {
if (e instanceof InvocationTargetException) {
Throwable cause = e.getCause();
if (cause instanceof SecurityException) {
String message = e.getCause().getMessage();
if (message != null && message.contains("INJECT_EVENTS permission")) {
// Do not flood the console, limit to one permission error log every 3 seconds
long now = System.currentTimeMillis();
if (lastPermissionLogDate <= now - 3000) {
Ln.e(message);
Ln.e("Make sure you have enabled \"USB debugging (Security Settings)\" and then rebooted your device.");
lastPermissionLogDate = now;
}
// Do not print the stack trace
return false;
}
}
}
Ln.e("Could not invoke method", e); Ln.e("Could not invoke method", e);
return false; return false;
} }
@ -107,40 +97,4 @@ public final class InputManager {
return false; return false;
} }
} }
private static Method getAddUniqueIdAssociationByPortMethod() throws NoSuchMethodException {
if (addUniqueIdAssociationByPortMethod == null) {
addUniqueIdAssociationByPortMethod = android.hardware.input.InputManager.class.getMethod(
"addUniqueIdAssociationByPort", String.class, String.class);
}
return addUniqueIdAssociationByPortMethod;
}
@TargetApi(AndroidVersions.API_35_ANDROID_15)
public void addUniqueIdAssociationByPort(String inputPort, String uniqueId) {
try {
Method method = getAddUniqueIdAssociationByPortMethod();
method.invoke(manager, inputPort, uniqueId);
} catch (ReflectiveOperationException e) {
Ln.e("Cannot add unique id association by port", e);
}
}
private static Method getRemoveUniqueIdAssociationByPortMethod() throws NoSuchMethodException {
if (removeUniqueIdAssociationByPortMethod == null) {
removeUniqueIdAssociationByPortMethod = android.hardware.input.InputManager.class.getMethod(
"removeUniqueIdAssociationByPort", String.class);
}
return removeUniqueIdAssociationByPortMethod;
}
@TargetApi(AndroidVersions.API_35_ANDROID_15)
public void removeUniqueIdAssociationByPort(String inputPort) {
try {
Method method = getRemoveUniqueIdAssociationByPortMethod();
method.invoke(manager, inputPort);
} catch (ReflectiveOperationException e) {
Ln.e("Cannot remove unique id association by port", e);
}
}
} }

View file

@ -54,8 +54,7 @@ public final class ServiceManager {
return windowManager; return windowManager;
} }
// The DisplayManager may be used from both the Controller thread and the video (main) thread public static DisplayManager getDisplayManager() {
public static synchronized DisplayManager getDisplayManager() {
if (displayManager == null) { if (displayManager == null) {
displayManager = DisplayManager.create(); displayManager = DisplayManager.create();
} }

View file

@ -125,7 +125,7 @@ public class ControlMessageReaderTest {
dos.writeShort(1080); dos.writeShort(1080);
dos.writeShort(1920); dos.writeShort(1920);
dos.writeShort(0); // 0.0f encoded as i16 dos.writeShort(0); // 0.0f encoded as i16
dos.writeShort(0x8000); // -16.0f encoded as i16 (the range is [-16, 16]) dos.writeShort(0x8000); // -1.0f encoded as i16
dos.writeInt(1); dos.writeInt(1);
byte[] packet = bos.toByteArray(); byte[] packet = bos.toByteArray();
@ -139,7 +139,7 @@ public class ControlMessageReaderTest {
Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth()); Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth());
Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight()); Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight());
Assert.assertEquals(0f, event.getHScroll(), 0f); Assert.assertEquals(0f, event.getHScroll(), 0f);
Assert.assertEquals(-16f, event.getVScroll(), 0f); Assert.assertEquals(-1f, event.getVScroll(), 0f);
Assert.assertEquals(1, event.getButtons()); Assert.assertEquals(1, event.getButtons());
Assert.assertEquals(-1, bis.read()); // EOS Assert.assertEquals(-1, bis.read()); // EOS