Compare commits

...

18 Commits

Author SHA1 Message Date
b0a4e6df25 Retry on recoverable MediaCodec errors
Refs <https://developer.android.com/reference/android/media/MediaCodec#error-handling>
Fixes #3693 <https://github.com/Genymobile/scrcpy/issues/3693>
2023-01-27 23:15:44 +01:00
545a8a8f32 Extract downsize-retry handling
Move the code to downscale and retry on error out of the catch-block.

Refs 26b4104844
2023-01-27 23:15:03 +01:00
a9b2697f3e Move local variables declarations
This makes it clear that these local variables are only passed to
setDisplaySurface().
2023-01-27 22:43:16 +01:00
b53d2c66e0 Remove useless setSize() method
Inline its content. It makes the logic of internalStreamScreen() more
readable (it reduces the number of indirections).
2023-01-27 22:39:28 +01:00
6cccf3ab2a Remove useless configure() method
Inline its single call.
2023-01-27 22:38:37 +01:00
52f85fd6f1 Keep the same MediaCodec instance across sessions
Calling codec.reset() is sufficient.
2023-01-27 22:31:09 +01:00
91c69ad95c Remove useless destroyDisplay() method
The method made exactly one simple call. Just make the call directly.
2023-01-27 22:28:11 +01:00
75d7c01a0c Keep the same display binder across sessions
Do not destroy/recreate the display when starting a new encoding session
(on device rotation for example).
2023-01-27 22:26:01 +01:00
74d32e612d Terminate loop explicitly on interrupted
Make explicit that the loop terminates when the current thread is
interrupted.
2023-01-27 22:20:35 +01:00
bdba554118 Use Java lambdas where possible 2023-01-27 22:16:36 +01:00
234ad7ee78 Support Java lambdas in build_without_gradle.sh
Building Java source code using lambdas requires core-lambda-stubs.jar.

Refs #3657 <https://github.com/Genymobile/scrcpy/issues/3657>
2023-01-27 22:08:25 +01:00
8cbbcc939f Add missing final modifiers 2023-01-27 22:08:17 +01:00
b22810b17c Use try-with-resources
Replace an explicit try-finally by a try-with-resources block.
2023-01-27 21:59:26 +01:00
4315be1648 Use random name for device socket
For the initial connection between the device and the computer, an adb
tunnel is established (with "adb reverse" or "adb forward").

The device-side of the tunnel is a local socket having the hard-coded
name "scrcpy". This may cause issues when several scrcpy instances are
started in a few seconds for the same device, since they will try to
bind the same name.

To avoid conflicts, make the client generate a random UID, and append
this UID to the local socket name ("scrcpy_01234567").
2023-01-27 21:51:59 +01:00
74e3f8b253 Add random util
Add a user-friendly tool to generate random numbers.
2023-01-27 19:26:19 +01:00
059ec45f82 Add jrand48()/nrand48() compat functions
These functions are not available on all platforms.
2023-01-26 18:11:23 +01:00
e6cd42355b Use separate gen dir to build without gradle
The generated source files were written to the classes dir. Use a
separate gen dir instead.
2023-01-26 10:44:31 +01:00
bf8696d02e Avoid unnecessary copy on config packets demuxing
Use av_packet_ref() to reference the packet without copy.

This also simplifies the logic, by making the "offset" variable and the
memcpy() call local to the if-block.
2023-01-02 16:18:23 +01:00
20 changed files with 274 additions and 125 deletions

View File

@ -37,6 +37,7 @@ src = [
'src/util/net_intr.c', 'src/util/net_intr.c',
'src/util/process.c', 'src/util/process.c',
'src/util/process_intr.c', 'src/util/process_intr.c',
'src/util/rand.c',
'src/util/strbuf.c', 'src/util/strbuf.c',
'src/util/str.c', 'src/util/str.c',
'src/util/term.c', 'src/util/term.c',
@ -170,6 +171,8 @@ check_functions = [
'strdup', 'strdup',
'asprintf', 'asprintf',
'vasprintf', 'vasprintf',
'nrand48',
'jrand48',
] ]
foreach f : check_functions foreach f : check_functions

View File

