Compare commits

..

8 Commits

Author SHA1 Message Date
Romain Vimont
323549e82f Prevent build from falling back to system libs
Ensure that if a file or function is not found, the build does not
attempt to use system libraries. Falling back could result in using
libraries that are incompatible due to wrong versions or features.

PR #6671 <https://github.com/Genymobile/scrcpy/pull/6671>
2026-02-21 12:24:41 +01:00
Romain Vimont
90d11810e3 Set MediaCodec KEY_LATENCY to the minimum value
The encoder must output a frame as soon as one frame is queued.

Refs <https://developer.android.com/reference/android/media/MediaFormat#KEY_LATENCY>
Refs #6238 comment <https://github.com/Genymobile/scrcpy/issues/6238#issuecomment-3828402687>
PR #6670 <https://github.com/Genymobile/scrcpy/pull/6670>
2026-02-21 12:21:54 +01:00
Romain Vimont
1f19ec4aec Set MediaCodec KEY_PRIORITY to real-time (0)
Refs <https://developer.android.com/reference/android/media/MediaFormat#KEY_PRIORITY>
Refs #6238 comment <https://github.com/Genymobile/scrcpy/issues/6238#issuecomment-3828402687>
PR #6670 <https://github.com/Genymobile/scrcpy/pull/6670>
2026-02-21 12:21:44 +01:00
Romain Vimont
8b0206f7be Keep Windows terminal open on error
If scrcpy is launched by double-clicking scrcpy.exe in Windows Explorer,
automatically set --pause-on-exit=if-error.

Without this, the terminal would close immediately, preventing the user
from seeing the error.

Also remove scrcpy-console.bat, which is now useless.

PR #6667 <https://github.com/Genymobile/scrcpy/pull/6667>
2026-02-21 12:17:31 +01:00
Romain Vimont
f91fa56593 Fix segfault on rotation while recording
The packet sink push_session() callback is optional, but it was called
unconditionnally even when not implemented, leading to a segfault.

Bug introduced by commit 78cba1b7c2.

Fixes #6687 <https://github.com/Genymobile/scrcpy/issues/6687>
2026-02-21 10:45:12 +01:00
Romain Vimont
23710e04c1 Disable audio stream with fatal error Throwable
Only an AudioCaptureException should disable the audio stream without
making scrcpy exit (it is not a fatal error).

A ConfigurationException or any other Throwable must be considered a
fatal error.

                               BEFORE      AFTER
  AudioCaptureException:    non-fatal  non-fatal
  ConfigurationException:       fatal      fatal
  any other Throwable:      non-fatal      fatal

Refs #6600 comment <https://github.com/Genymobile/scrcpy/issues/6600#issuecomment-3744934826>
2026-02-12 20:20:52 +01:00
Romain Vimont
51cbd9a5bc Detect TCP devices provided by mDNS
Their serial is not in the form ip:port, but rather a complex string
containing "adb-tls-connect".

Fixes #6248 <https://github.com/Genymobile/scrcpy/issues/6248>
PR #6665 <https://github.com/Genymobile/scrcpy/pull/6665>
2026-02-12 20:08:46 +01:00
Romain Vimont
82e102e036 Improve adb devices parsing
`adb devices -l` prints one device per line, containing, separated by
spaces:
 - the device serial,
 - the device state,
 - a list of key:value pairs.

However, the device serial itself may contain spaces, making a simple
split ambiguous.

To avoid ambiguity, parse the string backwards:
 - first, parse all the trailing values until the device state,
   identified using a list of well-known values;
 - finally, treat the remaining leading token as the device serial.

Fixes #3537 <https://github.com/Genymobile/scrcpy/issues/3537>
Refs #6248 <https://github.com/Genymobile/scrcpy/issues/6248>
PR #6664 <https://github.com/Genymobile/scrcpy/pull/6664>
2026-02-12 20:06:11 +01:00
14 changed files with 215 additions and 86 deletions

View File

@@ -1,2 +0,0 @@
@echo off
scrcpy.exe --pause-on-exit=if-error %*

View File

