Compare commits

..

3 Commits

Author SHA1 Message Date
Kartik Kushwaha
b4caa483dd Add Fedora instructions in README
Add the command to install the scrcpy package for Fedora directly.

PR #3715 <https://github.com/Genymobile/scrcpy/pull/3715>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-02-06 11:13:18 +01:00
Romain Vimont
87da137238 Remove "on Linux" in FAQ
HID now works on all platforms.
2023-01-18 14:37:55 +01:00
Romain Vimont
b3f626feee Add FAQ section about HID/OTG on Windows
Refs #3654 <https://github.com/Genymobile/scrcpy/issues/3654>
2023-01-03 08:48:46 +01:00
48 changed files with 368 additions and 981 deletions

20
FAQ.md
View File

@@ -7,7 +7,7 @@ Here are the common reported problems and their status.
If you encounter any error, the first step is to upgrade to the latest version. If you encounter any error, the first step is to upgrade to the latest version.
## `adb` issues ## `adb` and USB issues
`scrcpy` execute `adb` commands to initialize the connection with the device. If `scrcpy` execute `adb` commands to initialize the connection with the device. If
`adb` fails, then scrcpy will not work. `adb` fails, then scrcpy will not work.
@@ -133,6 +133,21 @@ Try with another USB cable or plug it into another USB port. See [#281] and
[#283]: https://github.com/Genymobile/scrcpy/issues/283 [#283]: https://github.com/Genymobile/scrcpy/issues/283
## HID/OTG issues on Windows
On Windows, if `scrcpy --otg` (or `--hid-keyboard`/`--hid-mouse`) results in:
> ERROR: Could not find any USB device
(or if only unrelated USB devices are detected), there might be drivers issues.
Please read [#3654], in particular [this comment][#3654-comment1] and [the next
one][#3654-comment2].
[#3654]: https://github.com/Genymobile/scrcpy/issues/3654
[#3654-comment1]: https://github.com/Genymobile/scrcpy/issues/3654#issuecomment-1369278232
[#3654-comment2]: https://github.com/Genymobile/scrcpy/issues/3654#issuecomment-1369295011
## Control issues ## Control issues
@@ -153,8 +168,7 @@ The default text injection method is [limited to ASCII characters][text-input].
A trick allows to also inject some [accented characters][accented-characters], A trick allows to also inject some [accented characters][accented-characters],
but that's all. See [#37]. but that's all. See [#37].
Since scrcpy v1.20 on Linux, it is possible to simulate a [physical Since scrcpy v1.20, it is possible to simulate a [physical keyboard][hid] (HID).
keyboard][hid] (HID).
[text-input]: https://github.com/Genymobile/scrcpy/issues?q=is%3Aopen+is%3Aissue+label%3Aunicode [text-input]: https://github.com/Genymobile/scrcpy/issues?q=is%3Aopen+is%3Aissue+label%3Aunicode
[accented-characters]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-accented-characters [accented-characters]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-accented-characters

View File

@@ -80,16 +80,22 @@ On Arch Linux:
pacman -S scrcpy pacman -S scrcpy
``` ```
On Fedora, a [COPR] package is available: [`scrcpy`][copr-link]:
```
dnf copr enable zeno/scrcpy
dnf install scrcpy
```
[COPR]: https://fedoraproject.org/wiki/Category:Copr
[copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/
A [Snap] package is available: [`scrcpy`][snap-link]. A [Snap] package is available: [`scrcpy`][snap-link].
[snap-link]: https://snapstats.org/snaps/scrcpy [snap-link]: https://snapstats.org/snaps/scrcpy
[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager) [snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager)
For Fedora, a [COPR] package is available: [`scrcpy`][copr-link].
[COPR]: https://fedoraproject.org/wiki/Category:Copr
[copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/
For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link]. For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link].
@@ -252,22 +258,10 @@ This affects recording orientation.
The [window may also be rotated](#rotation) independently. The [window may also be rotated](#rotation) independently.
#### Codec #### Encoder
The video codec can be selected. The possible values are `h264` (default), Some devices have more than one encoder, and some of them may cause issues or
`h265` and `av1`: crash. It is possible to select a different encoder:
```bash
scrcpy --codec=h264 # default
scrcpy --codec=h265
scrcpy --codec=av1
```
##### Encoder
Some devices have more than one encoder for a specific codec, and some of them
may cause issues or crash. It is possible to select a different encoder:
```bash ```bash
scrcpy --encoder=OMX.qcom.video.encoder.avc scrcpy --encoder=OMX.qcom.video.encoder.avc
@@ -277,8 +271,7 @@ To list the available encoders, you can pass an invalid encoder name; the
error will give the available encoders: error will give the available encoders:
```bash ```bash
scrcpy --encoder=_ # for the default codec scrcpy --encoder=_
scrcpy --codec=h265 --encoder=_ # for a specific codec
``` ```
### Capture ### Capture

View File

@@ -21,7 +21,6 @@ src = [
'src/mouse_inject.c', 'src/mouse_inject.c',
'src/opengl.c', 'src/opengl.c',
'src/options.c', 'src/options.c',
'src/packet_merger.c',
'src/receiver.c', 'src/receiver.c',
'src/recorder.c', 'src/recorder.c',
'src/scrcpy.c', 'src/scrcpy.c',
@@ -38,7 +37,6 @@ 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',
@@ -172,8 +170,6 @@ check_functions = [
'strdup', 'strdup',
'asprintf', 'asprintf',
'vasprintf', 'vasprintf',
'nrand48',
'jrand48',
] ]
foreach f : check_functions foreach f : check_functions

View File

@@ -26,11 +26,7 @@ Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are
Default is 8000000. Default is 8000000.
.TP .TP
.BI "\-\-codec " name .BI "\-\-codec\-options " key[:type]=value[,...]
Select a video codec (h264, h265 or av1).
.TP
.BI "\-\-codec\-options " key\fR[:\fItype\fR]=\fIvalue\fR[,...]
Set a list of comma-separated key:type=value options for the device encoder. Set a list of comma-separated key:type=value options for the device encoder.
The possible values for 'type' are 'int' (default), 'long', 'float' and 'string'. The possible values for 'type' are 'int' (default), 'long', 'float' and 'string'.
@@ -121,7 +117,7 @@ Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+S
This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically. This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically.
.TP .TP
\fB\-\-lock\-video\-orientation\fR[=\fIvalue\fR] .BI "\-\-lock\-video\-orientation[=value]
Lock video orientation to \fIvalue\fR. Possible values are "unlocked", "initial" (locked to the initial orientation), 0, 1, 2 and 3. Natural device orientation is 0, and each increment adds a 90 degrees rotation counterclockwise. Lock video orientation to \fIvalue\fR. Possible values are "unlocked", "initial" (locked to the initial orientation), 0, 1, 2 and 3. Natural device orientation is 0, and each increment adds a 90 degrees rotation counterclockwise.
Default is "unlocked". Default is "unlocked".
@@ -203,7 +199,7 @@ It may only work over USB.
See \fB\-\-hid\-keyboard\fR and \fB\-\-hid\-mouse\fR. See \fB\-\-hid\-keyboard\fR and \fB\-\-hid\-mouse\fR.
.TP .TP
.BI "\-p, \-\-port " port\fR[:\fIport\fR] .BI "\-p, \-\-port " port[:port]
Set the TCP port (range) used by the client to listen. Set the TCP port (range) used by the client to listen.
Default is 27183:27199. Default is 27183:27199.
@@ -264,7 +260,7 @@ Set the initial display rotation. Possibles values are 0, 1, 2 and 3. Each incre
The device serial number. Mandatory only if several devices are connected to adb. The device serial number. Mandatory only if several devices are connected to adb.
.TP .TP
.BI "\-\-shortcut\-mod " key\fR[+...]][,...] .BI "\-\-shortcut\-mod " key[+...]][,...]
Specify the modifiers to use for scrcpy shortcuts. Possible keys are "lctrl", "rctrl", "lalt", "ralt", "lsuper" and "rsuper". Specify the modifiers to use for scrcpy shortcuts. Possible keys are "lctrl", "rctrl", "lalt", "ralt", "lsuper" and "rsuper".
A shortcut can consist in several keys, separated by '+'. Several shortcuts can be specified, separated by ','. A shortcut can consist in several keys, separated by '+'. Several shortcuts can be specified, separated by ','.
@@ -274,7 +270,7 @@ For example, to use either LCtrl+LAlt or LSuper for scrcpy shortcuts, pass "lctr
Default is "lalt,lsuper" (left-Alt or left-Super). Default is "lalt,lsuper" (left-Alt or left-Super).
.TP .TP
.BI "\-\-tcpip\fR[=\fIip\fR[:\fIport\fR]] .BI "\-\-tcpip[=ip[:port]]
Configure and reconnect the device over TCP/IP. Configure and reconnect the device over TCP/IP.
If a destination address is provided, then scrcpy connects to this address before starting. The device must listen on the given TCP port (default is 5555). If a destination address is provided, then scrcpy connects to this address before starting. The device must listen on the given TCP port (default is 5555).

View File

@@ -7,6 +7,8 @@
#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);
@@ -15,11 +17,10 @@ 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, device_socket_name, port, if (!sc_adb_reverse(intr, serial, SC_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;
@@ -51,7 +52,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, device_socket_name, if (!sc_adb_reverse_remove(intr, serial, SC_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);
} }
@@ -77,13 +78,12 @@ 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, device_socket_name, if (sc_adb_forward(intr, serial, port, SC_SOCKET_NAME,
SC_ADB_NO_STDOUT)) { SC_ADB_NO_STDOUT)) {
// success // success
tunnel->local_port = port; tunnel->local_port = port;
@@ -123,14 +123,13 @@ 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, const char *device_socket_name, const char *serial, struct sc_port_range port_range,
struct sc_port_range port_range, bool force_adb_forward) { 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, if (enable_tunnel_reverse_any_port(tunnel, intr, serial, port_range)) {
device_socket_name, port_range)) {
return true; return true;
} }
@@ -140,13 +139,12 @@ 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, return enable_tunnel_forward_any_port(tunnel, intr, serial, port_range);
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 *device_socket_name) { const char *serial) {
assert(tunnel->enabled); assert(tunnel->enabled);
bool ret; bool ret;
@@ -154,7 +152,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, device_socket_name, ret = sc_adb_reverse_remove(intr, serial, SC_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, const char *device_socket_name, const char *serial, struct sc_port_range port_range,
struct sc_port_range port_range, bool force_adb_forward); 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 *device_socket_name); const char *serial);
#endif #endif

View File

@@ -57,7 +57,6 @@
#define OPT_NO_CLEANUP 1037 #define OPT_NO_CLEANUP 1037
#define OPT_PRINT_FPS 1038 #define OPT_PRINT_FPS 1038
#define OPT_NO_POWER_ON 1039 #define OPT_NO_POWER_ON 1039
#define OPT_CODEC 1040
struct sc_option { struct sc_option {
char shortopt; char shortopt;
@@ -106,12 +105,6 @@ static const struct sc_option options[] = {
"Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n" "Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
"Default is " STR(DEFAULT_BIT_RATE) ".", "Default is " STR(DEFAULT_BIT_RATE) ".",
}, },
{
.longopt_id = OPT_CODEC,
.longopt = "codec",
.argdesc = "name",
.text = "Select a video codec (h264, h265 or av1).",
},
{ {
.longopt_id = OPT_CODEC_OPTIONS, .longopt_id = OPT_CODEC_OPTIONS,
.longopt = "codec-options", .longopt = "codec-options",
@@ -1384,24 +1377,6 @@ guess_record_format(const char *filename) {
return 0; return 0;
} }
static bool
parse_codec(const char *optarg, enum sc_codec *codec) {
if (!strcmp(optarg, "h264")) {
*codec = SC_CODEC_H264;
return true;
}
if (!strcmp(optarg, "h265")) {
*codec = SC_CODEC_H265;
return true;
}
if (!strcmp(optarg, "av1")) {
*codec = SC_CODEC_AV1;
return true;
}
LOGE("Unsupported codec: %s (expected h264, h265 or av1)", optarg);
return false;
}
static bool static bool
parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
const char *optstring, const struct option *longopts) { const char *optstring, const struct option *longopts) {
@@ -1635,11 +1610,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
case OPT_PRINT_FPS: case OPT_PRINT_FPS:
opts->start_fps_counter = true; opts->start_fps_counter = true;
break; break;
case OPT_CODEC:
if (!parse_codec(optarg, &opts->codec)) {
return false;
}
break;
case OPT_OTG: case OPT_OTG:
#ifdef HAVE_USB #ifdef HAVE_USB
opts->otg = true; opts->otg = true;
@@ -1748,13 +1718,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
} }
} }
if (opts->record_format == SC_RECORD_FORMAT_MP4
&& opts->codec == SC_CODEC_AV1) {
LOGE("Could not mux AV1 stream into MP4 container "
"(record to mkv or select another video codec)");
return false;
}
if (!opts->control) { if (!opts->control) {
if (opts->turn_screen_off) { if (opts->turn_screen_off) {
LOGE("Could not request to turn screen off if control is disabled"); LOGE("Could not request to turn screen off if control is disabled");

View File

@@ -51,47 +51,3 @@ 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,12 +59,4 @@ 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

@@ -117,9 +117,8 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, unsigned char *buf) {
uint16_t pressure = uint16_t pressure =
sc_float_to_u16fp(msg->inject_touch_event.pressure); sc_float_to_u16fp(msg->inject_touch_event.pressure);
sc_write16be(&buf[22], pressure); sc_write16be(&buf[22], pressure);
sc_write32be(&buf[24], msg->inject_touch_event.action_button); sc_write32be(&buf[24], msg->inject_touch_event.buttons);
sc_write32be(&buf[28], msg->inject_touch_event.buttons); return 28;
return 32;
case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT: case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT:
write_position(&buf[1], &msg->inject_scroll_event.position); write_position(&buf[1], &msg->inject_scroll_event.position);
int16_t hscroll = int16_t hscroll =
@@ -180,25 +179,22 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
if (pointer_name) { if (pointer_name) {
// string pointer id // string pointer id
LOG_CMSG("touch [id=%s] %-4s position=%" PRIi32 ",%" PRIi32 LOG_CMSG("touch [id=%s] %-4s position=%" PRIi32 ",%" PRIi32
" pressure=%f action_button=%06lx buttons=%06lx", " pressure=%f buttons=%06lx",
pointer_name, pointer_name,
MOTIONEVENT_ACTION_LABEL(action), MOTIONEVENT_ACTION_LABEL(action),
msg->inject_touch_event.position.point.x, msg->inject_touch_event.position.point.x,
msg->inject_touch_event.position.point.y, msg->inject_touch_event.position.point.y,
msg->inject_touch_event.pressure, msg->inject_touch_event.pressure,
(long) msg->inject_touch_event.action_button,
(long) msg->inject_touch_event.buttons); (long) msg->inject_touch_event.buttons);
} else { } else {
// numeric pointer id // numeric pointer id
LOG_CMSG("touch [id=%" PRIu64_ "] %-4s position=%" PRIi32 ",%" LOG_CMSG("touch [id=%" PRIu64_ "] %-4s position=%" PRIi32 ",%"
PRIi32 " pressure=%f action_button=%06lx" PRIi32 " pressure=%f buttons=%06lx",
" buttons=%06lx",
id, id,
MOTIONEVENT_ACTION_LABEL(action), MOTIONEVENT_ACTION_LABEL(action),
msg->inject_touch_event.position.point.x, msg->inject_touch_event.position.point.x,
msg->inject_touch_event.position.point.y, msg->inject_touch_event.position.point.y,
msg->inject_touch_event.pressure, msg->inject_touch_event.pressure,
(long) msg->inject_touch_event.action_button,
(long) msg->inject_touch_event.buttons); (long) msg->inject_touch_event.buttons);
} }
break; break;

View File

@@ -65,7 +65,6 @@ struct sc_control_msg {
} inject_text; } inject_text;
struct { struct {
enum android_motionevent_action action; enum android_motionevent_action action;
enum android_motionevent_buttons action_button;
enum android_motionevent_buttons buttons; enum android_motionevent_buttons buttons;
uint64_t pointer_id; uint64_t pointer_id;
struct sc_position position; struct sc_position position;

View File

@@ -6,7 +6,6 @@
#include "decoder.h" #include "decoder.h"
#include "events.h" #include "events.h"
#include "packet_merger.h"
#include "recorder.h" #include "recorder.h"
#include "util/binary.h" #include "util/binary.h"
#include "util/log.h" #include "util/log.h"
@@ -18,36 +17,6 @@
#define SC_PACKET_PTS_MASK (SC_PACKET_FLAG_KEY_FRAME - 1) #define SC_PACKET_PTS_MASK (SC_PACKET_FLAG_KEY_FRAME - 1)
static enum AVCodecID
sc_demuxer_to_avcodec_id(uint32_t codec_id) {
#define SC_CODEC_ID_H264 UINT32_C(0x68323634) // "h264" in ASCII
#define SC_CODEC_ID_H265 UINT32_C(0x68323635) // "h265" in ASCII
#define SC_CODEC_ID_AV1 UINT32_C(0x00617631) // "av1" in ASCII
switch (codec_id) {
case SC_CODEC_ID_H264:
return AV_CODEC_ID_H264;
case SC_CODEC_ID_H265:
return AV_CODEC_ID_HEVC;
case SC_CODEC_ID_AV1:
return AV_CODEC_ID_AV1;
default:
LOGE("Unknown codec id 0x%08" PRIx32, codec_id);
return AV_CODEC_ID_NONE;
}
}
static bool
sc_demuxer_recv_codec_id(struct sc_demuxer *demuxer, uint32_t *codec_id) {
uint8_t data[4];
ssize_t r = net_recv_all(demuxer->socket, data, 4);
if (r < 4) {
return false;
}
*codec_id = sc_read32be(data);
return true;
}
static bool static bool
sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) { sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) {
// The video stream contains raw packets, without time information. When we // The video stream contains raw packets, without time information. When we
@@ -111,7 +80,7 @@ push_packet_to_sinks(struct sc_demuxer *demuxer, const AVPacket *packet) {
for (unsigned i = 0; i < demuxer->sink_count; ++i) { for (unsigned i = 0; i < demuxer->sink_count; ++i) {
struct sc_packet_sink *sink = demuxer->sinks[i]; struct sc_packet_sink *sink = demuxer->sinks[i];
if (!sink->ops->push(sink, packet)) { if (!sink->ops->push(sink, packet)) {
LOGE("Could not send packet to sink %d", i); LOGE("Could not send config packet to sink %d", i);
return false; return false;
} }
} }
@@ -121,7 +90,50 @@ push_packet_to_sinks(struct sc_demuxer *demuxer, const AVPacket *packet) {
static bool static bool
sc_demuxer_push_packet(struct sc_demuxer *demuxer, AVPacket *packet) { sc_demuxer_push_packet(struct sc_demuxer *demuxer, AVPacket *packet) {
bool is_config = packet->pts == AV_NOPTS_VALUE;
// 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;
if (av_grow_packet(demuxer->pending, packet->size)) {
LOG_OOM();
return false;
}
} else {
offset = 0;
demuxer->pending = av_packet_alloc();
if (!demuxer->pending) {
LOG_OOM();
return false;
}
if (av_new_packet(demuxer->pending, packet->size)) {
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;
demuxer->pending->dts = packet->dts;
demuxer->pending->flags = packet->flags;
packet = demuxer->pending;
}
}
bool ok = push_packet_to_sinks(demuxer, packet); bool ok = push_packet_to_sinks(demuxer, packet);
if (!is_config && demuxer->pending) {
// the pending packet must be discarded (consumed or error)
av_packet_free(&demuxer->pending);
}
if (!ok) { if (!ok) {
LOGE("Could not process packet"); LOGE("Could not process packet");
return false; return false;
@@ -161,53 +173,43 @@ static int
run_demuxer(void *data) { run_demuxer(void *data) {
struct sc_demuxer *demuxer = data; struct sc_demuxer *demuxer = data;
// Flag to report end-of-stream (i.e. device disconnected) const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
bool eos = false;
uint32_t raw_codec_id;
bool ok = sc_demuxer_recv_codec_id(demuxer, &raw_codec_id);
if (!ok) {
eos = true;
goto end;
}
enum AVCodecID codec_id = sc_demuxer_to_avcodec_id(raw_codec_id);
if (codec_id == AV_CODEC_ID_NONE) {
// Error already logged
goto end;
}
const AVCodec *codec = avcodec_find_decoder(codec_id);
if (!codec) { if (!codec) {
LOGE("H.264 decoder not found"); LOGE("H.264 decoder not found");
goto end; goto end;
} }
if (!sc_demuxer_open_sinks(demuxer, codec)) { demuxer->codec_ctx = avcodec_alloc_context3(codec);
LOGE("Could not open demuxer sinks"); if (!demuxer->codec_ctx) {
LOG_OOM();
goto end; goto end;
} }
struct sc_packet_merger merger; if (!sc_demuxer_open_sinks(demuxer, codec)) {
sc_packet_merger_init(&merger); LOGE("Could not open demuxer sinks");
goto finally_free_codec_ctx;
}
demuxer->parser = av_parser_init(AV_CODEC_ID_H264);
if (!demuxer->parser) {
LOGE("Could not initialize parser");
goto finally_close_sinks;
}
// We must only pass complete frames to av_parser_parse2()!
// It's more complicated, but this allows to reduce the latency by 1 frame!
demuxer->parser->flags |= PARSER_FLAG_COMPLETE_FRAMES;
AVPacket *packet = av_packet_alloc(); AVPacket *packet = av_packet_alloc();
if (!packet) { if (!packet) {
LOG_OOM(); LOG_OOM();
goto finally_close_sinks; goto finally_close_parser;
} }
for (;;) { for (;;) {
bool ok = sc_demuxer_recv_packet(demuxer, packet); bool ok = sc_demuxer_recv_packet(demuxer, packet);
if (!ok) { if (!ok) {
// end of stream // end of stream
eos = true;
break;
}
// Prepend any config packet to the next media packet
ok = sc_packet_merger_merge(&merger, packet);
if (!ok) {
break; break;
} }
@@ -221,13 +223,19 @@ run_demuxer(void *data) {
LOGD("End of frames"); LOGD("End of frames");
sc_packet_merger_destroy(&merger); if (demuxer->pending) {
av_packet_free(&demuxer->pending);
}
av_packet_free(&packet); av_packet_free(&packet);
finally_close_parser:
av_parser_close(demuxer->parser);
finally_close_sinks: finally_close_sinks:
sc_demuxer_close_sinks(demuxer); sc_demuxer_close_sinks(demuxer);
finally_free_codec_ctx:
avcodec_free_context(&demuxer->codec_ctx);
end: end:
demuxer->cbs->on_ended(demuxer, eos, demuxer->cbs_userdata); demuxer->cbs->on_eos(demuxer, demuxer->cbs_userdata);
return 0; return 0;
} }
@@ -236,9 +244,10 @@ void
sc_demuxer_init(struct sc_demuxer *demuxer, sc_socket socket, sc_demuxer_init(struct sc_demuxer *demuxer, sc_socket socket,
const struct sc_demuxer_callbacks *cbs, void *cbs_userdata) { const struct sc_demuxer_callbacks *cbs, void *cbs_userdata) {
demuxer->socket = socket; demuxer->socket = socket;
demuxer->pending = NULL;
demuxer->sink_count = 0; demuxer->sink_count = 0;
assert(cbs && cbs->on_ended); assert(cbs && cbs->on_eos);
demuxer->cbs = cbs; demuxer->cbs = cbs;
demuxer->cbs_userdata = cbs_userdata; demuxer->cbs_userdata = cbs_userdata;

View File

@@ -21,12 +21,18 @@ struct sc_demuxer {
struct sc_packet_sink *sinks[SC_DEMUXER_MAX_SINKS]; struct sc_packet_sink *sinks[SC_DEMUXER_MAX_SINKS];
unsigned sink_count; unsigned sink_count;
AVCodecContext *codec_ctx;
AVCodecParserContext *parser;
// successive packets may need to be concatenated, until a non-config
// packet is available
AVPacket *pending;
const struct sc_demuxer_callbacks *cbs; const struct sc_demuxer_callbacks *cbs;
void *cbs_userdata; void *cbs_userdata;
}; };
struct sc_demuxer_callbacks { struct sc_demuxer_callbacks {
void (*on_ended)(struct sc_demuxer *demuxer, bool eos, void *userdata); void (*on_eos)(struct sc_demuxer *demuxer, void *userdata);
}; };
void void

View File

@@ -1,6 +1,5 @@
#define SC_EVENT_NEW_FRAME SDL_USEREVENT #define EVENT_NEW_FRAME SDL_USEREVENT
#define SC_EVENT_DEVICE_DISCONNECTED (SDL_USEREVENT + 1) #define EVENT_STREAM_STOPPED (SDL_USEREVENT + 1)
#define SC_EVENT_SERVER_CONNECTION_FAILED (SDL_USEREVENT + 2) #define EVENT_SERVER_CONNECTION_FAILED (SDL_USEREVENT + 2)
#define SC_EVENT_SERVER_CONNECTED (SDL_USEREVENT + 3) #define EVENT_SERVER_CONNECTED (SDL_USEREVENT + 3)
#define SC_EVENT_USB_DEVICE_DISCONNECTED (SDL_USEREVENT + 4) #define EVENT_USB_DEVICE_DISCONNECTED (SDL_USEREVENT + 4)
#define SC_EVENT_DEMUXER_ERROR (SDL_USEREVENT + 5)

View File

@@ -339,7 +339,6 @@ simulate_virtual_finger(struct sc_input_manager *im,
im->forward_all_clicks ? POINTER_ID_VIRTUAL_MOUSE im->forward_all_clicks ? POINTER_ID_VIRTUAL_MOUSE
: POINTER_ID_VIRTUAL_FINGER; : POINTER_ID_VIRTUAL_FINGER;
msg.inject_touch_event.pressure = up ? 0.0f : 1.0f; msg.inject_touch_event.pressure = up ? 0.0f : 1.0f;
msg.inject_touch_event.action_button = 0;
msg.inject_touch_event.buttons = 0; msg.inject_touch_event.buttons = 0;
if (!sc_controller_push_msg(im->controller, &msg)) { if (!sc_controller_push_msg(im->controller, &msg)) {

View File

@@ -93,7 +93,6 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp,
.pointer_id = event->pointer_id, .pointer_id = event->pointer_id,
.position = event->position, .position = event->position,
.pressure = event->action == SC_ACTION_DOWN ? 1.f : 0.f, .pressure = event->action == SC_ACTION_DOWN ? 1.f : 0.f,
.action_button = convert_mouse_buttons(event->button),
.buttons = convert_mouse_buttons(event->buttons_state), .buttons = convert_mouse_buttons(event->buttons_state),
}, },
}; };

View File

@@ -13,7 +13,6 @@ const struct scrcpy_options scrcpy_options_default = {
.v4l2_device = NULL, .v4l2_device = NULL,
#endif #endif
.log_level = SC_LOG_LEVEL_INFO, .log_level = SC_LOG_LEVEL_INFO,
.codec = SC_CODEC_H264,
.record_format = SC_RECORD_FORMAT_AUTO, .record_format = SC_RECORD_FORMAT_AUTO,
.keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT, .keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT,
.port_range = { .port_range = {

View File

@@ -23,12 +23,6 @@ enum sc_record_format {
SC_RECORD_FORMAT_MKV, SC_RECORD_FORMAT_MKV,
}; };
enum sc_codec {
SC_CODEC_H264,
SC_CODEC_H265,
SC_CODEC_AV1,
};
enum sc_lock_video_orientation { enum sc_lock_video_orientation {
SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1, SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1,
// lock the current orientation when scrcpy starts // lock the current orientation when scrcpy starts
@@ -99,7 +93,6 @@ struct scrcpy_options {
const char *v4l2_device; const char *v4l2_device;
#endif #endif
enum sc_log_level log_level; enum sc_log_level log_level;
enum sc_codec codec;
enum sc_record_format record_format; enum sc_record_format record_format;
enum sc_keyboard_input_mode keyboard_input_mode; enum sc_keyboard_input_mode keyboard_input_mode;
enum sc_mouse_input_mode mouse_input_mode; enum sc_mouse_input_mode mouse_input_mode;

View File

@@ -1,52 +0,0 @@
#include "packet_merger.h"
#include "util/log.h"
void
sc_packet_merger_init(struct sc_packet_merger *merger) {
merger->pending_config = NULL;
}
void
sc_packet_merger_destroy(struct sc_packet_merger *merger) {
av_packet_free(&merger->pending_config);
}
bool
sc_packet_merger_merge(struct sc_packet_merger *merger, AVPacket *packet) {
bool is_config = packet->pts == AV_NOPTS_VALUE;
if (is_config) {
av_packet_free(&merger->pending_config);
merger->pending_config = av_packet_alloc();
if (!merger->pending_config) {
LOG_OOM();
goto error;
}
if (av_packet_ref(merger->pending_config, packet)) {
LOG_OOM();
av_packet_free(&merger->pending_config);
goto error;
}
} else if (merger->pending_config) {
size_t config_size = merger->pending_config->size;
size_t media_size = packet->size;
if (av_grow_packet(packet, config_size)) {
LOG_OOM();
goto error;
}
memmove(packet->data + config_size, packet->data, media_size);
memcpy(packet->data, merger->pending_config->data, config_size);
av_packet_free(&merger->pending_config);
}
return true;
error:
av_packet_unref(packet);
return false;
}

View File

@@ -1,41 +0,0 @@
#ifndef SC_PACKET_MERGER_H
#define SC_PACKET_MERGER_H
#include "common.h"
#include <stdbool.h>
#include <libavcodec/avcodec.h>
/**
* Config packets (containing the SPS/PPS) are sent in-band. A new config
* packet is sent whenever a new encoding session is started (on start and on
* device orientation change).
*
* Every time a config packet is received, it must be sent alone (for recorder
* extradata), then concatenated to the next media packet (for correct decoding
* and recording).
*
* This helper reads every input packet and modifies each media packet which
* immediately follows a config packet to prepend the config packet payload.
*/
struct sc_packet_merger {
AVPacket *pending_config;
};
void
sc_packet_merger_init(struct sc_packet_merger *merger);
void
sc_packet_merger_destroy(struct sc_packet_merger *merger);
/**
* If the packet is a config packet, then reference it for later.
* Otherwise (if the packet is a media packet), then if a config packet is
* pending, prepend the config packet to this packet (so the packet is
* modified!).
*/
bool
sc_packet_merger_merge(struct sc_packet_merger *merger, AVPacket *packet);
#endif

View File

@@ -11,8 +11,6 @@
/** Downcast packet_sink to recorder */ /** Downcast packet_sink to recorder */
#define DOWNCAST(SINK) container_of(SINK, struct sc_recorder, packet_sink) #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 AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us
static const AVOutputFormat * static const AVOutputFormat *
@@ -171,18 +169,6 @@ run_recorder(void *data) {
sc_mutex_unlock(&recorder->mutex); 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 // recorder->previous is only written from this thread, no need to lock
struct sc_record_packet *previous = recorder->previous; struct sc_record_packet *previous = recorder->previous;
recorder->previous = rec; recorder->previous = rec;
@@ -257,7 +243,6 @@ sc_recorder_open(struct sc_recorder *recorder, const AVCodec *input_codec) {
recorder->failed = false; recorder->failed = false;
recorder->header_written = false; recorder->header_written = false;
recorder->previous = NULL; recorder->previous = NULL;
recorder->pts_origin = SC_PTS_ORIGIN_NONE;
const char *format_name = sc_recorder_get_format_name(recorder->format); const char *format_name = sc_recorder_get_format_name(recorder->format);
assert(format_name); assert(format_name);

View File

@@ -28,8 +28,6 @@ struct sc_recorder {
struct sc_size declared_frame_size; struct sc_size declared_frame_size;
bool header_written; bool header_written;
uint64_t pts_origin;
sc_thread thread; sc_thread thread;
sc_mutex mutex; sc_mutex mutex;
sc_cond queue_cond; sc_cond queue_cond;

View File

@@ -32,7 +32,6 @@
#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
@@ -155,12 +154,9 @@ event_loop(struct scrcpy *s) {
SDL_Event event; SDL_Event event;
while (SDL_WaitEvent(&event)) { while (SDL_WaitEvent(&event)) {
switch (event.type) { switch (event.type) {
case SC_EVENT_DEVICE_DISCONNECTED: case EVENT_STREAM_STOPPED:
LOGW("Device disconnected"); LOGW("Device disconnected");
return SCRCPY_EXIT_DISCONNECTED; return SCRCPY_EXIT_DISCONNECTED;
case SC_EVENT_DEMUXER_ERROR:
LOGE("Demuxer error");
return SCRCPY_EXIT_FAILURE;
case SDL_QUIT: case SDL_QUIT:
LOGD("User requested to quit"); LOGD("User requested to quit");
return SCRCPY_EXIT_SUCCESS; return SCRCPY_EXIT_SUCCESS;
@@ -182,10 +178,10 @@ await_for_server(bool *connected) {
LOGD("User requested to quit"); LOGD("User requested to quit");
*connected = false; *connected = false;
return true; return true;
case SC_EVENT_SERVER_CONNECTION_FAILED: case EVENT_SERVER_CONNECTION_FAILED:
LOGE("Server connection failed"); LOGE("Server connection failed");
return false; return false;
case SC_EVENT_SERVER_CONNECTED: case EVENT_SERVER_CONNECTED:
LOGD("Server connected"); LOGD("Server connected");
*connected = true; *connected = true;
return true; return true;
@@ -236,15 +232,11 @@ av_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
} }
static void static void
sc_demuxer_on_ended(struct sc_demuxer *demuxer, bool eos, void *userdata) { sc_demuxer_on_eos(struct sc_demuxer *demuxer, void *userdata) {
(void) demuxer; (void) demuxer;
(void) userdata; (void) userdata;
if (eos) { PUSH_EVENT(EVENT_STREAM_STOPPED);
PUSH_EVENT(SC_EVENT_DEVICE_DISCONNECTED);
} else {
PUSH_EVENT(SC_EVENT_DEMUXER_ERROR);
}
} }
static void static void
@@ -252,7 +244,7 @@ sc_server_on_connection_failed(struct sc_server *server, void *userdata) {
(void) server; (void) server;
(void) userdata; (void) userdata;
PUSH_EVENT(SC_EVENT_SERVER_CONNECTION_FAILED); PUSH_EVENT(EVENT_SERVER_CONNECTION_FAILED);
} }
static void static void
@@ -260,7 +252,7 @@ sc_server_on_connected(struct sc_server *server, void *userdata) {
(void) server; (void) server;
(void) userdata; (void) userdata;
PUSH_EVENT(SC_EVENT_SERVER_CONNECTED); PUSH_EVENT(EVENT_SERVER_CONNECTED);
} }
static void static void
@@ -273,14 +265,6 @@ 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;
@@ -314,15 +298,11 @@ 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,
.log_level = options->log_level, .log_level = options->log_level,
.codec = options->codec,
.crop = options->crop, .crop = options->crop,
.port_range = options->port_range, .port_range = options->port_range,
.tunnel_host = options->tunnel_host, .tunnel_host = options->tunnel_host,
@@ -428,7 +408,7 @@ scrcpy(struct scrcpy_options *options) {
av_log_set_callback(av_log_callback); av_log_set_callback(av_log_callback);
static const struct sc_demuxer_callbacks demuxer_cbs = { static const struct sc_demuxer_callbacks demuxer_cbs = {
.on_ended = sc_demuxer_on_ended, .on_eos = sc_demuxer_on_eos,
}; };
sc_demuxer_init(&s->demuxer, s->server.video_socket, &demuxer_cbs, NULL); sc_demuxer_init(&s->demuxer, s->server.video_socket, &demuxer_cbs, NULL);

View File

@@ -306,13 +306,14 @@ sc_screen_render(struct sc_screen *screen, bool update_content_rect) {
} }
#if defined(__APPLE__) || defined(__WINDOWS__) #if defined(__APPLE__)
# define CONTINUOUS_RESIZING_WORKAROUND # define CONTINUOUS_RESIZING_WORKAROUND
#endif #endif
#ifdef CONTINUOUS_RESIZING_WORKAROUND #ifdef CONTINUOUS_RESIZING_WORKAROUND
// On Windows and MacOS, resizing blocks the event loop, so resizing events are // On Windows and MacOS, resizing blocks the event loop, so resizing events are
// not triggered. As a workaround, handle them in an event handler. // not triggered. On MacOS, as a workaround, handle them in an event handler
// (it does not work for Windows unfortunately).
// //
// <https://bugzilla.libsdl.org/show_bug.cgi?id=2077> // <https://bugzilla.libsdl.org/show_bug.cgi?id=2077>
// <https://stackoverflow.com/a/40693139/1987178> // <https://stackoverflow.com/a/40693139/1987178>
@@ -371,7 +372,7 @@ sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped,
bool need_new_event; bool need_new_event;
if (previous_skipped) { if (previous_skipped) {
sc_fps_counter_add_skipped_frame(&screen->fps_counter); sc_fps_counter_add_skipped_frame(&screen->fps_counter);
// The SC_EVENT_NEW_FRAME triggered for the previous frame will consume // The EVENT_NEW_FRAME triggered for the previous frame will consume
// this new frame instead, unless the previous event failed // this new frame instead, unless the previous event failed
need_new_event = screen->event_failed; need_new_event = screen->event_failed;
} else { } else {
@@ -380,7 +381,7 @@ sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped,
if (need_new_event) { if (need_new_event) {
static SDL_Event new_frame_event = { static SDL_Event new_frame_event = {
.type = SC_EVENT_NEW_FRAME, .type = EVENT_NEW_FRAME,
}; };
// Post the event on the UI thread // Post the event on the UI thread
@@ -820,7 +821,7 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) {
bool relative_mode = sc_screen_is_relative_mode(screen); bool relative_mode = sc_screen_is_relative_mode(screen);
switch (event->type) { switch (event->type) {
case SC_EVENT_NEW_FRAME: { case EVENT_NEW_FRAME: {
bool ok = sc_screen_update_frame(screen); bool ok = sc_screen_update_frame(screen);
if (!ok) { if (!ok) {
LOGW("Frame update failed\n"); LOGW("Frame update failed\n");

View File

@@ -8,7 +8,6 @@
#include <SDL2/SDL_platform.h> #include <SDL2/SDL_platform.h>
#include "adb/adb.h" #include "adb/adb.h"
#include "util/binary.h"
#include "util/file.h" #include "util/file.h"
#include "util/log.h" #include "util/log.h"
#include "util/net_intr.h" #include "util/net_intr.h"
@@ -21,7 +20,6 @@
#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) {
@@ -156,20 +154,6 @@ sc_server_sleep(struct sc_server *server, sc_tick deadline) {
return !stopped; return !stopped;
} }
static const char *
sc_server_get_codec_name(enum sc_codec codec) {
switch (codec) {
case SC_CODEC_H264:
return "h264";
case SC_CODEC_H265:
return "h265";
case SC_CODEC_AV1:
return "av1";
default:
return NULL;
}
}
static sc_pid static sc_pid
execute_server(struct sc_server *server, execute_server(struct sc_server *server,
const struct sc_server_params *params) { const struct sc_server_params *params) {
@@ -213,13 +197,9 @@ 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);
if (params->codec != SC_CODEC_H264) {
ADD_PARAM("codec=%s", sc_server_get_codec_name(params->codec));
}
if (params->max_size) { if (params->max_size) {
ADD_PARAM("max_size=%" PRIu16, params->max_size); ADD_PARAM("max_size=%" PRIu16, params->max_size);
} }
@@ -384,7 +364,6 @@ 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;
@@ -416,9 +395,10 @@ device_read_info(struct sc_intr *intr, sc_socket device_socket,
buf[SC_DEVICE_NAME_FIELD_LENGTH - 1] = '\0'; buf[SC_DEVICE_NAME_FIELD_LENGTH - 1] = '\0';
memcpy(info->device_name, (char *) buf, sizeof(info->device_name)); memcpy(info->device_name, (char *) buf, sizeof(info->device_name));
unsigned char *fields = &buf[SC_DEVICE_NAME_FIELD_LENGTH]; info->frame_size.width = (buf[SC_DEVICE_NAME_FIELD_LENGTH] << 8)
info->frame_size.width = sc_read16be(fields); | buf[SC_DEVICE_NAME_FIELD_LENGTH + 1];
info->frame_size.height = sc_read16be(&fields[2]); info->frame_size.height = (buf[SC_DEVICE_NAME_FIELD_LENGTH + 2] << 8)
| buf[SC_DEVICE_NAME_FIELD_LENGTH + 3];
return true; return true;
} }
@@ -483,8 +463,7 @@ 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);
@@ -515,8 +494,7 @@ 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;
@@ -786,23 +764,13 @@ 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,
server->device_socket_name, params->port_range, params->port_range, params->force_adb_forward);
params->force_adb_forward);
if (!ok) { if (!ok) {
goto error_connection_failed; goto error_connection_failed;
} }
@@ -810,8 +778,7 @@ 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;
} }
@@ -823,8 +790,7 @@ 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;
} }
@@ -918,7 +884,6 @@ 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,10 +22,8 @@ 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;
enum sc_codec codec;
const char *crop; const char *crop;
const char *codec_options; const char *codec_options;
const char *encoder_name; const char *encoder_name;
@@ -56,7 +54,6 @@ 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

View File

@@ -22,7 +22,7 @@ sc_usb_on_disconnected(struct sc_usb *usb, void *userdata) {
(void) userdata; (void) userdata;
SDL_Event event; SDL_Event event;
event.type = SC_EVENT_USB_DEVICE_DISCONNECTED; event.type = EVENT_USB_DEVICE_DISCONNECTED;
int ret = SDL_PushEvent(&event); int ret = SDL_PushEvent(&event);
if (ret < 0) { if (ret < 0) {
LOGE("Could not post USB disconnection event: %s", SDL_GetError()); LOGE("Could not post USB disconnection event: %s", SDL_GetError());
@@ -34,7 +34,7 @@ event_loop(struct scrcpy_otg *s) {
SDL_Event event; SDL_Event event;
while (SDL_WaitEvent(&event)) { while (SDL_WaitEvent(&event)) {
switch (event.type) { switch (event.type) {
case SC_EVENT_USB_DEVICE_DISCONNECTED: case EVENT_USB_DEVICE_DISCONNECTED:
LOGW("Device disconnected"); LOGW("Device disconnected");
return SCRCPY_EXIT_DISCONNECTED; return SCRCPY_EXIT_DISCONNECTED;
case SDL_QUIT: case SDL_QUIT:

View File

@@ -1,24 +0,0 @@
#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;
}

View File

@@ -1,16 +0,0 @@
#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

@@ -90,14 +90,13 @@ static void test_serialize_inject_touch_event(void) {
}, },
}, },
.pressure = 1.0f, .pressure = 1.0f,
.action_button = AMOTION_EVENT_BUTTON_PRIMARY,
.buttons = AMOTION_EVENT_BUTTON_PRIMARY, .buttons = AMOTION_EVENT_BUTTON_PRIMARY,
}, },
}; };
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; unsigned char buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf); size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 32); assert(size == 28);
const unsigned char expected[] = { const unsigned char expected[] = {
SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT,
@@ -106,8 +105,7 @@ static void test_serialize_inject_touch_event(void) {
0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0xc8, // 100 200 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0xc8, // 100 200
0x04, 0x38, 0x07, 0x80, // 1080 1920 0x04, 0x38, 0x07, 0x80, // 1080 1920
0xff, 0xff, // pressure 0xff, 0xff, // pressure
0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY (action button) 0x00, 0x00, 0x00, 0x01 // AMOTION_EVENT_BUTTON_PRIMARY
0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY (buttons)
}; };
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
} }

View File

@@ -12,7 +12,7 @@ echo "$PREBUILT_SERVER_SHA256 scrcpy-server" | sha256sum --check
echo "[scrcpy] Building client..." echo "[scrcpy] Building client..."
rm -rf "$BUILDDIR" rm -rf "$BUILDDIR"
meson setup "$BUILDDIR" --buildtype=release --strip -Db_lto=true \ meson "$BUILDDIR" --buildtype=release --strip -Db_lto=true \
-Dprebuilt_server=scrcpy-server -Dprebuilt_server=scrcpy-server
cd "$BUILDDIR" cd "$BUILDDIR"
ninja ninja

View File

@@ -20,21 +20,18 @@ 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" "$GEN_DIR" "$BUILD_DIR/$SERVER_BINARY" classes.dex rm -rf "$CLASSES_DIR" "$BUILD_DIR/$SERVER_BINARY" classes.dex
mkdir -p "$CLASSES_DIR" mkdir -p "$CLASSES_DIR/com/genymobile/scrcpy"
mkdir -p "$GEN_DIR/com/genymobile/scrcpy"
<< EOF cat > "$GEN_DIR/com/genymobile/scrcpy/BuildConfig.java" << EOF cat > "$CLASSES_DIR/com/genymobile/scrcpy/BuildConfig.java"
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
public final class BuildConfig { public final class BuildConfig {
@@ -45,15 +42,13 @@ 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"$GEN_DIR" android/view/IRotationWatcher.aidl "$BUILD_TOOLS_DIR/aidl" -o"$CLASSES_DIR" android/view/IRotationWatcher.aidl
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" \ "$BUILD_TOOLS_DIR/aidl" -o"$CLASSES_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" \ javac -bootclasspath "$ANDROID_JAR" -cp "$CLASSES_DIR" -d "$CLASSES_DIR" \
-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
@@ -73,7 +68,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 rm -rf classes.dex classes
else else
# use d8 # use d8
"$BUILD_TOOLS_DIR/d8" --classpath "$ANDROID_JAR" \ "$BUILD_TOOLS_DIR/d8" --classpath "$ANDROID_JAR" \
@@ -85,8 +80,7 @@ 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 final String key; private String key;
private final Object value; private Object value;
public CodecOption(String key, Object value) { public CodecOption(String key, Object value) {
this.key = key; this.key = key;

View File

@@ -29,7 +29,6 @@ public final class ControlMessage {
private int metaState; // KeyEvent.META_* private int metaState; // KeyEvent.META_*
private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or POWER_MODE_* private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or POWER_MODE_*
private int keycode; // KeyEvent.KEYCODE_* private int keycode; // KeyEvent.KEYCODE_*
private int actionButton; // MotionEvent.BUTTON_*
private int buttons; // MotionEvent.BUTTON_* private int buttons; // MotionEvent.BUTTON_*
private long pointerId; private long pointerId;
private float pressure; private float pressure;
@@ -61,15 +60,13 @@ public final class ControlMessage {
return msg; return msg;
} }
public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure, int actionButton, public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure, int buttons) {
int buttons) {
ControlMessage msg = new ControlMessage(); ControlMessage msg = new ControlMessage();
msg.type = TYPE_INJECT_TOUCH_EVENT; msg.type = TYPE_INJECT_TOUCH_EVENT;
msg.action = action; msg.action = action;
msg.pointerId = pointerId; msg.pointerId = pointerId;
msg.pressure = pressure; msg.pressure = pressure;
msg.position = position; msg.position = position;
msg.actionButton = actionButton;
msg.buttons = buttons; msg.buttons = buttons;
return msg; return msg;
} }
@@ -143,10 +140,6 @@ public final class ControlMessage {
return keycode; return keycode;
} }
public int getActionButton() {
return actionButton;
}
public int getButtons() { public int getButtons() {
return buttons; return buttons;
} }

View File

@@ -9,7 +9,7 @@ import java.nio.charset.StandardCharsets;
public class ControlMessageReader { public class ControlMessageReader {
static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 13; static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 13;
static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 31; static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27;
static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20;
static final int BACK_OR_SCREEN_ON_LENGTH = 1; static final int BACK_OR_SCREEN_ON_LENGTH = 1;
static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1;
@@ -140,9 +140,8 @@ public class ControlMessageReader {
long pointerId = buffer.getLong(); long pointerId = buffer.getLong();
Position position = readPosition(buffer); Position position = readPosition(buffer);
float pressure = Binary.u16FixedPointToFloat(buffer.getShort()); float pressure = Binary.u16FixedPointToFloat(buffer.getShort());
int actionButton = buffer.getInt();
int buttons = buffer.getInt(); int buttons = buffer.getInt();
return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, actionButton, buttons); return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, buttons);
} }
private ControlMessage parseInjectScrollEvent() { private ControlMessage parseInjectScrollEvent() {

View File

@@ -1,7 +1,5 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.InputManager;
import android.os.Build; import android.os.Build;
import android.os.SystemClock; import android.os.SystemClock;
import android.view.InputDevice; import android.view.InputDevice;
@@ -24,8 +22,6 @@ public class Controller {
private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor(); private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor();
private Thread thread;
private final Device device; private final Device device;
private final DesktopConnection connection; private final DesktopConnection connection;
private final DeviceMessageSender sender; private final DeviceMessageSender sender;
@@ -64,7 +60,7 @@ public class Controller {
} }
} }
private void control() throws IOException { public void control() throws IOException {
// on start, power on the device // on start, power on the device
if (powerOn && !Device.isScreenOn()) { if (powerOn && !Device.isScreenOn()) {
device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC); device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC);
@@ -79,39 +75,11 @@ public class Controller {
SystemClock.sleep(500); SystemClock.sleep(500);
} }
while (!Thread.currentThread().isInterrupted()) { while (true) {
handleEvent(); handleEvent();
} }
} }
public void start() {
thread = new Thread(() -> {
try {
control();
} catch (IOException e) {
// this is expected on close
} finally {
Ln.d("Controller stopped");
}
});
thread.start();
sender.start();
}
public void stop() {
if (thread != null) {
thread.interrupt();
}
sender.stop();
}
public void join() throws InterruptedException {
if (thread != null) {
thread.join();
}
sender.join();
}
public DeviceMessageSender getSender() { public DeviceMessageSender getSender() {
return sender; return sender;
} }
@@ -131,7 +99,7 @@ public class Controller {
break; break;
case ControlMessage.TYPE_INJECT_TOUCH_EVENT: case ControlMessage.TYPE_INJECT_TOUCH_EVENT:
if (device.supportsInputEvents()) { if (device.supportsInputEvents()) {
injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getActionButton(), msg.getButtons()); injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons());
} }
break; break;
case ControlMessage.TYPE_INJECT_SCROLL_EVENT: case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
@@ -211,7 +179,7 @@ public class Controller {
return successCount; return successCount;
} }
private boolean injectTouch(int action, long pointerId, Position position, float pressure, int actionButton, int buttons) { private boolean injectTouch(int action, long pointerId, Position position, float pressure, int buttons) {
long now = SystemClock.uptimeMillis(); long now = SystemClock.uptimeMillis();
Point point = device.getPhysicalPoint(position); Point point = device.getPhysicalPoint(position);
@@ -228,23 +196,22 @@ public class Controller {
Pointer pointer = pointersState.get(pointerIndex); Pointer pointer = pointersState.get(pointerIndex);
pointer.setPoint(point); pointer.setPoint(point);
pointer.setPressure(pressure); pointer.setPressure(pressure);
pointer.setUp(action == MotionEvent.ACTION_UP);
int source; int source;
int pointerCount = pointersState.update(pointerProperties, pointerCoords);
if (pointerId == POINTER_ID_MOUSE || pointerId == POINTER_ID_VIRTUAL_MOUSE) { if (pointerId == POINTER_ID_MOUSE || pointerId == POINTER_ID_VIRTUAL_MOUSE) {
// real mouse event (forced by the client when --forward-on-click) // real mouse event (forced by the client when --forward-on-click)
pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_MOUSE; pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_MOUSE;
source = InputDevice.SOURCE_MOUSE; source = InputDevice.SOURCE_MOUSE;
pointer.setUp(buttons == 0);
} else { } else {
// POINTER_ID_GENERIC_FINGER, POINTER_ID_VIRTUAL_FINGER or real touch from device // POINTER_ID_GENERIC_FINGER, POINTER_ID_VIRTUAL_FINGER or real touch from device
pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_FINGER; pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_FINGER;
source = InputDevice.SOURCE_TOUCHSCREEN; source = InputDevice.SOURCE_TOUCHSCREEN;
// Buttons must not be set for touch events // Buttons must not be set for touch events
buttons = 0; buttons = 0;
pointer.setUp(action == MotionEvent.ACTION_UP);
} }
int pointerCount = pointersState.update(pointerProperties, pointerCoords);
if (pointerCount == 1) { if (pointerCount == 1) {
if (action == MotionEvent.ACTION_DOWN) { if (action == MotionEvent.ACTION_DOWN) {
lastTouchDown = now; lastTouchDown = now;
@@ -258,62 +225,6 @@ 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 MotionEvent event = MotionEvent
.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, .obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source,
0); 0);
@@ -347,9 +258,12 @@ 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(() -> { EXECUTOR.schedule(new Runnable() {
Ln.i("Forcing screen off"); @Override
Device.setScreenPowerMode(Device.POWER_MODE_OFF); public void run() {
Ln.i("Forcing screen 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_PREFIX = "scrcpy"; private static final String SOCKET_NAME = "scrcpy";
private final LocalSocket videoSocket; private final LocalSocket videoSocket;
private final FileDescriptor videoFd; private final FileDescriptor videoFd;
@@ -46,22 +46,12 @@ public final class DesktopConnection implements Closeable {
return localSocket; return localSocket;
} }
private static String getSocketName(int uid) { public static DesktopConnection open(boolean tunnelForward, boolean control, boolean sendDummyByte) throws IOException {
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) {
try (LocalServerSocket localServerSocket = new LocalServerSocket(socketName)) { LocalServerSocket localServerSocket = new LocalServerSocket(SOCKET_NAME);
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
@@ -75,12 +65,14 @@ public final class DesktopConnection implements Closeable {
throw e; throw e;
} }
} }
} finally {
localServerSocket.close();
} }
} else { } else {
videoSocket = connect(socketName); videoSocket = connect(SOCKET_NAME);
if (control) { if (control) {
try { try {
controlSocket = connect(socketName); controlSocket = connect(SOCKET_NAME);
} catch (IOException | RuntimeException e) { } catch (IOException | RuntimeException e) {
videoSocket.close(); videoSocket.close();
throw e; throw e;

View File

@@ -277,26 +277,6 @@ public final class Device {
* @param mode one of the {@code POWER_MODE_*} constants * @param mode one of the {@code POWER_MODE_*} constants
*/ */
public static boolean setScreenPowerMode(int mode) { public static boolean setScreenPowerMode(int mode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Change the power mode for all physical displays
long[] physicalDisplayIds = SurfaceControl.getPhysicalDisplayIds();
if (physicalDisplayIds == null) {
Ln.e("Could not get physical display ids");
return false;
}
boolean allOk = true;
for (long physicalDisplayId : physicalDisplayIds) {
IBinder binder = SurfaceControl.getPhysicalDisplayToken(physicalDisplayId);
boolean ok = SurfaceControl.setDisplayPowerMode(binder, mode);
if (!ok) {
allOk = false;
}
}
return allOk;
}
// Older Android versions, only 1 display
IBinder d = SurfaceControl.getBuiltInDisplay(); IBinder d = SurfaceControl.getBuiltInDisplay();
if (d == null) { if (d == null) {
Ln.e("Could not get built-in display"); Ln.e("Could not get built-in display");

View File

@@ -6,8 +6,6 @@ public final class DeviceMessageSender {
private final DesktopConnection connection; private final DesktopConnection connection;
private Thread thread;
private String clipboardText; private String clipboardText;
private long ack; private long ack;
@@ -26,8 +24,8 @@ public final class DeviceMessageSender {
notify(); notify();
} }
private void loop() throws IOException, InterruptedException { public void loop() throws IOException, InterruptedException {
while (!Thread.currentThread().isInterrupted()) { while (true) {
String text; String text;
long sequence; long sequence;
synchronized (this) { synchronized (this) {
@@ -51,28 +49,4 @@ public final class DeviceMessageSender {
} }
} }
} }
public void start() {
thread = new Thread(() -> {
try {
loop();
} catch (IOException | InterruptedException e) {
// this is expected on close
} finally {
Ln.d("Device message sender stopped");
}
});
thread.start();
}
public void stop() {
if (thread != null) {
thread.interrupt();
}
}
public void join() throws InterruptedException {
if (thread != null) {
thread.join();
}
}
} }

View File

@@ -5,11 +5,8 @@ import android.graphics.Rect;
import java.util.List; 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 VideoCodec codec = VideoCodec.H264;
private int bitRate = 8000000; private int bitRate = 8000000;
private int maxFps; private int maxFps;
private int lockVideoOrientation = -1; private int lockVideoOrientation = -1;
@@ -31,7 +28,6 @@ public class Options {
private boolean sendDeviceMeta = true; // send device name and size private boolean sendDeviceMeta = true; // send device name and size
private boolean sendFrameMeta = true; // send PTS so that the client may record properly private boolean sendFrameMeta = true; // send PTS so that the client may record properly
private boolean sendDummyByte = true; // write a byte on start to detect connection issues private boolean sendDummyByte = true; // write a byte on start to detect connection issues
private boolean sendCodecId = true; // write the codec ID (4 bytes) before the stream
public Ln.Level getLogLevel() { public Ln.Level getLogLevel() {
return logLevel; return logLevel;
@@ -41,14 +37,6 @@ 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;
} }
@@ -57,14 +45,6 @@ public class Options {
this.maxSize = maxSize; this.maxSize = maxSize;
} }
public VideoCodec getCodec() {
return codec;
}
public void setCodec(VideoCodec codec) {
this.codec = codec;
}
public int getBitRate() { public int getBitRate() {
return bitRate; return bitRate;
} }
@@ -216,12 +196,4 @@ public class Options {
public void setSendDummyByte(boolean sendDummyByte) { public void setSendDummyByte(boolean sendDummyByte) {
this.sendDummyByte = sendDummyByte; this.sendDummyByte = sendDummyByte;
} }
public boolean getSendCodecId() {
return sendCodecId;
}
public void setSendCodecId(boolean sendCodecId) {
this.sendCodecId = sendCodecId;
}
} }

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 final Point point; private Point point;
private final Size screenSize; private Size screenSize;
public Position(Point point, Size screenSize) { public Position(Point point, Size screenSize) {
this.point = point; this.point = point;

View File

@@ -9,9 +9,9 @@ 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.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
@@ -21,32 +21,32 @@ import java.util.concurrent.atomic.AtomicBoolean;
public class ScreenEncoder implements Device.RotationListener { public class ScreenEncoder implements Device.RotationListener {
public interface Callbacks {
void onPacket(ByteBuffer codecBuffer, MediaCodec.BufferInfo bufferInfo) throws IOException;
}
private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds
private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms
private static final String KEY_MAX_FPS_TO_ENCODER = "max-fps-to-encoder"; private static final String KEY_MAX_FPS_TO_ENCODER = "max-fps-to-encoder";
// Keep the values in descending order // Keep the values in descending order
private static final int[] MAX_SIZE_FALLBACK = {2560, 1920, 1600, 1280, 1024, 800}; 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;
private final AtomicBoolean rotationChanged = new AtomicBoolean(); private final AtomicBoolean rotationChanged = new AtomicBoolean();
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
private final String videoMimeType;
private final String encoderName; private final String encoderName;
private final List<CodecOption> codecOptions; private final List<CodecOption> codecOptions;
private final int bitRate; private final int bitRate;
private final int maxFps; private final int maxFps;
private final boolean sendFrameMeta;
private final boolean downsizeOnError; private final boolean downsizeOnError;
private long ptsOrigin;
private boolean firstFrameSent; private boolean firstFrameSent;
private int consecutiveErrors;
public ScreenEncoder(String videoMimeType, int bitRate, int maxFps, List<CodecOption> codecOptions, String encoderName, boolean downsizeOnError) { public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List<CodecOption> codecOptions, String encoderName,
this.videoMimeType = videoMimeType; boolean downsizeOnError) {
this.sendFrameMeta = sendFrameMeta;
this.bitRate = bitRate; this.bitRate = bitRate;
this.maxFps = maxFps; this.maxFps = maxFps;
this.codecOptions = codecOptions; this.codecOptions = codecOptions;
@@ -63,92 +63,75 @@ public class ScreenEncoder implements Device.RotationListener {
return rotationChanged.getAndSet(false); return rotationChanged.getAndSet(false);
} }
public void streamScreen(Device device, Callbacks callbacks) throws IOException { public void streamScreen(Device device, FileDescriptor fd) throws IOException {
MediaCodec codec = createCodec(videoMimeType, encoderName); Workarounds.prepareMainLooper();
MediaFormat format = createFormat(videoMimeType, bitRate, maxFps, codecOptions); if (Build.BRAND.equalsIgnoreCase("meizu")) {
IBinder display = createDisplay(); // <https://github.com/Genymobile/scrcpy/issues/240>
// <https://github.com/Genymobile/scrcpy/issues/2656>
Workarounds.fillAppInfo();
}
internalStreamScreen(device, fd);
}
private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException {
MediaFormat format = createFormat(bitRate, maxFps, codecOptions);
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()); // does not include the locked video orientation
format.setInteger(MediaFormat.KEY_HEIGHT, videoRect.height()); Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect();
int videoRotation = screenInfo.getVideoRotation();
int layerStack = device.getLayerStack();
setSize(format, videoRect.width(), videoRect.height());
Surface surface = null; Surface surface = null;
try { try {
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); configure(codec, format);
surface = codec.createInputSurface(); 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); setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack);
codec.start(); codec.start();
alive = encode(codec, callbacks); 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 (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 (!prepareRetry(device, screenInfo)) { if (!downsizeOnError || firstFrameSent) {
// Fail immediately
throw e; throw e;
} }
Ln.i("Retrying...");
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 {
codec.reset(); destroyDisplay(display);
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 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) { 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) {
@@ -161,30 +144,30 @@ public class ScreenEncoder implements Device.RotationListener {
return 0; return 0;
} }
private boolean encode(MediaCodec codec, Callbacks callbacks) throws IOException { private boolean encode(MediaCodec codec, FileDescriptor fd) throws IOException {
boolean eof = false; boolean eof = false;
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
while (!consumeRotationChange() && !eof) { while (!consumeRotationChange() && !eof) {
int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1); int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1);
eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
try { try {
if (consumeRotationChange()) { if (consumeRotationChange()) {
// must restart encoding with new size // must restart encoding with new size
break; break;
} }
eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
if (outputBufferId >= 0) { if (outputBufferId >= 0) {
ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId); ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId);
boolean isConfig = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0; if (sendFrameMeta) {
if (!isConfig) { writeFrameMeta(fd, bufferInfo, codecBuffer.remaining());
// If this is not a config packet, then it contains a frame
firstFrameSent = true;
consecutiveErrors = 0;
} }
callbacks.onPacket(codecBuffer, bufferInfo); IO.writeFully(fd, codecBuffer);
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) {
// If this is not a config packet, then it contains a frame
firstFrameSent = true;
}
} }
} finally { } finally {
if (outputBufferId >= 0) { if (outputBufferId >= 0) {
@@ -196,28 +179,50 @@ public class ScreenEncoder implements Device.RotationListener {
return !eof; return !eof;
} }
private static MediaCodecInfo[] listEncoders(String videoMimeType) { private void writeFrameMeta(FileDescriptor fd, MediaCodec.BufferInfo bufferInfo, int packetSize) throws IOException {
headerBuffer.clear();
long pts;
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;
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) {
pts |= PACKET_FLAG_KEY_FRAME;
}
}
headerBuffer.putLong(pts);
headerBuffer.putInt(packetSize);
headerBuffer.flip();
IO.writeFully(fd, headerBuffer);
}
private static MediaCodecInfo[] listEncoders() {
List<MediaCodecInfo> result = new ArrayList<>(); List<MediaCodecInfo> result = new ArrayList<>();
MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS); MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
for (MediaCodecInfo codecInfo : list.getCodecInfos()) { for (MediaCodecInfo codecInfo : list.getCodecInfos()) {
if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(videoMimeType)) { if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(MediaFormat.MIMETYPE_VIDEO_AVC)) {
result.add(codecInfo); result.add(codecInfo);
} }
} }
return result.toArray(new MediaCodecInfo[result.size()]); return result.toArray(new MediaCodecInfo[result.size()]);
} }
private static MediaCodec createCodec(String videoMimeType, String encoderName) throws IOException { private static MediaCodec createCodec(String encoderName) throws IOException {
if (encoderName != null) { if (encoderName != null) {
Ln.d("Creating encoder by name: '" + encoderName + "'"); Ln.d("Creating encoder by name: '" + encoderName + "'");
try { try {
return MediaCodec.createByCodecName(encoderName); return MediaCodec.createByCodecName(encoderName);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
MediaCodecInfo[] encoders = listEncoders(videoMimeType); MediaCodecInfo[] encoders = listEncoders();
throw new InvalidEncoderException(encoderName, encoders); throw new InvalidEncoderException(encoderName, encoders);
} }
} }
MediaCodec codec = MediaCodec.createEncoderByType(videoMimeType); MediaCodec codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
Ln.d("Using encoder: '" + codec.getName() + "'"); Ln.d("Using encoder: '" + codec.getName() + "'");
return codec; return codec;
} }
@@ -239,9 +244,9 @@ public class ScreenEncoder implements Device.RotationListener {
Ln.d("Codec option set: " + key + " (" + value.getClass().getSimpleName() + ") = " + value); Ln.d("Codec option set: " + key + " (" + value.getClass().getSimpleName() + ") = " + value);
} }
private static MediaFormat createFormat(String videoMimeType, int bitRate, int maxFps, List<CodecOption> codecOptions) { private static MediaFormat createFormat(int bitRate, int maxFps, List<CodecOption> codecOptions) {
MediaFormat format = new MediaFormat(); MediaFormat format = new MediaFormat();
format.setString(MediaFormat.KEY_MIME, videoMimeType); format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC);
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
// must be present to configure the encoder, but does not impact the actual frame rate, which is variable // must be present to configure the encoder, but does not impact the actual frame rate, which is variable
format.setInteger(MediaFormat.KEY_FRAME_RATE, 60); format.setInteger(MediaFormat.KEY_FRAME_RATE, 60);
@@ -273,6 +278,15 @@ 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 {
@@ -283,4 +297,8 @@ public class ScreenEncoder implements Device.RotationListener {
SurfaceControl.closeTransaction(); SurfaceControl.closeTransaction();
} }
} }
private static void destroyDisplay(IBinder display) {
SurfaceControl.destroyDisplay(display);
}
} }

View File

@@ -66,72 +66,92 @@ 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();
Workarounds.prepareMainLooper(); try (DesktopConnection connection = DesktopConnection.open(tunnelForward, control, sendDummyByte)) {
if (Build.BRAND.equalsIgnoreCase("meizu")) {
// Workarounds must be applied for Meizu phones:
// - <https://github.com/Genymobile/scrcpy/issues/240>
// - <https://github.com/Genymobile/scrcpy/issues/365>
// - <https://github.com/Genymobile/scrcpy/issues/2656>
//
// But only apply when strictly necessary, since workarounds can cause other issues:
// - <https://github.com/Genymobile/scrcpy/issues/940>
// - <https://github.com/Genymobile/scrcpy/issues/994>
Workarounds.fillAppInfo();
}
try (DesktopConnection connection = DesktopConnection.open(uid, tunnelForward, control, sendDummyByte)) {
VideoCodec codec = options.getCodec();
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());
} }
ScreenEncoder screenEncoder = new ScreenEncoder(codec.getMimeType(), options.getBitRate(), options.getMaxFps(), codecOptions, ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions,
options.getEncoderName(), options.getDownsizeOnError()); options.getEncoderName(), options.getDownsizeOnError());
Controller controller = null; Thread controllerThread = null;
Thread deviceMessageSenderThread = null;
if (control) { if (control) {
controller = new Controller(device, connection, options.getClipboardAutosync(), options.getPowerOn()); final Controller controller = new Controller(device, connection, options.getClipboardAutosync(), options.getPowerOn());
controller.start();
final Controller controllerRef = controller; // asynchronous
device.setClipboardListener(text -> controllerRef.getSender().pushClipboardText(text)); controllerThread = startController(controller);
deviceMessageSenderThread = startDeviceMessageSender(controller.getSender());
device.setClipboardListener(new Device.ClipboardListener() {
@Override
public void onClipboardTextChanged(String text) {
controller.getSender().pushClipboardText(text);
}
});
} }
try { try {
// synchronous // synchronous
VideoStreamer videoStreamer = new VideoStreamer(connection.getVideoFd(), options.getSendFrameMeta()); screenEncoder.streamScreen(device, connection.getVideoFd());
if (options.getSendCodecId()) {
videoStreamer.writeHeader(codec.getId());
}
screenEncoder.streamScreen(device, videoStreamer);
} catch (IOException e) { } catch (IOException e) {
// this is expected on close // this is expected on close
} finally {
Ln.d("Screen streaming stopped"); Ln.d("Screen streaming stopped");
} finally {
initThread.interrupt(); initThread.interrupt();
if (controller != null) { if (controllerThread != null) {
controller.stop(); controllerThread.interrupt();
} }
if (deviceMessageSenderThread != null) {
try { deviceMessageSenderThread.interrupt();
initThread.join();
if (controller != null) {
controller.join();
}
} catch (InterruptedException e) {
// ignore
} }
} }
} }
} }
private static Thread startInitThread(final Options options) { private static Thread startInitThread(final Options options) {
Thread thread = new Thread(() -> initAndCleanUp(options)); Thread thread = new Thread(new Runnable() {
@Override
public void run() {
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.start();
return thread;
}
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.start(); thread.start();
return thread; return thread;
} }
@@ -158,24 +178,10 @@ 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);
break; break;
case "codec":
VideoCodec codec = VideoCodec.findByName(value);
if (codec == null) {
throw new IllegalArgumentException("Video codec " + value + " not supported");
}
options.setCodec(codec);
break;
case "max_size": case "max_size":
int maxSize = Integer.parseInt(value) & ~7; // multiple of 8 int maxSize = Integer.parseInt(value) & ~7; // multiple of 8
options.setMaxSize(maxSize); options.setMaxSize(maxSize);
@@ -257,17 +263,12 @@ public final class Server {
boolean sendDummyByte = Boolean.parseBoolean(value); boolean sendDummyByte = Boolean.parseBoolean(value);
options.setSendDummyByte(sendDummyByte); options.setSendDummyByte(sendDummyByte);
break; break;
case "send_codec_id":
boolean sendCodecId = Boolean.parseBoolean(value);
options.setSendCodecId(sendCodecId);
break;
case "raw_video_stream": case "raw_video_stream":
boolean rawVideoStream = Boolean.parseBoolean(value); boolean rawVideoStream = Boolean.parseBoolean(value);
if (rawVideoStream) { if (rawVideoStream) {
options.setSendDeviceMeta(false); options.setSendDeviceMeta(false);
options.setSendFrameMeta(false); options.setSendFrameMeta(false);
options.setSendDummyByte(false); options.setSendDummyByte(false);
options.setSendCodecId(false);
} }
break; break;
default: default:
@@ -318,9 +319,12 @@ public final class Server {
} }
public static void main(String... args) throws Exception { public static void main(String... args) throws Exception {
Thread.setDefaultUncaughtExceptionHandler((t, e) -> { Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
Ln.e("Exception on thread " + t, e); @Override
suggestFix(e); public void uncaughtException(Thread t, Throwable e) {
Ln.e("Exception on thread " + t, e);
suggestFix(e);
}
}); });
Options options = createOptions(args); Options options = createOptions(args);

View File

@@ -1,36 +0,0 @@
package com.genymobile.scrcpy;
import android.media.MediaFormat;
public enum VideoCodec {
H264(0x68_32_36_34, "h264", MediaFormat.MIMETYPE_VIDEO_AVC),
H265(0x68_32_36_35, "h265", MediaFormat.MIMETYPE_VIDEO_HEVC),
AV1(0x00_61_76_31, "av1", MediaFormat.MIMETYPE_VIDEO_AV1);
private final int id; // 4-byte ASCII representation of the name
private final String name;
private final String mimeType;
VideoCodec(int id, String name, String mimeType) {
this.id = id;
this.name = name;
this.mimeType = mimeType;
}
public int getId() {
return id;
}
public String getMimeType() {
return mimeType;
}
public static VideoCodec findByName(String name) {
for (VideoCodec codec : values()) {
if (codec.name.equals(name)) {
return codec;
}
}
return null;
}
}

View File

@@ -1,58 +0,0 @@
package com.genymobile.scrcpy;
import android.media.MediaCodec;
import java.io.FileDescriptor;
import java.io.IOException;
import java.nio.ByteBuffer;
public final class VideoStreamer implements ScreenEncoder.Callbacks {
private static final long PACKET_FLAG_CONFIG = 1L << 63;
private static final long PACKET_FLAG_KEY_FRAME = 1L << 62;
private final FileDescriptor fd;
private final boolean sendFrameMeta;
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
public VideoStreamer(FileDescriptor fd, boolean sendFrameMeta) {
this.fd = fd;
this.sendFrameMeta = sendFrameMeta;
}
public void writeHeader(int codecId) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(4);
buffer.putInt(codecId);
buffer.flip();
IO.writeFully(fd, buffer);
}
@Override
public void onPacket(ByteBuffer codecBuffer, MediaCodec.BufferInfo bufferInfo) throws IOException {
if (sendFrameMeta) {
writeFrameMeta(fd, bufferInfo, codecBuffer.remaining());
}
IO.writeFully(fd, codecBuffer);
}
private void writeFrameMeta(FileDescriptor fd, MediaCodec.BufferInfo bufferInfo, int packetSize) throws IOException {
headerBuffer.clear();
long ptsAndFlags;
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
ptsAndFlags = PACKET_FLAG_CONFIG; // non-media data packet
} else {
ptsAndFlags = bufferInfo.presentationTimeUs;
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) {
ptsAndFlags |= PACKET_FLAG_KEY_FRAME;
}
}
headerBuffer.putLong(ptsAndFlags);
headerBuffer.putInt(packetSize);
headerBuffer.flip();
IO.writeFully(fd, headerBuffer);
}
}

View File

@@ -3,7 +3,6 @@ package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln; import com.genymobile.scrcpy.Ln;
import android.view.InputEvent; import android.view.InputEvent;
import android.view.MotionEvent;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
@@ -18,7 +17,6 @@ public final class InputManager {
private Method injectInputEventMethod; private Method injectInputEventMethod;
private static Method setDisplayIdMethod; private static Method setDisplayIdMethod;
private static Method setActionButtonMethod;
public InputManager(android.hardware.input.InputManager manager) { public InputManager(android.hardware.input.InputManager manager) {
this.manager = manager; this.manager = manager;
@@ -58,22 +56,4 @@ public final class InputManager {
return false; 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;
}
}
} }

View File

@@ -30,8 +30,6 @@ public final class SurfaceControl {
private static Method getBuiltInDisplayMethod; private static Method getBuiltInDisplayMethod;
private static Method setDisplayPowerModeMethod; private static Method setDisplayPowerModeMethod;
private static Method getPhysicalDisplayTokenMethod;
private static Method getPhysicalDisplayIdsMethod;
private SurfaceControl() { private SurfaceControl() {
// only static methods // only static methods
@@ -100,6 +98,7 @@ public final class SurfaceControl {
} }
public static IBinder getBuiltInDisplay() { public static IBinder getBuiltInDisplay() {
try { try {
Method method = getGetBuiltInDisplayMethod(); Method method = getGetBuiltInDisplayMethod();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
@@ -115,40 +114,6 @@ public final class SurfaceControl {
} }
} }
private static Method getGetPhysicalDisplayTokenMethod() throws NoSuchMethodException {
if (getPhysicalDisplayTokenMethod == null) {
getPhysicalDisplayTokenMethod = CLASS.getMethod("getPhysicalDisplayToken", long.class);
}
return getPhysicalDisplayTokenMethod;
}
public static IBinder getPhysicalDisplayToken(long physicalDisplayId) {
try {
Method method = getGetPhysicalDisplayTokenMethod();
return (IBinder) method.invoke(null, physicalDisplayId);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
return null;
}
}
private static Method getGetPhysicalDisplayIdsMethod() throws NoSuchMethodException {
if (getPhysicalDisplayIdsMethod == null) {
getPhysicalDisplayIdsMethod = CLASS.getMethod("getPhysicalDisplayIds");
}
return getPhysicalDisplayIdsMethod;
}
public static long[] getPhysicalDisplayIds() {
try {
Method method = getGetPhysicalDisplayIdsMethod();
return (long[]) method.invoke(null);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
return null;
}
}
private static Method getSetDisplayPowerModeMethod() throws NoSuchMethodException { private static Method getSetDisplayPowerModeMethod() throws NoSuchMethodException {
if (setDisplayPowerModeMethod == null) { if (setDisplayPowerModeMethod == null) {
setDisplayPowerModeMethod = CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class); setDisplayPowerModeMethod = CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class);

View File

@@ -94,8 +94,7 @@ public class ControlMessageReaderTest {
dos.writeShort(1080); dos.writeShort(1080);
dos.writeShort(1920); dos.writeShort(1920);
dos.writeShort(0xffff); // pressure dos.writeShort(0xffff); // pressure
dos.writeInt(MotionEvent.BUTTON_PRIMARY); // action button dos.writeInt(MotionEvent.BUTTON_PRIMARY);
dos.writeInt(MotionEvent.BUTTON_PRIMARY); // buttons
byte[] packet = bos.toByteArray(); byte[] packet = bos.toByteArray();
@@ -113,7 +112,6 @@ public class ControlMessageReaderTest {
Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth()); Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth());
Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight()); Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight());
Assert.assertEquals(1f, event.getPressure(), 0f); // must be exact Assert.assertEquals(1f, event.getPressure(), 0f); // must be exact
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getActionButton());
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getButtons()); Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getButtons());
} }