Compare commits

..

15 Commits

Author SHA1 Message Date
Romain Vimont
e0025ef185 Detect frame size mismatch in decoder
Warn if the size of a decoded video frame does not match the session
metadata.
2025-10-12 17:13:01 +02:00
Romain Vimont
67bb26d62a Properly handle session packets in delay_buffer
The delay buffer must forward the session packets while preserving
their order relative to media packets.
2025-10-12 17:13:01 +02:00
Romain Vimont
34df0b476a Add session metadata for video stream
Introduce a new packet type, a "session" packet, containing metadata
about the encoding session. It is used only for the video stream,
and currently includes the video resolution.

For illustration, here is a sequence of packets on the video stream:

                                        device rotation
                                        v
    CODEC | SESSION | MEDIA | MEDIA | … | SESSION | MEDIA | MEDIA | …
           1920x1080 <-----------------> 1080x1920 <------------------
                      encoding session 1            encoding session 2

This metadata is not strictly necessary, since the video resolution can
be determined after decoding. However, it allows detection of cases
where the encoder does not respect the requested size (and logs a
warning), even without decoding (e.g., when there is no video playback).

Additional metadata could be added later if necessary, for example the
actual device rotation.

Refs #5918 <https://github.com/Genymobile/scrcpy/pull/5918>
Refs #5894 <https://github.com/Genymobile/scrcpy/pull/5894>

Co-authored-by: gz0119 <liyong2@4399.com>
2025-10-12 17:12:59 +02:00
Romain Vimont
6072409048 Rename "codec meta" to "stream meta"
The stream metadata will contain both:
 - the codec id at the start of the stream
 - the session metadata (video width and height) at the start of every
   "session" (typically on rotation)
2025-10-12 17:09:43 +02:00
Romain Vimont
d16c9e3a26 Remove workaround for macOS TODO
TODO refs 3031
TODO refs SDL 5340 5976
2025-10-12 17:03:23 +02:00
dddddjent
f6dd00540e Fix SDL colorspace matching AVCOL_SPC_RGB
Use BT.709 color space for AVCOL_SPC_RGB.

Refs #1868 comment <https://github.com/Genymobile/scrcpy/issues/1868#issuecomment-3293917796>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2025-10-12 17:02:17 +02:00
Romain Vimont
1d2d7d7965 Build SDL3 for test step on Github Actions
The latest Ubuntu does not provide the SDL3 package yet.
2025-10-12 17:02:17 +02:00
Romain Vimont
79ab94c5a9 Upgrade SDL build script for SDL3 2025-10-12 17:02:17 +02:00
Romain Vimont
ae47396e27 Explicitly ignore useless SDL3 API return values
Some functions that returned void in SDL2 have become fallible in SDL3.
However, these should really correspond to assertions rather than errors that need to be handled by the caller.

Use assertions to explicitly ignore these return values.

TODO: refs SDL issue 14223
2025-10-12 16:58:04 +02:00
Romain Vimont
61fa8cefd6 Migrate from SDL2 to SDL3
Refs <https://wiki.libsdl.org/SDL3/README-migration>
2025-10-12 16:57:47 +02:00
Romain Vimont
808900468b Improve color space conversion
Use both the color space and color range from FFmpeg to determine the
appropriate SDL YUV conversion mode.
2025-10-12 12:48:54 +02:00
Romain Vimont
e66d012ab7 Set color range during texture creation
This prepares for the migration to SDL3, where the color range can only
be specified at the time of texture creation.
2025-10-12 12:48:54 +02:00
Romain Vimont
b266119fdf Create texture on first frame
The texture was created as soon as the initial video size was known,
even before the first frame arrived.

However, texture creation will require other data, such as the color
range, which is only available once the first frame is received.

Therefore, delay texture creation until the first frame.
2025-10-12 12:48:54 +02:00
Romain Vimont
1680673d04 Remove prepare_for_frame()
Inline the function body in its only caller.

This will simplify further refactors.
2025-10-12 12:48:54 +02:00
Romain Vimont
60970b8214 Handle mouse integer scrolling locally
Do not rely on SDL to expose integer scroll values, as they are not
available in all versions.

It was reimplemented in SDL 3.2.12 by this commit:
<0447c2f3c3>

Refs #6156 <https://github.com/Genymobile/scrcpy/issues/6156>
2025-10-12 12:48:54 +02:00
68 changed files with 806 additions and 813 deletions

View File

@@ -72,19 +72,13 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
# Same as build-linux-x86_64
- name: Install dependencies
run: |
sudo apt update
sudo apt install -y meson ninja-build nasm ffmpeg libavcodec-dev \
libavdevice-dev libavformat-dev libavutil-dev libswresample-dev \
libusb-1.0-0 libusb-1.0-0-dev libv4l-dev \
libasound2-dev libpulse-dev \
libaudio-dev libfribidi-dev libjack-dev libsndio-dev libx11-dev libxext-dev \
libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev libxtst-dev \
libxkbcommon-dev libdrm-dev libgbm-dev libgl1-mesa-dev libgles2-mesa-dev \
libegl1-mesa-dev libdbus-1-dev libibus-1.0-dev libudev-dev libthai-dev \
libpipewire-0.3-dev libwayland-dev libdecor-0-dev liburing-dev
sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \
libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \
libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \
libv4l-dev
# SDL3 is not available in Ubuntu yet
- name: Install SDL3
@@ -112,19 +106,13 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
# https://wiki.libsdl.org/SDL3/README-linux#build-dependencies
- name: Install dependencies
run: |
sudo apt update
sudo apt install -y meson ninja-build nasm ffmpeg libavcodec-dev \
libavdevice-dev libavformat-dev libavutil-dev libswresample-dev \
libusb-1.0-0 libusb-1.0-0-dev libv4l-dev \
libasound2-dev libpulse-dev \
libaudio-dev libfribidi-dev libjack-dev libsndio-dev libx11-dev libxext-dev \
libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev libxtst-dev \
libxkbcommon-dev libdrm-dev libgbm-dev libgl1-mesa-dev libgles2-mesa-dev \
libegl1-mesa-dev libdbus-1-dev libibus-1.0-dev libudev-dev libthai-dev \
libpipewire-0.3-dev libwayland-dev libdecor-0-dev liburing-dev
sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \
libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \
libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \
libv4l-dev
- name: Build
run: release/build_linux.sh x86_64
@@ -152,7 +140,9 @@ jobs:
- name: Install dependencies
run: |
sudo apt update
sudo apt install -y meson ninja-build nasm \
sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \
libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \
libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \
mingw-w64 mingw-w64-tools libz-mingw-w64-dev
- name: Build
@@ -181,7 +171,9 @@ jobs:
- name: Install dependencies
run: |
sudo apt update
sudo apt install -y meson ninja-build nasm \
sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \
libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \
libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \
mingw-w64 mingw-w64-tools libz-mingw-w64-dev
- name: Build
@@ -245,7 +237,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: |

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.3)
<img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />
@@ -129,7 +129,7 @@ Here are just some common examples.
scrcpy --otg
```
- Control the device using gamepads plugged into the computer:
- Control the device using gamepad controllers plugged into the computer:
```bash
scrcpy --gamepad=uhid

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,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-darwin.zip"
FILENAME=platform-tools_r$VERSION-darwin.zip
PROJECT_DIR=platform-tools-$VERSION-darwin
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 "$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,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-win.zip"
FILENAME=platform-tools_r$VERSION-win.zip
PROJECT_DIR=platform-tools-$VERSION-windows
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 "$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,14 +1,14 @@
#!/usr/bin/env bash
set -ex
. $(dirname ${BASH_SOURCE[0]})/_init
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
process_args "$@"
VERSION=3.4.0
URL="https://github.com/libsdl-org/SDL/archive/refs/tags/release-$VERSION.tar.gz"
SHA256SUM=9614b9696abc4597ffce6b888829dc6537ae500423474c342ac4a67222c5654c
PROJECT_DIR="sdl-$VERSION"
FILENAME="$PROJECT_DIR.tar.gz"
VERSION=3.2.18
FILENAME=SDL-$VERSION.tar.gz
PROJECT_DIR=SDL-release-$VERSION
SHA256SUM=51539fa13e546bc50c632beed3f34257de2baa38a4c642048de56377903b4265
cd "$SOURCES_DIR"
@@ -16,9 +16,8 @@ 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"
@@ -37,7 +36,6 @@ else
conf=(
-DCMAKE_INSTALL_PREFIX="$INSTALL_DIR/$DIRNAME"
-DSDL_TESTS=OFF
)
if [[ "$HOST" == linux ]]

View File

@@ -57,13 +57,13 @@ src = [
'src/util/process.c',
'src/util/process_intr.c',
'src/util/rand.c',
'src/util/sdl.c',
'src/util/strbuf.c',
'src/util/str.c',
'src/util/term.c',
'src/util/thread.c',
'src/util/tick.c',
'src/util/timeout.c',
'src/util/window.c',
]
conf = configuration_data()
@@ -276,7 +276,7 @@ if get_option('buildtype') == 'debug'
exe = executable(t[0], sources,
include_directories: src_dir,
dependencies: dependencies,
c_args: ['-DSC_TEST'])
c_args: ['-DSDL_MAIN_HANDLED', '-DSC_TEST'])
test(t[0], exe)
endforeach
endif

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.3"
END
END
BLOCK "VarFileInfo"

View File

