diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 27e3abf2..c4ca1829 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -229,7 +229,7 @@ jobs:
path: release/work/build-macos-aarch64/dist-tar/
build-macos-x86_64:
- runs-on: macos-13
+ runs-on: macos-15-intel
steps:
- name: Check architecture
run: |
diff --git a/FAQ.md b/FAQ.md
index 24722c74..f9da5d1f 100644
--- a/FAQ.md
+++ b/FAQ.md
@@ -141,12 +141,13 @@ 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.
-Please read [#3654], in particular [this comment][#3654-comment1] and [the next
-one][#3654-comment2].
+Please read [#3654], in particular [this comment][#3654-comment1], [the next
+one][#3654-comment2] and [this one][#3654-comment3].
[#3654]: https://github.com/Genymobile/scrcpy/issues/3654
[#3654-comment1]: https://github.com/Genymobile/scrcpy/issues/3654#issuecomment-1369278232
[#3654-comment2]: https://github.com/Genymobile/scrcpy/issues/3654#issuecomment-1369295011
+[#3654-comment3]: https://github.com/Genymobile/scrcpy/issues/3654#issuecomment-2613219725
## Control issues
diff --git a/LICENSE b/LICENSE
index 1196b3da..3325e9f3 100644
--- a/LICENSE
+++ b/LICENSE
@@ -188,7 +188,7 @@
identification within third-party archives.
Copyright (C) 2018 Genymobile
- Copyright (C) 2018-2025 Romain Vimont
+ Copyright (C) 2018-2026 Romain Vimont
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/README.md b/README.md
index a610a8ba..2ee76736 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
source for the project. Do not download releases from random websites, even if
their name contains `scrcpy`.**
-# scrcpy (v3.3.2)
+# scrcpy (v3.3.4)
@@ -210,7 +210,7 @@ work][donate]:
## License
Copyright (C) 2018 Genymobile
- Copyright (C) 2018-2025 Romain Vimont
+ Copyright (C) 2018-2026 Romain Vimont
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/app/deps/common b/app/deps/_init
similarity index 92%
rename from app/deps/common
rename to app/deps/_init
index daaa96c0..4612d9fe 100644
--- a/app/deps/common
+++ b/app/deps/_init
@@ -1,10 +1,9 @@
-#!/usr/bin/env bash
# This file is intended to be sourced by other scripts, not executed
process_args() {
if [[ $# != 3 ]]
then
- # : win32 or win64
+ # : linux, macos, win32 or win64
# : native or cross
# : static or shared
echo "Syntax: $0 " >&2
@@ -12,8 +11,8 @@ process_args() {
fi
HOST="$1"
- BUILD_TYPE="$2" # native or cross
- LINK_TYPE="$3" # static or shared
+ BUILD_TYPE="$2"
+ LINK_TYPE="$3"
DIRNAME="$HOST-$BUILD_TYPE-$LINK_TYPE"
if [[ "$BUILD_TYPE" != native && "$BUILD_TYPE" != cross ]]
diff --git a/app/deps/adb_linux.sh b/app/deps/adb_linux.sh
index a3e339ec..e47edf88 100755
--- a/app/deps/adb_linux.sh
+++ b/app/deps/adb_linux.sh
@@ -1,21 +1,21 @@
#!/usr/bin/env bash
set -ex
-DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
-cd "$DEPS_DIR"
-. common
+. $(dirname ${BASH_SOURCE[0]})/_init "$@"
VERSION=36.0.0
-FILENAME=platform-tools_r$VERSION-linux.zip
-PROJECT_DIR=platform-tools-$VERSION-linux
+URL="https://dl.google.com/android/repository/platform-tools_r$VERSION-linux.zip"
SHA256SUM=0ead642c943ffe79701fccca8f5f1c69c4ce4f43df2eefee553f6ccb27cbfbe8
+PROJECT_DIR="platform-tools-$VERSION-linux"
+FILENAME="$PROJECT_DIR.zip"
+
cd "$SOURCES_DIR"
if [[ -d "$PROJECT_DIR" ]]
then
echo "$PWD/$PROJECT_DIR" found
else
- get_file "https://dl.google.com/android/repository/$FILENAME" "$FILENAME" "$SHA256SUM"
+ get_file "$URL" "$FILENAME" "$SHA256SUM"
mkdir -p "$PROJECT_DIR"
cd "$PROJECT_DIR"
ZIP_PREFIX=platform-tools
diff --git a/app/deps/adb_macos.sh b/app/deps/adb_macos.sh
index 6880fb93..a56117dc 100755
--- a/app/deps/adb_macos.sh
+++ b/app/deps/adb_macos.sh
@@ -1,21 +1,21 @@
#!/usr/bin/env bash
set -ex
-DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
-cd "$DEPS_DIR"
-. common
+. $(dirname ${BASH_SOURCE[0]})/_init "$@"
VERSION=36.0.0
-FILENAME=platform-tools_r$VERSION-darwin.zip
-PROJECT_DIR=platform-tools-$VERSION-darwin
+URL="https://dl.google.com/android/repository/platform-tools_r$VERSION-darwin.zip"
SHA256SUM=d3e9fa1df3345cf728586908426615a60863d2632f73f1ce14f0f1349ef000fd
+PROJECT_DIR="platform-tools-$VERSION-darwin"
+FILENAME="$PROJECT_DIR.zip"
+
cd "$SOURCES_DIR"
if [[ -d "$PROJECT_DIR" ]]
then
echo "$PWD/$PROJECT_DIR" found
else
- get_file "https://dl.google.com/android/repository/$FILENAME" "$FILENAME" "$SHA256SUM"
+ get_file "$URL" "$FILENAME" "$SHA256SUM"
mkdir -p "$PROJECT_DIR"
cd "$PROJECT_DIR"
ZIP_PREFIX=platform-tools
diff --git a/app/deps/adb_windows.sh b/app/deps/adb_windows.sh
index b9bd05dc..69287b1d 100755
--- a/app/deps/adb_windows.sh
+++ b/app/deps/adb_windows.sh
@@ -1,21 +1,21 @@
#!/usr/bin/env bash
set -ex
-DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
-cd "$DEPS_DIR"
-. common
+. $(dirname ${BASH_SOURCE[0]})/_init "$@"
VERSION=36.0.0
-FILENAME=platform-tools_r$VERSION-win.zip
-PROJECT_DIR=platform-tools-$VERSION-windows
+URL="https://dl.google.com/android/repository/platform-tools_r$VERSION-win.zip"
SHA256SUM=12c2841f354e92a0eb2fd7bf6f0f9bf8538abce7bd6b060ac8349d6f6a61107c
+PROJECT_DIR="platform-tools-$VERSION-windows"
+FILENAME="$PROJECT_DIR.zip"
+
cd "$SOURCES_DIR"
if [[ -d "$PROJECT_DIR" ]]
then
echo "$PWD/$PROJECT_DIR" found
else
- get_file "https://dl.google.com/android/repository/$FILENAME" "$FILENAME" "$SHA256SUM"
+ get_file "$URL" "$FILENAME" "$SHA256SUM"
mkdir -p "$PROJECT_DIR"
cd "$PROJECT_DIR"
ZIP_PREFIX=platform-tools
diff --git a/app/deps/dav1d.sh b/app/deps/dav1d.sh
index 3069b6fe..46fa3954 100755
--- a/app/deps/dav1d.sh
+++ b/app/deps/dav1d.sh
@@ -1,22 +1,22 @@
#!/usr/bin/env bash
set -ex
-DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
-cd "$DEPS_DIR"
-. common
+. $(dirname ${BASH_SOURCE[0]})/_init
process_args "$@"
VERSION=1.5.0
-FILENAME=dav1d-$VERSION.tar.gz
-PROJECT_DIR=dav1d-$VERSION
+URL="https://code.videolan.org/videolan/dav1d/-/archive/$VERSION/dav1d-$VERSION.tar.gz"
SHA256SUM=78b15d9954b513ea92d27f39362535ded2243e1b0924fde39f37a31ebed5f76b
+PROJECT_DIR="dav1d-$VERSION"
+FILENAME="$PROJECT_DIR.tar.gz"
+
cd "$SOURCES_DIR"
if [[ -d "$PROJECT_DIR" ]]
then
echo "$PWD/$PROJECT_DIR" found
else
- get_file "https://code.videolan.org/videolan/dav1d/-/archive/$VERSION/$FILENAME" "$FILENAME" "$SHA256SUM"
+ get_file "$URL" "$FILENAME" "$SHA256SUM"
tar xf "$FILENAME" # First level directory is "$PROJECT_DIR"
fi
diff --git a/app/deps/ffmpeg.sh b/app/deps/ffmpeg.sh
index fb8b9a25..d6cfad54 100755
--- a/app/deps/ffmpeg.sh
+++ b/app/deps/ffmpeg.sh
@@ -1,22 +1,22 @@
#!/usr/bin/env bash
set -ex
-DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
-cd "$DEPS_DIR"
-. common
+. $(dirname ${BASH_SOURCE[0]})/_init
process_args "$@"
VERSION=7.1.1
-FILENAME=ffmpeg-$VERSION.tar.xz
-PROJECT_DIR=ffmpeg-$VERSION
+URL="https://ffmpeg.org/releases/ffmpeg-$VERSION.tar.xz"
SHA256SUM=733984395e0dbbe5c046abda2dc49a5544e7e0e1e2366bba849222ae9e3a03b1
+PROJECT_DIR="ffmpeg-$VERSION"
+FILENAME="$PROJECT_DIR.tar.xz"
+
cd "$SOURCES_DIR"
if [[ -d "$PROJECT_DIR" ]]
then
echo "$PWD/$PROJECT_DIR" found
else
- get_file "https://ffmpeg.org/releases/$FILENAME" "$FILENAME" "$SHA256SUM"
+ get_file "$URL" "$FILENAME" "$SHA256SUM"
tar xf "$FILENAME" # First level directory is "$PROJECT_DIR"
fi
diff --git a/app/deps/libusb.sh b/app/deps/libusb.sh
index 887a2a77..72170b9e 100755
--- a/app/deps/libusb.sh
+++ b/app/deps/libusb.sh
@@ -1,22 +1,22 @@
#!/usr/bin/env bash
set -ex
-DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
-cd "$DEPS_DIR"
-. common
+. $(dirname ${BASH_SOURCE[0]})/_init
process_args "$@"
VERSION=1.0.29
-FILENAME=libusb-$VERSION.tar.gz
-PROJECT_DIR=libusb-$VERSION
+URL="https://github.com/libusb/libusb/archive/refs/tags/v$VERSION.tar.gz"
SHA256SUM=7c2dd39c0b2589236e48c93247c986ae272e27570942b4163cb00a060fcf1b74
+PROJECT_DIR="libusb-$VERSION"
+FILENAME="$PROJECT_DIR.tar.gz"
+
cd "$SOURCES_DIR"
if [[ -d "$PROJECT_DIR" ]]
then
echo "$PWD/$PROJECT_DIR" found
else
- get_file "https://github.com/libusb/libusb/archive/refs/tags/v$VERSION.tar.gz" "$FILENAME" "$SHA256SUM"
+ get_file "$URL" "$FILENAME" "$SHA256SUM"
tar xf "$FILENAME" # First level directory is "$PROJECT_DIR"
fi
diff --git a/app/deps/sdl.sh b/app/deps/sdl.sh
index e04deb0d..f74ccf41 100755
--- a/app/deps/sdl.sh
+++ b/app/deps/sdl.sh
@@ -1,23 +1,24 @@
#!/usr/bin/env bash
set -ex
-DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
-cd "$DEPS_DIR"
-. common
+. $(dirname ${BASH_SOURCE[0]})/_init
process_args "$@"
VERSION=2.32.8
-FILENAME=SDL-$VERSION.tar.gz
-PROJECT_DIR=SDL-release-$VERSION
+URL="https://github.com/libsdl-org/SDL/archive/refs/tags/release-$VERSION.tar.gz"
SHA256SUM=dd35e05644ae527848d02433bec24dd0ea65db59faecf1a0e5d1880c533dac2c
+PROJECT_DIR="sdl-$VERSION"
+FILENAME="$PROJECT_DIR.tar.gz"
+
cd "$SOURCES_DIR"
if [[ -d "$PROJECT_DIR" ]]
then
echo "$PWD/$PROJECT_DIR" found
else
- get_file "https://github.com/libsdl-org/SDL/archive/refs/tags/release-$VERSION.tar.gz" "$FILENAME" "$SHA256SUM"
- tar xf "$FILENAME" # First level directory is "$PROJECT_DIR"
+ get_file "$URL" "$FILENAME" "$SHA256SUM"
+ tar xf "$FILENAME" # First level directory is "SDL-release-$VERSION"
+ mv "SDL-release-$VERSION" "$PROJECT_DIR"
fi
mkdir -p "$BUILD_DIR/$PROJECT_DIR"
diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc
index f75a8976..07df992f 100644
--- a/app/scrcpy-windows.rc
+++ b/app/scrcpy-windows.rc
@@ -13,7 +13,7 @@ BEGIN
VALUE "LegalCopyright", "Romain Vimont, Genymobile"
VALUE "OriginalFilename", "scrcpy.exe"
VALUE "ProductName", "scrcpy"
- VALUE "ProductVersion", "3.3.2"
+ VALUE "ProductVersion", "3.3.4"
END
END
BLOCK "VarFileInfo"
diff --git a/app/scrcpy.1 b/app/scrcpy.1
index d72fda13..d6940449 100644
--- a/app/scrcpy.1
+++ b/app/scrcpy.1
@@ -852,7 +852,7 @@ Report bugs to .
.SH COPYRIGHT
Copyright \(co 2018 Genymobile
-Copyright \(co 2018\-2025 Romain Vimont
+Copyright \(co 2018\-2026 Romain Vimont
Licensed under the Apache License, Version 2.0.
diff --git a/app/src/device_msg.c b/app/src/device_msg.c
index 7621c040..2172d59b 100644
--- a/app/src/device_msg.c
+++ b/app/src/device_msg.c
@@ -53,7 +53,7 @@ sc_device_msg_deserialize(const uint8_t *buf, size_t len,
}
uint16_t id = sc_read16be(&buf[1]);
size_t size = sc_read16be(&buf[3]);
- if (size < len - 5) {
+ if (size > len - 5) {
return 0; // not available
}
uint8_t *data = malloc(size);
diff --git a/app/src/display.c b/app/src/display.c
index aee8ef80..15f9a1f1 100644
--- a/app/src/display.c
+++ b/app/src/display.c
@@ -170,6 +170,7 @@ 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);
if (r) {
LOGE("Could not ref frame: %d", r);
@@ -181,6 +182,11 @@ sc_display_set_pending_frame(struct sc_display *display, const AVFrame *frame) {
return true;
}
+// Forward declaration
+static bool
+sc_display_update_texture_internal(struct sc_display *display,
+ const AVFrame *frame);
+
static bool
sc_display_apply_pending(struct sc_display *display) {
if (display->pending.flags & SC_DISPLAY_PENDING_FLAG_SIZE) {
@@ -196,7 +202,8 @@ sc_display_apply_pending(struct sc_display *display) {
if (display->pending.flags & SC_DISPLAY_PENDING_FLAG_FRAME) {
assert(display->pending.frame);
- bool ok = sc_display_update_texture(display, display->pending.frame);
+ bool ok = sc_display_update_texture_internal(display,
+ display->pending.frame);
if (!ok) {
return false;
}
diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c
index 0d19919e..e43c7ce4 100644
--- a/app/tests/test_control_msg_serialize.c
+++ b/app/tests/test_control_msg_serialize.c
@@ -411,6 +411,26 @@ static void test_serialize_open_hard_keyboard(void) {
assert(!memcmp(buf, expected, sizeof(expected)));
}
+static void test_serialize_start_app(void) {
+ struct sc_control_msg msg = {
+ .type = SC_CONTROL_MSG_TYPE_START_APP,
+ .start_app = {
+ .name = "firefox",
+ },
+ };
+
+ uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
+ size_t size = sc_control_msg_serialize(&msg, buf);
+ assert(size == 9);
+
+ const uint8_t expected[] = {
+ SC_CONTROL_MSG_TYPE_START_APP,
+ 7, // length
+ 'f', 'i', 'r', 'e', 'f', 'o', 'x', // app name
+ };
+ assert(!memcmp(buf, expected, sizeof(expected)));
+}
+
static void test_serialize_reset_video(void) {
struct sc_control_msg msg = {
.type = SC_CONTROL_MSG_TYPE_RESET_VIDEO,
@@ -448,6 +468,7 @@ int main(int argc, char *argv[]) {
test_serialize_uhid_input();
test_serialize_uhid_destroy();
test_serialize_open_hard_keyboard();
+ test_serialize_start_app();
test_serialize_reset_video();
return 0;
}
diff --git a/build.gradle b/build.gradle
index 81c91d37..8c671e6f 100644
--- a/build.gradle
+++ b/build.gradle
@@ -7,7 +7,7 @@ buildscript {
mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:8.7.1'
+ classpath 'com.android.tools.build:gradle:8.13.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
diff --git a/doc/build.md b/doc/build.md
index d6e067d3..e1873f93 100644
--- a/doc/build.md
+++ b/doc/build.md
@@ -154,7 +154,11 @@ install it manually and make it available from the `PATH`:
export PATH="$JAVA_HOME/bin:$PATH"
```
-### Mac OS
+When following the rest of the build instructions below, make sure you use the
+MinGW terminal within MSYS2.
+
+
+### macOS
Install the packages with [Homebrew]:
@@ -172,8 +176,7 @@ Additionally, if you want to build the server, install Java 17 from Caskroom, an
make it available from the `PATH`:
```bash
-brew tap homebrew/cask-versions
-brew install adoptopenjdk/openjdk/adoptopenjdk17
+brew install openjdk@17
export JAVA_HOME="$(/usr/libexec/java_home --version 1.17)"
export PATH="$JAVA_HOME/bin:$PATH"
```
@@ -233,10 +236,10 @@ install` must be run as root)._
#### Option 2: Use prebuilt server
- - [`scrcpy-server-v3.3.2`][direct-scrcpy-server]
- SHA-256: `2ee5ca0863ef440f5b7c75856bb475c5283d0a8359cb370b1c161314fd29dfd9`
+ - [`scrcpy-server-v3.3.4`][direct-scrcpy-server]
+ SHA-256: `8588238c9a5a00aa542906b6ec7e6d5541d9ffb9b5d0f6e1bc0e365e2303079e`
-[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.2/scrcpy-server-v3.3.2
+[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-server-v3.3.4
Download the prebuilt server somewhere, and specify its path during the Meson
configuration:
@@ -271,7 +274,7 @@ This installs several files:
- `/usr/local/bin/scrcpy` (main app)
- `/usr/local/share/scrcpy/scrcpy-server` (server to push to the device)
- `/usr/local/share/man/man1/scrcpy.1` (manpage)
- - `/usr/local/share/icons/hicolor/256x256/apps/icon.png` (app icon)
+ - `/usr/local/share/icons/hicolor/256x256/apps/scrcpy.png` (app icon)
- `/usr/local/share/zsh/site-functions/_scrcpy` (zsh completion)
- `/usr/local/share/bash-completion/completions/scrcpy` (bash completion)
diff --git a/doc/develop.md b/doc/develop.md
index 21949ea6..61102bf4 100644
--- a/doc/develop.md
+++ b/doc/develop.md
@@ -409,8 +409,8 @@ with any client which uses the same protocol.
For simplicity, some [server-specific options] have been added to produce raw
streams easily:
- - `send_device_meta=false`: disable the device metata (in practice, the device
- name) sent on the _first_ socket
+ - `send_device_meta=false`: disable the device metadata (in practice, the
+ device name) sent on the _first_ socket
- `send_frame_meta=false`: disable the 12-byte header for each packet
- `send_dummy_byte`: disable the dummy byte sent on forward connections
- `send_codec_meta`: disable the codec information (and initial device size for
diff --git a/doc/linux.md b/doc/linux.md
index 7498ed98..fd9eca8c 100644
--- a/doc/linux.md
+++ b/doc/linux.md
@@ -6,11 +6,11 @@
Download a static build of the [latest release]:
- - [`scrcpy-linux-x86_64-v3.3.2.tar.gz`][direct-linux-x86_64] (x86_64)
- SHA-256: `92bed0fa274b9165eb8740e07cf2e2692ebe09ad6911175b0ee42e08799dc51c`
+ - [`scrcpy-linux-x86_64-v3.3.4.tar.gz`][direct-linux-x86_64] (x86_64)
+ SHA-256: `0305d98c06178c67e12427bbf340c436d0d58c9e2a39bf9ffbbf8f54d7ef95a5`
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
-[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.2/scrcpy-linux-x86_64-v3.3.2.tar.gz
+[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-linux-x86_64-v3.3.4.tar.gz
and extract it.
diff --git a/doc/macos.md b/doc/macos.md
index 81e0d65d..5435e613 100644
--- a/doc/macos.md
+++ b/doc/macos.md
@@ -6,15 +6,14 @@
Download a static build of the [latest release]:
- - [`scrcpy-macos-aarch64-v3.3.2.tar.gz`][direct-macos-aarch64] (aarch64)
- SHA-256: `a213eeff8ac95893e69c4bc6a001a402c6680dbfcb74cb353c0124184ed88e8d`
-
- - [`scrcpy-macos-x86_64-v3.3.2.tar.gz`][direct-macos-x86_64] (x86_64)
- SHA-256: `2a1b27fbb67821a886c7e8dea641899836c0abbe7afd37905584b99bcd21bc04`
+ - [`scrcpy-macos-aarch64-v3.3.4.tar.gz`][direct-macos-aarch64] (aarch64)
+ SHA-256: `8fef43520405dd523c74e1530ac68febcc5a405ea89712c874936675da8513dd`
+ - [`scrcpy-macos-x86_64-v3.3.4.tar.gz`][direct-macos-x86_64] (x86_64)
+ SHA-256: `cf9b3453a33279b6009dfb256b1a84c374bd4c30a71edd74bacab28d72a5d929`
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
-[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.2/scrcpy-macos-aarch64-v3.3.2.tar.gz
-[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.2/scrcpy-macos-x86_64-v3.3.2.tar.gz
+[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-macos-aarch64-v3.3.4.tar.gz
+[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-macos-x86_64-v3.3.4.tar.gz
and extract it.
diff --git a/doc/v4l2.md b/doc/v4l2.md
index 54272b2b..b50eb9b8 100644
--- a/doc/v4l2.md
+++ b/doc/v4l2.md
@@ -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
`exclusive_caps` mode:
-```
+```bash
sudo modprobe v4l2loopback exclusive_caps=1
```
@@ -38,6 +38,13 @@ v4l2-ctl --list-devices
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:
```bash
diff --git a/doc/windows.md b/doc/windows.md
index 63fff892..cdc30bdc 100644
--- a/doc/windows.md
+++ b/doc/windows.md
@@ -6,14 +6,14 @@
Download the [latest release]:
- - [`scrcpy-win64-v3.3.2.zip`][direct-win64] (64-bit)
- SHA-256: `8f7b19371657b872e271e6b02a0c758c61c6e31e032e9df55a83aa3aab960bfa`
- - [`scrcpy-win32-v3.3.2.zip`][direct-win32] (32-bit)
- SHA-256: `cff2bbebdcfe14a023b77cd601fc4420b5631b19bd4b09ce4dcd4e5bf8e63244`
+ - [`scrcpy-win64-v3.3.4.zip`][direct-win64] (64-bit)
+ SHA-256: `d8a155b7c180b7ca4cdadd40712b8750b63f3aab48cb5b8a2a39ac2d0d4c5d38`
+ - [`scrcpy-win32-v3.3.4.zip`][direct-win32] (32-bit)
+ SHA-256: `393f7d5379dabd8aacc41184755c3d0df975cd2861353cb7a8d50e0835e2eb72`
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
-[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.2/scrcpy-win64-v3.3.2.zip
-[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.2/scrcpy-win32-v3.3.2.zip
+[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-win64-v3.3.4.zip
+[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-win32-v3.3.4.zip
and extract it.
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index b34b7096..fbd6c374 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
# https://gradle.org/release-checksums/
-distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab
+distributionSha256Sum=bd71102213493060956ec229d946beee57158dbd89d0e62b91bca0fa2c5f3531
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/install_release.sh b/install_release.sh
index f6e94dae..5930e54e 100755
--- a/install_release.sh
+++ b/install_release.sh
@@ -2,8 +2,8 @@
set -e
BUILDDIR=build-auto
-PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.3.2/scrcpy-server-v3.3.2
-PREBUILT_SERVER_SHA256=2ee5ca0863ef440f5b7c75856bb475c5283d0a8359cb370b1c161314fd29dfd9
+PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-server-v3.3.4
+PREBUILT_SERVER_SHA256=8588238c9a5a00aa542906b6ec7e6d5541d9ffb9b5d0f6e1bc0e365e2303079e
echo "[scrcpy] Downloading prebuilt server..."
wget "$PREBUILT_SERVER_URL" -O scrcpy-server
diff --git a/meson.build b/meson.build
index 4a03d122..fc82dee1 100644
--- a/meson.build
+++ b/meson.build
@@ -1,5 +1,5 @@
project('scrcpy', 'c',
- version: '3.3.2',
+ version: '3.3.4',
meson_version: '>= 0.49',
default_options: [
'c_std=c11',
diff --git a/server/build.gradle b/server/build.gradle
index 7afd1e7a..abbbb60a 100644
--- a/server/build.gradle
+++ b/server/build.gradle
@@ -1,15 +1,15 @@
apply plugin: 'com.android.application'
android {
- namespace 'com.genymobile.scrcpy'
- compileSdk 35
+ namespace = 'com.genymobile.scrcpy'
+ compileSdk 36
defaultConfig {
- applicationId "com.genymobile.scrcpy"
+ applicationId = "com.genymobile.scrcpy"
minSdkVersion 21
- targetSdkVersion 35
- versionCode 30302
- versionName "3.3.2"
- testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+ targetSdkVersion 36
+ versionCode 30304
+ versionName "3.3.4"
+ testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
@@ -18,8 +18,11 @@ android {
}
}
buildFeatures {
- buildConfig true
- aidl true
+ buildConfig = true
+ aidl = true
+ }
+ lint {
+ disable 'UseRequiresApi'
}
}
diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh
index dd1fe623..435ac8f6 100755
--- a/server/build_without_gradle.sh
+++ b/server/build_without_gradle.sh
@@ -12,10 +12,10 @@
set -e
SCRCPY_DEBUG=false
-SCRCPY_VERSION_NAME=3.3.2
+SCRCPY_VERSION_NAME=3.3.4
-PLATFORM=${ANDROID_PLATFORM:-35}
-BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.0.0}
+PLATFORM=${ANDROID_PLATFORM:-36}
+BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-36.0.0}
PLATFORM_TOOLS="$ANDROID_HOME/platforms/android-$PLATFORM"
BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS"
@@ -86,7 +86,7 @@ javac -encoding UTF-8 -bootclasspath "$ANDROID_JAR" \
echo "Dexing..."
cd "$CLASSES_DIR"
-if [[ $PLATFORM -lt 31 ]]
+if [[ "${PLATFORM%%.*}" -lt 31 ]]
then
# use dx
"$BUILD_TOOLS_DIR/dx" --dex --output "$BUILD_DIR/classes.dex" \
diff --git a/server/src/main/aidl/android/view/IDisplayWindowListener.aidl b/server/src/main/aidl/android/view/IDisplayWindowListener.aidl
index 3e664e81..0d1f1979 100644
--- a/server/src/main/aidl/android/view/IDisplayWindowListener.aidl
+++ b/server/src/main/aidl/android/view/IDisplayWindowListener.aidl
@@ -48,27 +48,4 @@ oneway interface IDisplayWindowListener {
* Called when a display is removed from the hierarchy.
*/
void onDisplayRemoved(int displayId);
-
- /**
- * Called when fixed rotation is started on a display.
- */
- void onFixedRotationStarted(int displayId, int newRotation);
-
- /**
- * Called when the previous fixed rotation on a display is finished.
- */
- void onFixedRotationFinished(int displayId);
-
- /**
- * Called when the keep clear ares on a display have changed.
- */
- void onKeepClearAreasChanged(int displayId, in List restricted, in List unrestricted);
-
- /**
- * Called when the eligibility of the desktop mode for a display have changed.
- */
- void onDesktopModeEligibleChanged(int displayId);
-
- void onDisplayAddSystemDecorations(int displayId);
- void onDisplayRemoveSystemDecorations(int displayId);
}
diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java
index 77018afa..a5816c32 100644
--- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java
+++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java
@@ -196,6 +196,7 @@ public final class CleanUp {
// Needed for workarounds
prepareMainLooper();
+ Workarounds.apply();
int displayId = Integer.parseInt(args[0]);
int restoreStayOn = Integer.parseInt(args[1]);
diff --git a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java
index 7c0f3645..5d41a8f3 100644
--- a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java
+++ b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java
@@ -109,8 +109,10 @@ public final class FakeContext extends ContextWrapper {
}
// "semclipboard" is a Samsung-internal service
- // See
- if (Context.CLIPBOARD_SERVICE.equals(name) || "semclipboard".equals(name)) {
+ // See:
+ // -
+ // -
+ if (Context.CLIPBOARD_SERVICE.equals(name) || "semclipboard".equals(name) || Context.ACTIVITY_SERVICE.equals(name)) {
try {
Field field = service.getClass().getDeclaredField("mContext");
field.setAccessible(true);
diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java
index a08c948c..04e4a837 100644
--- a/server/src/main/java/com/genymobile/scrcpy/Server.java
+++ b/server/src/main/java/com/genymobile/scrcpy/Server.java
@@ -225,8 +225,12 @@ public final class Server {
}
private static void internalMain(String... args) throws Exception {
+ Thread.UncaughtExceptionHandler defaultHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
Ln.e("Exception on thread " + t, e);
+ if (defaultHandler != null) {
+ defaultHandler.uncaughtException(t, e);
+ }
});
prepareMainLooper();
diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java
index b89f19ae..1e012de6 100644
--- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java
+++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java
@@ -6,9 +6,9 @@ import com.genymobile.scrcpy.util.Ln;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Application;
+import android.app.Instrumentation;
import android.content.AttributionSource;
import android.content.Context;
-import android.content.ContextWrapper;
import android.content.pm.ApplicationInfo;
import android.media.AudioAttributes;
import android.media.AudioManager;
@@ -103,10 +103,7 @@ public final class Workarounds {
private static void fillAppContext() {
try {
- Application app = new Application();
- Field baseField = ContextWrapper.class.getDeclaredField("mBase");
- baseField.setAccessible(true);
- baseField.set(app, FakeContext.get());
+ Application app = Instrumentation.newApplication(Application.class, FakeContext.get());
// activityThread.mInitialApplication = app;
Field mInitialApplicationField = ACTIVITY_THREAD_CLASS.getDeclaredField("mInitialApplication");
diff --git a/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLRunner.java b/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLRunner.java
index 86bd1859..06bcd7eb 100644
--- a/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLRunner.java
+++ b/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLRunner.java
@@ -1,6 +1,7 @@
package com.genymobile.scrcpy.opengl;
import com.genymobile.scrcpy.device.Size;
+import com.genymobile.scrcpy.util.Threads;
import android.graphics.SurfaceTexture;
import android.opengl.EGL14;
@@ -15,6 +16,7 @@ import android.os.Handler;
import android.os.HandlerThread;
import android.view.Surface;
+import java.util.concurrent.Callable;
import java.util.concurrent.Semaphore;
public final class OpenGLRunner {
@@ -80,31 +82,17 @@ public final class OpenGLRunner {
public Surface start(Size inputSize, Size outputSize, Surface outputSurface) throws OpenGLException {
initOnce();
- // Simulate CompletableFuture, but working for all Android versions
- final Semaphore sem = new Semaphore(0);
- Throwable[] throwableRef = new Throwable[1];
-
// The whole OpenGL execution must be performed on a Handler, so that SurfaceTexture.setOnFrameAvailableListener() works correctly.
// See
- handler.post(() -> {
- try {
- run(inputSize, outputSize, outputSurface);
- } catch (Throwable throwable) {
- throwableRef[0] = throwable;
- } finally {
- sem.release();
- }
- });
-
try {
- sem.acquire();
- } catch (InterruptedException e) {
- // Behave as if this method call was synchronous
- Thread.currentThread().interrupt();
- }
-
- Throwable throwable = throwableRef[0];
- if (throwable != null) {
+ Threads.executeSynchronouslyOn(handler, new Callable() {
+ @Override
+ public Void call() throws Exception {
+ run(inputSize, outputSize, outputSurface);
+ return null;
+ }
+ });
+ } catch (Throwable throwable) {
if (throwable instanceof OpenGLException) {
throw (OpenGLException) throwable;
}
diff --git a/server/src/main/java/com/genymobile/scrcpy/util/Ln.java b/server/src/main/java/com/genymobile/scrcpy/util/Ln.java
index c0700125..811a4f31 100644
--- a/server/src/main/java/com/genymobile/scrcpy/util/Ln.java
+++ b/server/src/main/java/com/genymobile/scrcpy/util/Ln.java
@@ -74,9 +74,11 @@ public final class Ln {
public static void w(String message, Throwable throwable) {
if (isEnabled(Level.WARN)) {
Log.w(TAG, message, throwable);
- CONSOLE_ERR.print(PREFIX + "WARN: " + message + '\n');
- if (throwable != null) {
- throwable.printStackTrace(CONSOLE_ERR);
+ synchronized (CONSOLE_ERR) {
+ CONSOLE_ERR.print(PREFIX + "WARN: " + message + '\n');
+ if (throwable != null) {
+ throwable.printStackTrace(CONSOLE_ERR);
+ }
}
}
}
@@ -88,9 +90,11 @@ public final class Ln {
public static void e(String message, Throwable throwable) {
if (isEnabled(Level.ERROR)) {
Log.e(TAG, message, throwable);
- CONSOLE_ERR.print(PREFIX + "ERROR: " + message + '\n');
- if (throwable != null) {
- throwable.printStackTrace(CONSOLE_ERR);
+ synchronized (CONSOLE_ERR) {
+ CONSOLE_ERR.print(PREFIX + "ERROR: " + message + '\n');
+ if (throwable != null) {
+ throwable.printStackTrace(CONSOLE_ERR);
+ }
}
}
}
diff --git a/server/src/main/java/com/genymobile/scrcpy/util/Threads.java b/server/src/main/java/com/genymobile/scrcpy/util/Threads.java
new file mode 100644
index 00000000..c561d1ef
--- /dev/null
+++ b/server/src/main/java/com/genymobile/scrcpy/util/Threads.java
@@ -0,0 +1,43 @@
+package com.genymobile.scrcpy.util;
+
+import android.os.Handler;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.Semaphore;
+
+public final class Threads {
+ private Threads() {
+ // not instantiable
+ }
+
+ public static T executeSynchronouslyOn(Handler handler, Callable 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];
+ }
+}
diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java
index 792b3a8a..e933f60e 100644
--- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java
+++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java
@@ -25,6 +25,7 @@ public class NewDisplayCapture extends SurfaceCapture {
// 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_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_SUPPORTS_TOUCH = 1 << 6;
private static final int VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT = 1 << 7;
@@ -169,6 +170,7 @@ public class NewDisplayCapture extends SurfaceCapture {
int virtualDisplayId;
try {
int flags = VIRTUAL_DISPLAY_FLAG_PUBLIC
+ | VIRTUAL_DISPLAY_FLAG_PRESENTATION
| VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
| VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH
| VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT;
diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java
index a12470a4..9e84ec00 100644
--- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java
+++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java
@@ -139,7 +139,13 @@ public final class DisplayManager {
int layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo);
int flags = cls.getDeclaredField("flags").getInt(displayInfo);
int dpi = cls.getDeclaredField("logicalDensityDpi").getInt(displayInfo);
- String uniqueId = (String) cls.getDeclaredField("uniqueId").get(displayInfo);
+ String uniqueId;
+ try {
+ uniqueId = (String) cls.getDeclaredField("uniqueId").get(displayInfo);
+ } catch (NoSuchFieldException e) {
+ // This field might not exist:
+ uniqueId = null;
+ }
return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, dpi, uniqueId);
} catch (ReflectiveOperationException e) {
throw new AssertionError(e);
diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayWindowListener.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayWindowListener.java
index b8b00e44..1573d817 100644
--- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayWindowListener.java
+++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayWindowListener.java
@@ -1,10 +1,11 @@
package com.genymobile.scrcpy.wrappers;
-import android.content.res.Configuration;
-import android.graphics.Rect;
-import android.view.IDisplayWindowListener;
+import com.genymobile.scrcpy.util.Ln;
-import java.util.List;
+import android.content.res.Configuration;
+import android.os.Parcel;
+import android.os.RemoteException;
+import android.view.IDisplayWindowListener;
public class DisplayWindowListener extends IDisplayWindowListener.Stub {
@Override
@@ -23,32 +24,14 @@ public class DisplayWindowListener extends IDisplayWindowListener.Stub {
}
@Override
- public void onFixedRotationStarted(int displayId, int newRotation) {
- // empty default implementation
- }
-
- @Override
- public void onFixedRotationFinished(int displayId) {
- // empty default implementation
- }
-
- @Override
- public void onKeepClearAreasChanged(int displayId, List restricted, List unrestricted) {
- // empty default implementation
- }
-
- @Override
- public void onDesktopModeEligibleChanged(int displayId) {
- // empty default implementation
- }
-
- @Override
- public void onDisplayAddSystemDecorations(int displayId) {
- // empty default implementation
- }
-
- @Override
- public void onDisplayRemoveSystemDecorations(int displayId) {
- // empty default implementation
+ public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
+ try {
+ 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;
+ }
}
}