Compare commits
25 Commits
v1.25
...
pts_client
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4fef4e0484 | ||
|
|
9b286ec8a7 | ||
|
|
8c5c55f9e1 | ||
|
|
0afef0c634 | ||
|
|
07806ba915 | ||
|
|
a52053421a | ||
|
|
a9b2697f3e | ||
|
|
b53d2c66e0 | ||
|
|
6cccf3ab2a | ||
|
|
52f85fd6f1 | ||
|
|
91c69ad95c | ||
|
|
75d7c01a0c | ||
|
|
74d32e612d | ||
|
|
bdba554118 | ||
|
|
234ad7ee78 | ||
|
|
8cbbcc939f | ||
|
|
b22810b17c | ||
|
|
4315be1648 | ||
|
|
74e3f8b253 | ||
|
|
059ec45f82 | ||
|
|
e6cd42355b | ||
|
|
bf8696d02e | ||
|
|
d8c2fe6ef2 | ||
|
|
54c7baceac | ||
|
|
4c43784fd1 |
6
BUILD.md
6
BUILD.md
@@ -272,10 +272,10 @@ install` must be run as root)._
|
||||
|
||||
#### Option 2: Use prebuilt server
|
||||
|
||||
- [`scrcpy-server-v1.24`][direct-scrcpy-server]
|
||||
<sub>SHA-256: `ae74a81ea79c0dc7250e586627c278c0a9a8c5de46c9fb5c38c167fb1a36f056`</sub>
|
||||
- [`scrcpy-server-v1.25`][direct-scrcpy-server]
|
||||
<sub>SHA-256: `ce0306c7bbd06ae72f6d06f7ec0ee33774995a65de71e0a83813ecb67aec9bdb`</sub>
|
||||
|
||||
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.24/scrcpy-server-v1.24
|
||||
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.25/scrcpy-server-v1.25
|
||||
|
||||
Download the prebuilt server somewhere, and specify its path during the Meson
|
||||
configuration:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# scrcpy (v1.24)
|
||||
# scrcpy (v1.25)
|
||||
|
||||
<img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />
|
||||
|
||||
@@ -106,10 +106,10 @@ process][BUILD_simple]).
|
||||
For Windows, a prebuilt archive with all the dependencies (including `adb`) is
|
||||
available:
|
||||
|
||||
- [`scrcpy-win64-v1.24.zip`][direct-win64]
|
||||
<sub>SHA-256: `6ccb64cba0a3e75715e85a188daeb4f306a1985f8ce123eba92ba74fc9b27367`</sub>
|
||||
- [`scrcpy-win64-v1.25.zip`][direct-win64]
|
||||
<sub>SHA-256: `db65125e9c65acd00359efb7cea9c05f63cc7ccd5833000cd243cc92f5053028`</sub>
|
||||
|
||||
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.24/scrcpy-win64-v1.24.zip
|
||||
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.25/scrcpy-win64-v1.25.zip
|
||||
|
||||
It is also available in [Chocolatey]:
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ src = [
|
||||
'src/util/net_intr.c',
|
||||
'src/util/process.c',
|
||||
'src/util/process_intr.c',
|
||||
'src/util/rand.c',
|
||||
'src/util/strbuf.c',
|
||||
'src/util/str.c',
|
||||
'src/util/term.c',
|
||||
@@ -170,6 +171,8 @@ check_functions = [
|
||||
'strdup',
|
||||
'asprintf',
|
||||
'vasprintf',
|
||||
'nrand48',
|
||||
'jrand48',
|
||||
]
|
||||
|
||||
foreach f : check_functions
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
#include "util/net_intr.h"
|
||||
#include "util/process_intr.h"
|
||||
|
||||
#define SC_SOCKET_NAME "scrcpy"
|
||||
|
||||
static bool
|
||||
listen_on_port(struct sc_intr *intr, sc_socket socket, uint16_t port) {
|
||||
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
|
||||
enable_tunnel_reverse_any_port(struct sc_adb_tunnel *tunnel,
|
||||
struct sc_intr *intr, const char *serial,
|
||||
const char *device_socket_name,
|
||||
struct sc_port_range port_range) {
|
||||
uint16_t port = port_range.first;
|
||||
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)) {
|
||||
// the command itself failed, it will fail on any port
|
||||
return false;
|
||||
@@ -52,7 +51,7 @@ enable_tunnel_reverse_any_port(struct sc_adb_tunnel *tunnel,
|
||||
}
|
||||
|
||||
// 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)) {
|
||||
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
|
||||
enable_tunnel_forward_any_port(struct sc_adb_tunnel *tunnel,
|
||||
struct sc_intr *intr, const char *serial,
|
||||
const char *device_socket_name,
|
||||
struct sc_port_range port_range) {
|
||||
tunnel->forward = true;
|
||||
|
||||
uint16_t port = port_range.first;
|
||||
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)) {
|
||||
// success
|
||||
tunnel->local_port = port;
|
||||
@@ -123,13 +123,14 @@ sc_adb_tunnel_init(struct sc_adb_tunnel *tunnel) {
|
||||
|
||||
bool
|
||||
sc_adb_tunnel_open(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
|
||||
const char *serial, struct sc_port_range port_range,
|
||||
bool force_adb_forward) {
|
||||
const char *serial, const char *device_socket_name,
|
||||
struct sc_port_range port_range, bool force_adb_forward) {
|
||||
assert(!tunnel->enabled);
|
||||
|
||||
if (!force_adb_forward) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -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'");
|
||||
}
|
||||
|
||||
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
|
||||
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);
|
||||
|
||||
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,
|
||||
SC_ADB_NO_STDOUT);
|
||||
} 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);
|
||||
|
||||
assert(tunnel->server_socket != SC_SOCKET_NONE);
|
||||
|
||||
@@ -34,14 +34,14 @@ sc_adb_tunnel_init(struct sc_adb_tunnel *tunnel);
|
||||
*/
|
||||
bool
|
||||
sc_adb_tunnel_open(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
|
||||
const char *serial, struct sc_port_range port_range,
|
||||
bool force_adb_forward);
|
||||
const char *serial, const char *device_socket_name,
|
||||
struct sc_port_range port_range, bool force_adb_forward);
|
||||
|
||||
/**
|
||||
* Close the tunnel
|
||||
*/
|
||||
bool
|
||||
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
|
||||
|
||||
@@ -51,3 +51,47 @@ int vasprintf(char **strp, const char *fmt, va_list ap) {
|
||||
return len;
|
||||
}
|
||||
#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
|
||||
|
||||
@@ -59,4 +59,12 @@ int asprintf(char **strp, const char *fmt, ...);
|
||||
int vasprintf(char **strp, const char *fmt, va_list ap);
|
||||
#endif
|
||||
|
||||
#ifndef HAVE_NRAND48
|
||||
long nrand48(unsigned short xsubi[3]);
|
||||
#endif
|
||||
|
||||
#ifndef HAVE_JRAND48
|
||||
long jrand48(unsigned short xsubi[3]);
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
@@ -117,8 +117,9 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, unsigned char *buf) {
|
||||
uint16_t pressure =
|
||||
sc_float_to_u16fp(msg->inject_touch_event.pressure);
|
||||
sc_write16be(&buf[22], pressure);
|
||||
sc_write32be(&buf[24], msg->inject_touch_event.buttons);
|
||||
return 28;
|
||||
sc_write32be(&buf[24], msg->inject_touch_event.action_button);
|
||||
sc_write32be(&buf[28], msg->inject_touch_event.buttons);
|
||||
return 32;
|
||||
case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT:
|
||||
write_position(&buf[1], &msg->inject_scroll_event.position);
|
||||
int16_t hscroll =
|
||||
@@ -179,22 +180,25 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
|
||||
if (pointer_name) {
|
||||
// string pointer id
|
||||
LOG_CMSG("touch [id=%s] %-4s position=%" PRIi32 ",%" PRIi32
|
||||
" pressure=%f buttons=%06lx",
|
||||
" pressure=%f action_button=%06lx buttons=%06lx",
|
||||
pointer_name,
|
||||
MOTIONEVENT_ACTION_LABEL(action),
|
||||
msg->inject_touch_event.position.point.x,
|
||||
msg->inject_touch_event.position.point.y,
|
||||
msg->inject_touch_event.pressure,
|
||||
(long) msg->inject_touch_event.action_button,
|
||||
(long) msg->inject_touch_event.buttons);
|
||||
} else {
|
||||
// numeric pointer id
|
||||
LOG_CMSG("touch [id=%" PRIu64_ "] %-4s position=%" PRIi32 ",%"
|
||||
PRIi32 " pressure=%f buttons=%06lx",
|
||||
PRIi32 " pressure=%f action_button=%06lx"
|
||||
" buttons=%06lx",
|
||||
id,
|
||||
MOTIONEVENT_ACTION_LABEL(action),
|
||||
msg->inject_touch_event.position.point.x,
|
||||
msg->inject_touch_event.position.point.y,
|
||||
msg->inject_touch_event.pressure,
|
||||
(long) msg->inject_touch_event.action_button,
|
||||
(long) msg->inject_touch_event.buttons);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -65,6 +65,7 @@ struct sc_control_msg {
|
||||
} inject_text;
|
||||
struct {
|
||||
enum android_motionevent_action action;
|
||||
enum android_motionevent_buttons action_button;
|
||||
enum android_motionevent_buttons buttons;
|
||||
uint64_t pointer_id;
|
||||
struct sc_position position;
|
||||
|
||||
@@ -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
|
||||
// frame); instead, it must be concatenated with the future data packet.
|
||||
if (demuxer->pending || is_config) {
|
||||
size_t offset;
|
||||
if (demuxer->pending) {
|
||||
offset = demuxer->pending->size;
|
||||
size_t offset = demuxer->pending->size;
|
||||
if (av_grow_packet(demuxer->pending, packet->size)) {
|
||||
LOG_OOM();
|
||||
return false;
|
||||
}
|
||||
|
||||
memcpy(demuxer->pending->data + offset, packet->data, packet->size);
|
||||
} else {
|
||||
offset = 0;
|
||||
demuxer->pending = av_packet_alloc();
|
||||
if (!demuxer->pending) {
|
||||
LOG_OOM();
|
||||
return false;
|
||||
}
|
||||
if (av_new_packet(demuxer->pending, packet->size)) {
|
||||
if (av_packet_ref(demuxer->pending, packet)) {
|
||||
LOG_OOM();
|
||||
av_packet_free(&demuxer->pending);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
memcpy(demuxer->pending->data + offset, packet->data, packet->size);
|
||||
|
||||
if (!is_config) {
|
||||
// prepare the concat packet to send to the decoder
|
||||
demuxer->pending->pts = packet->pts;
|
||||
|
||||
@@ -339,6 +339,7 @@ simulate_virtual_finger(struct sc_input_manager *im,
|
||||
im->forward_all_clicks ? POINTER_ID_VIRTUAL_MOUSE
|
||||
: POINTER_ID_VIRTUAL_FINGER;
|
||||
msg.inject_touch_event.pressure = up ? 0.0f : 1.0f;
|
||||
msg.inject_touch_event.action_button = 0;
|
||||
msg.inject_touch_event.buttons = 0;
|
||||
|
||||
if (!sc_controller_push_msg(im->controller, &msg)) {
|
||||
|
||||
@@ -93,6 +93,7 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp,
|
||||
.pointer_id = event->pointer_id,
|
||||
.position = event->position,
|
||||
.pressure = event->action == SC_ACTION_DOWN ? 1.f : 0.f,
|
||||
.action_button = convert_mouse_buttons(event->button),
|
||||
.buttons = convert_mouse_buttons(event->buttons_state),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
/** Downcast packet_sink to recorder */
|
||||
#define DOWNCAST(SINK) container_of(SINK, struct sc_recorder, packet_sink)
|
||||
|
||||
#define SC_PTS_ORIGIN_NONE UINT64_C(-1)
|
||||
|
||||
static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us
|
||||
|
||||
static const AVOutputFormat *
|
||||
@@ -128,6 +130,8 @@ sc_recorder_write(struct sc_recorder *recorder, AVPacket *packet) {
|
||||
return true;
|
||||
}
|
||||
|
||||
LOGI("==== %" PRIu64, packet->pts);
|
||||
|
||||
sc_recorder_rescale_packet(recorder, packet);
|
||||
return av_write_frame(recorder->ctx, packet) >= 0;
|
||||
}
|
||||
@@ -169,6 +173,18 @@ run_recorder(void *data) {
|
||||
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
|
||||
if (recorder->pts_origin == SC_PTS_ORIGIN_NONE
|
||||
&& rec->packet->pts != AV_NOPTS_VALUE) {
|
||||
// First PTS received
|
||||
recorder->pts_origin = rec->packet->pts;
|
||||
}
|
||||
|
||||
if (rec->packet->pts != AV_NOPTS_VALUE) {
|
||||
// Set PTS relatve to the origin
|
||||
rec->packet->pts -= recorder->pts_origin;
|
||||
rec->packet->dts = rec->packet->pts;
|
||||
}
|
||||
|
||||
// recorder->previous is only written from this thread, no need to lock
|
||||
struct sc_record_packet *previous = recorder->previous;
|
||||
recorder->previous = rec;
|
||||
@@ -243,6 +259,7 @@ sc_recorder_open(struct sc_recorder *recorder, const AVCodec *input_codec) {
|
||||
recorder->failed = false;
|
||||
recorder->header_written = false;
|
||||
recorder->previous = NULL;
|
||||
recorder->pts_origin = SC_PTS_ORIGIN_NONE;
|
||||
|
||||
const char *format_name = sc_recorder_get_format_name(recorder->format);
|
||||
assert(format_name);
|
||||
|
||||
@@ -28,6 +28,8 @@ struct sc_recorder {
|
||||
struct sc_size declared_frame_size;
|
||||
bool header_written;
|
||||
|
||||
uint64_t pts_origin;
|
||||
|
||||
sc_thread thread;
|
||||
sc_mutex mutex;
|
||||
sc_cond queue_cond;
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
#include "util/acksync.h"
|
||||
#include "util/log.h"
|
||||
#include "util/net.h"
|
||||
#include "util/rand.h"
|
||||
#ifdef HAVE_V4L2
|
||||
# include "v4l2_sink.h"
|
||||
#endif
|
||||
@@ -265,6 +266,14 @@ sc_server_on_disconnected(struct sc_server *server, void *userdata) {
|
||||
// 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
|
||||
scrcpy(struct scrcpy_options *options) {
|
||||
static struct scrcpy scrcpy;
|
||||
@@ -298,7 +307,10 @@ scrcpy(struct scrcpy_options *options) {
|
||||
|
||||
struct sc_acksync *acksync = NULL;
|
||||
|
||||
uint32_t uid = scrcpy_generate_uid();
|
||||
|
||||
struct sc_server_params params = {
|
||||
.uid = uid,
|
||||
.req_serial = options->serial,
|
||||
.select_usb = options->select_usb,
|
||||
.select_tcpip = options->select_tcpip,
|
||||
|
||||
@@ -306,14 +306,13 @@ sc_screen_render(struct sc_screen *screen, bool update_content_rect) {
|
||||
}
|
||||
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#if defined(__APPLE__) || defined(__WINDOWS__)
|
||||
# define CONTINUOUS_RESIZING_WORKAROUND
|
||||
#endif
|
||||
|
||||
#ifdef CONTINUOUS_RESIZING_WORKAROUND
|
||||
// On Windows and MacOS, resizing blocks the event loop, so resizing events are
|
||||
// not triggered. On MacOS, as a workaround, handle them in an event handler
|
||||
// (it does not work for Windows unfortunately).
|
||||
// not triggered. As a workaround, handle them in an event handler.
|
||||
//
|
||||
// <https://bugzilla.libsdl.org/show_bug.cgi?id=2077>
|
||||
// <https://stackoverflow.com/a/40693139/1987178>
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#define SC_DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar"
|
||||
|
||||
#define SC_ADB_PORT_DEFAULT 5555
|
||||
#define SC_SOCKET_NAME_PREFIX "scrcpy_"
|
||||
|
||||
static char *
|
||||
get_server_path(void) {
|
||||
@@ -197,6 +198,7 @@ execute_server(struct sc_server *server,
|
||||
cmd[count++] = p; \
|
||||
}
|
||||
|
||||
ADD_PARAM("uid=%08x", params->uid);
|
||||
ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level));
|
||||
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->device_socket_name = NULL;
|
||||
server->stopped = false;
|
||||
|
||||
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
|
||||
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
|
||||
bool ok = device_read_info(&server->intr, video_socket, info);
|
||||
@@ -494,7 +498,8 @@ fail:
|
||||
|
||||
if (tunnel->enabled) {
|
||||
// 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;
|
||||
@@ -764,13 +769,23 @@ run_server(void *data) {
|
||||
assert(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);
|
||||
if (!ok) {
|
||||
goto error_connection_failed;
|
||||
}
|
||||
|
||||
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) {
|
||||
goto error_connection_failed;
|
||||
}
|
||||
@@ -778,7 +793,8 @@ run_server(void *data) {
|
||||
// server will connect to our server socket
|
||||
sc_pid pid = execute_server(server, params);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -790,7 +806,8 @@ run_server(void *data) {
|
||||
if (!ok) {
|
||||
sc_process_terminate(pid);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -884,6 +901,7 @@ sc_server_destroy(struct sc_server *server) {
|
||||
}
|
||||
|
||||
free(server->serial);
|
||||
free(server->device_socket_name);
|
||||
sc_server_params_destroy(&server->params);
|
||||
sc_intr_destroy(&server->intr);
|
||||
sc_cond_destroy(&server->cond_stopped);
|
||||
|
||||
@@ -22,6 +22,7 @@ struct sc_server_info {
|
||||
};
|
||||
|
||||
struct sc_server_params {
|
||||
uint32_t uid;
|
||||
const char *req_serial;
|
||||
enum sc_log_level log_level;
|
||||
const char *crop;
|
||||
@@ -54,6 +55,7 @@ struct sc_server {
|
||||
// The internal allocated strings are copies owned by the server
|
||||
struct sc_server_params params;
|
||||
char *serial;
|
||||
char *device_socket_name;
|
||||
|
||||
sc_thread thread;
|
||||
struct sc_server_info info; // initialized once connected
|
||||
|
||||
24
app/src/util/rand.c
Normal file
24
app/src/util/rand.c
Normal 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
16
app/src/util/rand.h
Normal 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
|
||||
@@ -90,13 +90,14 @@ static void test_serialize_inject_touch_event(void) {
|
||||
},
|
||||
},
|
||||
.pressure = 1.0f,
|
||||
.action_button = AMOTION_EVENT_BUTTON_PRIMARY,
|
||||
.buttons = AMOTION_EVENT_BUTTON_PRIMARY,
|
||||
},
|
||||
};
|
||||
|
||||
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE];
|
||||
size_t size = sc_control_msg_serialize(&msg, buf);
|
||||
assert(size == 28);
|
||||
assert(size == 32);
|
||||
|
||||
const unsigned char expected[] = {
|
||||
SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT,
|
||||
@@ -105,7 +106,8 @@ static void test_serialize_inject_touch_event(void) {
|
||||
0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0xc8, // 100 200
|
||||
0x04, 0x38, 0x07, 0x80, // 1080 1920
|
||||
0xff, 0xff, // pressure
|
||||
0x00, 0x00, 0x00, 0x01 // AMOTION_EVENT_BUTTON_PRIMARY
|
||||
0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY (action button)
|
||||
0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY (buttons)
|
||||
};
|
||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
set -e
|
||||
|
||||
BUILDDIR=build-auto
|
||||
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v1.24/scrcpy-server-v1.24
|
||||
PREBUILT_SERVER_SHA256=ae74a81ea79c0dc7250e586627c278c0a9a8c5de46c9fb5c38c167fb1a36f056
|
||||
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v1.25/scrcpy-server-v1.25
|
||||
PREBUILT_SERVER_SHA256=ce0306c7bbd06ae72f6d06f7ec0ee33774995a65de71e0a83813ecb67aec9bdb
|
||||
|
||||
echo "[scrcpy] Downloading prebuilt server..."
|
||||
wget "$PREBUILT_SERVER_URL" -O scrcpy-server
|
||||
@@ -12,7 +12,7 @@ echo "$PREBUILT_SERVER_SHA256 scrcpy-server" | sha256sum --check
|
||||
|
||||
echo "[scrcpy] Building client..."
|
||||
rm -rf "$BUILDDIR"
|
||||
meson "$BUILDDIR" --buildtype=release --strip -Db_lto=true \
|
||||
meson setup "$BUILDDIR" --buildtype=release --strip -Db_lto=true \
|
||||
-Dprebuilt_server=scrcpy-server
|
||||
cd "$BUILDDIR"
|
||||
ninja
|
||||
|
||||
@@ -20,18 +20,21 @@ BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS"
|
||||
|
||||
BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})"
|
||||
CLASSES_DIR="$BUILD_DIR/classes"
|
||||
GEN_DIR="$BUILD_DIR/gen"
|
||||
SERVER_DIR=$(dirname "$0")
|
||||
SERVER_BINARY=scrcpy-server
|
||||
ANDROID_JAR="$ANDROID_HOME/platforms/android-$PLATFORM/android.jar"
|
||||
LAMBDA_JAR="$BUILD_TOOLS_DIR/core-lambda-stubs.jar"
|
||||
|
||||
echo "Platform: android-$PLATFORM"
|
||||
echo "Build-tools: $BUILD_TOOLS"
|
||||
echo "Build dir: $BUILD_DIR"
|
||||
|
||||
rm -rf "$CLASSES_DIR" "$BUILD_DIR/$SERVER_BINARY" classes.dex
|
||||
mkdir -p "$CLASSES_DIR/com/genymobile/scrcpy"
|
||||
rm -rf "$CLASSES_DIR" "$GEN_DIR" "$BUILD_DIR/$SERVER_BINARY" classes.dex
|
||||
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;
|
||||
|
||||
public final class BuildConfig {
|
||||
@@ -42,13 +45,15 @@ EOF
|
||||
|
||||
echo "Generating java from aidl..."
|
||||
cd "$SERVER_DIR/src/main/aidl"
|
||||
"$BUILD_TOOLS_DIR/aidl" -o"$CLASSES_DIR" android/view/IRotationWatcher.aidl
|
||||
"$BUILD_TOOLS_DIR/aidl" -o"$CLASSES_DIR" \
|
||||
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" android/view/IRotationWatcher.aidl
|
||||
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" \
|
||||
android/content/IOnPrimaryClipChangedListener.aidl
|
||||
|
||||
echo "Compiling java sources..."
|
||||
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 \
|
||||
com/genymobile/scrcpy/*.java \
|
||||
com/genymobile/scrcpy/wrappers/*.java
|
||||
@@ -68,7 +73,7 @@ then
|
||||
echo "Archiving..."
|
||||
cd "$BUILD_DIR"
|
||||
jar cvf "$SERVER_BINARY" classes.dex
|
||||
rm -rf classes.dex classes
|
||||
rm -rf classes.dex
|
||||
else
|
||||
# use d8
|
||||
"$BUILD_TOOLS_DIR/d8" --classpath "$ANDROID_JAR" \
|
||||
@@ -80,7 +85,8 @@ else
|
||||
|
||||
cd "$BUILD_DIR"
|
||||
mv classes.zip "$SERVER_BINARY"
|
||||
rm -rf classes
|
||||
fi
|
||||
|
||||
rm -rf "$GEN_DIR" "$CLASSES_DIR"
|
||||
|
||||
echo "Server generated in $BUILD_DIR/$SERVER_BINARY"
|
||||
|
||||
@@ -4,8 +4,8 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class CodecOption {
|
||||
private String key;
|
||||
private Object value;
|
||||
private final String key;
|
||||
private final Object value;
|
||||
|
||||
public CodecOption(String key, Object value) {
|
||||
this.key = key;
|
||||
|
||||
@@ -29,6 +29,7 @@ public final class ControlMessage {
|
||||
private int metaState; // KeyEvent.META_*
|
||||
private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or POWER_MODE_*
|
||||
private int keycode; // KeyEvent.KEYCODE_*
|
||||
private int actionButton; // MotionEvent.BUTTON_*
|
||||
private int buttons; // MotionEvent.BUTTON_*
|
||||
private long pointerId;
|
||||
private float pressure;
|
||||
@@ -60,13 +61,15 @@ public final class ControlMessage {
|
||||
return msg;
|
||||
}
|
||||
|
||||
public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure, int buttons) {
|
||||
public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure, int actionButton,
|
||||
int buttons) {
|
||||
ControlMessage msg = new ControlMessage();
|
||||
msg.type = TYPE_INJECT_TOUCH_EVENT;
|
||||
msg.action = action;
|
||||
msg.pointerId = pointerId;
|
||||
msg.pressure = pressure;
|
||||
msg.position = position;
|
||||
msg.actionButton = actionButton;
|
||||
msg.buttons = buttons;
|
||||
return msg;
|
||||
}
|
||||
@@ -140,6 +143,10 @@ public final class ControlMessage {
|
||||
return keycode;
|
||||
}
|
||||
|
||||
public int getActionButton() {
|
||||
return actionButton;
|
||||
}
|
||||
|
||||
public int getButtons() {
|
||||
return buttons;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import java.nio.charset.StandardCharsets;
|
||||
public class ControlMessageReader {
|
||||
|
||||
static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 13;
|
||||
static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27;
|
||||
static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 31;
|
||||
static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20;
|
||||
static final int BACK_OR_SCREEN_ON_LENGTH = 1;
|
||||
static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1;
|
||||
@@ -140,8 +140,9 @@ public class ControlMessageReader {
|
||||
long pointerId = buffer.getLong();
|
||||
Position position = readPosition(buffer);
|
||||
float pressure = Binary.u16FixedPointToFloat(buffer.getShort());
|
||||
int actionButton = buffer.getInt();
|
||||
int buttons = buffer.getInt();
|
||||
return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, buttons);
|
||||
return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, actionButton, buttons);
|
||||
}
|
||||
|
||||
private ControlMessage parseInjectScrollEvent() {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
import com.genymobile.scrcpy.wrappers.InputManager;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.SystemClock;
|
||||
import android.view.InputDevice;
|
||||
@@ -75,7 +77,7 @@ public class Controller {
|
||||
SystemClock.sleep(500);
|
||||
}
|
||||
|
||||
while (true) {
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
handleEvent();
|
||||
}
|
||||
}
|
||||
@@ -99,7 +101,7 @@ public class Controller {
|
||||
break;
|
||||
case ControlMessage.TYPE_INJECT_TOUCH_EVENT:
|
||||
if (device.supportsInputEvents()) {
|
||||
injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons());
|
||||
injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getActionButton(), msg.getButtons());
|
||||
}
|
||||
break;
|
||||
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
|
||||
@@ -179,7 +181,7 @@ public class Controller {
|
||||
return successCount;
|
||||
}
|
||||
|
||||
private boolean injectTouch(int action, long pointerId, Position position, float pressure, int buttons) {
|
||||
private boolean injectTouch(int action, long pointerId, Position position, float pressure, int actionButton, int buttons) {
|
||||
long now = SystemClock.uptimeMillis();
|
||||
|
||||
Point point = device.getPhysicalPoint(position);
|
||||
@@ -196,22 +198,23 @@ public class Controller {
|
||||
Pointer pointer = pointersState.get(pointerIndex);
|
||||
pointer.setPoint(point);
|
||||
pointer.setPressure(pressure);
|
||||
pointer.setUp(action == MotionEvent.ACTION_UP);
|
||||
|
||||
int source;
|
||||
int pointerCount = pointersState.update(pointerProperties, pointerCoords);
|
||||
if (pointerId == POINTER_ID_MOUSE || pointerId == POINTER_ID_VIRTUAL_MOUSE) {
|
||||
// real mouse event (forced by the client when --forward-on-click)
|
||||
pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_MOUSE;
|
||||
source = InputDevice.SOURCE_MOUSE;
|
||||
pointer.setUp(buttons == 0);
|
||||
} else {
|
||||
// POINTER_ID_GENERIC_FINGER, POINTER_ID_VIRTUAL_FINGER or real touch from device
|
||||
pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_FINGER;
|
||||
source = InputDevice.SOURCE_TOUCHSCREEN;
|
||||
// Buttons must not be set for touch events
|
||||
buttons = 0;
|
||||
pointer.setUp(action == MotionEvent.ACTION_UP);
|
||||
}
|
||||
|
||||
int pointerCount = pointersState.update(pointerProperties, pointerCoords);
|
||||
if (pointerCount == 1) {
|
||||
if (action == MotionEvent.ACTION_DOWN) {
|
||||
lastTouchDown = now;
|
||||
@@ -225,6 +228,62 @@ public class Controller {
|
||||
}
|
||||
}
|
||||
|
||||
/* If the input device is a mouse (on API >= 23):
|
||||
* - the first button pressed must first generate ACTION_DOWN;
|
||||
* - all button pressed (including the first one) must generate ACTION_BUTTON_PRESS;
|
||||
* - all button released (including the last one) must generate ACTION_BUTTON_RELEASE;
|
||||
* - the last button released must in addition generate ACTION_UP.
|
||||
*
|
||||
* Otherwise, Chrome does not work properly: <https://github.com/Genymobile/scrcpy/issues/3635>
|
||||
*/
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && source == InputDevice.SOURCE_MOUSE) {
|
||||
if (action == MotionEvent.ACTION_DOWN) {
|
||||
if (actionButton == buttons) {
|
||||
// First button pressed: ACTION_DOWN
|
||||
MotionEvent downEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_DOWN, pointerCount, pointerProperties,
|
||||
pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0);
|
||||
if (!device.injectEvent(downEvent, Device.INJECT_MODE_ASYNC)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Any button pressed: ACTION_BUTTON_PRESS
|
||||
MotionEvent pressEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_BUTTON_PRESS, pointerCount, pointerProperties,
|
||||
pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0);
|
||||
if (!InputManager.setActionButton(pressEvent, actionButton)) {
|
||||
return false;
|
||||
}
|
||||
if (!device.injectEvent(pressEvent, Device.INJECT_MODE_ASYNC)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (action == MotionEvent.ACTION_UP) {
|
||||
// Any button released: ACTION_BUTTON_RELEASE
|
||||
MotionEvent releaseEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_BUTTON_RELEASE, pointerCount, pointerProperties,
|
||||
pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0);
|
||||
if (!InputManager.setActionButton(releaseEvent, actionButton)) {
|
||||
return false;
|
||||
}
|
||||
if (!device.injectEvent(releaseEvent, Device.INJECT_MODE_ASYNC)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (buttons == 0) {
|
||||
// Last button released: ACTION_UP
|
||||
MotionEvent upEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_UP, pointerCount, pointerProperties,
|
||||
pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0);
|
||||
if (!device.injectEvent(upEvent, Device.INJECT_MODE_ASYNC)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent event = MotionEvent
|
||||
.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source,
|
||||
0);
|
||||
@@ -258,12 +317,9 @@ public class Controller {
|
||||
* Schedule a call to set power mode to off after a small delay.
|
||||
*/
|
||||
private static void schedulePowerModeOff() {
|
||||
EXECUTOR.schedule(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Ln.i("Forcing screen off");
|
||||
Device.setScreenPowerMode(Device.POWER_MODE_OFF);
|
||||
}
|
||||
EXECUTOR.schedule(() -> {
|
||||
Ln.i("Forcing screen off");
|
||||
Device.setScreenPowerMode(Device.POWER_MODE_OFF);
|
||||
}, 200, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ public final class DesktopConnection implements Closeable {
|
||||
|
||||
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 FileDescriptor videoFd;
|
||||
@@ -46,12 +46,22 @@ public final class DesktopConnection implements Closeable {
|
||||
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 controlSocket = null;
|
||||
if (tunnelForward) {
|
||||
LocalServerSocket localServerSocket = new LocalServerSocket(SOCKET_NAME);
|
||||
try {
|
||||
try (LocalServerSocket localServerSocket = new LocalServerSocket(socketName)) {
|
||||
videoSocket = localServerSocket.accept();
|
||||
if (sendDummyByte) {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
localServerSocket.close();
|
||||
}
|
||||
} else {
|
||||
videoSocket = connect(SOCKET_NAME);
|
||||
videoSocket = connect(socketName);
|
||||
if (control) {
|
||||
try {
|
||||
controlSocket = connect(SOCKET_NAME);
|
||||
controlSocket = connect(socketName);
|
||||
} catch (IOException | RuntimeException e) {
|
||||
videoSocket.close();
|
||||
throw e;
|
||||
|
||||
@@ -25,7 +25,7 @@ public final class DeviceMessageSender {
|
||||
}
|
||||
|
||||
public void loop() throws IOException, InterruptedException {
|
||||
while (true) {
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
String text;
|
||||
long sequence;
|
||||
synchronized (this) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import java.util.List;
|
||||
|
||||
public class Options {
|
||||
private Ln.Level logLevel = Ln.Level.DEBUG;
|
||||
private int uid = -1; // 31-bit non-negative value, or -1
|
||||
private int maxSize;
|
||||
private int bitRate = 8000000;
|
||||
private int maxFps;
|
||||
@@ -37,6 +38,14 @@ public class Options {
|
||||
this.logLevel = logLevel;
|
||||
}
|
||||
|
||||
public int getUid() {
|
||||
return uid;
|
||||
}
|
||||
|
||||
public void setUid(int uid) {
|
||||
this.uid = uid;
|
||||
}
|
||||
|
||||
public int getMaxSize() {
|
||||
return maxSize;
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ package com.genymobile.scrcpy;
|
||||
import java.util.Objects;
|
||||
|
||||
public class Position {
|
||||
private Point point;
|
||||
private Size screenSize;
|
||||
private final Point point;
|
||||
private final Size screenSize;
|
||||
|
||||
public Position(Point point, Size screenSize) {
|
||||
this.point = point;
|
||||
|
||||
@@ -9,6 +9,7 @@ import android.media.MediaCodecList;
|
||||
import android.media.MediaFormat;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.os.SystemClock;
|
||||
import android.view.Surface;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
@@ -27,6 +28,7 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
|
||||
// Keep the values in descending order
|
||||
private static final int[] MAX_SIZE_FALLBACK = {2560, 1920, 1600, 1280, 1024, 800};
|
||||
private static final int MAX_CONSECUTIVE_ERRORS = 3;
|
||||
|
||||
private static final long PACKET_FLAG_CONFIG = 1L << 63;
|
||||
private static final long PACKET_FLAG_KEY_FRAME = 1L << 62;
|
||||
@@ -40,9 +42,9 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
private final int maxFps;
|
||||
private final boolean sendFrameMeta;
|
||||
private final boolean downsizeOnError;
|
||||
private long ptsOrigin;
|
||||
|
||||
private boolean firstFrameSent;
|
||||
private int consecutiveErrors;
|
||||
|
||||
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List<CodecOption> codecOptions, String encoderName,
|
||||
boolean downsizeOnError) {
|
||||
@@ -75,28 +77,32 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
}
|
||||
|
||||
private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException {
|
||||
MediaCodec codec = createCodec(encoderName);
|
||||
MediaFormat format = createFormat(bitRate, maxFps, codecOptions);
|
||||
IBinder display = createDisplay();
|
||||
device.setRotationListener(this);
|
||||
boolean alive;
|
||||
try {
|
||||
do {
|
||||
MediaCodec codec = createCodec(encoderName);
|
||||
IBinder display = createDisplay();
|
||||
ScreenInfo screenInfo = device.getScreenInfo();
|
||||
Rect contentRect = screenInfo.getContentRect();
|
||||
|
||||
// include the locked video orientation
|
||||
Rect videoRect = screenInfo.getVideoSize().toRect();
|
||||
// does not include the locked video orientation
|
||||
Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect();
|
||||
int videoRotation = screenInfo.getVideoRotation();
|
||||
int layerStack = device.getLayerStack();
|
||||
setSize(format, videoRect.width(), videoRect.height());
|
||||
format.setInteger(MediaFormat.KEY_WIDTH, videoRect.width());
|
||||
format.setInteger(MediaFormat.KEY_HEIGHT, videoRect.height());
|
||||
|
||||
Surface surface = null;
|
||||
try {
|
||||
configure(codec, format);
|
||||
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
||||
surface = codec.createInputSurface();
|
||||
|
||||
// does not include the locked video orientation
|
||||
Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect();
|
||||
int videoRotation = screenInfo.getVideoRotation();
|
||||
int layerStack = device.getLayerStack();
|
||||
setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack);
|
||||
|
||||
codec.start();
|
||||
|
||||
alive = encode(codec, fd);
|
||||
@@ -104,34 +110,58 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
codec.stop();
|
||||
} catch (IllegalStateException | IllegalArgumentException e) {
|
||||
Ln.e("Encoding error: " + e.getClass().getName() + ": " + e.getMessage());
|
||||
if (!downsizeOnError || firstFrameSent) {
|
||||
// Fail immediately
|
||||
if (!prepareRetry(device, screenInfo)) {
|
||||
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);
|
||||
Ln.i("Retrying...");
|
||||
alive = true;
|
||||
} finally {
|
||||
destroyDisplay(display);
|
||||
codec.release();
|
||||
codec.reset();
|
||||
if (surface != null) {
|
||||
surface.release();
|
||||
}
|
||||
}
|
||||
} while (alive);
|
||||
} finally {
|
||||
codec.release();
|
||||
device.setRotationListener(null);
|
||||
SurfaceControl.destroyDisplay(display);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean prepareRetry(Device device, ScreenInfo screenInfo) {
|
||||
if (firstFrameSent) {
|
||||
++consecutiveErrors;
|
||||
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
||||
// Definitively fail
|
||||
return false;
|
||||
}
|
||||
|
||||
// Wait a bit to increase the probability that retrying will fix the problem
|
||||
SystemClock.sleep(50);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!downsizeOnError) {
|
||||
// Must fail immediately
|
||||
return false;
|
||||
}
|
||||
|
||||
// Downsizing on error is only enabled if an encoding failure occurs before the first frame (downsizing later could be surprising)
|
||||
|
||||
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) {
|
||||
int currentMaxSize = Math.max(failedSize.getWidth(), failedSize.getHeight());
|
||||
for (int value : MAX_SIZE_FALLBACK) {
|
||||
@@ -167,6 +197,7 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) {
|
||||
// If this is not a config packet, then it contains a frame
|
||||
firstFrameSent = true;
|
||||
consecutiveErrors = 0;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
@@ -186,10 +217,7 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
|
||||
pts = PACKET_FLAG_CONFIG; // non-media data packet
|
||||
} else {
|
||||
if (ptsOrigin == 0) {
|
||||
ptsOrigin = bufferInfo.presentationTimeUs;
|
||||
}
|
||||
pts = bufferInfo.presentationTimeUs - ptsOrigin;
|
||||
pts = bufferInfo.presentationTimeUs;
|
||||
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) {
|
||||
pts |= PACKET_FLAG_KEY_FRAME;
|
||||
}
|
||||
@@ -278,15 +306,6 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
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) {
|
||||
SurfaceControl.openTransaction();
|
||||
try {
|
||||
@@ -297,8 +316,4 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
SurfaceControl.closeTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
private static void destroyDisplay(IBinder display) {
|
||||
SurfaceControl.destroyDisplay(display);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,11 +66,12 @@ public final class Server {
|
||||
|
||||
Thread initThread = startInitThread(options);
|
||||
|
||||
int uid = options.getUid();
|
||||
boolean tunnelForward = options.isTunnelForward();
|
||||
boolean control = options.getControl();
|
||||
boolean sendDummyByte = options.getSendDummyByte();
|
||||
|
||||
try (DesktopConnection connection = DesktopConnection.open(tunnelForward, control, sendDummyByte)) {
|
||||
try (DesktopConnection connection = DesktopConnection.open(uid, tunnelForward, control, sendDummyByte)) {
|
||||
if (options.getSendDeviceMeta()) {
|
||||
Size videoSize = device.getScreenInfo().getVideoSize();
|
||||
connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
|
||||
@@ -87,12 +88,7 @@ public final class Server {
|
||||
controllerThread = startController(controller);
|
||||
deviceMessageSenderThread = startDeviceMessageSender(controller.getSender());
|
||||
|
||||
device.setClipboardListener(new Device.ClipboardListener() {
|
||||
@Override
|
||||
public void onClipboardTextChanged(String text) {
|
||||
controller.getSender().pushClipboardText(text);
|
||||
}
|
||||
});
|
||||
device.setClipboardListener(text -> controller.getSender().pushClipboardText(text));
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -114,26 +110,18 @@ public final class Server {
|
||||
}
|
||||
|
||||
private static Thread startInitThread(final Options options) {
|
||||
Thread thread = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
initAndCleanUp(options);
|
||||
}
|
||||
});
|
||||
Thread thread = new Thread(() -> initAndCleanUp(options));
|
||||
thread.start();
|
||||
return thread;
|
||||
}
|
||||
|
||||
private static Thread startController(final Controller controller) {
|
||||
Thread thread = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
controller.control();
|
||||
} catch (IOException e) {
|
||||
// this is expected on close
|
||||
Ln.d("Controller stopped");
|
||||
}
|
||||
Thread thread = new Thread(() -> {
|
||||
try {
|
||||
controller.control();
|
||||
} catch (IOException e) {
|
||||
// this is expected on close
|
||||
Ln.d("Controller stopped");
|
||||
}
|
||||
});
|
||||
thread.start();
|
||||
@@ -141,15 +129,12 @@ public final class Server {
|
||||
}
|
||||
|
||||
private static Thread startDeviceMessageSender(final DeviceMessageSender sender) {
|
||||
Thread thread = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
sender.loop();
|
||||
} catch (IOException | InterruptedException e) {
|
||||
// this is expected on close
|
||||
Ln.d("Device message sender stopped");
|
||||
}
|
||||
Thread thread = new Thread(() -> {
|
||||
try {
|
||||
sender.loop();
|
||||
} catch (IOException | InterruptedException e) {
|
||||
// this is expected on close
|
||||
Ln.d("Device message sender stopped");
|
||||
}
|
||||
});
|
||||
thread.start();
|
||||
@@ -178,6 +163,13 @@ public final class Server {
|
||||
String key = arg.substring(0, equalIndex);
|
||||
String value = arg.substring(equalIndex + 1);
|
||||
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":
|
||||
Ln.Level level = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH));
|
||||
options.setLogLevel(level);
|
||||
@@ -319,12 +311,9 @@ public final class Server {
|
||||
}
|
||||
|
||||
public static void main(String... args) throws Exception {
|
||||
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
|
||||
@Override
|
||||
public void uncaughtException(Thread t, Throwable e) {
|
||||
Ln.e("Exception on thread " + t, e);
|
||||
suggestFix(e);
|
||||
}
|
||||
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
|
||||
Ln.e("Exception on thread " + t, e);
|
||||
suggestFix(e);
|
||||
});
|
||||
|
||||
Options options = createOptions(args);
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.genymobile.scrcpy.wrappers;
|
||||
import com.genymobile.scrcpy.Ln;
|
||||
|
||||
import android.view.InputEvent;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
@@ -17,6 +18,7 @@ public final class InputManager {
|
||||
private Method injectInputEventMethod;
|
||||
|
||||
private static Method setDisplayIdMethod;
|
||||
private static Method setActionButtonMethod;
|
||||
|
||||
public InputManager(android.hardware.input.InputManager manager) {
|
||||
this.manager = manager;
|
||||
@@ -56,4 +58,22 @@ public final class InputManager {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static Method getSetActionButtonMethod() throws NoSuchMethodException {
|
||||
if (setActionButtonMethod == null) {
|
||||
setActionButtonMethod = MotionEvent.class.getMethod("setActionButton", int.class);
|
||||
}
|
||||
return setActionButtonMethod;
|
||||
}
|
||||
|
||||
public static boolean setActionButton(MotionEvent motionEvent, int actionButton) {
|
||||
try {
|
||||
Method method = getSetActionButtonMethod();
|
||||
method.invoke(motionEvent, actionButton);
|
||||
return true;
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Cannot set action button on MotionEvent", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +94,8 @@ public class ControlMessageReaderTest {
|
||||
dos.writeShort(1080);
|
||||
dos.writeShort(1920);
|
||||
dos.writeShort(0xffff); // pressure
|
||||
dos.writeInt(MotionEvent.BUTTON_PRIMARY);
|
||||
dos.writeInt(MotionEvent.BUTTON_PRIMARY); // action button
|
||||
dos.writeInt(MotionEvent.BUTTON_PRIMARY); // buttons
|
||||
|
||||
byte[] packet = bos.toByteArray();
|
||||
|
||||
@@ -112,6 +113,7 @@ public class ControlMessageReaderTest {
|
||||
Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth());
|
||||
Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight());
|
||||
Assert.assertEquals(1f, event.getPressure(), 0f); // must be exact
|
||||
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getActionButton());
|
||||
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getButtons());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user