@@ -47,7 +47,10 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
static bool
sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
const AVCodecContext *ctx) {
const AVCodecContext *ctx,
const struct sc_stream_session *session) {
(void) session;
struct sc_audio_player *ap = DOWNCAST(sink);
#ifdef SCRCPY_LAVU_HAS_CHLAYOUT

View File

@@ -10,20 +10,30 @@
#define DOWNCAST(SINK) container_of(SINK, struct sc_decoder, packet_sink)
static bool
sc_decoder_open(struct sc_decoder *decoder, AVCodecContext *ctx) {
sc_decoder_open(struct sc_decoder *decoder, AVCodecContext *ctx,
const struct sc_stream_session *session) {
decoder->frame = av_frame_alloc();
if (!decoder->frame) {
LOG_OOM();
return false;
}
if (!sc_frame_source_sinks_open(&decoder->frame_source, ctx)) {
if (!sc_frame_source_sinks_open(&decoder->frame_source, ctx, session)) {
av_frame_free(&decoder->frame);
return false;
}
decoder->ctx = ctx;
// A video stream must have a session
assert(session || ctx->codec_type != AVMEDIA_TYPE_VIDEO);
if (session) {
decoder->session = *session;
}
memset(&decoder->frame_size, 0, sizeof(decoder->frame_size));
return true;
}
@@ -61,6 +71,32 @@ sc_decoder_push(struct sc_decoder *decoder, const AVPacket *packet) {
}
// a frame was received
if (decoder->ctx->codec_type == AVMEDIA_TYPE_VIDEO) {
assert(decoder->frame->width >= 0);
assert(decoder->frame->height >= 0);
struct sc_size frame_size = {
.width = decoder->frame->width,
.height = decoder->frame->height,
};
if (decoder->frame_size.width != frame_size.width
|| decoder->frame_size.height != frame_size.height) {
// The frame size has changed, check if it matches the session
uint32_t sw = decoder->session.video.width;
uint32_t sh = decoder->session.video.height;
if (frame_size.width != sw || frame_size.height != sh) {
LOGW("Unexpected video size: %" PRIu32 "x%" PRIu32
" (expected %" PRIu32 "x%" PRIu32 ")",
frame_size.width, frame_size.height, sw, sh);
LOGW("The encoder did not respect the requested size, "
"please retry with a lower resolution (-m/--max-size)");
}
}
decoder->frame_size = frame_size;
}
bool ok = sc_frame_source_sinks_push(&decoder->frame_source,
decoder->frame);
av_frame_unref(decoder->frame);
@@ -74,9 +110,17 @@ sc_decoder_push(struct sc_decoder *decoder, const AVPacket *packet) {
}
static bool
sc_decoder_packet_sink_open(struct sc_packet_sink *sink, AVCodecContext *ctx) {
sc_decoder_push_session(struct sc_decoder *decoder,
const struct sc_stream_session *session) {
decoder->session = *session;
return sc_frame_source_sinks_push_session(&decoder->frame_source, session);
}
static bool
sc_decoder_packet_sink_open(struct sc_packet_sink *sink, AVCodecContext *ctx,
const struct sc_stream_session *session) {
struct sc_decoder *decoder = DOWNCAST(sink);
return sc_decoder_open(decoder, ctx);
return sc_decoder_open(decoder, ctx, session);
}
static void
@@ -92,6 +136,14 @@ sc_decoder_packet_sink_push(struct sc_packet_sink *sink,
return sc_decoder_push(decoder, packet);
}
static bool
sc_decoder_packet_sink_push_session(struct sc_packet_sink *sink,
const struct sc_stream_session *session) {
struct sc_decoder *decoder = DOWNCAST(sink);
return sc_decoder_push_session(decoder, session);
}
void
sc_decoder_init(struct sc_decoder *decoder, const char *name) {
decoder->name = name; // statically allocated
@@ -101,6 +153,7 @@ sc_decoder_init(struct sc_decoder *decoder, const char *name) {
.open = sc_decoder_packet_sink_open,
.close = sc_decoder_packet_sink_close,
.push = sc_decoder_packet_sink_push,
.push_session = sc_decoder_packet_sink_push_session,
};
decoder->packet_sink.ops = &ops;

View File

@@ -5,6 +5,7 @@
#include <libavcodec/avcodec.h>
#include "coords.h"
#include "trait/frame_source.h"
#include "trait/packet_sink.h"
@@ -16,6 +17,9 @@ struct sc_decoder {
AVCodecContext *ctx;
AVFrame *frame;
struct sc_stream_session session; // only initialized for video stream
struct sc_size frame_size;
};
// The name must be statically allocated (e.g. a string literal)

View File

@@ -10,16 +10,18 @@
#define DOWNCAST(SINK) container_of(SINK, struct sc_delay_buffer, frame_sink)
static bool
sc_delayed_frame_init(struct sc_delayed_frame *dframe, const AVFrame *frame) {
dframe->frame = av_frame_alloc();
if (!dframe->frame) {
sc_delayed_packet_init_frame(struct sc_delayed_packet *dpacket,
const AVFrame *frame) {
dpacket->type = SC_DELAYED_PACKET_TYPE_FRAME;
dpacket->frame = av_frame_alloc();
if (!dpacket->frame) {
LOG_OOM();
return false;
}
if (av_frame_ref(dframe->frame, frame)) {
if (av_frame_ref(dpacket->frame, frame)) {
LOG_OOM();
av_frame_free(&dframe->frame);
av_frame_free(&dpacket->frame);
return false;
}
@@ -27,9 +29,18 @@ sc_delayed_frame_init(struct sc_delayed_frame *dframe, const AVFrame *frame) {
}
static void
sc_delayed_frame_destroy(struct sc_delayed_frame *dframe) {
av_frame_unref(dframe->frame);
av_frame_free(&dframe->frame);
sc_delayed_packet_init_session(struct sc_delayed_packet *dpacket,
const struct sc_stream_session *session) {
dpacket->type = SC_DELAYED_PACKET_TYPE_SESSION;
dpacket->session = *session;
}
static void
sc_delayed_packet_destroy(struct sc_delayed_packet *dpacket) {
if (dpacket->type == SC_DELAYED_PACKET_TYPE_FRAME) {
av_frame_unref(dpacket->frame);
av_frame_free(&dpacket->frame);
}
}
static int
@@ -50,43 +61,52 @@ run_buffering(void *data) {
goto stopped;
}
struct sc_delayed_frame dframe = sc_vecdeque_pop(&db->queue);
struct sc_delayed_packet dpacket = sc_vecdeque_pop(&db->queue);
sc_tick max_deadline = sc_tick_now() + db->delay;
// PTS (written by the server) are expressed in microseconds
sc_tick pts = SC_TICK_FROM_US(dframe.frame->pts);
bool ok;
if (dpacket.type == SC_DELAYED_PACKET_TYPE_FRAME) {
sc_tick max_deadline = sc_tick_now() + db->delay;
// PTS (written by the server) are expressed in microseconds
sc_tick pts = SC_TICK_FROM_US(dpacket.frame->pts);
bool timed_out = false;
while (!db->stopped && !timed_out) {
sc_tick deadline = sc_clock_to_system_time(&db->clock, pts)
+ db->delay;
if (deadline > max_deadline) {
deadline = max_deadline;
bool timed_out = false;
while (!db->stopped && !timed_out) {
sc_tick deadline = sc_clock_to_system_time(&db->clock, pts)
+ db->delay;
if (deadline > max_deadline) {
deadline = max_deadline;
}
timed_out =
!sc_cond_timedwait(&db->wait_cond, &db->mutex, deadline);
}
timed_out =
!sc_cond_timedwait(&db->wait_cond, &db->mutex, deadline);
}
bool stopped = db->stopped;
sc_mutex_unlock(&db->mutex);
bool stopped = db->stopped;
sc_mutex_unlock(&db->mutex);
if (stopped) {
sc_delayed_frame_destroy(&dframe);
goto stopped;
}
if (stopped) {
sc_delayed_packet_destroy(&dpacket);
goto stopped;
}
#ifdef SC_BUFFERING_DEBUG
LOGD("Buffering: %" PRItick ";%" PRItick ";%" PRItick,
pts, dframe.push_date, sc_tick_now());
LOGD("Buffering: %" PRItick ";%" PRItick ";%" PRItick,
pts, dframe.push_date, sc_tick_now());
#endif
bool ok = sc_frame_source_sinks_push(&db->frame_source, dframe.frame);
sc_delayed_frame_destroy(&dframe);
ok = sc_frame_source_sinks_push(&db->frame_source, dpacket.frame);
} else {
assert(dpacket.type == SC_DELAYED_PACKET_TYPE_SESSION);
sc_mutex_unlock(&db->mutex);
ok = sc_frame_source_sinks_push_session(&db->frame_source,
&dpacket.session);
}
sc_delayed_packet_destroy(&dpacket);
if (!ok) {
LOGE("Delayed frame could not be pushed, stopping");
LOGE("Delayed packet could not be pushed, stopping");
sc_mutex_lock(&db->mutex);
// Prevent to push any new frame
// Prevent to push any new packet
db->stopped = true;
sc_mutex_unlock(&db->mutex);
goto stopped;
@@ -98,8 +118,8 @@ stopped:
// Flush queue
while (!sc_vecdeque_is_empty(&db->queue)) {
struct sc_delayed_frame *dframe = sc_vecdeque_popref(&db->queue);
sc_delayed_frame_destroy(dframe);
struct sc_delayed_packet *dpacket = sc_vecdeque_popref(&db->queue);
sc_delayed_packet_destroy(dpacket);
}
LOGD("Buffering thread ended");
@@ -109,9 +129,11 @@ stopped:
static bool
sc_delay_buffer_frame_sink_open(struct sc_frame_sink *sink,
const AVCodecContext *ctx) {
const AVCodecContext *ctx,
const struct sc_stream_session *session) {
struct sc_delay_buffer *db = DOWNCAST(sink);
(void) ctx;
(void) session;
bool ok = sc_mutex_init(&db->mutex);
if (!ok) {
@@ -132,7 +154,7 @@ sc_delay_buffer_frame_sink_open(struct sc_frame_sink *sink,
sc_vecdeque_init(&db->queue);
db->stopped = false;
if (!sc_frame_source_sinks_open(&db->frame_source, ctx)) {
if (!sc_frame_source_sinks_open(&db->frame_source, ctx, session)) {
goto error_destroy_wait_cond;
}
@@ -196,24 +218,56 @@ sc_delay_buffer_frame_sink_push(struct sc_frame_sink *sink,
return sc_frame_source_sinks_push(&db->frame_source, frame);
}
struct sc_delayed_frame dframe;
bool ok = sc_delayed_frame_init(&dframe, frame);
if (!ok) {
struct sc_delayed_packet *dpacket = sc_vecdeque_push_hole(&db->queue);
if (!dpacket) {
sc_mutex_unlock(&db->mutex);
LOG_OOM();
return false;
}
#ifdef SC_BUFFERING_DEBUG
dframe.push_date = sc_tick_now();
#endif
ok = sc_vecdeque_push(&db->queue, dframe);
bool ok = sc_delayed_packet_init_frame(dpacket, frame);
if (!ok) {
sc_mutex_unlock(&db->mutex);
LOG_OOM();
return false;
}
#ifdef SC_BUFFERING_DEBUG
dpacket->push_date = sc_tick_now();
#endif
sc_cond_signal(&db->queue_cond);
sc_mutex_unlock(&db->mutex);
return true;
}
static bool
sc_delay_buffer_frame_sink_push_session(struct sc_frame_sink *sink,
const struct sc_stream_session *session) {
struct sc_delay_buffer *db = DOWNCAST(sink);
sc_mutex_lock(&db->mutex);
if (db->stopped) {
sc_mutex_unlock(&db->mutex);
return false;
}
struct sc_delayed_packet *dpacket = sc_vecdeque_push_hole(&db->queue);
if (!dpacket) {
sc_mutex_unlock(&db->mutex);
LOG_OOM();
return false;
}
sc_delayed_packet_init_session(dpacket, session);
#ifdef SC_BUFFERING_DEBUG
dpacket->push_date = sc_tick_now();
#endif
sc_cond_signal(&db->queue_cond);
sc_mutex_unlock(&db->mutex);
@@ -235,6 +289,7 @@ sc_delay_buffer_init(struct sc_delay_buffer *db, sc_tick delay,
.open = sc_delay_buffer_frame_sink_open,
.close = sc_delay_buffer_frame_sink_close,
.push = sc_delay_buffer_frame_sink_push,
.push_session = sc_delay_buffer_frame_sink_push_session,
};
db->frame_sink.ops = &ops;

View File

@@ -18,14 +18,23 @@
// forward declarations
typedef struct AVFrame AVFrame;
struct sc_delayed_frame {
AVFrame *frame;
enum sc_delayed_packet_type {
SC_DELAYED_PACKET_TYPE_FRAME,
SC_DELAYED_PACKET_TYPE_SESSION,
};
struct sc_delayed_packet {
enum sc_delayed_packet_type type;
union {
AVFrame *frame;
struct sc_stream_session session;
};
#ifdef SC_BUFFERING_DEBUG
sc_tick push_date;
#endif
};
struct sc_delayed_frame_queue SC_VECDEQUE(struct sc_delayed_frame);
struct sc_delayed_packet_queue SC_VECDEQUE(struct sc_delayed_packet);
struct sc_delay_buffer {
struct sc_frame_source frame_source; // frame source trait
@@ -40,7 +49,7 @@ struct sc_delay_buffer {
sc_cond wait_cond;
struct sc_clock clock;
struct sc_delayed_frame_queue queue;
struct sc_delayed_packet_queue queue;
bool stopped;
};

View File

@@ -11,8 +11,8 @@
#define SC_PACKET_HEADER_SIZE 12
#define SC_PACKET_FLAG_CONFIG (UINT64_C(1) << 63)
#define SC_PACKET_FLAG_KEY_FRAME (UINT64_C(1) << 62)
#define SC_PACKET_FLAG_CONFIG (UINT64_C(1) << 62)
#define SC_PACKET_FLAG_KEY_FRAME (UINT64_C(1) << 61)
#define SC_PACKET_PTS_MASK (SC_PACKET_FLAG_KEY_FRAME - 1)
@@ -63,48 +63,75 @@ sc_demuxer_recv_codec_id(struct sc_demuxer *demuxer, uint32_t *codec_id) {
return true;
}
static bool
sc_demuxer_recv_video_size(struct sc_demuxer *demuxer, uint32_t *width,
uint32_t *height) {
uint8_t data[8];
ssize_t r = net_recv_all(demuxer->socket, data, 8);
if (r < 8) {
return false;
}
*width = sc_read32be(data);
*height = sc_read32be(data + 4);
return true;
}
static bool
sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) {
static inline bool
sc_demuxer_recv_header(struct sc_demuxer *demuxer,
uint8_t buf[static SC_PACKET_HEADER_SIZE]) {
// The video and audio streams contain a sequence of raw packets (as
// provided by MediaCodec), each prefixed with a "meta" header.
//
// The "meta" header length is 12 bytes:
// The "meta" header length is 12 bytes.
//
//
// If the MSB is 1, then it is a session packet (for a video stream only),
// which only contains a 12-byte header:
//
// byte 0 byte 1 byte 2 byte 3
// 10000000 00000000 00000000 00000000
// ^<-------------------------------->
// | padding
// `- session packet flag
//
// byte 4 byte 5 byte 6 byte 7 byte 8 byte 9 byte 10 byte 11
// ........ ........ ........ ........ ........ ........ ........ ........
// <---------------------------------> <--------------------------------->
// video width video height
//
//
// If the MSB is 0, then it is a media packet, comprised of a 12-byte header
// followed by <packet_size> bytes containing the packet/frame:
//
// [. . . . . . . .|. . . .]. . . . . . . . . . . . . . . ...
// <-------------> <-----> <-----------------------------...
// PTS packet raw packet
// size
//
// It is followed by <packet_size> bytes containing the packet/frame.
//
// The most significant bits of the PTS are used for packet flags:
//
// byte 7 byte 6 byte 5 byte 4 byte 3 byte 2 byte 1 byte 0
// CK...... ........ ........ ........ ........ ........ ........ ........
// ^^<------------------------------------------------------------------->
// || PTS
// | `- key frame
// `-- config packet
// byte 0 byte 1 byte 2 byte 3 byte 4 byte 5 byte 6 byte 7
// 0CK..... ........ ........ ........ ........ ........ ........ ........
// ^^^<------------------------------------------------------------------>
// ||| PTS
// || `- key frame
// | `-- config packet
// `--- media packet flag
//
// byte 8 byte 9 byte 10 byte 11
// ........ ........ ........ ........ ........ ........ . . .
// <---------------------------------> <---------------- . . .
// packet size raw packet
//
ssize_t r = net_recv_all(demuxer->socket, buf, SC_PACKET_HEADER_SIZE);
assert(r <= SC_PACKET_HEADER_SIZE);
return r == SC_PACKET_HEADER_SIZE;
}
uint8_t header[SC_PACKET_HEADER_SIZE];
ssize_t r = net_recv_all(demuxer->socket, header, SC_PACKET_HEADER_SIZE);
if (r < SC_PACKET_HEADER_SIZE) {
return false;
}
static bool
sc_demuxer_is_session(const uint8_t *header) {
return header[0] & 0x80;
}
static void
sc_demuxer_parse_session(const uint8_t *header,
struct sc_stream_session *session) {
assert(sc_demuxer_is_session(header));
session->video.width = sc_read32be(&header[4]);
session->video.height = sc_read32be(&header[8]);
}
static bool
sc_demuxer_recv_packet(struct sc_demuxer *demuxer, const uint8_t *header,
AVPacket *packet) {
assert(!sc_demuxer_is_session(header));
uint64_t pts_flags = sc_read64be(header);
uint32_t len = sc_read32be(&header[8]);
assert(len);
@@ -114,7 +141,7 @@ sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) {
return false;
}
r = net_recv_all(demuxer->socket, packet->data, len);
ssize_t r = net_recv_all(demuxer->socket, packet->data, len);
if (r < 0 || ((uint32_t) r) < len) {
av_packet_unref(packet);
return false;
@@ -187,17 +214,28 @@ run_demuxer(void *data) {
codec_ctx->flags |= AV_CODEC_FLAG_LOW_DELAY;
uint8_t header[SC_PACKET_HEADER_SIZE];
struct sc_stream_session session_data;
struct sc_stream_session *session = NULL;
if (codec->type == AVMEDIA_TYPE_VIDEO) {
uint32_t width;
uint32_t height;
ok = sc_demuxer_recv_video_size(demuxer, &width, &height);
bool ok = sc_demuxer_recv_header(demuxer, header);
if (!ok) {
goto finally_free_context;
}
codec_ctx->width = width;
codec_ctx->height = height;
if (!sc_demuxer_is_session(header)) {
LOGE("Unexpected packet (not a session header)");
goto finally_free_context;
}
session = &session_data;
sc_demuxer_parse_session(header, session);
codec_ctx->width = session_data.video.width;
codec_ctx->height = session_data.video.height;
codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
} else {
// Hardcoded audio properties
#ifdef SCRCPY_LAVU_HAS_CHLAYOUT
@@ -219,7 +257,8 @@ run_demuxer(void *data) {
goto finally_free_context;
}
if (!sc_packet_source_sinks_open(&demuxer->packet_source, codec_ctx)) {
if (!sc_packet_source_sinks_open(&demuxer->packet_source, codec_ctx,
session)) {
goto finally_free_context;
}
@@ -241,27 +280,39 @@ run_demuxer(void *data) {
}
for (;;) {
bool ok = sc_demuxer_recv_packet(demuxer, packet);
bool ok = sc_demuxer_recv_header(demuxer, header);
if (!ok) {
// end of stream
status = SC_DEMUXER_STATUS_EOS;
break;
}
if (must_merge_config_packet) {
// Prepend any config packet to the next media packet
ok = sc_packet_merger_merge(&merger, packet);
if (sc_demuxer_is_session(header)) {
sc_demuxer_parse_session(header, &session_data);
ok = sc_packet_source_sinks_push_session(&demuxer->packet_source,
&session_data);
if (!ok) {
av_packet_unref(packet);
// The sink already logged its concrete error
break;
}
}
} else {
sc_demuxer_recv_packet(demuxer, header, packet);
ok = sc_packet_source_sinks_push(&demuxer->packet_source, packet);
av_packet_unref(packet);
if (!ok) {
// The sink already logged its concrete error
break;
if (must_merge_config_packet) {
// Prepend any config packet to the next media packet
ok = sc_packet_merger_merge(&merger, packet);
if (!ok) {
av_packet_unref(packet);
break;
}
}
ok = sc_packet_source_sinks_push(&demuxer->packet_source, packet);
av_packet_unref(packet);
if (!ok) {
// The sink already logged its concrete error
break;
}
}
}

View File

@@ -6,7 +6,6 @@
#include <libavutil/pixfmt.h>
#include "util/log.h"
#include "util/sdl.h"
static bool
sc_display_init_novideo_icon(struct sc_display *display,
@@ -349,7 +348,7 @@ sc_display_update_texture_internal(struct sc_display *display,
struct sc_opengl *gl = &display->gl;
gl->BindTexture(GL_TEXTURE_2D, display->texture_id);
gl->GenerateMipmap(GL_TEXTURE_2D);
display->gl.GenerateMipmap(GL_TEXTURE_2D);
gl->BindTexture(GL_TEXTURE_2D, 0);
}
@@ -375,7 +374,9 @@ sc_display_update_texture(struct sc_display *display, const AVFrame *frame) {
enum sc_display_result
sc_display_render(struct sc_display *display, const SDL_Rect *geometry,
enum sc_orientation orientation) {
sc_sdl_render_clear(display->renderer);
bool always_ok = SDL_RenderClear(display->renderer);
(void) always_ok;
assert(always_ok);
if (display->pending.flags) {
bool ok = sc_display_apply_pending(display);
@@ -420,6 +421,8 @@ sc_display_render(struct sc_display *display, const SDL_Rect *geometry,
}
}
sc_sdl_render_present(display->renderer);
always_ok = SDL_RenderPresent(display->renderer);
assert(always_ok);
return SC_DISPLAY_RESULT_OK;
}

View File

@@ -9,7 +9,7 @@
#include "hid/hid_event.h"
#include "input_events.h"
// See "SDL3/SDL_scancode.h".
// See "SDL2/SDL_scancode.h".
// Maybe SDL_Keycode is used by most people, but SDL_Scancode is taken from USB
// HID protocol.
// 0x65 is Application, typically AT-101 Keyboard ends here.

View File

@@ -261,7 +261,7 @@ load_from_path(const char *path) {
// frame owns the data
bool ok = SDL_SetPointerProperty(props, "sc_frame", frame);
if (!ok) {
LOGE("Could not set pointer property: %s", SDL_GetError());
LOGE("Could not get surface properties: %s", SDL_GetError());
SDL_DestroySurface(surface);
goto error;
}

View File

@@ -14,8 +14,7 @@
* for simplicity.
*
* This scrcpy input events API is designed to be consumed by input event
* processors (sc_key_processor, sc_mouse_processor and sc_gamepad_processor,
* see app/src/trait/).
* processors (sc_key_processor and sc_mouse_processor, see app/src/trait/).
*
* One major semantic difference between SDL input events and scrcpy input
* events is their frame of reference (for mouse and touch events): SDL events
@@ -323,6 +322,9 @@ enum sc_mouse_button {
SC_MOUSE_BUTTON_X2 = SDL_BUTTON_MASK(SDL_BUTTON_X2),
};
// Use the naming from SDL3 for gamepad axis and buttons:
// <https://wiki.libsdl.org/SDL3/README/migration>
enum sc_gamepad_axis {
SC_GAMEPAD_AXIS_UNKNOWN = -1,
SC_GAMEPAD_AXIS_LEFTX = SDL_GAMEPAD_AXIS_LEFTX,
@@ -409,9 +411,10 @@ struct sc_touch_event {
float pressure;
};
// As documented in <https://wiki.libsdl.org/SDL3/SDL_JoystickID>:
// The value 0 is an invalid ID.
#define SC_GAMEPAD_ID_INVALID 0
// As documented in <https://wiki.libsdl.org/SDL2/SDL_JoystickID>:
// The ID value starts at 0 and increments from there. The value -1 is an
// invalid ID.
#define SC_GAMEPAD_ID_INVALID UINT32_C(-1)
struct sc_gamepad_device_event {
uint32_t gamepad_id;
@@ -497,7 +500,7 @@ static inline enum sc_gamepad_axis
sc_gamepad_axis_from_sdl(uint8_t axis) {
if (axis <= SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) {
// SC_GAMEPAD_AXIS_* constants are initialized from
// SDL_GAMEPAD_AXIS_*
// SDL_CONTROLLER_AXIS_*
return axis;
}
return SC_GAMEPAD_AXIS_UNKNOWN;
@@ -507,14 +510,14 @@ static inline enum sc_gamepad_button
sc_gamepad_button_from_sdl(uint8_t button) {
if (button <= SDL_GAMEPAD_BUTTON_DPAD_RIGHT) {
// SC_GAMEPAD_BUTTON_* constants are initialized from
// SDL_GAMEPAD_BUTTON_*
// SDL_CONTROLLER_BUTTON_*
return button;
}
return SC_GAMEPAD_BUTTON_UNKNOWN;
}
static inline enum sc_action
sc_action_from_sdl_gamepad_button_type(uint32_t type) {
sc_action_from_sdl_controllerbutton_type(uint32_t type) {
assert(type == SDL_EVENT_GAMEPAD_BUTTON_DOWN || type == SDL_EVENT_GAMEPAD_BUTTON_UP);
if (type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
return SC_ACTION_DOWN;

View File

@@ -11,7 +11,6 @@
#include "screen.h"
#include "shortcut_mod.h"
#include "util/log.h"
#include "util/sdl.h"
void
sc_input_manager_init(struct sc_input_manager *im,
@@ -673,12 +672,15 @@ sc_input_manager_process_touch(struct sc_input_manager *im,
return;
}
struct sc_size drawable_size =
sc_sdl_get_window_size_in_pixels(im->screen->window);
int dw;
int dh;
bool always_ok = SDL_GetWindowSizeInPixels(im->screen->window, &dw, &dh);
(void) always_ok;
assert(always_ok);
// SDL touch event coordinates are normalized in the range [0; 1]
int32_t x = event->x * (int32_t) drawable_size.width;
int32_t y = event->y * (int32_t) drawable_size.height;
int32_t x = event->x * dw;
int32_t y = event->y * dh;
struct sc_touch_event evt = {
.position = {
@@ -908,16 +910,16 @@ static void
sc_input_manager_process_gamepad_device(struct sc_input_manager *im,
const SDL_GamepadDeviceEvent *event) {
if (event->type == SDL_EVENT_GAMEPAD_ADDED) {
SDL_Gamepad *sdl_gamepad = SDL_OpenGamepad(event->which);
if (!sdl_gamepad) {
LOGW("Could not open gamepad");
SDL_Gamepad *gc = SDL_OpenGamepad(event->which);
if (!gc) {
LOGW("Could not open game controller");
return;
}
SDL_Joystick *joystick = SDL_GetGamepadJoystick(sdl_gamepad);
SDL_Joystick *joystick = SDL_GetGamepadJoystick(gc);
if (!joystick) {
LOGW("Could not get gamepad joystick");
SDL_CloseGamepad(sdl_gamepad);
LOGW("Could not get controller joystick");
SDL_CloseGamepad(gc);
return;
}
@@ -928,9 +930,9 @@ sc_input_manager_process_gamepad_device(struct sc_input_manager *im,
} else if (event->type == SDL_EVENT_GAMEPAD_REMOVED) {
SDL_JoystickID id = event->which;
SDL_Gamepad *sdl_gamepad = SDL_GetGamepadFromID(id);
if (sdl_gamepad) {
SDL_CloseGamepad(sdl_gamepad);
SDL_Gamepad *gc = SDL_GetGamepadFromID(id);
if (gc) {
SDL_CloseGamepad(gc);
} else {
LOGW("Unknown gamepad device removed");
}
@@ -971,7 +973,7 @@ sc_input_manager_process_gamepad_button(struct sc_input_manager *im,
struct sc_gamepad_button_event evt = {
.gamepad_id = event->which,
.action = sc_action_from_sdl_gamepad_button_type(event->type),
.action = sc_action_from_sdl_controllerbutton_type(event->type),
.button = button,
};
im->gp->ops->process_gamepad_button(im->gp, &evt);

View File

@@ -5,14 +5,14 @@
#ifdef HAVE_V4L2
# include <libavdevice/avdevice.h>
#endif
#define SDL_FUNCTION_POINTER_IS_VOID_POINTER
#include <SDL3/SDL_stdinc.h>
#include <SDL3/SDL.h>
#include "cli.h"
#include "options.h"
#include "scrcpy.h"
#ifdef HAVE_USB
# include "usb/scrcpy_otg.h"
#endif
#include "usb/scrcpy_otg.h"
#include "util/log.h"
#include "util/net.h"
#include "util/thread.h"

View File

@@ -57,8 +57,8 @@ sc_mouse_capture_handle_event(struct sc_mouse_capture *mc,
case SDL_EVENT_MOUSE_MOTION:
case SDL_EVENT_MOUSE_BUTTON_DOWN:
if (!sc_mouse_capture_is_active(mc)) {
// The mouse will be captured on SDL_EVENT_MOUSE_BUTTON_UP, so
// consume the event
// The mouse will be captured on SDL_MOUSEBUTTONUP, so consume
// the event
return true;
}
break;

View File

@@ -541,7 +541,10 @@ sc_recorder_set_orientation(AVStream *stream, enum sc_orientation orientation) {
static bool
sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink,
AVCodecContext *ctx) {
AVCodecContext *ctx,
const struct sc_stream_session *session) {
(void) session;
struct sc_recorder *recorder = DOWNCAST_VIDEO(sink);
// only written from this thread, no need to lock
assert(!recorder->video_init);
@@ -635,7 +638,10 @@ sc_recorder_video_packet_sink_push(struct sc_packet_sink *sink,
static bool
sc_recorder_audio_packet_sink_open(struct sc_packet_sink *sink,
AVCodecContext *ctx) {
AVCodecContext *ctx,
const struct sc_stream_session *session) {
(void) session;
struct sc_recorder *recorder = DOWNCAST_AUDIO(sink);
assert(recorder->audio);
// only written from this thread, no need to lock

View File

@@ -355,7 +355,7 @@ scrcpy_generate_scid(void) {
static void
init_sdl_gamepads(void) {
// Trigger a SDL_EVENT_GAMEPAD_ADDED event for all gamepads already
// Trigger a SDL_CONTROLLERDEVICEADDED event for all gamepads already
// connected
int count;
SDL_JoystickID *joysticks = SDL_GetJoysticks(&count);

View File

@@ -8,7 +8,7 @@
#include "icon.h"
#include "options.h"
#include "util/log.h"
#include "util/sdl.h"
#include "util/window.h"
#define DISPLAY_MARGINS 96
@@ -27,11 +27,45 @@ get_oriented_size(struct sc_size size, enum sc_orientation orientation) {
return oriented_size;
}
static inline bool
is_windowed(struct sc_screen *screen) {
return !(SDL_GetWindowFlags(screen->window) & (SDL_WINDOW_FULLSCREEN
| SDL_WINDOW_MINIMIZED
| SDL_WINDOW_MAXIMIZED));
// get the window size in a struct sc_size
static struct sc_size
get_window_size(const struct sc_screen *screen) {
int width;
int height;
bool always_ok = SDL_GetWindowSize(screen->window, &width, &height);
(void) always_ok;
assert(always_ok);
struct sc_size size;
size.width = width;
size.height = height;
return size;
}
static struct sc_point
get_window_position(const struct sc_screen *screen) {
int x;
int y;
bool always_ok = SDL_GetWindowPosition(screen->window, &x, &y);
(void) always_ok;
assert(always_ok);
struct sc_point point;
point.x = x;
point.y = y;
return point;
}
// set the window size to be applied when fullscreen is disabled
static void
set_window_size(struct sc_screen *screen, struct sc_size new_size) {
assert(!screen->fullscreen);
assert(!screen->maximized);
assert(!screen->minimized);
bool always_ok =
SDL_SetWindowSize(screen->window, new_size.width, new_size.height);
(void) always_ok;
assert(always_ok);
}
// get the preferred display bounds (i.e. the screen bounds with some margins)
@@ -39,11 +73,6 @@ static bool
get_preferred_display_bounds(struct sc_size *bounds) {
SDL_Rect rect;
SDL_DisplayID display = SDL_GetPrimaryDisplay();
if (!display) {
LOGW("Could not get primary display: %s", SDL_GetError());
return false;
}
bool ok = SDL_GetDisplayUsableBounds(display, &rect);
if (!ok) {
LOGW("Could not get display usable bounds: %s", SDL_GetError());
@@ -147,10 +176,15 @@ static void
sc_screen_update_content_rect(struct sc_screen *screen) {
assert(screen->video);
int dw;
int dh;
bool always_ok = SDL_GetWindowSizeInPixels(screen->window, &dw, &dh);
(void) always_ok;
assert(always_ok);
struct sc_size content_size = screen->content_size;
// The drawable size is the window size * the HiDPI scale
struct sc_size drawable_size =
sc_sdl_get_window_size_in_pixels(screen->window);
struct sc_size drawable_size = {dw, dh};
SDL_Rect *rect = &screen->rect;
@@ -219,7 +253,7 @@ event_watcher(void *data, SDL_Event *event) {
struct sc_screen *screen = data;
assert(screen->video);
if (event->type == SDL_EVENT_WINDOW_RESIZED) {
if (event->type == SDL_EVENT_WINDOW_EXPOSED) {
// In practice, it seems to always be called from the same thread in
// that specific case. Anyway, it's just a workaround.
sc_screen_render(screen, true);
@@ -231,9 +265,11 @@ event_watcher(void *data, SDL_Event *event) {
static bool
sc_screen_frame_sink_open(struct sc_frame_sink *sink,
const AVCodecContext *ctx) {
const AVCodecContext *ctx,
const struct sc_stream_session *session) {
assert(ctx->pix_fmt == AV_PIX_FMT_YUV420P);
(void) ctx;
(void) session;
struct sc_screen *screen = DOWNCAST(sink);
(void) screen;
@@ -295,6 +331,9 @@ sc_screen_init(struct sc_screen *screen,
screen->resize_pending = false;
screen->has_frame = false;
screen->has_video_window = false;
screen->fullscreen = false;
screen->maximized = false;
screen->minimized = false;
screen->paused = false;
screen->resume_frame = NULL;
screen->orientation = SC_ORIENTATION_0;
@@ -360,7 +399,7 @@ sc_screen_init(struct sc_screen *screen,
// The window will be positioned and sized on first video frame
screen->window =
sc_sdl_create_window(title, x, y, width, height, window_flags);
sc_create_sdl_window(title, x, y, width, height, window_flags);
if (!screen->window) {
LOGE("Could not create window: %s", SDL_GetError());
goto error_destroy_fps_counter;
@@ -468,18 +507,18 @@ sc_screen_show_initial_window(struct sc_screen *screen) {
? screen->req.x : (int) SDL_WINDOWPOS_CENTERED;
int y = screen->req.y != SC_WINDOW_POSITION_UNDEFINED
? screen->req.y : (int) SDL_WINDOWPOS_CENTERED;
struct sc_point position = {
.x = x,
.y = y,
};
struct sc_size window_size =
get_initial_optimal_size(screen->content_size, screen->req.width,
screen->req.height);
assert(is_windowed(screen));
sc_sdl_set_window_size(screen->window, window_size);
sc_sdl_set_window_position(screen->window, position);
set_window_size(screen, window_size);
bool always_ok;
(void) always_ok;
always_ok = SDL_SetWindowPosition(screen->window, x, y);
assert(always_ok);
if (screen->req.fullscreen) {
sc_screen_toggle_fullscreen(screen);
@@ -489,13 +528,17 @@ sc_screen_show_initial_window(struct sc_screen *screen) {
sc_fps_counter_start(&screen->fps_counter);
}
sc_sdl_show_window(screen->window);
always_ok = SDL_ShowWindow(screen->window);
assert(always_ok);
sc_screen_update_content_rect(screen);
}
void
sc_screen_hide_window(struct sc_screen *screen) {
sc_sdl_hide_window(screen->window);
bool always_ok = SDL_HideWindow(screen->window);
(void) always_ok;
assert(always_ok);
}
void
@@ -525,7 +568,7 @@ resize_for_content(struct sc_screen *screen, struct sc_size old_content_size,
struct sc_size new_content_size) {
assert(screen->video);
struct sc_size window_size = sc_sdl_get_window_size(screen->window);
struct sc_size window_size = get_window_size(screen);
struct sc_size target_size = {
.width = (uint32_t) window_size.width * new_content_size.width
/ old_content_size.width,
@@ -533,15 +576,14 @@ resize_for_content(struct sc_screen *screen, struct sc_size old_content_size,
/ old_content_size.height,
};
target_size = get_optimal_size(target_size, new_content_size, true);
assert(is_windowed(screen));
sc_sdl_set_window_size(screen->window, target_size);
set_window_size(screen, target_size);
}
static void
set_content_size(struct sc_screen *screen, struct sc_size new_content_size) {
assert(screen->video);
if (is_windowed(screen)) {
if (!screen->fullscreen && !screen->maximized && !screen->minimized) {
resize_for_content(screen, screen->content_size, new_content_size);
} else if (!screen->resize_pending) {
// Store the windowed size to be able to compute the optimal size once
@@ -557,7 +599,9 @@ static void
apply_pending_resize(struct sc_screen *screen) {
assert(screen->video);
assert(is_windowed(screen));
assert(!screen->fullscreen);
assert(!screen->maximized);
assert(!screen->minimized);
if (screen->resize_pending) {
resize_for_content(screen, screen->windowed_content_size,
screen->content_size);
@@ -705,28 +749,31 @@ void
sc_screen_toggle_fullscreen(struct sc_screen *screen) {
assert(screen->video);
bool req_fullscreen =
!(SDL_GetWindowFlags(screen->window) & SDL_WINDOW_FULLSCREEN);
bool ok = SDL_SetWindowFullscreen(screen->window, req_fullscreen);
bool ok = SDL_SetWindowFullscreen(screen->window, !screen->fullscreen);
if (!ok) {
LOGW("Could not switch fullscreen mode: %s", SDL_GetError());
return;
}
LOGD("Requested %s mode", req_fullscreen ? "fullscreen" : "windowed");
screen->fullscreen = !screen->fullscreen;
if (!screen->fullscreen && !screen->maximized && !screen->minimized) {
apply_pending_resize(screen);
}
LOGD("Switched to %s mode", screen->fullscreen ? "fullscreen" : "windowed");
sc_screen_render(screen, true);
}
void
sc_screen_resize_to_fit(struct sc_screen *screen) {
assert(screen->video);
if (!is_windowed(screen)) {
if (screen->fullscreen || screen->maximized || screen->minimized) {
return;
}
struct sc_point point = sc_sdl_get_window_position(screen->window);
struct sc_size window_size = sc_sdl_get_window_size(screen->window);
struct sc_point point = get_window_position(screen);
struct sc_size window_size = get_window_size(screen);
struct sc_size optimal_size =
get_optimal_size(window_size, screen->content_size, false);
@@ -734,14 +781,19 @@ sc_screen_resize_to_fit(struct sc_screen *screen) {
// Center the window related to the device screen
assert(optimal_size.width <= window_size.width);
assert(optimal_size.height <= window_size.height);
uint32_t new_x = point.x + (window_size.width - optimal_size.width) / 2;
uint32_t new_y = point.y + (window_size.height - optimal_size.height) / 2;
struct sc_point new_position = {
.x = point.x + (window_size.width - optimal_size.width) / 2,
.y = point.y + (window_size.height - optimal_size.height) / 2,
};
bool always_ok;
(void) always_ok;
always_ok = SDL_SetWindowSize(screen->window, optimal_size.width,
optimal_size.height);
assert(always_ok);
always_ok = SDL_SetWindowPosition(screen->window, new_x, new_y);
assert(always_ok);
sc_sdl_set_window_size(screen->window, optimal_size);
sc_sdl_set_window_position(screen->window, new_position);
LOGD("Resized to optimal size: %ux%u", optimal_size.width,
optimal_size.height);
}
@@ -750,12 +802,24 @@ void
sc_screen_resize_to_pixel_perfect(struct sc_screen *screen) {
assert(screen->video);
if (!is_windowed(screen)) {
if (screen->fullscreen || screen->minimized) {
return;
}
bool always_ok;
(void) always_ok;
if (screen->maximized) {
always_ok = SDL_RestoreWindow(screen->window);
assert(always_ok);
screen->maximized = false;
}
struct sc_size content_size = screen->content_size;
sc_sdl_set_window_size(screen->window, content_size);
always_ok = SDL_SetWindowSize(screen->window, content_size.width,
content_size.height);
assert(always_ok);
LOGD("Resized to pixel-perfect: %ux%u", content_size.width,
content_size.height);
}
@@ -785,20 +849,24 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
sc_screen_render(screen, true);
}
return true;
case SDL_EVENT_WINDOW_MAXIMIZED:
screen->maximized = true;
return true;
case SDL_EVENT_WINDOW_MINIMIZED:
screen->minimized = true;
return true;
case SDL_EVENT_WINDOW_RESTORED:
if (screen->has_video_window && is_windowed(screen)) {
apply_pending_resize(screen);
sc_screen_render(screen, true);
if (screen->fullscreen) {
// On Windows, in maximized+fullscreen, disabling
// fullscreen mode unexpectedly triggers the "restored"
// then "maximized" events, leaving the window in a
// weird state (maximized according to the events, but
// not maximized visually).
return true;
}
return true;
case SDL_EVENT_WINDOW_ENTER_FULLSCREEN:
LOGD("Switched to fullscreen mode");
assert(screen->has_video_window);
return true;
case SDL_EVENT_WINDOW_LEAVE_FULLSCREEN:
LOGD("Switched to windowed mode");
assert(screen->has_video_window);
if (is_windowed(screen)) {
screen->maximized = false;
screen->minimized = false;
if (screen->has_video_window) {
apply_pending_resize(screen);
sc_screen_render(screen, true);
}
@@ -880,16 +948,16 @@ sc_screen_convert_window_to_frame_coords(struct sc_screen *screen,
void
sc_screen_hidpi_scale_coords(struct sc_screen *screen, int32_t *x, int32_t *y) {
bool always_ok;
(void) always_ok;
// take the HiDPI scaling (dw/ww and dh/wh) into account
int ww, wh, dw, dh;
always_ok = SDL_GetWindowSize(screen->window, &ww, &wh);
assert(always_ok);
struct sc_size window_size = sc_sdl_get_window_size(screen->window);
int64_t ww = window_size.width;
int64_t wh = window_size.height;
struct sc_size drawable_size =
sc_sdl_get_window_size_in_pixels(screen->window);
int64_t dw = drawable_size.width;
int64_t dh = drawable_size.height;
always_ok = SDL_GetWindowSizeInPixels(screen->window, &dw, &dh);
assert(always_ok);
// scale for HiDPI (64 bits for intermediate multiplications)
*x = (int64_t) *x * dw / ww;

View File

@@ -62,6 +62,9 @@ struct sc_screen {
struct SDL_Rect rect;
bool has_frame;
bool has_video_window;
bool fullscreen;
bool maximized;
bool minimized;
AVFrame *frame;

View File

@@ -6,6 +6,8 @@
#include <stdbool.h>
#include <libavcodec/avcodec.h>
#include "trait/packet_sink.h"
/**
* Frame sink trait.
*
@@ -17,9 +19,16 @@ struct sc_frame_sink {
struct sc_frame_sink_ops {
/* The codec context is valid until the sink is closed */
bool (*open)(struct sc_frame_sink *sink, const AVCodecContext *ctx);
bool (*open)(struct sc_frame_sink *sink, const AVCodecContext *ctx,
const struct sc_stream_session *session);
void (*close)(struct sc_frame_sink *sink);
bool (*push)(struct sc_frame_sink *sink, const AVFrame *frame);
/**
* Optional callback to be notified of a new stream session.
*/
bool (*push_session)(struct sc_frame_sink *sink,
const struct sc_stream_session *session);
};
#endif

View File

@@ -27,11 +27,12 @@ sc_frame_source_sinks_close_firsts(struct sc_frame_source *source,
bool
sc_frame_source_sinks_open(struct sc_frame_source *source,
const AVCodecContext *ctx) {
const AVCodecContext *ctx,
const struct sc_stream_session *session) {
assert(source->sink_count);
for (unsigned i = 0; i < source->sink_count; ++i) {
struct sc_frame_sink *sink = source->sinks[i];
if (!sink->ops->open(sink, ctx)) {
if (!sink->ops->open(sink, ctx, session)) {
sc_frame_source_sinks_close_firsts(source, i);
return false;
}
@@ -59,3 +60,18 @@ sc_frame_source_sinks_push(struct sc_frame_source *source,
return true;
}
bool
sc_frame_source_sinks_push_session(struct sc_frame_source *source,
const struct sc_stream_session *session) {
assert(source->sink_count);
for (unsigned i = 0; i < source->sink_count; ++i) {
struct sc_frame_sink *sink = source->sinks[i];
if (sink->ops->push_session &&
!sink->ops->push_session(sink, session)) {
return false;
}
}
return true;
}

View File

@@ -28,7 +28,8 @@ sc_frame_source_add_sink(struct sc_frame_source *source,
bool
sc_frame_source_sinks_open(struct sc_frame_source *source,
const AVCodecContext *ctx);
const AVCodecContext *ctx,
const struct sc_stream_session *session);
void
sc_frame_source_sinks_close(struct sc_frame_source *source);
@@ -37,4 +38,8 @@ bool
sc_frame_source_sinks_push(struct sc_frame_source *source,
const AVFrame *frame);
bool
sc_frame_source_sinks_push_session(struct sc_frame_source *source,
const struct sc_stream_session *session);
#endif

View File

@@ -15,12 +15,28 @@ struct sc_packet_sink {
const struct sc_packet_sink_ops *ops;
};
struct sc_stream_session_video {
uint32_t width;
uint32_t height;
};
struct sc_stream_session {
struct sc_stream_session_video video;
};
struct sc_packet_sink_ops {
/* The codec context is valid until the sink is closed */
bool (*open)(struct sc_packet_sink *sink, AVCodecContext *ctx);
bool (*open)(struct sc_packet_sink *sink, AVCodecContext *ctx,
const struct sc_stream_session *session);
void (*close)(struct sc_packet_sink *sink);
bool (*push)(struct sc_packet_sink *sink, const AVPacket *packet);
/**
* Optional callback to be notified of a new stream session.
*/
bool (*push_session)(struct sc_packet_sink *sink,
const struct sc_stream_session *session);
/*/
* Called when the input stream has been disabled at runtime.
*

View File

@@ -27,11 +27,12 @@ sc_packet_source_sinks_close_firsts(struct sc_packet_source *source,
bool
sc_packet_source_sinks_open(struct sc_packet_source *source,
AVCodecContext *ctx) {
AVCodecContext *ctx,
const struct sc_stream_session *session) {
assert(source->sink_count);
for (unsigned i = 0; i < source->sink_count; ++i) {
struct sc_packet_sink *sink = source->sinks[i];
if (!sink->ops->open(sink, ctx)) {
if (!sink->ops->open(sink, ctx, session)) {
sc_packet_source_sinks_close_firsts(source, i);
return false;
}
@@ -60,6 +61,20 @@ sc_packet_source_sinks_push(struct sc_packet_source *source,
return true;
}
bool
sc_packet_source_sinks_push_session(struct sc_packet_source *source,
const struct sc_stream_session *session) {
assert(source->sink_count);
for (unsigned i = 0; i < source->sink_count; ++i) {
struct sc_packet_sink *sink = source->sinks[i];
if (!sink->ops->push_session(sink, session)) {
return false;
}
}
return true;
}
void
sc_packet_source_sinks_disable(struct sc_packet_source *source) {
assert(source->sink_count);

View File

@@ -28,7 +28,8 @@ sc_packet_source_add_sink(struct sc_packet_source *source,
bool
sc_packet_source_sinks_open(struct sc_packet_source *source,
AVCodecContext *ctx);
AVCodecContext *ctx,
const struct sc_stream_session *session);
void
sc_packet_source_sinks_close(struct sc_packet_source *source);
@@ -37,6 +38,10 @@ bool
sc_packet_source_sinks_push(struct sc_packet_source *source,
const AVPacket *packet);
bool
sc_packet_source_sinks_push_session(struct sc_packet_source *source,
const struct sc_stream_session *session);
void
sc_packet_source_sinks_disable(struct sc_packet_source *source);

View File

@@ -74,9 +74,10 @@ sc_gamepad_processor_process_gamepad_added(struct sc_gamepad_processor *gp,
return;
}
SDL_Gamepad *sdl_gamepad = SDL_GetGamepadFromID(event->gamepad_id);
assert(sdl_gamepad);
const char *name = SDL_GetGamepadName(sdl_gamepad);
SDL_Gamepad * game_controller =
SDL_GetGamepadFromID(event->gamepad_id);
assert(game_controller);
const char *name = SDL_GetGamepadName(game_controller);
LOGI("Gamepad added: [%" PRIu32 "] %s", event->gamepad_id, name);
sc_gamepad_uhid_send_open(gamepad, &hid_open);

View File

@@ -75,7 +75,7 @@ scrcpy_otg(struct scrcpy_options *options) {
if (options->gamepad_input_mode != SC_GAMEPAD_INPUT_MODE_DISABLED) {
if (!SDL_Init(SDL_INIT_GAMEPAD)) {
LOGE("Could not initialize SDL gamepad: %s", SDL_GetError());
LOGE("Could not initialize SDL controller: %s", SDL_GetError());
// Not fatal, keyboard/mouse should still work
}
}

View File

@@ -7,11 +7,14 @@
#include "options.h"
#include "util/acksync.h"
#include "util/log.h"
#include "util/sdl.h"
#include "util/window.h"
static void
sc_screen_otg_render(struct sc_screen_otg *screen) {
sc_sdl_render_clear(screen->renderer);
bool always_ok = SDL_RenderClear(screen->renderer);
(void) always_ok;
assert(always_ok);
if (screen->texture) {
bool ok =
SDL_RenderTexture(screen->renderer, screen->texture, NULL, NULL);
@@ -19,7 +22,9 @@ sc_screen_otg_render(struct sc_screen_otg *screen) {
LOGW("Could not render texture: %s", SDL_GetError());
}
}
sc_sdl_render_present(screen->renderer);
always_ok = SDL_RenderPresent(screen->renderer);
assert(always_ok);
}
bool
@@ -48,7 +53,7 @@ sc_screen_otg_init(struct sc_screen_otg *screen,
}
screen->window =
sc_sdl_create_window(title, x, y, width, height, window_flags);
sc_create_sdl_window(title, x, y, width, height, window_flags);
if (!screen->window) {
LOGE("Could not create window: %s", SDL_GetError());
return false;
@@ -192,16 +197,16 @@ sc_screen_otg_process_gamepad_device(struct sc_screen_otg *screen,
struct sc_gamepad_processor *gp = &screen->gamepad->gamepad_processor;
if (event->type == SDL_EVENT_GAMEPAD_ADDED) {
SDL_Gamepad *sdl_gamepad = SDL_OpenGamepad(event->which);
if (!sdl_gamepad) {
LOGW("Could not open gamepad");
SDL_Gamepad *gc = SDL_OpenGamepad(event->which);
if (!gc) {
LOGW("Could not open game controller");
return;
}
SDL_Joystick *joystick = SDL_GetGamepadJoystick(sdl_gamepad);
SDL_Joystick *joystick = SDL_GetGamepadJoystick(gc);
if (!joystick) {
LOGW("Could not get gamepad joystick");
SDL_CloseGamepad(sdl_gamepad);
LOGW("Could not get controller joystick");
SDL_CloseGamepad(gc);
return;
}
@@ -212,9 +217,9 @@ sc_screen_otg_process_gamepad_device(struct sc_screen_otg *screen,
} else if (event->type == SDL_EVENT_GAMEPAD_REMOVED) {
SDL_JoystickID id = event->which;
SDL_Gamepad *sdl_gamepad = SDL_GetGamepadFromID(id);
if (sdl_gamepad) {
SDL_CloseGamepad(sdl_gamepad);
SDL_Gamepad *gc = SDL_GetGamepadFromID(id);
if (gc) {
SDL_CloseGamepad(gc);
} else {
LOGW("Unknown gamepad device removed");
}
@@ -258,7 +263,7 @@ sc_screen_otg_process_gamepad_button(struct sc_screen_otg *screen,
struct sc_gamepad_button_event evt = {
.gamepad_id = event->which,
.action = sc_action_from_sdl_gamepad_button_type(event->type),
.action = sc_action_from_sdl_controllerbutton_type(event->type),
.button = button,
};
gp->ops->process_gamepad_button(gp, &evt);

View File

@@ -1,149 +0,0 @@
#include "sdl.h"
#include <assert.h>
#include <stdlib.h>
#include "util/log.h"
SDL_Window *
sc_sdl_create_window(const char *title, int64_t x, int64_t y, int64_t width,
int64_t height, int64_t flags) {
SDL_Window *window = NULL;
SDL_PropertiesID props = SDL_CreateProperties();
if (!props) {
return NULL;
}
bool ok =
SDL_SetStringProperty(props, SDL_PROP_WINDOW_CREATE_TITLE_STRING,
title);
ok &= SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_X_NUMBER, x);
ok &= SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_Y_NUMBER, y);
ok &= SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER,
width);
ok &= SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER,
height);
ok &= SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_FLAGS_NUMBER,
flags);
if (!ok) {
SDL_DestroyProperties(props);
return NULL;
}
window = SDL_CreateWindowWithProperties(props);
SDL_DestroyProperties(props);
return window;
}
struct sc_size
sc_sdl_get_window_size(SDL_Window *window) {
int width;
int height;
bool ok = SDL_GetWindowSize(window, &width, &height);
if (!ok) {
LOGE("Could not get window size: %s", SDL_GetError());
LOGE("Please report the error");
// fatal error
abort();
}
struct sc_size size = {
.width = width,
.height = height,
};
return size;
}
struct sc_size
sc_sdl_get_window_size_in_pixels(SDL_Window *window) {
int width;
int height;
bool ok = SDL_GetWindowSizeInPixels(window, &width, &height);
if (!ok) {
LOGE("Could not get window size: %s", SDL_GetError());
LOGE("Please report the error");
// fatal error
abort();
}
struct sc_size size = {
.width = width,
.height = height,
};
return size;
}
void
sc_sdl_set_window_size(SDL_Window *window, struct sc_size size) {
bool ok = SDL_SetWindowSize(window, size.width, size.height);
if (!ok) {
LOGE("Could not set window size: %s", SDL_GetError());
assert(!"unexpected");
}
}
struct sc_point
sc_sdl_get_window_position(SDL_Window *window) {
int x;
int y;
bool ok = SDL_GetWindowPosition(window, &x, &y);
if (!ok) {
LOGE("Could not get window position: %s", SDL_GetError());
LOGE("Please report the error");
// fatal error
abort();
}
struct sc_point point = {
.x = x,
.y = y,
};
return point;
}
void
sc_sdl_set_window_position(SDL_Window *window, struct sc_point point) {
bool ok = SDL_SetWindowPosition(window, point.x, point.y);
if (!ok) {
LOGE("Could not set window position: %s", SDL_GetError());
assert(!"unexpected");
}
}
void
sc_sdl_show_window(SDL_Window *window) {
bool ok = SDL_ShowWindow(window);
if (!ok) {
LOGE("Could not show window: %s", SDL_GetError());
assert(!"unexpected");
}
}
void
sc_sdl_hide_window(SDL_Window *window) {
bool ok = SDL_HideWindow(window);
if (!ok) {
LOGE("Could not hide window: %s", SDL_GetError());
assert(!"unexpected");
}
}
bool
sc_sdl_render_clear(SDL_Renderer *renderer) {
bool ok = SDL_RenderClear(renderer);
if (!ok) {
LOGW("Could not clear rendering: %s", SDL_GetError());
}
return ok;
}
void
sc_sdl_render_present(SDL_Renderer *renderer) {
bool ok = SDL_RenderPresent(renderer);
if (!ok) {
LOGE("Could not render: %s", SDL_GetError());
assert(!"unexpected");
}
}

View File

@@ -1,43 +0,0 @@
#ifndef SC_SDL_H
#define SC_SDL_H
#include "common.h"
#include <stdint.h>
#include <SDL3/SDL_render.h>
#include <SDL3/SDL_video.h>
#include "coords.h"
SDL_Window *
sc_sdl_create_window(const char *title, int64_t x, int64_t y, int64_t width,
int64_t height, int64_t flags);
struct sc_size
sc_sdl_get_window_size(SDL_Window *window);
struct sc_size
sc_sdl_get_window_size_in_pixels(SDL_Window *window);
void
sc_sdl_set_window_size(SDL_Window *window, struct sc_size size);
struct sc_point
sc_sdl_get_window_position(SDL_Window *window);
void
sc_sdl_set_window_position(SDL_Window *window, struct sc_point point);
void
sc_sdl_show_window(SDL_Window *window);
void
sc_sdl_hide_window(SDL_Window *window);
bool
sc_sdl_render_clear(SDL_Renderer *renderer);
void
sc_sdl_render_present(SDL_Renderer *renderer);
#endif

33
app/src/util/window.c Normal file
View File

@@ -0,0 +1,33 @@
#include "window.h"
SDL_Window *
sc_create_sdl_window(const char *title, int64_t x, int64_t y, int64_t width,
int64_t height, int64_t flags) {
SDL_Window *window = NULL;
SDL_PropertiesID props = SDL_CreateProperties();
if (!props) {
return NULL;
}
bool ok =
SDL_SetStringProperty(props, SDL_PROP_WINDOW_CREATE_TITLE_STRING,
title);
ok &= SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_X_NUMBER, x);
ok &= SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_Y_NUMBER, y);
ok &= SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER,
width);
ok &= SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER,
height);
ok &= SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_FLAGS_NUMBER,
flags);
if (!ok) {
SDL_DestroyProperties(props);
return NULL;
}
window = SDL_CreateWindowWithProperties(props);
SDL_DestroyProperties(props);
return window;
}

13
app/src/util/window.h Normal file
View File

@@ -0,0 +1,13 @@
#ifndef SC_WINDOW_H
#define SC_WINDOW_H
#include "common.h"
#include <stdint.h>
#include <SDL3/SDL_video.h>
SDL_Window *
sc_create_sdl_window(const char *title, int64_t x, int64_t y, int64_t width,
int64_t height, int64_t flags);
#endif

View File

@@ -146,9 +146,11 @@ run_v4l2_sink(void *data) {
}
static bool
sc_v4l2_sink_open(struct sc_v4l2_sink *vs, const AVCodecContext *ctx) {
sc_v4l2_sink_open(struct sc_v4l2_sink *vs, const AVCodecContext *ctx,
const struct sc_stream_session *session) {
assert(ctx->pix_fmt == AV_PIX_FMT_YUV420P);
(void) ctx;
(void) session;
bool ok = sc_frame_buffer_init(&vs->fb);
if (!ok) {
@@ -326,9 +328,10 @@ sc_v4l2_sink_push(struct sc_v4l2_sink *vs, const AVFrame *frame) {
}
static bool
sc_v4l2_frame_sink_open(struct sc_frame_sink *sink, const AVCodecContext *ctx) {
sc_v4l2_frame_sink_open(struct sc_frame_sink *sink, const AVCodecContext *ctx,
const struct sc_stream_session *session) {
struct sc_v4l2_sink *vs = DOWNCAST(sink);
return sc_v4l2_sink_open(vs, ctx);
return sc_v4l2_sink_open(vs, ctx, session);
}
static void

View File

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

@@ -1,73 +0,0 @@
# Build with:
# podman build -t scrcpy-builder .
#
# Run with:
# podman run \
# -v PATH_TO_SCRCPY:/home/debian/scrcpy \
# --userns=keep-id \
# -it scrcpy-builder \
# bash
FROM debian:trixie
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
sudo wget unzip gcc git pkg-config meson ninja-build cmake \
libsdl3-dev libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \
libswresample-dev libusb-1.0-0-dev openjdk-21-jdk \
mingw-w64 mingw-w64-tools libz-mingw-w64-dev
RUN groupadd -g 1000 debian \
&& useradd -m -u 1000 -g 1000 -s /bin/bash debian
RUN echo "debian ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
ENV ANDROID_HOME=/opt/android/sdk
RUN mkdir -p "$ANDROID_HOME" \
&& chown debian:debian "$ANDROID_HOME"
USER debian
WORKDIR /home/debian
ENV CMDLINETOOLS_URL=https://dl.google.com/android/repository/commandlinetools-linux-13114758_latest.zip
ENV CMDLINETOOLS_SHA256=7ec965280a073311c339e571cd5de778b9975026cfcbe79f2b1cdcb1e15317ee
RUN wget -q "$CMDLINETOOLS_URL" -O cmdlinetools.zip \
&& echo "$CMDLINETOOLS_SHA256 cmdlinetools.zip" | sha256sum -c
RUN mkdir tmp \
&& unzip -q cmdlinetools.zip -d tmp \
&& mkdir -p "$ANDROID_HOME/cmdline-tools" \
&& mv tmp/cmdline-tools "$ANDROID_HOME/cmdline-tools/latest" \
&& rmdir tmp \
&& rm cmdlinetools.zip
# To get the licence hash, run manually: "$ANDROID_HOME"/cmdline-tools/latest/bin/sdkmanager --licenses
# Build-tools 35 is still needed for the current AGP version
RUN mkdir -p "$ANDROID_HOME/licenses" \
&& echo 24333f8a63b6825ea9c5514f83c2829b004d1fee > "$ANDROID_HOME/licenses/android-sdk-license" \
&& $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager "platform-tools" "platforms;android-36" "build-tools;35.0.0" "build-tools;36.0.0"
# For scrcpy build scripts
ENV GRADLE_VERSION=8.14.3
ENV GRADLE_HOME=/opt/gradle-$GRADLE_VERSION
ENV GRADLE_URL=https://services.gradle.org/distributions/gradle-$GRADLE_VERSION-bin.zip
ENV GRADLE_SHA256=bd71102213493060956ec229d946beee57158dbd89d0e62b91bca0fa2c5f3531
ENV GRADLE=gradle
ENV PATH=$GRADLE_HOME/bin:$PATH
USER root
RUN wget -q "$GRADLE_URL" -O gradle.zip \
&& echo "$GRADLE_SHA256 gradle.zip" | sha256sum -c
RUN unzip -q gradle.zip -d /opt \
&& rm gradle.zip
USER debian
# Pre-download gradle dependencies for scrcpy
RUN mkdir -p /home/debian/fake-scrcpy/app
COPY fake.gradle /home/debian/fake-scrcpy/build.gradle
COPY fake_app.gradle /home/debian/fake-scrcpy/app/build.gradle
RUN echo "include ':app'" > /home/debian/fake-scrcpy/settings.gradle \
&& gradle -p /home/debian/fake-scrcpy dependencies androidDependencies --no-daemon \
&& rm -rf /home/debian/fake-scrcpy

View File

@@ -1,17 +0,0 @@
// Fake build.gradle to pre-download gradle dependencies
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.13.0'
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}

View File

@@ -1,21 +0,0 @@
// Fake app/build.gradle to pre-download gradle dependencies
apply plugin: 'com.android.application'
apply plugin: 'checkstyle'
android {
buildToolsVersion = "36.0.0"
namespace = "com.genymobile.scrcpy"
compileSdk 36
defaultConfig {
minSdkVersion 21
targetSdkVersion 36
}
}
checkstyle {
toolVersion = '10.12.5'
}
dependencies {
testImplementation 'junit:junit:4.13.2'
}

View File

@@ -30,13 +30,13 @@ the following files to a directory accessible from your `PATH`:
It is also available in scrcpy releases.
The client requires [FFmpeg] and [SDL]. Just follow the instructions.
The client requires [FFmpeg] and [LibSDL2]. Just follow the instructions.
[adb]: https://developer.android.com/studio/command-line/adb.html
[platform-tools]: https://developer.android.com/studio/releases/platform-tools.html
[platform-tools-windows]: https://dl.google.com/android/repository/platform-tools-latest-windows.zip
[ffmpeg]: https://en.wikipedia.org/wiki/FFmpeg
[SDL]: https://en.wikipedia.org/wiki/Simple_DirectMedia_Layer
[LibSDL2]: https://en.wikipedia.org/wiki/Simple_DirectMedia_Layer
@@ -50,10 +50,10 @@ Install the required packages from your package manager.
```bash
# runtime dependencies
sudo apt install ffmpeg libsdl3-0 adb libusb-1.0-0
sudo apt install ffmpeg libsdl2-2.0-0 adb libusb-1.0-0
# client build dependencies
sudo apt install gcc git pkg-config meson ninja-build libsdl3-dev \
sudo apt install gcc git pkg-config meson ninja-build libsdl2-dev \
libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \
libswresample-dev libusb-1.0-0-dev
@@ -77,7 +77,7 @@ pip3 install meson
sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm
# client build dependencies
sudo dnf install SDL3-devel ffms2-devel libusb1-devel libavdevice-free-devel meson gcc make
sudo dnf install SDL2-devel ffms2-devel libusb1-devel libavdevice-free-devel meson gcc make
# server build dependencies
sudo dnf install java-devel
@@ -121,7 +121,7 @@ install the required packages:
```bash
# runtime dependencies
pacman -S mingw-w64-x86_64-sdl3 \
pacman -S mingw-w64-x86_64-SDL2 \
mingw-w64-x86_64-ffmpeg \
mingw-w64-x86_64-libusb
@@ -136,7 +136,7 @@ For a 32 bits version, replace `x86_64` by `i686`:
```bash
# runtime dependencies
pacman -S mingw-w64-i686-sdl3 \
pacman -S mingw-w64-i686-SDL2 \
mingw-w64-i686-ffmpeg \
mingw-w64-i686-libusb
@@ -162,7 +162,7 @@ Install the packages with [Homebrew]:
```bash
# runtime dependencies
brew install sdl3 ffmpeg libusb
brew install sdl2 ffmpeg libusb
# client build dependencies
brew install pkg-config meson
@@ -233,10 +233,10 @@ install` must be run as root)._
#### Option 2: Use prebuilt server
- [`scrcpy-server-v3.3.4`][direct-scrcpy-server]
<sub>SHA-256: `8588238c9a5a00aa542906b6ec7e6d5541d9ffb9b5d0f6e1bc0e365e2303079e`</sub>
- [`scrcpy-server-v3.3.3`][direct-scrcpy-server]
<sub>SHA-256: `7e70323ba7f259649dd4acce97ac4fefbae8102b2c6d91e2e7be613fd5354be0`</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.3/scrcpy-server-v3.3.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

@@ -409,12 +409,11 @@ 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
- `send_device_meta=false`: disable 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
video)
- `send_stream_meta`: disable codec and video size metadata
- `raw_stream`: disable all the above
[server-specific options]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/Options.java#L309-L329

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.3.tar.gz`][direct-linux-x86_64] (x86_64)
<sub>SHA-256: `9b30e813e8191329ba8025dc80cb0f198fb0a318960a3b5c15395cf675c9c638`</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.3/scrcpy-linux-x86_64-v3.3.3.tar.gz
and extract it.
@@ -39,8 +39,8 @@ First, you need to install the required packages:
```bash
# for Debian/Ubuntu
sudo apt install ffmpeg libsdl3-0 adb wget \
gcc git pkg-config meson ninja-build libsdl3-dev \
sudo apt install ffmpeg libsdl2-2.0-0 adb wget \
gcc git pkg-config meson ninja-build libsdl2-dev \
libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \
libswresample-dev libusb-1.0-0 libusb-1.0-0-dev
```

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.3.tar.gz`][direct-macos-aarch64] (aarch64)
<sub>SHA-256: `b93299468f19ae89ac70f7c1453914c41f1f2bcd31f6ab530038da885c19581f`</sub>
- [`scrcpy-macos-x86_64-v3.3.3.tar.gz`][direct-macos-x86_64] (x86_64)
<sub>SHA-256: `c767fc1d41e4ae26e40558656570962f474739924fd22ee023d8754889ee4366`</sub>
[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.3/scrcpy-macos-aarch64-v3.3.3.tar.gz
[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.3/scrcpy-macos-x86_64-v3.3.3.tar.gz
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.3.zip`][direct-win64] (64-bit)
<sub>SHA-256: `4b458d33d0436688c69875cd267cae6fa8be08aa3c17772edf3a940a3dc4b17e`</sub>
- [`scrcpy-win32-v3.3.3.zip`][direct-win32] (32-bit)
<sub>SHA-256: `e3d43e21c0bd6e070381c390c1e4cccd48a1e71ae73a8c217e6e6b8506598c79`</sub>
[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.3/scrcpy-win64-v3.3.3.zip
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.3/scrcpy-win32-v3.3.3.zip
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.3/scrcpy-server-v3.3.3
PREBUILT_SERVER_SHA256=7e70323ba7f259649dd4acce97ac4fefbae8102b2c6d91e2e7be613fd5354be0
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.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 30303
versionName "3.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.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

@@ -109,10 +109,8 @@ public final class FakeContext extends ContextWrapper {
}
// "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)) {
// See <https://github.com/Genymobile/scrcpy/issues/6224>
if (Context.CLIPBOARD_SERVICE.equals(name) || "semclipboard".equals(name)) {
try {
Field field = service.getClass().getDeclaredField("mContext");
field.setAccessible(true);

View File

@@ -78,7 +78,7 @@ public class Options {
private boolean sendDeviceMeta = true; // send device name and size
private boolean sendFrameMeta = true; // send PTS so that the client may record properly
private boolean sendDummyByte = true; // write a byte on start to detect connection issues
private boolean sendCodecMeta = true; // write the codec metadata before the stream
private boolean sendStreamMeta = true; // write the stream metadata (codec and session)
public Ln.Level getLogLevel() {
return logLevel;
@@ -284,8 +284,8 @@ public class Options {
return sendDummyByte;
}
public boolean getSendCodecMeta() {
return sendCodecMeta;
public boolean getSendStreamMeta() {
return sendStreamMeta;
}
@SuppressWarnings("MethodLength")
@@ -414,7 +414,6 @@ public class Options {
if (!value.isEmpty()) {
options.audioEncoder = value;
}
break;
case "power_off_on_close":
options.powerOffScreenOnClose = Boolean.parseBoolean(value);
break;
@@ -501,8 +500,8 @@ public class Options {
case "send_dummy_byte":
options.sendDummyByte = Boolean.parseBoolean(value);
break;
case "send_codec_meta":
options.sendCodecMeta = Boolean.parseBoolean(value);
case "send_stream_meta":
options.sendStreamMeta = Boolean.parseBoolean(value);
break;
case "raw_stream":
boolean rawStream = Boolean.parseBoolean(value);
@@ -510,7 +509,7 @@ public class Options {
options.sendDeviceMeta = false;
options.sendFrameMeta = false;
options.sendDummyByte = false;
options.sendCodecMeta = false;
options.sendStreamMeta = false;
}
break;
default:

View File

@@ -27,7 +27,6 @@ import com.genymobile.scrcpy.video.VideoSource;
import android.annotation.SuppressLint;
import android.os.Build;
import android.os.Looper;
import android.system.Os;
import java.io.File;
import java.io.IOException;
@@ -126,7 +125,7 @@ public final class Server {
audioCapture = new AudioPlaybackCapture(options.getAudioDup());
}
Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(), options.getSendFrameMeta());
Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendStreamMeta(), options.getSendFrameMeta());
AsyncProcessor audioRecorder;
if (audioCodec == AudioCodec.RAW) {
audioRecorder = new AudioRawRecorder(audioCapture, audioStreamer);
@@ -137,7 +136,7 @@ public final class Server {
}
if (video) {
Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendCodecMeta(),
Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendStreamMeta(),
options.getSendFrameMeta());
SurfaceCapture surfaceCapture;
if (options.getVideoSource() == VideoSource.DISPLAY) {
@@ -226,16 +225,10 @@ public final class Server {
}
private static void internalMain(String... args) throws Exception {
Thread.UncaughtExceptionHandler defaultHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
Ln.e("Exception on thread " + t, e);
if (defaultHandler != null) {
defaultHandler.uncaughtException(t, e);
}
});
dropRootPrivileges();
prepareMainLooper();
Options options = Options.parse(args);
@@ -276,17 +269,4 @@ public final class Server {
// Do not print stack trace, a user-friendly error-message has already been logged
}
}
@SuppressWarnings("deprecation")
private static void dropRootPrivileges() {
try {
if (Os.getuid() == 0) {
// Copy-paste does not work with root user
// <https://github.com/Genymobile/scrcpy/issues/6224>
Os.setuid(2000);
}
} catch (Exception e) {
Ln.w("Cannot set UID", e);
}
}
}

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

@@ -14,12 +14,13 @@ import java.util.Arrays;
public final class Streamer {
private static final long PACKET_FLAG_CONFIG = 1L << 63;
private static final long PACKET_FLAG_KEY_FRAME = 1L << 62;
private static final long PACKET_FLAG_SESSION = 1L << 63;
private static final long PACKET_FLAG_CONFIG = 1L << 62;
private static final long PACKET_FLAG_KEY_FRAME = 1L << 61;
private final FileDescriptor fd;
private final Codec codec;
private final boolean sendCodecMeta;
private final boolean sendStreamMeta;
private final boolean sendFrameMeta;
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
@@ -27,7 +28,7 @@ public final class Streamer {
public Streamer(FileDescriptor fd, Codec codec, boolean sendCodecMeta, boolean sendFrameMeta) {
this.fd = fd;
this.codec = codec;
this.sendCodecMeta = sendCodecMeta;
this.sendStreamMeta = sendCodecMeta;
this.sendFrameMeta = sendFrameMeta;
}
@@ -36,7 +37,7 @@ public final class Streamer {
}
public void writeAudioHeader() throws IOException {
if (sendCodecMeta) {
if (sendStreamMeta) {
ByteBuffer buffer = ByteBuffer.allocate(4);
buffer.putInt(codec.getId());
buffer.flip();
@@ -44,12 +45,10 @@ public final class Streamer {
}
}
public void writeVideoHeader(Size videoSize) throws IOException {
if (sendCodecMeta) {
ByteBuffer buffer = ByteBuffer.allocate(12);
public void writeVideoHeader() throws IOException {
if (sendStreamMeta) {
ByteBuffer buffer = ByteBuffer.allocate(4);
buffer.putInt(codec.getId());
buffer.putInt(videoSize.getWidth());
buffer.putInt(videoSize.getHeight());
buffer.flip();
IO.writeFully(fd, buffer);
}
@@ -89,6 +88,18 @@ public final class Streamer {
writePacket(codecBuffer, pts, config, keyFrame);
}
public void writeSessionMeta(int width, int height) throws IOException {
if (sendStreamMeta) {
headerBuffer.clear();
headerBuffer.putInt((int) (PACKET_FLAG_SESSION >> 32)); // Set the first bit to 1
headerBuffer.putInt(width);
headerBuffer.putInt(height);
headerBuffer.flip();
IO.writeFully(fd, headerBuffer);
}
}
private void writeFrameMeta(FileDescriptor fd, int packetSize, long pts, boolean config, boolean keyFrame) throws IOException {
headerBuffer.clear();

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,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

@@ -71,16 +71,13 @@ public class SurfaceEncoder implements AsyncProcessor {
try {
boolean alive;
boolean headerWritten = false;
streamer.writeVideoHeader();
do {
reset.consumeReset(); // If a capture reset was requested, it is implicitly fulfilled
capture.prepare();
Size size = capture.getSize();
if (!headerWritten) {
streamer.writeVideoHeader(size);
headerWritten = true;
}
format.setInteger(MediaFormat.KEY_WIDTH, size.getWidth());
format.setInteger(MediaFormat.KEY_HEIGHT, size.getHeight());
@@ -107,6 +104,7 @@ public class SurfaceEncoder implements AsyncProcessor {
boolean resetRequested = reset.consumeReset();
if (!resetRequested) {
// If a reset is requested during encode(), it will interrupt the encoding by an EOS
streamer.writeSessionMeta(size.getWidth(), size.getHeight());
encode(mediaCodec, streamer);
}
// The capture might have been closed internally (for example if the camera is disconnected)

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