Compare commits

..

3 Commits

Author SHA1 Message Date
Romain Vimont
b81e0f92b5 Add horizontal scrolling support for HID mouse
PR #6172 <https://github.com/Genymobile/scrcpy/pull/6172>
2025-06-20 18:31:31 +02:00
Romain Vimont
be40ee5dd9 Fix HID mouse support with SDL precise input
Over HID, only integral scroll values can be sent. When SDL precise
scrolling is active, scroll events may include fractional values (e.g.,
0.05), which are truncated to 0 in the HID event.

To fix the problem, use the integral scroll value reported by SDL, which
internally accumulates fractional deltas.

Fixes #6156 <https://github.com/Genymobile/scrcpy/issues/6156>
PR #6172 <https://github.com/Genymobile/scrcpy/pull/6172>
2025-06-20 18:31:24 +02:00
Romain Vimont
0498459c1f Extend value range for SDK mouse scrolling
SDL precise scrolling can sometimes produce values greater than 1 or
less than -1.

On the wire, the value is encoded as a 16-bit fixed-point number.

Previously, the range was interpreted as [-1, 1], using 1 bit for the
integral part (the sign) and 15 bits for the fractional part.

To support larger values, interpret the range as [-16, 16] instead,
using 5 bits for the integral part and 11 bits for the fractional part
(which is more than enough).

PR #6172 <https://github.com/Genymobile/scrcpy/pull/6172>
2025-06-20 18:31:17 +02:00
45 changed files with 245 additions and 277 deletions

View File

@@ -202,7 +202,8 @@ jobs:
- name: Install dependencies
run: |
brew install meson nasm libiconv zlib automake autoconf libtool
brew install meson ninja nasm libiconv zlib automake autoconf \
libtool
- name: Build
env:
@@ -229,7 +230,7 @@ jobs:
path: release/work/build-macos-aarch64/dist-tar/
build-macos-x86_64:
runs-on: macos-15-intel
runs-on: macos-13
steps:
- name: Check architecture
run: |
@@ -244,7 +245,7 @@ jobs:
uses: actions/checkout@v4
- 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
- name: Build

View File

@@ -2,7 +2,7 @@
source for the project. Do not download releases from random websites, even if
their name contains `scrcpy`.**
# scrcpy (v3.3.4)
# scrcpy (v3.3)
<img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />

View File

@@ -1,21 +1,21 @@
#!/usr/bin/env bash
set -ex
. $(dirname ${BASH_SOURCE[0]})/_init "$@"
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
VERSION=36.0.0
URL="https://dl.google.com/android/repository/platform-tools_r$VERSION-linux.zip"
FILENAME=platform-tools_r$VERSION-linux.zip
PROJECT_DIR=platform-tools-$VERSION-linux
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 "$URL" "$FILENAME" "$SHA256SUM"
get_file "https://dl.google.com/android/repository/$FILENAME" "$FILENAME" "$SHA256SUM"
mkdir -p "$PROJECT_DIR"
cd "$PROJECT_DIR"
ZIP_PREFIX=platform-tools

View File

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

View File