@ -7,8 +7,6 @@
#include "util/net_intr.h" #include "util/net_intr.h"
#include "util/process_intr.h" #include "util/process_intr.h"
#define SC_SOCKET_NAME "scrcpy"
static bool static bool
listen_on_port(struct sc_intr *intr, sc_socket socket, uint16_t port) { listen_on_port(struct sc_intr *intr, sc_socket socket, uint16_t port) {
return net_listen_intr(intr, socket, IPV4_LOCALHOST, port, 1); return net_listen_intr(intr, socket, IPV4_LOCALHOST, port, 1);
@ -17,10 +15,11 @@ listen_on_port(struct sc_intr *intr, sc_socket socket, uint16_t port) {
static bool static bool
enable_tunnel_reverse_any_port(struct sc_adb_tunnel *tunnel, enable_tunnel_reverse_any_port(struct sc_adb_tunnel *tunnel,
struct sc_intr *intr, const char *serial, struct sc_intr *intr, const char *serial,
const char *device_socket_name,
struct sc_port_range port_range) { struct sc_port_range port_range) {
uint16_t port = port_range.first; uint16_t port = port_range.first;
for (;;) { for (;;) {
if (!sc_adb_reverse(intr, serial, SC_SOCKET_NAME, port, if (!sc_adb_reverse(intr, serial, device_socket_name, port,
SC_ADB_NO_STDOUT)) { SC_ADB_NO_STDOUT)) {
// the command itself failed, it will fail on any port // the command itself failed, it will fail on any port
return false; return false;
@ -52,7 +51,7 @@ enable_tunnel_reverse_any_port(struct sc_adb_tunnel *tunnel,
} }
// failure, disable tunnel and try another port // failure, disable tunnel and try another port
if (!sc_adb_reverse_remove(intr, serial, SC_SOCKET_NAME, if (!sc_adb_reverse_remove(intr, serial, device_socket_name,
SC_ADB_NO_STDOUT)) { SC_ADB_NO_STDOUT)) {
LOGW("Could not remove reverse tunnel on port %" PRIu16, port); LOGW("Could not remove reverse tunnel on port %" PRIu16, port);
} }
@ -78,12 +77,13 @@ enable_tunnel_reverse_any_port(struct sc_adb_tunnel *tunnel,
static bool static bool
enable_tunnel_forward_any_port(struct sc_adb_tunnel *tunnel, enable_tunnel_forward_any_port(struct sc_adb_tunnel *tunnel,
struct sc_intr *intr, const char *serial, struct sc_intr *intr, const char *serial,
const char *device_socket_name,
struct sc_port_range port_range) { struct sc_port_range port_range) {
tunnel->forward = true; tunnel->forward = true;
uint16_t port = port_range.first; uint16_t port = port_range.first;
for (;;) { for (;;) {
if (sc_adb_forward(intr, serial, port, SC_SOCKET_NAME, if (sc_adb_forward(intr, serial, port, device_socket_name,
SC_ADB_NO_STDOUT)) { SC_ADB_NO_STDOUT)) {
// success // success
tunnel->local_port = port; tunnel->local_port = port;
@ -123,13 +123,14 @@ sc_adb_tunnel_init(struct sc_adb_tunnel *tunnel) {
bool bool
sc_adb_tunnel_open(struct sc_adb_tunnel *tunnel, struct sc_intr *intr, sc_adb_tunnel_open(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
const char *serial, struct sc_port_range port_range, const char *serial, const char *device_socket_name,
bool force_adb_forward) { struct sc_port_range port_range, bool force_adb_forward) {
assert(!tunnel->enabled); assert(!tunnel->enabled);
if (!force_adb_forward) { if (!force_adb_forward) {
// Attempt to use "adb reverse" // Attempt to use "adb reverse"
if (enable_tunnel_reverse_any_port(tunnel, intr, serial, port_range)) { if (enable_tunnel_reverse_any_port(tunnel, intr, serial,
device_socket_name, port_range)) {
return true; return true;
} }
@ -139,12 +140,13 @@ sc_adb_tunnel_open(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
LOGW("'adb reverse' failed, fallback to 'adb forward'"); LOGW("'adb reverse' failed, fallback to 'adb forward'");
} }
return enable_tunnel_forward_any_port(tunnel, intr, serial, port_range); return enable_tunnel_forward_any_port(tunnel, intr, serial,
device_socket_name, port_range);
} }
bool bool
sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr, sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
const char *serial) { const char *serial, const char *device_socket_name) {
assert(tunnel->enabled); assert(tunnel->enabled);
bool ret; bool ret;
@ -152,7 +154,7 @@ sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
ret = sc_adb_forward_remove(intr, serial, tunnel->local_port, ret = sc_adb_forward_remove(intr, serial, tunnel->local_port,
SC_ADB_NO_STDOUT); SC_ADB_NO_STDOUT);
} else { } else {
ret = sc_adb_reverse_remove(intr, serial, SC_SOCKET_NAME, ret = sc_adb_reverse_remove(intr, serial, device_socket_name,
SC_ADB_NO_STDOUT); SC_ADB_NO_STDOUT);
assert(tunnel->server_socket != SC_SOCKET_NONE); assert(tunnel->server_socket != SC_SOCKET_NONE);

View File

@ -34,14 +34,14 @@ sc_adb_tunnel_init(struct sc_adb_tunnel *tunnel);
*/ */
bool bool
sc_adb_tunnel_open(struct sc_adb_tunnel *tunnel, struct sc_intr *intr, sc_adb_tunnel_open(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
const char *serial, struct sc_port_range port_range, const char *serial, const char *device_socket_name,
bool force_adb_forward); struct sc_port_range port_range, bool force_adb_forward);
/** /**
* Close the tunnel * Close the tunnel
*/ */
bool bool
sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr, sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
const char *serial); const char *serial, const char *device_socket_name);
#endif #endif

View File

@ -51,3 +51,47 @@ int vasprintf(char **strp, const char *fmt, va_list ap) {
return len; return len;
} }
#endif #endif
#if !defined(HAVE_NRAND48) || !defined(HAVE_JRAND48)
#define SC_RAND48_MASK UINT64_C(0xFFFFFFFFFFFF) // 48 bits
#define SC_RAND48_A UINT64_C(0x5DEECE66D)
#define SC_RAND48_C 0xB
static inline uint64_t rand_iter48(uint64_t x) {
assert((x & ~SC_RAND48_MASK) == 0);
return (x * SC_RAND48_A + SC_RAND48_C) & SC_RAND48_MASK;
}
static uint64_t rand_iter48_xsubi(unsigned short xsubi[3]) {
uint64_t x = ((uint64_t) xsubi[0] << 32)
| ((uint64_t) xsubi[1] << 16)
| xsubi[2];
x = rand_iter48(x);
xsubi[0] = (x >> 32) & 0XFFFF;
xsubi[1] = (x >> 16) & 0XFFFF;
xsubi[2] = x & 0XFFFF;
return x;
}
#ifndef HAVE_NRAND48
long nrand48(unsigned short xsubi[3]) {
// range [0, 2^31)
return rand_iter48_xsubi(xsubi) >> 17;
}
#endif
#ifndef HAVE_JRAND48
long jrand48(unsigned short xsubi[3]) {
// range [-2^31, 2^31)
union {
uint32_t u;
int32_t i;
} v;
v.u = rand_iter48_xsubi(xsubi) >> 16;
return v.i;
}
#endif
#endif

View File

@ -59,4 +59,12 @@ int asprintf(char **strp, const char *fmt, ...);
int vasprintf(char **strp, const char *fmt, va_list ap); int vasprintf(char **strp, const char *fmt, va_list ap);
#endif #endif
#ifndef HAVE_NRAND48
long nrand48(unsigned short xsubi[3]);
#endif
#ifndef HAVE_JRAND48
long jrand48(unsigned short xsubi[3]);
#endif
#endif #endif

View File

@ -95,29 +95,27 @@ sc_demuxer_push_packet(struct sc_demuxer *demuxer, AVPacket *packet) {
// A config packet must not be decoded immediately (it contains no // A config packet must not be decoded immediately (it contains no
// frame); instead, it must be concatenated with the future data packet. // frame); instead, it must be concatenated with the future data packet.
if (demuxer->pending || is_config) { if (demuxer->pending || is_config) {
size_t offset;
if (demuxer->pending) { if (demuxer->pending) {
offset = demuxer->pending->size; size_t offset = demuxer->pending->size;
if (av_grow_packet(demuxer->pending, packet->size)) { if (av_grow_packet(demuxer->pending, packet->size)) {
LOG_OOM(); LOG_OOM();
return false; return false;
} }
memcpy(demuxer->pending->data + offset, packet->data, packet->size);
} else { } else {
offset = 0;
demuxer->pending = av_packet_alloc(); demuxer->pending = av_packet_alloc();
if (!demuxer->pending) { if (!demuxer->pending) {
LOG_OOM(); LOG_OOM();
return false; return false;
} }
if (av_new_packet(demuxer->pending, packet->size)) { if (av_packet_ref(demuxer->pending, packet)) {
LOG_OOM(); LOG_OOM();
av_packet_free(&demuxer->pending); av_packet_free(&demuxer->pending);
return false; return false;
} }
} }
memcpy(demuxer->pending->data + offset, packet->data, packet->size);
if (!is_config) { if (!is_config) {
// prepare the concat packet to send to the decoder // prepare the concat packet to send to the decoder
demuxer->pending->pts = packet->pts; demuxer->pending->pts = packet->pts;

View File

@ -32,6 +32,7 @@
#include "util/acksync.h" #include "util/acksync.h"
#include "util/log.h" #include "util/log.h"
#include "util/net.h" #include "util/net.h"
#include "util/rand.h"
#ifdef HAVE_V4L2 #ifdef HAVE_V4L2
# include "v4l2_sink.h" # include "v4l2_sink.h"
#endif #endif
@ -265,6 +266,14 @@ sc_server_on_disconnected(struct sc_server *server, void *userdata) {
// event // event
} }
static uint32_t
scrcpy_generate_uid() {
struct sc_rand rand;
sc_rand_init(&rand);
// Only use 31 bits to avoid issues with signed values on the Java-side
return sc_rand_u32(&rand) & 0x7FFFFFFF;
}
enum scrcpy_exit_code enum scrcpy_exit_code
scrcpy(struct scrcpy_options *options) { scrcpy(struct scrcpy_options *options) {
static struct scrcpy scrcpy; static struct scrcpy scrcpy;
@ -298,7 +307,10 @@ scrcpy(struct scrcpy_options *options) {
struct sc_acksync *acksync = NULL; struct sc_acksync *acksync = NULL;
uint32_t uid = scrcpy_generate_uid();
struct sc_server_params params = { struct sc_server_params params = {
.uid = uid,
.req_serial = options->serial, .req_serial = options->serial,
.select_usb = options->select_usb, .select_usb = options->select_usb,
.select_tcpip = options->select_tcpip, .select_tcpip = options->select_tcpip,

View File

@ -20,6 +20,7 @@
#define SC_DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar" #define SC_DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar"
#define SC_ADB_PORT_DEFAULT 5555 #define SC_ADB_PORT_DEFAULT 5555
#define SC_SOCKET_NAME_PREFIX "scrcpy_"
static char * static char *
get_server_path(void) { get_server_path(void) {
@ -197,6 +198,7 @@ execute_server(struct sc_server *server,
cmd[count++] = p; \ cmd[count++] = p; \
} }
ADD_PARAM("uid=%08x", params->uid);
ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level)); ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level));
ADD_PARAM("bit_rate=%" PRIu32, params->bit_rate); ADD_PARAM("bit_rate=%" PRIu32, params->bit_rate);
@ -364,6 +366,7 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params,
} }
server->serial = NULL; server->serial = NULL;
server->device_socket_name = NULL;
server->stopped = false; server->stopped = false;
server->video_socket = SC_SOCKET_NONE; server->video_socket = SC_SOCKET_NONE;
@ -463,7 +466,8 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
} }
// we don't need the adb tunnel anymore // we don't need the adb tunnel anymore
sc_adb_tunnel_close(tunnel, &server->intr, serial); sc_adb_tunnel_close(tunnel, &server->intr, serial,
server->device_socket_name);
// The sockets will be closed on stop if device_read_info() fails // The sockets will be closed on stop if device_read_info() fails
bool ok = device_read_info(&server->intr, video_socket, info); bool ok = device_read_info(&server->intr, video_socket, info);
@ -494,7 +498,8 @@ fail:
if (tunnel->enabled) { if (tunnel->enabled) {
// Always leave this function with tunnel disabled // Always leave this function with tunnel disabled
sc_adb_tunnel_close(tunnel, &server->intr, serial); sc_adb_tunnel_close(tunnel, &server->intr, serial,
server->device_socket_name);
} }
return false; return false;
@ -764,13 +769,23 @@ run_server(void *data) {
assert(serial); assert(serial);
LOGD("Device serial: %s", serial); LOGD("Device serial: %s", serial);
int r = asprintf(&server->device_socket_name, SC_SOCKET_NAME_PREFIX "%08x",
params->uid);
if (r == -1) {
LOG_OOM();
goto error_connection_failed;
}
assert(r == sizeof(SC_SOCKET_NAME_PREFIX) - 1 + 8);
assert(server->device_socket_name);
ok = push_server(&server->intr, serial); ok = push_server(&server->intr, serial);
if (!ok) { if (!ok) {
goto error_connection_failed; goto error_connection_failed;
} }
ok = sc_adb_tunnel_open(&server->tunnel, &server->intr, serial, ok = sc_adb_tunnel_open(&server->tunnel, &server->intr, serial,
params->port_range, params->force_adb_forward); server->device_socket_name, params->port_range,
params->force_adb_forward);
if (!ok) { if (!ok) {
goto error_connection_failed; goto error_connection_failed;
} }
@ -778,7 +793,8 @@ run_server(void *data) {
// server will connect to our server socket // server will connect to our server socket
sc_pid pid = execute_server(server, params); sc_pid pid = execute_server(server, params);
if (pid == SC_PROCESS_NONE) { if (pid == SC_PROCESS_NONE) {
sc_adb_tunnel_close(&server->tunnel, &server->intr, serial); sc_adb_tunnel_close(&server->tunnel, &server->intr, serial,
server->device_socket_name);
goto error_connection_failed; goto error_connection_failed;
} }
@ -790,7 +806,8 @@ run_server(void *data) {
if (!ok) { if (!ok) {
sc_process_terminate(pid); sc_process_terminate(pid);
sc_process_wait(pid, true); // ignore exit code sc_process_wait(pid, true); // ignore exit code
sc_adb_tunnel_close(&server->tunnel, &server->intr, serial); sc_adb_tunnel_close(&server->tunnel, &server->intr, serial,
server->device_socket_name);
goto error_connection_failed; goto error_connection_failed;
} }
@ -884,6 +901,7 @@ sc_server_destroy(struct sc_server *server) {
} }
free(server->serial); free(server->serial);
free(server->device_socket_name);
sc_server_params_destroy(&server->params); sc_server_params_destroy(&server->params);
sc_intr_destroy(&server->intr); sc_intr_destroy(&server->intr);
sc_cond_destroy(&server->cond_stopped); sc_cond_destroy(&server->cond_stopped);

View File

@ -22,6 +22,7 @@ struct sc_server_info {
}; };
struct sc_server_params { struct sc_server_params {
uint32_t uid;
const char *req_serial; const char *req_serial;
enum sc_log_level log_level; enum sc_log_level log_level;
const char *crop; const char *crop;
@ -54,6 +55,7 @@ struct sc_server {
// The internal allocated strings are copies owned by the server // The internal allocated strings are copies owned by the server
struct sc_server_params params; struct sc_server_params params;
char *serial; char *serial;
char *device_socket_name;
sc_thread thread; sc_thread thread;
struct sc_server_info info; // initialized once connected struct sc_server_info info; // initialized once connected

24
app/src/util/rand.c Normal file
View File

@ -0,0 +1,24 @@
#include "rand.h"
#include <stdlib.h>
#include "tick.h"
void sc_rand_init(struct sc_rand *rand) {
sc_tick seed = sc_tick_now(); // microsecond precision
rand->xsubi[0] = (seed >> 32) & 0xFFFF;
rand->xsubi[1] = (seed >> 16) & 0xFFFF;
rand->xsubi[2] = seed & 0xFFFF;
}
uint32_t sc_rand_u32(struct sc_rand *rand) {
// jrand returns a value in range [-2^31, 2^31]
// conversion from signed to unsigned is well-defined to wrap-around
return jrand48(rand->xsubi);
}
uint64_t sc_rand_u64(struct sc_rand *rand) {
uint32_t msb = sc_rand_u32(rand);
uint32_t lsb = sc_rand_u32(rand);
return ((uint64_t) msb << 32) | lsb;
}

16
app/src/util/rand.h Normal file
View File

@ -0,0 +1,16 @@
#ifndef SC_RAND_H
#define SC_RAND_H
#include "common.h"
#include <inttypes.h>
struct sc_rand {
unsigned short xsubi[3];
};
void sc_rand_init(struct sc_rand *rand);
uint32_t sc_rand_u32(struct sc_rand *rand);
uint64_t sc_rand_u64(struct sc_rand *rand);
#endif

View File

@ -20,18 +20,21 @@ BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS"
BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})" BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})"
CLASSES_DIR="$BUILD_DIR/classes" CLASSES_DIR="$BUILD_DIR/classes"
GEN_DIR="$BUILD_DIR/gen"
SERVER_DIR=$(dirname "$0") SERVER_DIR=$(dirname "$0")
SERVER_BINARY=scrcpy-server SERVER_BINARY=scrcpy-server
ANDROID_JAR="$ANDROID_HOME/platforms/android-$PLATFORM/android.jar" ANDROID_JAR="$ANDROID_HOME/platforms/android-$PLATFORM/android.jar"
LAMBDA_JAR="$BUILD_TOOLS_DIR/core-lambda-stubs.jar"
echo "Platform: android-$PLATFORM" echo "Platform: android-$PLATFORM"
echo "Build-tools: $BUILD_TOOLS" echo "Build-tools: $BUILD_TOOLS"
echo "Build dir: $BUILD_DIR" echo "Build dir: $BUILD_DIR"
rm -rf "$CLASSES_DIR" "$BUILD_DIR/$SERVER_BINARY" classes.dex rm -rf "$CLASSES_DIR" "$GEN_DIR" "$BUILD_DIR/$SERVER_BINARY" classes.dex
mkdir -p "$CLASSES_DIR/com/genymobile/scrcpy" mkdir -p "$CLASSES_DIR"
mkdir -p "$GEN_DIR/com/genymobile/scrcpy"
<< EOF cat > "$CLASSES_DIR/com/genymobile/scrcpy/BuildConfig.java" << EOF cat > "$GEN_DIR/com/genymobile/scrcpy/BuildConfig.java"
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
public final class BuildConfig { public final class BuildConfig {
@ -42,13 +45,15 @@ EOF
echo "Generating java from aidl..." echo "Generating java from aidl..."
cd "$SERVER_DIR/src/main/aidl" cd "$SERVER_DIR/src/main/aidl"
"$BUILD_TOOLS_DIR/aidl" -o"$CLASSES_DIR" android/view/IRotationWatcher.aidl "$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" android/view/IRotationWatcher.aidl
"$BUILD_TOOLS_DIR/aidl" -o"$CLASSES_DIR" \ "$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" \
android/content/IOnPrimaryClipChangedListener.aidl android/content/IOnPrimaryClipChangedListener.aidl
echo "Compiling java sources..." echo "Compiling java sources..."
cd ../java cd ../java
javac -bootclasspath "$ANDROID_JAR" -cp "$CLASSES_DIR" -d "$CLASSES_DIR" \ javac -bootclasspath "$ANDROID_JAR" \
-cp "$LAMBDA_JAR:$GEN_DIR" \
-d "$CLASSES_DIR" \
-source 1.8 -target 1.8 \ -source 1.8 -target 1.8 \
com/genymobile/scrcpy/*.java \ com/genymobile/scrcpy/*.java \
com/genymobile/scrcpy/wrappers/*.java com/genymobile/scrcpy/wrappers/*.java
@ -68,7 +73,7 @@ then
echo "Archiving..." echo "Archiving..."
cd "$BUILD_DIR" cd "$BUILD_DIR"
jar cvf "$SERVER_BINARY" classes.dex jar cvf "$SERVER_BINARY" classes.dex
rm -rf classes.dex classes rm -rf classes.dex
else else
# use d8 # use d8
"$BUILD_TOOLS_DIR/d8" --classpath "$ANDROID_JAR" \ "$BUILD_TOOLS_DIR/d8" --classpath "$ANDROID_JAR" \
@ -80,7 +85,8 @@ else
cd "$BUILD_DIR" cd "$BUILD_DIR"
mv classes.zip "$SERVER_BINARY" mv classes.zip "$SERVER_BINARY"
rm -rf classes
fi fi
rm -rf "$GEN_DIR" "$CLASSES_DIR"
echo "Server generated in $BUILD_DIR/$SERVER_BINARY" echo "Server generated in $BUILD_DIR/$SERVER_BINARY"

View File

@ -4,8 +4,8 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
public class CodecOption { public class CodecOption {
private String key; private final String key;
private Object value; private final Object value;
public CodecOption(String key, Object value) { public CodecOption(String key, Object value) {
this.key = key; this.key = key;

View File

@ -75,7 +75,7 @@ public class Controller {
SystemClock.sleep(500); SystemClock.sleep(500);
} }
while (true) { while (!Thread.currentThread().isInterrupted()) {
handleEvent(); handleEvent();
} }
} }
@ -258,12 +258,9 @@ public class Controller {
* Schedule a call to set power mode to off after a small delay. * Schedule a call to set power mode to off after a small delay.
*/ */
private static void schedulePowerModeOff() { private static void schedulePowerModeOff() {
EXECUTOR.schedule(new Runnable() { EXECUTOR.schedule(() -> {
@Override
public void run() {
Ln.i("Forcing screen off"); Ln.i("Forcing screen off");
Device.setScreenPowerMode(Device.POWER_MODE_OFF); Device.setScreenPowerMode(Device.POWER_MODE_OFF);
}
}, 200, TimeUnit.MILLISECONDS); }, 200, TimeUnit.MILLISECONDS);
} }

View File

@ -15,7 +15,7 @@ public final class DesktopConnection implements Closeable {
private static final int DEVICE_NAME_FIELD_LENGTH = 64; private static final int DEVICE_NAME_FIELD_LENGTH = 64;
private static final String SOCKET_NAME = "scrcpy"; private static final String SOCKET_NAME_PREFIX = "scrcpy";
private final LocalSocket videoSocket; private final LocalSocket videoSocket;
private final FileDescriptor videoFd; private final FileDescriptor videoFd;
@ -46,12 +46,22 @@ public final class DesktopConnection implements Closeable {
return localSocket; return localSocket;
} }
public static DesktopConnection open(boolean tunnelForward, boolean control, boolean sendDummyByte) throws IOException { private static String getSocketName(int uid) {
if (uid == -1) {
// If no UID is set, use "scrcpy" to simplify using scrcpy-server alone
return SOCKET_NAME_PREFIX;
}
return SOCKET_NAME_PREFIX + String.format("_%08x", uid);
}
public static DesktopConnection open(int uid, boolean tunnelForward, boolean control, boolean sendDummyByte) throws IOException {
String socketName = getSocketName(uid);
LocalSocket videoSocket; LocalSocket videoSocket;
LocalSocket controlSocket = null; LocalSocket controlSocket = null;
if (tunnelForward) { if (tunnelForward) {
LocalServerSocket localServerSocket = new LocalServerSocket(SOCKET_NAME); try (LocalServerSocket localServerSocket = new LocalServerSocket(socketName)) {
try {
videoSocket = localServerSocket.accept(); videoSocket = localServerSocket.accept();
if (sendDummyByte) { if (sendDummyByte) {
// send one byte so the client may read() to detect a connection error // send one byte so the client may read() to detect a connection error
@ -65,14 +75,12 @@ public final class DesktopConnection implements Closeable {
throw e; throw e;
} }
} }
} finally {
localServerSocket.close();
} }
} else { } else {
videoSocket = connect(SOCKET_NAME); videoSocket = connect(socketName);
if (control) { if (control) {
try { try {
controlSocket = connect(SOCKET_NAME); controlSocket = connect(socketName);
} catch (IOException | RuntimeException e) { } catch (IOException | RuntimeException e) {
videoSocket.close(); videoSocket.close();
throw e; throw e;

View File

@ -25,7 +25,7 @@ public final class DeviceMessageSender {
} }
public void loop() throws IOException, InterruptedException { public void loop() throws IOException, InterruptedException {
while (true) { while (!Thread.currentThread().isInterrupted()) {
String text; String text;
long sequence; long sequence;
synchronized (this) { synchronized (this) {

View File

@ -6,6 +6,7 @@ import java.util.List;
public class Options { public class Options {
private Ln.Level logLevel = Ln.Level.DEBUG; private Ln.Level logLevel = Ln.Level.DEBUG;
private int uid = -1; // 31-bit non-negative value, or -1
private int maxSize; private int maxSize;
private int bitRate = 8000000; private int bitRate = 8000000;
private int maxFps; private int maxFps;
@ -37,6 +38,14 @@ public class Options {
this.logLevel = logLevel; this.logLevel = logLevel;
} }
public int getUid() {
return uid;
}
public void setUid(int uid) {
this.uid = uid;
}
public int getMaxSize() { public int getMaxSize() {
return maxSize; return maxSize;
} }

View File

@ -3,8 +3,8 @@ package com.genymobile.scrcpy;
import java.util.Objects; import java.util.Objects;
public class Position { public class Position {
private Point point; private final Point point;
private Size screenSize; private final Size screenSize;
public Position(Point point, Size screenSize) { public Position(Point point, Size screenSize) {
this.point = point; this.point = point;

View File

@ -9,6 +9,7 @@ import android.media.MediaCodecList;
import android.media.MediaFormat; import android.media.MediaFormat;
import android.os.Build; import android.os.Build;
import android.os.IBinder; import android.os.IBinder;
import android.os.SystemClock;
import android.view.Surface; import android.view.Surface;
import java.io.FileDescriptor; import java.io.FileDescriptor;
@ -75,63 +76,88 @@ public class ScreenEncoder implements Device.RotationListener {
} }
private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException { private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException {
MediaCodec codec = createCodec(encoderName);
MediaFormat format = createFormat(bitRate, maxFps, codecOptions); MediaFormat format = createFormat(bitRate, maxFps, codecOptions);
IBinder display = createDisplay();
device.setRotationListener(this); device.setRotationListener(this);
boolean alive; boolean alive;
try { try {
do { do {
MediaCodec codec = createCodec(encoderName);
IBinder display = createDisplay();
ScreenInfo screenInfo = device.getScreenInfo(); ScreenInfo screenInfo = device.getScreenInfo();
Rect contentRect = screenInfo.getContentRect(); Rect contentRect = screenInfo.getContentRect();
// include the locked video orientation // include the locked video orientation
Rect videoRect = screenInfo.getVideoSize().toRect(); Rect videoRect = screenInfo.getVideoSize().toRect();
format.setInteger(MediaFormat.KEY_WIDTH, videoRect.width());
format.setInteger(MediaFormat.KEY_HEIGHT, videoRect.height());
Surface surface = null;
try {
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
surface = codec.createInputSurface();
// does not include the locked video orientation // does not include the locked video orientation
Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect(); Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect();
int videoRotation = screenInfo.getVideoRotation(); int videoRotation = screenInfo.getVideoRotation();
int layerStack = device.getLayerStack(); int layerStack = device.getLayerStack();
setSize(format, videoRect.width(), videoRect.height());
Surface surface = null;
try {
configure(codec, format);
surface = codec.createInputSurface();
setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack); setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack);
codec.start(); codec.start();
alive = encode(codec, fd); alive = encode(codec, fd);
// do not call stop() on exception, it would trigger an IllegalStateException // do not call stop() on exception, it would trigger an IllegalStateException
codec.stop(); codec.stop();
} catch (MediaCodec.CodecException e) {
Ln.e("Codec error: " + e.getMessage());
// <https://developer.android.com/reference/android/media/MediaCodec#error-handling>
// For simplicity, handle isTransient() like isRecoverable()
if (e.isRecoverable() || e.isTransient()) {
// Avoid busy-loop if too many errors are generated
SystemClock.sleep(50);
} else if (!prepareDownsizeRetry(device, screenInfo)) {
throw e;
}
alive = true;
} catch (IllegalStateException | IllegalArgumentException e) { } catch (IllegalStateException | IllegalArgumentException e) {
Ln.e("Encoding error: " + e.getClass().getName() + ": " + e.getMessage()); Ln.e("Encoding error: " + e.getClass().getName() + ": " + e.getMessage());
if (!downsizeOnError || firstFrameSent) { if (!prepareDownsizeRetry(device, screenInfo)) {
// Fail immediately
throw e; throw e;
} }
int newMaxSize = chooseMaxSizeFallback(screenInfo.getVideoSize());
if (newMaxSize == 0) {
// Definitively fail
throw e;
}
// Retry with a smaller device size
Ln.i("Retrying with -m" + newMaxSize + "...");
device.setMaxSize(newMaxSize);
alive = true; alive = true;
} finally { } finally {
destroyDisplay(display); codec.reset();
codec.release();
if (surface != null) { if (surface != null) {
surface.release(); surface.release();
} }
} }
} while (alive); } while (alive);
} finally { } finally {
codec.release();
device.setRotationListener(null); device.setRotationListener(null);
SurfaceControl.destroyDisplay(display);
} }
} }
private boolean prepareDownsizeRetry(Device device, ScreenInfo screenInfo) {
if (!downsizeOnError || firstFrameSent) {
Ln.i("#1 " + downsizeOnError + " " + firstFrameSent);
// Must fail immediately
return false;
}
int newMaxSize = chooseMaxSizeFallback(screenInfo.getVideoSize());
Ln.i("newMaxSize = " + newMaxSize);
if (newMaxSize == 0) {
// Must definitively fail
return false;
}
// Retry with a smaller device size
Ln.i("Retrying with -m" + newMaxSize + "...");
device.setMaxSize(newMaxSize);
return true;
}
private static int chooseMaxSizeFallback(Size failedSize) { private static int chooseMaxSizeFallback(Size failedSize) {
int currentMaxSize = Math.max(failedSize.getWidth(), failedSize.getHeight()); int currentMaxSize = Math.max(failedSize.getWidth(), failedSize.getHeight());
for (int value : MAX_SIZE_FALLBACK) { for (int value : MAX_SIZE_FALLBACK) {
@ -278,15 +304,6 @@ public class ScreenEncoder implements Device.RotationListener {
return SurfaceControl.createDisplay("scrcpy", secure); return SurfaceControl.createDisplay("scrcpy", secure);
} }
private static void configure(MediaCodec codec, MediaFormat format) {
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
}
private static void setSize(MediaFormat format, int width, int height) {
format.setInteger(MediaFormat.KEY_WIDTH, width);
format.setInteger(MediaFormat.KEY_HEIGHT, height);
}
private static void setDisplaySurface(IBinder display, Surface surface, int orientation, Rect deviceRect, Rect displayRect, int layerStack) { private static void setDisplaySurface(IBinder display, Surface surface, int orientation, Rect deviceRect, Rect displayRect, int layerStack) {
SurfaceControl.openTransaction(); SurfaceControl.openTransaction();
try { try {
@ -297,8 +314,4 @@ public class ScreenEncoder implements Device.RotationListener {
SurfaceControl.closeTransaction(); SurfaceControl.closeTransaction();
} }
} }
private static void destroyDisplay(IBinder display) {
SurfaceControl.destroyDisplay(display);
}
} }

View File

@ -66,11 +66,12 @@ public final class Server {
Thread initThread = startInitThread(options); Thread initThread = startInitThread(options);
int uid = options.getUid();
boolean tunnelForward = options.isTunnelForward(); boolean tunnelForward = options.isTunnelForward();
boolean control = options.getControl(); boolean control = options.getControl();
boolean sendDummyByte = options.getSendDummyByte(); boolean sendDummyByte = options.getSendDummyByte();
try (DesktopConnection connection = DesktopConnection.open(tunnelForward, control, sendDummyByte)) { try (DesktopConnection connection = DesktopConnection.open(uid, tunnelForward, control, sendDummyByte)) {
if (options.getSendDeviceMeta()) { if (options.getSendDeviceMeta()) {
Size videoSize = device.getScreenInfo().getVideoSize(); Size videoSize = device.getScreenInfo().getVideoSize();
connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight()); connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
@ -87,12 +88,7 @@ public final class Server {
controllerThread = startController(controller); controllerThread = startController(controller);
deviceMessageSenderThread = startDeviceMessageSender(controller.getSender()); deviceMessageSenderThread = startDeviceMessageSender(controller.getSender());
device.setClipboardListener(new Device.ClipboardListener() { device.setClipboardListener(text -> controller.getSender().pushClipboardText(text));
@Override
public void onClipboardTextChanged(String text) {
controller.getSender().pushClipboardText(text);
}
});
} }
try { try {
@ -114,43 +110,32 @@ public final class Server {
} }
private static Thread startInitThread(final Options options) { private static Thread startInitThread(final Options options) {
Thread thread = new Thread(new Runnable() { Thread thread = new Thread(() -> initAndCleanUp(options));
@Override
public void run() {
initAndCleanUp(options);
}
});
thread.start(); thread.start();
return thread; return thread;
} }
private static Thread startController(final Controller controller) { private static Thread startController(final Controller controller) {
Thread thread = new Thread(new Runnable() { Thread thread = new Thread(() -> {
@Override
public void run() {
try { try {
controller.control(); controller.control();
} catch (IOException e) { } catch (IOException e) {
// this is expected on close // this is expected on close
Ln.d("Controller stopped"); Ln.d("Controller stopped");
} }
}
}); });
thread.start(); thread.start();
return thread; return thread;
} }
private static Thread startDeviceMessageSender(final DeviceMessageSender sender) { private static Thread startDeviceMessageSender(final DeviceMessageSender sender) {
Thread thread = new Thread(new Runnable() { Thread thread = new Thread(() -> {
@Override
public void run() {
try { try {
sender.loop(); sender.loop();
} catch (IOException | InterruptedException e) { } catch (IOException | InterruptedException e) {
// this is expected on close // this is expected on close
Ln.d("Device message sender stopped"); Ln.d("Device message sender stopped");
} }
}
}); });
thread.start(); thread.start();
return thread; return thread;
@ -178,6 +163,13 @@ public final class Server {
String key = arg.substring(0, equalIndex); String key = arg.substring(0, equalIndex);
String value = arg.substring(equalIndex + 1); String value = arg.substring(equalIndex + 1);
switch (key) { switch (key) {
case "uid":
int uid = Integer.parseInt(value, 0x10);
if (uid < -1) {
throw new IllegalArgumentException("uid may not be negative (except -1 for 'none'): " + uid);
}
options.setUid(uid);
break;
case "log_level": case "log_level":
Ln.Level level = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH)); Ln.Level level = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH));
options.setLogLevel(level); options.setLogLevel(level);
@ -319,12 +311,9 @@ public final class Server {
} }
public static void main(String... args) throws Exception { public static void main(String... args) throws Exception {
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
@Override
public void uncaughtException(Thread t, Throwable e) {
Ln.e("Exception on thread " + t, e); Ln.e("Exception on thread " + t, e);
suggestFix(e); suggestFix(e);
}
}); });
Options options = createOptions(args); Options options = createOptions(args);