@@ -39,5 +39,11 @@ sc_adb_device_get_type(const char *serial) {
return SC_ADB_DEVICE_TYPE_TCPIP;
}
// TCP/IP devices provided by mDNS contain "adb-tls-connect"
// <https://github.com/Genymobile/scrcpy/issues/6248>
if (strstr(serial, "adb-tls-connect")) {
return SC_ADB_DEVICE_TYPE_TCPIP;
}
return SC_ADB_DEVICE_TYPE_USB;
}

View File

@@ -8,6 +8,49 @@
#include "util/log.h"
#include "util/str.h"
static size_t
rstrip_len(const char *s, size_t len) {
size_t i = len;
// Ignore trailing whitespaces
while (i > 0 && (s[i-1] == ' ' || s[i-1] == '\t')) {
--i;
}
return i;
}
static void
locate_last_token(const char *s, size_t len, size_t *start, size_t *end) {
size_t i = rstrip_len(s, len);
*end = i; // excluded
// The token contains non-whitespace chars
while (i > 0 && (s[i-1] != ' ' && s[i-1] != '\t')) {
--i;
}
*start = i; // included
}
static bool
is_device_state(const char *s) {
// <https://android.googlesource.com/platform/packages/modules/adb/+/1cf2f017d312f73b3dc53bda85ef2610e35a80e9/adb.cpp#144>
// "device", "unauthorized" and "offline" are the most common states, so
// check them first.
return !strcmp(s, "device")
|| !strcmp(s, "unauthorized")
|| !strcmp(s, "offline")
|| !strcmp(s, "bootloader")
|| !strcmp(s, "host")
|| !strcmp(s, "recovery")
|| !strcmp(s, "rescue")
|| !strcmp(s, "sideload")
|| !strcmp(s, "authorizing")
|| !strcmp(s, "connecting")
|| !strcmp(s, "detached");
}
static bool
sc_adb_parse_device(char *line, struct sc_adb_device *device) {
// One device line looks like:
@@ -25,64 +68,54 @@ sc_adb_parse_device(char *line, struct sc_adb_device *device) {
return false;
}
char *s = line; // cursor in the line
size_t len = strlen(line);
// After the serial:
// - "adb devices" writes a single '\t'
// - "adb devices -l" writes multiple spaces
// For flexibility, accept both.
size_t serial_len = strcspn(s, " \t");
size_t start;
size_t end;
// The serial (the first token) may contain spaces, which are also token
// separators. To avoid ambiguity, parse the string backwards:
// - first, parse all the trailing values until the device state,
// identified using a list of well-known values;
// - finally, treat the remaining leading token as the device serial.
//
// Refs:
// - <https://github.com/Genymobile/scrcpy/issues/6248>
// - <https://github.com/Genymobile/scrcpy/issues/3537>
const char *state;
const char *model = NULL;
for (;;) {
locate_last_token(line, len, &start, &end);
if (start == end) {
// No more tokens, unexpected
return false;
}
const char *token = &line[start];
line[end] = '\0';
if (!strncmp("model:", token, sizeof("model:") - 1)) {
model = &token[sizeof("model:") - 1];
// We only need the model
} else if (is_device_state(token)) {
state = token;
// The device state is the item immediately after the device serial
break;
}
// Remove the trailing parts already handled
len = start;
}
assert(state);
size_t serial_len = rstrip_len(line, start);
if (!serial_len) {
// empty serial
return false;
}
bool eol = s[serial_len] == '\0';
if (eol) {
// serial alone is unexpected
return false;
}
s[serial_len] = '\0';
char *serial = s;
s += serial_len + 1;
// After the serial, there might be several spaces
s += strspn(s, " \t"); // consume all separators
size_t state_len = strcspn(s, " ");
if (!state_len) {
// empty state
return false;
}
eol = s[state_len] == '\0';
s[state_len] = '\0';
char *state = s;
char *model = NULL;
if (!eol) {
s += state_len + 1;
// Iterate over all properties "key:value key:value ..."
for (;;) {
size_t token_len = strcspn(s, " ");
if (!token_len) {
break;
}
eol = s[token_len] == '\0';
s[token_len] = '\0';
char *token = s;
if (!strncmp("model:", token, sizeof("model:") - 1)) {
model = &token[sizeof("model:") - 1];
// We only need the model
break;
}
if (eol) {
break;
} else {
s+= token_len + 1;
}
}
}
char *serial = line;
line[serial_len] = '\0';
device->serial = strdup(serial);
if (!device->serial) {

View File

@@ -3371,7 +3371,7 @@ sc_get_pause_on_exit(int argc, char *argv[]) {
}
if (arg[15] != '=') {
// Invalid parameter, ignore
return SC_PAUSE_ON_EXIT_FALSE;
return SC_PAUSE_ON_EXIT_UNDEFINED;
}
const char *value = &arg[16];
if (!strcmp(value, "true")) {
@@ -3380,14 +3380,44 @@ sc_get_pause_on_exit(int argc, char *argv[]) {
if (!strcmp(value, "if-error")) {
return SC_PAUSE_ON_EXIT_IF_ERROR;
}
// Set to false, including when the value is invalid
return SC_PAUSE_ON_EXIT_FALSE;
if (!strcmp(value, "false")) {
return SC_PAUSE_ON_EXIT_FALSE;
}
return SC_PAUSE_ON_EXIT_UNDEFINED;
}
}
return SC_PAUSE_ON_EXIT_FALSE;
return SC_PAUSE_ON_EXIT_UNDEFINED;
}
#ifdef _WIN32
/**
* Attempt to detect whether the user launched scrcpy by double-clicking
* scrcpy.exe in Windows Explorer.
*
* If so, the console should remain open on error.
*/
static bool
scrcpy_launched_by_double_click(void) {
// No console window
if (GetConsoleWindow() == NULL) {
return false;
}
// Must be interactive
if (!_isatty(_fileno(stdin)) || !_isatty(_fileno(stdout))) {
return false;
}
// Check how many processes share the console
DWORD dummy;
DWORD count = GetConsoleProcessList(&dummy, 1);
// Only this process attached, assume it was started by double-clicking
return count == 1;
}
#endif
bool
scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
struct sc_getopt_adapter adapter;
@@ -3401,11 +3431,22 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
sc_getopt_adapter_destroy(&adapter);
if (!ret && args->pause_on_exit == SC_PAUSE_ON_EXIT_FALSE) {
if (!ret && args->pause_on_exit == SC_PAUSE_ON_EXIT_UNDEFINED) {
// Check if "--pause-on-exit" is present in the arguments list, because
// it must be taken into account even if command line parsing failed
args->pause_on_exit = sc_get_pause_on_exit(argc, argv);
}
if (args->pause_on_exit == SC_PAUSE_ON_EXIT_UNDEFINED) {
args->pause_on_exit = SC_PAUSE_ON_EXIT_FALSE;
#ifdef _WIN32
if (scrcpy_launched_by_double_click()) {
args->pause_on_exit = SC_PAUSE_ON_EXIT_IF_ERROR;
}
#endif
}
assert(args->pause_on_exit != SC_PAUSE_ON_EXIT_UNDEFINED);
return ret;
}

View File

@@ -8,6 +8,7 @@
#include "options.h"
enum sc_pause_on_exit {
SC_PAUSE_ON_EXIT_UNDEFINED,
SC_PAUSE_ON_EXIT_TRUE,
SC_PAUSE_ON_EXIT_FALSE,
SC_PAUSE_ON_EXIT_IF_ERROR,

View File

@@ -39,7 +39,7 @@ main_scrcpy(int argc, char *argv[]) {
.opts = scrcpy_options_default,
.help = false,
.version = false,
.pause_on_exit = SC_PAUSE_ON_EXIT_FALSE,
.pause_on_exit = SC_PAUSE_ON_EXIT_UNDEFINED,
};
#ifndef NDEBUG

View File

@@ -67,7 +67,8 @@ sc_packet_source_sinks_push_session(struct sc_packet_source *source,
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)) {
if (sink->ops->push_session
&& !sink->ops->push_session(sink, session)) {
return false;
}
}

View File

@@ -163,6 +163,45 @@ static void test_adb_devices_spaces(void) {
sc_adb_devices_destroy(&vec);
}
static void test_adb_devices_serial_with_spaces(void) {
char output[] =
"List of devices attached\n"
"adb-0123456789AB-CDdefg (2)._adb-tls-connect._tcp device "
"product:blazer model:Pixel_10_Pro device:blazer transport_id:3\n";
struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER;
bool ok = sc_adb_parse_devices(output, &vec);
assert(ok);
assert(vec.size == 1);
struct sc_adb_device *device = &vec.data[0];
assert(!strcmp("adb-0123456789AB-CDdefg (2)._adb-tls-connect._tcp",
device->serial));
assert(!strcmp("device", device->state));
assert(!strcmp("Pixel_10_Pro", device->model));
sc_adb_devices_destroy(&vec);
}
static void test_adb_devices_with_devpath_without_colon(void) {
char output[] =
"List of devices attached\n"
"12345678 device 3-1 product:manet model:23117RK66C "
"device:manet transport_id:2\n";
struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER;
bool ok = sc_adb_parse_devices(output, &vec);
assert(ok);
assert(vec.size == 1);
struct sc_adb_device *device = &vec.data[0];
assert(!strcmp("12345678", device->serial));
assert(!strcmp("device", device->state));
assert(!strcmp("23117RK66C", device->model));
sc_adb_devices_destroy(&vec);
}
static void test_get_ip_single_line(void) {
char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src "
"192.168.12.34\r\r\n";
@@ -265,6 +304,8 @@ int main(int argc, char *argv[]) {
test_adb_devices_without_header();
test_adb_devices_corrupted();
test_adb_devices_spaces();
test_adb_devices_serial_with_spaces();
test_adb_devices_with_devpath_without_colon();
test_get_ip_single_line();
test_get_ip_single_line_without_eol();

View File

@@ -72,18 +72,6 @@ Documentation for command line arguments is available:
- `scrcpy --help`
- on [github](/README.md)
To start scrcpy directly without opening a terminal, double-click on one of
these files:
- `scrcpy-console.bat`: start with a terminal open (it will close when scrcpy
terminates, unless an error occurs);
- `scrcpy-noconsole.vbs`: start without a terminal (but you won't see any error
message).
_Avoid double-clicking on `scrcpy.exe` directly: on error, the terminal would
close immediately and you won't have time to read any error message (this
executable is intended to be run from the terminal). Use `scrcpy-console.bat`
instead._
If you plan to always use the same arguments, create a file `myscrcpy.bat`
(enable [show file extensions] to avoid confusion) containing your command, For
example:
@@ -92,9 +80,17 @@ example:
scrcpy --prefer-text --turn-screen-off --stay-awake
```
Add `--pause-on-exit=if-error` if you want the console to remain open when
scrcpy fails:
```bash
scrcpy --prefer-text --turn-screen-off --stay-awake --pause-on-exit=if-error
```
[show file extensions]: https://www.howtogeek.com/205086/beginner-how-to-make-windows-show-file-extensions/
Then just double-click on that file.
Then just double-click on that file to run it.
You could also edit (a copy of) `scrcpy-console.bat` or `scrcpy-noconsole.vbs`
to add some arguments.
To start scrcpy without opening a terminal, double-click `scrcpy-noconsole.vbs`
(note that errors won't be shown). To pass arguments, edit (a copy of)
`scrcpy-noconsole.vbs` add and the desired arguments.

View File

@@ -22,9 +22,12 @@ app/deps/libusb.sh linux native static
DEPS_INSTALL_DIR="$PWD/app/deps/work/install/linux-native-static"
ADB_INSTALL_DIR="$PWD/app/deps/work/install/adb-linux"
# Never fall back to system libs
unset PKG_CONFIG_PATH
export PKG_CONFIG_LIBDIR="$DEPS_INSTALL_DIR/lib/pkgconfig"
rm -rf "$LINUX_BUILD_DIR"
meson setup "$LINUX_BUILD_DIR" \
--pkg-config-path="$DEPS_INSTALL_DIR/lib/pkgconfig" \
-Dc_args="-I$DEPS_INSTALL_DIR/include" \
-Dc_link_args="-L$DEPS_INSTALL_DIR/lib" \
--buildtype=release \

View File

@@ -22,9 +22,12 @@ app/deps/libusb.sh macos native static
DEPS_INSTALL_DIR="$PWD/app/deps/work/install/macos-native-static"
ADB_INSTALL_DIR="$PWD/app/deps/work/install/adb-macos"
# Never fall back to system libs
unset PKG_CONFIG_PATH
export PKG_CONFIG_LIBDIR="$DEPS_INSTALL_DIR/lib/pkgconfig"
rm -rf "$MACOS_BUILD_DIR"
meson setup "$MACOS_BUILD_DIR" \
--pkg-config-path="$DEPS_INSTALL_DIR/lib/pkgconfig" \
-Dc_args="-I$DEPS_INSTALL_DIR/include" \
-Dc_link_args="-L$DEPS_INSTALL_DIR/lib" \
--buildtype=release \

View File

@@ -29,9 +29,12 @@ app/deps/libusb.sh $WINXX cross shared
DEPS_INSTALL_DIR="$PWD/app/deps/work/install/$WINXX-cross-shared"
ADB_INSTALL_DIR="$PWD/app/deps/work/install/adb-windows"
# Never fall back to system libs
unset PKG_CONFIG_PATH
export PKG_CONFIG_LIBDIR="$DEPS_INSTALL_DIR/lib/pkgconfig"
rm -rf "$WINXX_BUILD_DIR"
meson setup "$WINXX_BUILD_DIR" \
--pkg-config-path="$DEPS_INSTALL_DIR/lib/pkgconfig" \
-Dc_args="-I$DEPS_INSTALL_DIR/include" \
-Dc_link_args="-L$DEPS_INSTALL_DIR/lib" \
--cross-file=cross_$WINXX.txt \
@@ -45,7 +48,6 @@ ninja -C "$WINXX_BUILD_DIR"
# Group intermediate outputs into a 'dist' directory
mkdir -p "$WINXX_BUILD_DIR/dist"
cp "$WINXX_BUILD_DIR"/app/scrcpy.exe "$WINXX_BUILD_DIR/dist/"
cp app/data/scrcpy-console.bat "$WINXX_BUILD_DIR/dist/"
cp app/data/scrcpy-noconsole.vbs "$WINXX_BUILD_DIR/dist/"
cp app/data/icon.png "$WINXX_BUILD_DIR/dist/"
cp app/data/open_a_terminal_here.bat "$WINXX_BUILD_DIR/dist/"

View File

@@ -266,14 +266,14 @@ public final class AudioEncoder implements AsyncProcessor {
outputThread.start();
waitEnded();
} catch (ConfigurationException e) {
// Notify the error to make scrcpy exit
streamer.writeDisableStream(true);
throw e;
} catch (Throwable e) {
} catch (AudioCaptureException e) {
// Notify the client that the audio could not be captured
streamer.writeDisableStream(false);
throw e;
} catch (Throwable e) {
// Notify the error to make scrcpy exit
streamer.writeDisableStream(true);
throw e;
} finally {
// Cleanup everything (either at the end or on error at any step of the initialization)
if (mediaCodecThread != null) {

View File

@@ -264,6 +264,10 @@ public class SurfaceEncoder implements AsyncProcessor {
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, DEFAULT_I_FRAME_INTERVAL);
// display the very first frame, and recover from bad quality when no new frames
format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, REPEAT_FRAME_DELAY_US); // µs
// real-time priority
format.setInteger(MediaFormat.KEY_PRIORITY, 0);
// output 1 frame as soon as 1 frame is queued
format.setInteger(MediaFormat.KEY_LATENCY, 1);
if (maxFps > 0) {
// The key existed privately before Android 10:
// <https://android.googlesource.com/platform/frameworks/base/+/625f0aad9f7a259b6881006ad8710adce57d1384%5E%21/>