@@ -1,13 +1,13 @@
#!/usr/bin/env bash
set -ex
. $(dirname ${BASH_SOURCE[0]})/_init "$@"
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
VERSION=36.0.0
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"
FILENAME=platform-tools_r$VERSION-win.zip
PROJECT_DIR=platform-tools-$VERSION-windows
SHA256SUM=24bd8bebbbb58b9870db202b5c6775c4a49992632021c60750d9d8ec8179d5f0
cd "$SOURCES_DIR"
@@ -15,7 +15,7 @@ if [[ -d "$PROJECT_DIR" ]]
then
echo "$PWD/$PROJECT_DIR" found
else
get_file "$URL" "$FILENAME" "$SHA256SUM"
get_file "https://dl.google.com/android/repository/$FILENAME" "$FILENAME" "$SHA256SUM"
mkdir -p "$PROJECT_DIR"
cd "$PROJECT_DIR"
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
process_args() {
if [[ $# != 3 ]]
then
# <host>: linux, macos, win32 or win64
# <host>: win32 or win64
# <build_type>: native or cross
# <link_type>: static or shared
echo "Syntax: $0 <host> <build_type> <link_type>" >&2
@@ -11,8 +12,8 @@ process_args() {
fi
HOST="$1"
BUILD_TYPE="$2"
LINK_TYPE="$3"
BUILD_TYPE="$2" # native or cross
LINK_TYPE="$3" # static or shared
DIRNAME="$HOST-$BUILD_TYPE-$LINK_TYPE"
if [[ "$BUILD_TYPE" != native && "$BUILD_TYPE" != cross ]]

View File

@@ -1,22 +1,22 @@
#!/usr/bin/env bash
set -ex
. $(dirname ${BASH_SOURCE[0]})/_init
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
process_args "$@"
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
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 "$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"
fi

View File

@@ -1,22 +1,22 @@
#!/usr/bin/env bash
set -ex
. $(dirname ${BASH_SOURCE[0]})/_init
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
process_args "$@"
VERSION=7.1.1
URL="https://ffmpeg.org/releases/ffmpeg-$VERSION.tar.xz"
FILENAME=ffmpeg-$VERSION.tar.xz
PROJECT_DIR=ffmpeg-$VERSION
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 "$URL" "$FILENAME" "$SHA256SUM"
get_file "https://ffmpeg.org/releases/$FILENAME" "$FILENAME" "$SHA256SUM"
tar xf "$FILENAME" # First level directory is "$PROJECT_DIR"
fi

View File

@@ -1,22 +1,22 @@
#!/usr/bin/env bash
set -ex
. $(dirname ${BASH_SOURCE[0]})/_init
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
process_args "$@"
VERSION=1.0.29
URL="https://github.com/libusb/libusb/archive/refs/tags/v$VERSION.tar.gz"
FILENAME=libusb-$VERSION.tar.gz
PROJECT_DIR=libusb-$VERSION
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 "$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"
fi

View File

@@ -1,24 +1,23 @@
#!/usr/bin/env bash
set -ex
. $(dirname ${BASH_SOURCE[0]})/_init
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
process_args "$@"
VERSION=2.32.8
URL="https://github.com/libsdl-org/SDL/archive/refs/tags/release-$VERSION.tar.gz"
FILENAME=SDL-$VERSION.tar.gz
PROJECT_DIR=SDL-release-$VERSION
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 "$URL" "$FILENAME" "$SHA256SUM"
tar xf "$FILENAME" # First level directory is "SDL-release-$VERSION"
mv "SDL-release-$VERSION" "$PROJECT_DIR"
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"
fi
mkdir -p "$BUILD_DIR/$PROJECT_DIR"
@@ -29,7 +28,7 @@ export CXXFLAGS="$CFLAGS"
if [[ -d "$DIRNAME" ]]
then
echo "'$PWD/$DIRNAME' already exists, not reconfigured"
echo "'$PWD/$HDIRNAME' already exists, not reconfigured"
cd "$DIRNAME"
else
mkdir "$DIRNAME"

View File

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

View File

@@ -103,7 +103,7 @@ argv_to_string(const char *const *argv, char *buf, size_t bufsize) {
static void
show_adb_installation_msg(void) {
#ifndef _WIN32
#ifndef __WINDOWS__
static const struct {
const char *binary;
const char *command;
@@ -331,7 +331,7 @@ sc_adb_reverse_remove(struct sc_intr *intr, const char *serial,
bool
sc_adb_push(struct sc_intr *intr, const char *serial, const char *local,
const char *remote, unsigned flags) {
#ifdef _WIN32
#ifdef __WINDOWS__
// Windows will parse the string, so the paths must be quoted
// (see sys/win/command.c)
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);
#ifdef _WIN32
#ifdef __WINDOWS__
free((void *) remote);
free((void *) local);
#endif
@@ -362,7 +362,7 @@ sc_adb_push(struct sc_intr *intr, const char *serial, const char *local,
bool
sc_adb_install(struct sc_intr *intr, const char *serial, const char *local,
unsigned flags) {
#ifdef _WIN32
#ifdef __WINDOWS__
// Windows will parse the string, so the local name must be quoted
// (see sys/win/command.c)
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);
#ifdef _WIN32
#ifdef __WINDOWS__
free((void *) local);
#endif

View File

@@ -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);

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);
if (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;
}
// 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) {
@@ -202,8 +196,7 @@ 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_internal(display,
display->pending.frame);
bool ok = sc_display_update_texture(display, display->pending.frame);
if (!ok) {
return false;
}

View File

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

View File

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

View File

@@ -93,7 +93,7 @@ struct scrcpy {
#ifdef _WIN32
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);
return TRUE;
}

View File

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

View File

@@ -2,7 +2,6 @@
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#ifdef _WIN32
# 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);
assert(right_len);
memcpy(newptr, (char *) ptr + (oldorigin * item_size),
right_len * item_size);
memcpy(newptr, (char *) ptr + (oldorigin * item_size), right_len * item_size);
if (size > right_len) {
memcpy((char *) newptr + (right_len * item_size), ptr,

View File

@@ -127,8 +127,8 @@ static void test_serialize_inject_scroll_event(void) {
.height = 1920,
},
},
.hscroll = 16,
.vscroll = -16,
.hscroll = 1,
.vscroll = -1,
.buttons = 1,
},
};
@@ -141,8 +141,8 @@ static void test_serialize_inject_scroll_event(void) {
SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT,
0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026
0x04, 0x38, 0x07, 0x80, // 1080 1920
0x7F, 0xFF, // 16 (float encoded as i16 in the range [-16, 16])
0x80, 0x00, // -16 (float encoded as i16 in the range [-16, 16])
0x7F, 0xFF, // 1 (float encoded as i16)
0x80, 0x00, // -1 (float encoded as i16)
0x00, 0x00, 0x00, 0x01, // 1
};
assert(!memcmp(buf, expected, sizeof(expected)));
@@ -411,26 +411,6 @@ 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,
@@ -468,7 +448,6 @@ 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;
}

View File

@@ -7,7 +7,7 @@ buildscript {
mavenCentral()
}
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
// in the individual module build.gradle files

View File

@@ -233,10 +233,10 @@ install` must be run as root)._
#### Option 2: Use prebuilt server
- [`scrcpy-server-v3.3.4`][direct-scrcpy-server]
<sub>SHA-256: `8588238c9a5a00aa542906b6ec7e6d5541d9ffb9b5d0f6e1bc0e365e2303079e`</sub>
- [`scrcpy-server-v3.3`][direct-scrcpy-server]
<sub>SHA-256: `351cb2edc7e4c2c75f09a7933fdabcf137be52e2602df154f24ec02db46e9e51`</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.3/scrcpy-server-v3.3
Download the prebuilt server somewhere, and specify its path during the Meson
configuration:
@@ -271,7 +271,7 @@ This installs several files:
- `/usr/local/bin/scrcpy` (main app)
- `/usr/local/share/scrcpy/scrcpy-server` (server to push to the device)
- `/usr/local/share/man/man1/scrcpy.1` (manpage)
- `/usr/local/share/icons/hicolor/256x256/apps/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/bash-completion/completions/scrcpy` (bash completion)

View File

@@ -6,11 +6,11 @@
Download a static build of the [latest release]:
- [`scrcpy-linux-x86_64-v3.3.4.tar.gz`][direct-linux-x86_64] (x86_64)
<sub>SHA-256: `0305d98c06178c67e12427bbf340c436d0d58c9e2a39bf9ffbbf8f54d7ef95a5`</sub>
- [`scrcpy-linux-x86_64-v3.3.tar.gz`][direct-linux-x86_64] (x86_64)
<sub>SHA-256: `a0abf37003c3c47a53c1b2a12420296a2b0ee323cf3610fd6fbf9d9bab9d99f3`</sub>
[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.3/scrcpy-linux-x86_64-v3.3.tar.gz
and extract it.

View File

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

View File

@@ -6,14 +6,14 @@
Download the [latest release]:
- [`scrcpy-win64-v3.3.4.zip`][direct-win64] (64-bit)
<sub>SHA-256: `d8a155b7c180b7ca4cdadd40712b8750b63f3aab48cb5b8a2a39ac2d0d4c5d38`</sub>
- [`scrcpy-win32-v3.3.4.zip`][direct-win32] (32-bit)
<sub>SHA-256: `393f7d5379dabd8aacc41184755c3d0df975cd2861353cb7a8d50e0835e2eb72`</sub>
- [`scrcpy-win64-v3.3.zip`][direct-win64] (64-bit)
<sub>SHA-256: `a120cb4be7cde2891af38e83d2008173a0b6b6b5e344b2dfe668d0f892999933`</sub>
- [`scrcpy-win32-v3.3.zip`][direct-win32] (32-bit)
<sub>SHA-256: `e409ab83f8c57bd6ac741d652635cab7699fcf3d384e233833872f117b993ca6`</sub>
[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-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-win32-v3.3.4.zip
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3/scrcpy-win64-v3.3.zip
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.3/scrcpy-win32-v3.3.zip
and extract it.

View File

@@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME
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/
distributionSha256Sum=bd71102213493060956ec229d946beee57158dbd89d0e62b91bca0fa2c5f3531
distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

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

View File

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

View File

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

View File

@@ -12,10 +12,10 @@
set -e
SCRCPY_DEBUG=false
SCRCPY_VERSION_NAME=3.3.4
SCRCPY_VERSION_NAME=3.3
PLATFORM=${ANDROID_PLATFORM:-36}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-36.0.0}
PLATFORM=${ANDROID_PLATFORM:-35}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.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" \

View File

@@ -48,4 +48,19 @@ 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<Rect> restricted, in List<Rect> unrestricted);
}

View File

@@ -196,7 +196,6 @@ public final class CleanUp {
// Needed for workarounds
prepareMainLooper();
Workarounds.apply();
int displayId = Integer.parseInt(args[0]);
int restoreStayOn = Integer.parseInt(args[1]);

View File

@@ -5,6 +5,7 @@ import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.AttributionSource;
import android.content.ClipboardManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.ContextWrapper;
@@ -90,11 +91,6 @@ public final class FakeContext extends ContextWrapper {
return this;
}
@Override
public Context createPackageContext(String packageName, int flags) {
return this;
}
@Override
public ContentResolver getContentResolver() {
return contentResolver;
@@ -108,13 +104,9 @@ public final class FakeContext extends ContextWrapper {
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)) {
if (Context.CLIPBOARD_SERVICE.equals(name)) {
try {
Field field = service.getClass().getDeclaredField("mContext");
Field field = ClipboardManager.class.getDeclaredField("mContext");
field.setAccessible(true);
field.set(service, this);
} catch (ReflectiveOperationException e) {

View File

@@ -225,12 +225,8 @@ 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();

View File

@@ -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,7 +103,10 @@ public final class Workarounds {
private static void fillAppContext() {
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;
Field mInitialApplicationField = ACTIVITY_THREAD_CLASS.getDeclaredField("mInitialApplication");

View File

@@ -1,7 +1,6 @@
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;
@@ -16,7 +15,6 @@ 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 {
@@ -82,17 +80,31 @@ public final class OpenGLRunner {
public Surface start(Size inputSize, Size outputSize, Surface outputSurface) throws OpenGLException {
initOnce();
// Simulate CompletableFuture, but working for all Android versions
final Semaphore sem = new Semaphore(0);
Throwable[] throwableRef = new Throwable[1];
// The whole OpenGL execution must be performed on a Handler, so that SurfaceTexture.setOnFrameAvailableListener() works correctly.
// See <https://github.com/Genymobile/scrcpy/issues/5444>
handler.post(() -> {
try {
run(inputSize, outputSize, outputSurface);
} catch (Throwable throwable) {
throwableRef[0] = throwable;
} finally {
sem.release();
}
});
try {
Threads.executeSynchronouslyOn(handler, new Callable<Void>() {
@Override
public Void call() throws Exception {
run(inputSize, outputSize, outputSurface);
return null;
}
});
} catch (Throwable throwable) {
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) {
throw (OpenGLException) throwable;
}

View File

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

View File

@@ -1,8 +1,13 @@
package com.genymobile.scrcpy.util;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.wrappers.ContentProvider;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.os.Build;
import java.io.IOException;
public final class Settings {
public static final String TABLE_SYSTEM = ContentProvider.TABLE_SYSTEM;
@@ -13,26 +18,66 @@ public final class Settings {
/* not instantiable */
}
public static String getValue(String table, String key) throws SettingsException {
try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) {
return provider.getValue(table, key);
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 {
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()) {
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 {
try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) {
provider.putValue(table, key, value);
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()) {
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 {
try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) {
String oldValue = provider.getValue(table, key);
if (!value.equals(oldValue)) {
provider.putValue(table, key, value);
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()) {
String oldValue = provider.getValue(table, key);
if (!value.equals(oldValue)) {
provider.putValue(table, key, value);
}
return oldValue;
} catch (SettingsException e) {
Ln.w("Could not get and put settings value via ContentProvider, fallback to settings process", e);
}
return oldValue;
}
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
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;
@@ -170,7 +169,6 @@ 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;

View File

@@ -129,26 +129,17 @@ public class ScreenCapture extends SurfaceCapture {
.createVirtualDisplay("scrcpy", inputSize.getWidth(), inputSize.getHeight(), displayId, surface);
Ln.d("Display: using DisplayManager API");
} catch (Exception displayManagerException) {
if (Build.BRAND.equalsIgnoreCase("oculus") && Build.MODEL.equalsIgnoreCase("Quest 3")) {
// Workaround for buggy createVirtualDisplay on Quest 3
try {
virtualDisplay = (VirtualDisplay) VirtualDisplay.class.getDeclaredConstructors()[0].newInstance(null, null, null, surface);
} catch (ReflectiveOperationException e) {
Ln.e("Could not create VirtualDisplay", e);
}
} else {
try {
display = createDisplay();
try {
display = createDisplay();
Size deviceSize = displayInfo.getSize();
int layerStack = displayInfo.getLayerStack();
setDisplaySurface(display, surface, deviceSize.toRect(), inputSize.toRect(), layerStack);
Ln.d("Display: using SurfaceControl API");
} catch (Exception surfaceControlException) {
Ln.e("Could not create display using DisplayManager", displayManagerException);
Ln.e("Could not create display using SurfaceControl", surfaceControlException);
throw new AssertionError("Could not create display");
}
Size deviceSize = displayInfo.getSize();
int layerStack = displayInfo.getLayerStack();
setDisplaySurface(display, surface, deviceSize.toRect(), inputSize.toRect(), layerStack);
Ln.d("Display: using SurfaceControl API");
} catch (Exception surfaceControlException) {
Ln.e("Could not create display using DisplayManager", displayManagerException);
Ln.e("Could not create display using SurfaceControl", surfaceControlException);
throw new AssertionError("Could not create display");
}
}

View File

@@ -139,13 +139,7 @@ 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;
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;
}
String uniqueId = (String) cls.getDeclaredField("uniqueId").get(displayInfo);
return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, dpi, uniqueId);
} catch (ReflectiveOperationException e) {
throw new AssertionError(e);

View File

@@ -1,12 +1,11 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.util.Ln;
import android.content.res.Configuration;
import android.os.Parcel;
import android.os.RemoteException;
import android.graphics.Rect;
import android.view.IDisplayWindowListener;
import java.util.List;
public class DisplayWindowListener extends IDisplayWindowListener.Stub {
@Override
public void onDisplayAdded(int displayId) {
@@ -24,14 +23,17 @@ public class DisplayWindowListener extends IDisplayWindowListener.Stub {
}
@Override
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;
}
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<Rect> restricted, List<Rect> unrestricted) {
// empty default implementation
}
}

View File

@@ -125,7 +125,7 @@ public class ControlMessageReaderTest {
dos.writeShort(1080);
dos.writeShort(1920);
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);
byte[] packet = bos.toByteArray();
@@ -139,7 +139,7 @@ public class ControlMessageReaderTest {
Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth());
Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight());
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, bis.read()); // EOS