Compare commits
16 Commits
refactor-e
...
codec
Author | SHA1 | Date | |
---|---|---|---|
d2dce51038 | |||
4342c5637d | |||
3e517cd40e | |||
f70f6cdd3e | |||
87972e2022 | |||
3aac74e9e9 | |||
1c82c3923d | |||
36d656e91f | |||
bdbf1f4eb7 | |||
4177de5880 | |||
6a07e3d470 | |||
9b286ec8a7 | |||
8c5c55f9e1 | |||
0afef0c634 | |||
07806ba915 | |||
a52053421a |
21
README.md
21
README.md
@ -252,10 +252,22 @@ This affects recording orientation.
|
||||
The [window may also be rotated](#rotation) independently.
|
||||
|
||||
|
||||
#### Encoder
|
||||
#### Codec
|
||||
|
||||
Some devices have more than one encoder, and some of them may cause issues or
|
||||
crash. It is possible to select a different encoder:
|
||||
The video codec can be selected. The possible values are `h264` (default),
|
||||
`h265` and `av1`:
|
||||
|
||||
```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
|
||||
scrcpy --encoder=OMX.qcom.video.encoder.avc
|
||||
@ -265,7 +277,8 @@ To list the available encoders, you can pass an invalid encoder name; the
|
||||
error will give the available encoders:
|
||||
|
||||
```bash
|
||||
scrcpy --encoder=_
|
||||
scrcpy --encoder=_ # for the default codec
|
||||
scrcpy --codec=h265 --encoder=_ # for a specific codec
|
||||
```
|
||||
|
||||
### Capture
|
||||
|
14
app/scrcpy.1
14
app/scrcpy.1
@ -26,7 +26,11 @@ Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are
|
||||
Default is 8000000.
|
||||
|
||||
.TP
|
||||
.BI "\-\-codec\-options " key[:type]=value[,...]
|
||||
.BI "\-\-codec " name
|
||||
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.
|
||||
|
||||
The possible values for 'type' are 'int' (default), 'long', 'float' and 'string'.
|
||||
@ -117,7 +121,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.
|
||||
|
||||
.TP
|
||||
.BI "\-\-lock\-video\-orientation[=value]
|
||||
\fB\-\-lock\-video\-orientation\fR[=\fIvalue\fR]
|
||||
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".
|
||||
@ -199,7 +203,7 @@ It may only work over USB.
|
||||
See \fB\-\-hid\-keyboard\fR and \fB\-\-hid\-mouse\fR.
|
||||
|
||||
.TP
|
||||
.BI "\-p, \-\-port " port[:port]
|
||||
.BI "\-p, \-\-port " port\fR[:\fIport\fR]
|
||||
Set the TCP port (range) used by the client to listen.
|
||||
|
||||
Default is 27183:27199.
|
||||
@ -260,7 +264,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.
|
||||
|
||||
.TP
|
||||
.BI "\-\-shortcut\-mod " key[+...]][,...]
|
||||
.BI "\-\-shortcut\-mod " key\fR[+...]][,...]
|
||||
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 ','.
|
||||
@ -270,7 +274,7 @@ For example, to use either LCtrl+LAlt or LSuper for scrcpy shortcuts, pass "lctr
|
||||
Default is "lalt,lsuper" (left-Alt or left-Super).
|
||||
|
||||
.TP
|
||||
.BI "\-\-tcpip[=ip[:port]]
|
||||
.BI "\-\-tcpip\fR[=\fIip\fR[:\fIport\fR]]
|
||||
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).
|
||||
|
@ -57,6 +57,7 @@
|
||||
#define OPT_NO_CLEANUP 1037
|
||||
#define OPT_PRINT_FPS 1038
|
||||
#define OPT_NO_POWER_ON 1039
|
||||
#define OPT_CODEC 1040
|
||||
|
||||
struct sc_option {
|
||||
char shortopt;
|
||||
@ -105,6 +106,12 @@ static const struct sc_option options[] = {
|
||||
"Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
|
||||
"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 = "codec-options",
|
||||
@ -1377,6 +1384,24 @@ guess_record_format(const char *filename) {
|
||||
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
|
||||
parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
const char *optstring, const struct option *longopts) {
|
||||
@ -1610,6 +1635,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
case OPT_PRINT_FPS:
|
||||
opts->start_fps_counter = true;
|
||||
break;
|
||||
case OPT_CODEC:
|
||||
if (!parse_codec(optarg, &opts->codec)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case OPT_OTG:
|
||||
#ifdef HAVE_USB
|
||||
opts->otg = true;
|
||||
@ -1718,6 +1748,13 @@ 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->turn_screen_off) {
|
||||
LOGE("Could not request to turn screen off if control is disabled");
|
||||
|
@ -117,8 +117,9 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, unsigned char *buf) {
|
||||
uint16_t pressure =
|
||||
sc_float_to_u16fp(msg->inject_touch_event.pressure);
|
||||
sc_write16be(&buf[22], pressure);
|
||||
sc_write32be(&buf[24], msg->inject_touch_event.buttons);
|
||||
return 28;
|
||||
sc_write32be(&buf[24], msg->inject_touch_event.action_button);
|
||||
sc_write32be(&buf[28], msg->inject_touch_event.buttons);
|
||||
return 32;
|
||||
case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT:
|
||||
write_position(&buf[1], &msg->inject_scroll_event.position);
|
||||
int16_t hscroll =
|
||||
@ -179,22 +180,25 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
|
||||
if (pointer_name) {
|
||||
// string pointer id
|
||||
LOG_CMSG("touch [id=%s] %-4s position=%" PRIi32 ",%" PRIi32
|
||||
" pressure=%f buttons=%06lx",
|
||||
" pressure=%f action_button=%06lx buttons=%06lx",
|
||||
pointer_name,
|
||||
MOTIONEVENT_ACTION_LABEL(action),
|
||||
msg->inject_touch_event.position.point.x,
|
||||
msg->inject_touch_event.position.point.y,
|
||||
msg->inject_touch_event.pressure,
|
||||
(long) msg->inject_touch_event.action_button,
|
||||
(long) msg->inject_touch_event.buttons);
|
||||
} else {
|
||||
// numeric pointer id
|
||||
LOG_CMSG("touch [id=%" PRIu64_ "] %-4s position=%" PRIi32 ",%"
|
||||
PRIi32 " pressure=%f buttons=%06lx",
|
||||
PRIi32 " pressure=%f action_button=%06lx"
|
||||
" buttons=%06lx",
|
||||
id,
|
||||
MOTIONEVENT_ACTION_LABEL(action),
|
||||
msg->inject_touch_event.position.point.x,
|
||||
msg->inject_touch_event.position.point.y,
|
||||
msg->inject_touch_event.pressure,
|
||||
(long) msg->inject_touch_event.action_button,
|
||||
(long) msg->inject_touch_event.buttons);
|
||||
}
|
||||
break;
|
||||
|
@ -65,6 +65,7 @@ struct sc_control_msg {
|
||||
} inject_text;
|
||||
struct {
|
||||
enum android_motionevent_action action;
|
||||
enum android_motionevent_buttons action_button;
|
||||
enum android_motionevent_buttons buttons;
|
||||
uint64_t pointer_id;
|
||||
struct sc_position position;
|
||||
|
@ -17,6 +17,31 @@
|
||||
|
||||
#define SC_PACKET_PTS_MASK (SC_PACKET_FLAG_KEY_FRAME - 1)
|
||||
|
||||
static enum AVCodecID
|
||||
sc_demuxer_recv_codec_id(struct sc_demuxer *demuxer) {
|
||||
uint8_t data[4];
|
||||
ssize_t r = net_recv_all(demuxer->socket, data, 4);
|
||||
if (r < 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#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
|
||||
uint32_t codec_id = sc_read32be(data);
|
||||
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_packet(struct sc_demuxer *demuxer, AVPacket *packet) {
|
||||
// The video stream contains raw packets, without time information. When we
|
||||
@ -171,7 +196,13 @@ static int
|
||||
run_demuxer(void *data) {
|
||||
struct sc_demuxer *demuxer = data;
|
||||
|
||||
const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
|
||||
enum AVCodecID codec_id = sc_demuxer_recv_codec_id(demuxer);
|
||||
if (codec_id == AV_CODEC_ID_NONE) {
|
||||
// Error already logged
|
||||
goto end;
|
||||
}
|
||||
|
||||
const AVCodec *codec = avcodec_find_decoder(codec_id);
|
||||
if (!codec) {
|
||||
LOGE("H.264 decoder not found");
|
||||
goto end;
|
||||
@ -188,7 +219,7 @@ run_demuxer(void *data) {
|
||||
goto finally_free_codec_ctx;
|
||||
}
|
||||
|
||||
demuxer->parser = av_parser_init(AV_CODEC_ID_H264);
|
||||
demuxer->parser = av_parser_init(codec_id);
|
||||
if (!demuxer->parser) {
|
||||
LOGE("Could not initialize parser");
|
||||
goto finally_close_sinks;
|
||||
|
@ -339,6 +339,7 @@ simulate_virtual_finger(struct sc_input_manager *im,
|
||||
im->forward_all_clicks ? POINTER_ID_VIRTUAL_MOUSE
|
||||
: POINTER_ID_VIRTUAL_FINGER;
|
||||
msg.inject_touch_event.pressure = up ? 0.0f : 1.0f;
|
||||
msg.inject_touch_event.action_button = 0;
|
||||
msg.inject_touch_event.buttons = 0;
|
||||
|
||||
if (!sc_controller_push_msg(im->controller, &msg)) {
|
||||
|
@ -93,6 +93,7 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp,
|
||||
.pointer_id = event->pointer_id,
|
||||
.position = event->position,
|
||||
.pressure = event->action == SC_ACTION_DOWN ? 1.f : 0.f,
|
||||
.action_button = convert_mouse_buttons(event->button),
|
||||
.buttons = convert_mouse_buttons(event->buttons_state),
|
||||
},
|
||||
};
|
||||
|
@ -13,6 +13,7 @@ const struct scrcpy_options scrcpy_options_default = {
|
||||
.v4l2_device = NULL,
|
||||
#endif
|
||||
.log_level = SC_LOG_LEVEL_INFO,
|
||||
.codec = SC_CODEC_H264,
|
||||
.record_format = SC_RECORD_FORMAT_AUTO,
|
||||
.keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT,
|
||||
.port_range = {
|
||||
|
@ -23,6 +23,12 @@ enum sc_record_format {
|
||||
SC_RECORD_FORMAT_MKV,
|
||||
};
|
||||
|
||||
enum sc_codec {
|
||||
SC_CODEC_H264,
|
||||
SC_CODEC_H265,
|
||||
SC_CODEC_AV1,
|
||||
};
|
||||
|
||||
enum sc_lock_video_orientation {
|
||||
SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1,
|
||||
// lock the current orientation when scrcpy starts
|
||||
@ -93,6 +99,7 @@ struct scrcpy_options {
|
||||
const char *v4l2_device;
|
||||
#endif
|
||||
enum sc_log_level log_level;
|
||||
enum sc_codec codec;
|
||||
enum sc_record_format record_format;
|
||||
enum sc_keyboard_input_mode keyboard_input_mode;
|
||||
enum sc_mouse_input_mode mouse_input_mode;
|
||||
|
@ -11,6 +11,8 @@
|
||||
/** Downcast packet_sink to recorder */
|
||||
#define DOWNCAST(SINK) container_of(SINK, struct sc_recorder, packet_sink)
|
||||
|
||||
#define SC_PTS_ORIGIN_NONE UINT64_C(-1)
|
||||
|
||||
static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us
|
||||
|
||||
static const AVOutputFormat *
|
||||
@ -169,6 +171,18 @@ run_recorder(void *data) {
|
||||
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
|
||||
if (recorder->pts_origin == SC_PTS_ORIGIN_NONE
|
||||
&& rec->packet->pts != AV_NOPTS_VALUE) {
|
||||
// First PTS received
|
||||
recorder->pts_origin = rec->packet->pts;
|
||||
}
|
||||
|
||||
if (rec->packet->pts != AV_NOPTS_VALUE) {
|
||||
// Set PTS relatve to the origin
|
||||
rec->packet->pts -= recorder->pts_origin;
|
||||
rec->packet->dts = rec->packet->pts;
|
||||
}
|
||||
|
||||
// recorder->previous is only written from this thread, no need to lock
|
||||
struct sc_record_packet *previous = recorder->previous;
|
||||
recorder->previous = rec;
|
||||
@ -243,6 +257,7 @@ sc_recorder_open(struct sc_recorder *recorder, const AVCodec *input_codec) {
|
||||
recorder->failed = false;
|
||||
recorder->header_written = false;
|
||||
recorder->previous = NULL;
|
||||
recorder->pts_origin = SC_PTS_ORIGIN_NONE;
|
||||
|
||||
const char *format_name = sc_recorder_get_format_name(recorder->format);
|
||||
assert(format_name);
|
||||
|
@ -28,6 +28,8 @@ struct sc_recorder {
|
||||
struct sc_size declared_frame_size;
|
||||
bool header_written;
|
||||
|
||||
uint64_t pts_origin;
|
||||
|
||||
sc_thread thread;
|
||||
sc_mutex mutex;
|
||||
sc_cond queue_cond;
|
||||
|
@ -315,6 +315,7 @@ scrcpy(struct scrcpy_options *options) {
|
||||
.select_usb = options->select_usb,
|
||||
.select_tcpip = options->select_tcpip,
|
||||
.log_level = options->log_level,
|
||||
.codec = options->codec,
|
||||
.crop = options->crop,
|
||||
.port_range = options->port_range,
|
||||
.tunnel_host = options->tunnel_host,
|
||||
|
@ -8,6 +8,7 @@
|
||||
#include <SDL2/SDL_platform.h>
|
||||
|
||||
#include "adb/adb.h"
|
||||
#include "util/binary.h"
|
||||
#include "util/file.h"
|
||||
#include "util/log.h"
|
||||
#include "util/net_intr.h"
|
||||
@ -155,6 +156,20 @@ sc_server_sleep(struct sc_server *server, sc_tick deadline) {
|
||||
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
|
||||
execute_server(struct sc_server *server,
|
||||
const struct sc_server_params *params) {
|
||||
@ -202,6 +217,9 @@ execute_server(struct sc_server *server,
|
||||
ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level));
|
||||
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) {
|
||||
ADD_PARAM("max_size=%" PRIu16, params->max_size);
|
||||
}
|
||||
@ -398,10 +416,9 @@ device_read_info(struct sc_intr *intr, sc_socket device_socket,
|
||||
buf[SC_DEVICE_NAME_FIELD_LENGTH - 1] = '\0';
|
||||
memcpy(info->device_name, (char *) buf, sizeof(info->device_name));
|
||||
|
||||
info->frame_size.width = (buf[SC_DEVICE_NAME_FIELD_LENGTH] << 8)
|
||||
| buf[SC_DEVICE_NAME_FIELD_LENGTH + 1];
|
||||
info->frame_size.height = (buf[SC_DEVICE_NAME_FIELD_LENGTH + 2] << 8)
|
||||
| buf[SC_DEVICE_NAME_FIELD_LENGTH + 3];
|
||||
unsigned char *fields = &buf[SC_DEVICE_NAME_FIELD_LENGTH];
|
||||
info->frame_size.width = sc_read16be(fields);
|
||||
info->frame_size.height = sc_read16be(&fields[2]);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -25,6 +25,7 @@ struct sc_server_params {
|
||||
uint32_t uid;
|
||||
const char *req_serial;
|
||||
enum sc_log_level log_level;
|
||||
enum sc_codec codec;
|
||||
const char *crop;
|
||||
const char *codec_options;
|
||||
const char *encoder_name;
|
||||
|
@ -90,13 +90,14 @@ static void test_serialize_inject_touch_event(void) {
|
||||
},
|
||||
},
|
||||
.pressure = 1.0f,
|
||||
.action_button = AMOTION_EVENT_BUTTON_PRIMARY,
|
||||
.buttons = AMOTION_EVENT_BUTTON_PRIMARY,
|
||||
},
|
||||
};
|
||||
|
||||
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE];
|
||||
size_t size = sc_control_msg_serialize(&msg, buf);
|
||||
assert(size == 28);
|
||||
assert(size == 32);
|
||||
|
||||
const unsigned char expected[] = {
|
||||
SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT,
|
||||
@ -105,7 +106,8 @@ static void test_serialize_inject_touch_event(void) {
|
||||
0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0xc8, // 100 200
|
||||
0x04, 0x38, 0x07, 0x80, // 1080 1920
|
||||
0xff, 0xff, // pressure
|
||||
0x00, 0x00, 0x00, 0x01 // AMOTION_EVENT_BUTTON_PRIMARY
|
||||
0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY (action button)
|
||||
0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY (buttons)
|
||||
};
|
||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ public final class ControlMessage {
|
||||
private int metaState; // KeyEvent.META_*
|
||||
private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or POWER_MODE_*
|
||||
private int keycode; // KeyEvent.KEYCODE_*
|
||||
private int actionButton; // MotionEvent.BUTTON_*
|
||||
private int buttons; // MotionEvent.BUTTON_*
|
||||
private long pointerId;
|
||||
private float pressure;
|
||||
@ -60,13 +61,15 @@ public final class ControlMessage {
|
||||
return msg;
|
||||
}
|
||||
|
||||
public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure, int buttons) {
|
||||
public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure, int actionButton,
|
||||
int buttons) {
|
||||
ControlMessage msg = new ControlMessage();
|
||||
msg.type = TYPE_INJECT_TOUCH_EVENT;
|
||||
msg.action = action;
|
||||
msg.pointerId = pointerId;
|
||||
msg.pressure = pressure;
|
||||
msg.position = position;
|
||||
msg.actionButton = actionButton;
|
||||
msg.buttons = buttons;
|
||||
return msg;
|
||||
}
|
||||
@ -140,6 +143,10 @@ public final class ControlMessage {
|
||||
return keycode;
|
||||
}
|
||||
|
||||
public int getActionButton() {
|
||||
return actionButton;
|
||||
}
|
||||
|
||||
public int getButtons() {
|
||||
return buttons;
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import java.nio.charset.StandardCharsets;
|
||||
public class ControlMessageReader {
|
||||
|
||||
static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 13;
|
||||
static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27;
|
||||
static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 31;
|
||||
static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20;
|
||||
static final int BACK_OR_SCREEN_ON_LENGTH = 1;
|
||||
static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1;
|
||||
@ -140,8 +140,9 @@ public class ControlMessageReader {
|
||||
long pointerId = buffer.getLong();
|
||||
Position position = readPosition(buffer);
|
||||
float pressure = Binary.u16FixedPointToFloat(buffer.getShort());
|
||||
int actionButton = buffer.getInt();
|
||||
int buttons = buffer.getInt();
|
||||
return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, buttons);
|
||||
return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, actionButton, buttons);
|
||||
}
|
||||
|
||||
private ControlMessage parseInjectScrollEvent() {
|
||||
|
@ -1,5 +1,7 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
import com.genymobile.scrcpy.wrappers.InputManager;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.SystemClock;
|
||||
import android.view.InputDevice;
|
||||
@ -22,6 +24,8 @@ public class Controller {
|
||||
|
||||
private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor();
|
||||
|
||||
private Thread thread;
|
||||
|
||||
private final Device device;
|
||||
private final DesktopConnection connection;
|
||||
private final DeviceMessageSender sender;
|
||||
@ -60,7 +64,7 @@ public class Controller {
|
||||
}
|
||||
}
|
||||
|
||||
public void control() throws IOException {
|
||||
private void control() throws IOException {
|
||||
// on start, power on the device
|
||||
if (powerOn && !Device.isScreenOn()) {
|
||||
device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC);
|
||||
@ -80,6 +84,27 @@ public class Controller {
|
||||
}
|
||||
}
|
||||
|
||||
public void start() {
|
||||
thread = new Thread(() -> {
|
||||
try {
|
||||
control();
|
||||
} catch (IOException e) {
|
||||
// this is expected on close
|
||||
Ln.d("Controller stopped");
|
||||
}
|
||||
});
|
||||
thread.start();
|
||||
sender.start();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (thread != null) {
|
||||
thread.interrupt();
|
||||
thread = null;
|
||||
}
|
||||
sender.stop();
|
||||
}
|
||||
|
||||
public DeviceMessageSender getSender() {
|
||||
return sender;
|
||||
}
|
||||
@ -99,7 +124,7 @@ public class Controller {
|
||||
break;
|
||||
case ControlMessage.TYPE_INJECT_TOUCH_EVENT:
|
||||
if (device.supportsInputEvents()) {
|
||||
injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons());
|
||||
injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getActionButton(), msg.getButtons());
|
||||
}
|
||||
break;
|
||||
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
|
||||
@ -179,7 +204,7 @@ public class Controller {
|
||||
return successCount;
|
||||
}
|
||||
|
||||
private boolean injectTouch(int action, long pointerId, Position position, float pressure, int buttons) {
|
||||
private boolean injectTouch(int action, long pointerId, Position position, float pressure, int actionButton, int buttons) {
|
||||
long now = SystemClock.uptimeMillis();
|
||||
|
||||
Point point = device.getPhysicalPoint(position);
|
||||
@ -196,22 +221,23 @@ public class Controller {
|
||||
Pointer pointer = pointersState.get(pointerIndex);
|
||||
pointer.setPoint(point);
|
||||
pointer.setPressure(pressure);
|
||||
pointer.setUp(action == MotionEvent.ACTION_UP);
|
||||
|
||||
int source;
|
||||
int pointerCount = pointersState.update(pointerProperties, pointerCoords);
|
||||
if (pointerId == POINTER_ID_MOUSE || pointerId == POINTER_ID_VIRTUAL_MOUSE) {
|
||||
// real mouse event (forced by the client when --forward-on-click)
|
||||
pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_MOUSE;
|
||||
source = InputDevice.SOURCE_MOUSE;
|
||||
pointer.setUp(buttons == 0);
|
||||
} else {
|
||||
// POINTER_ID_GENERIC_FINGER, POINTER_ID_VIRTUAL_FINGER or real touch from device
|
||||
pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_FINGER;
|
||||
source = InputDevice.SOURCE_TOUCHSCREEN;
|
||||
// Buttons must not be set for touch events
|
||||
buttons = 0;
|
||||
pointer.setUp(action == MotionEvent.ACTION_UP);
|
||||
}
|
||||
|
||||
int pointerCount = pointersState.update(pointerProperties, pointerCoords);
|
||||
if (pointerCount == 1) {
|
||||
if (action == MotionEvent.ACTION_DOWN) {
|
||||
lastTouchDown = now;
|
||||
@ -225,6 +251,62 @@ public class Controller {
|
||||
}
|
||||
}
|
||||
|
||||
/* If the input device is a mouse (on API >= 23):
|
||||
* - the first button pressed must first generate ACTION_DOWN;
|
||||
* - all button pressed (including the first one) must generate ACTION_BUTTON_PRESS;
|
||||
* - all button released (including the last one) must generate ACTION_BUTTON_RELEASE;
|
||||
* - the last button released must in addition generate ACTION_UP.
|
||||
*
|
||||
* Otherwise, Chrome does not work properly: <https://github.com/Genymobile/scrcpy/issues/3635>
|
||||
*/
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && source == InputDevice.SOURCE_MOUSE) {
|
||||
if (action == MotionEvent.ACTION_DOWN) {
|
||||
if (actionButton == buttons) {
|
||||
// First button pressed: ACTION_DOWN
|
||||
MotionEvent downEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_DOWN, pointerCount, pointerProperties,
|
||||
pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0);
|
||||
if (!device.injectEvent(downEvent, Device.INJECT_MODE_ASYNC)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Any button pressed: ACTION_BUTTON_PRESS
|
||||
MotionEvent pressEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_BUTTON_PRESS, pointerCount, pointerProperties,
|
||||
pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0);
|
||||
if (!InputManager.setActionButton(pressEvent, actionButton)) {
|
||||
return false;
|
||||
}
|
||||
if (!device.injectEvent(pressEvent, Device.INJECT_MODE_ASYNC)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (action == MotionEvent.ACTION_UP) {
|
||||
// Any button released: ACTION_BUTTON_RELEASE
|
||||
MotionEvent releaseEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_BUTTON_RELEASE, pointerCount, pointerProperties,
|
||||
pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0);
|
||||
if (!InputManager.setActionButton(releaseEvent, actionButton)) {
|
||||
return false;
|
||||
}
|
||||
if (!device.injectEvent(releaseEvent, Device.INJECT_MODE_ASYNC)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (buttons == 0) {
|
||||
// Last button released: ACTION_UP
|
||||
MotionEvent upEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_UP, pointerCount, pointerProperties,
|
||||
pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0);
|
||||
if (!device.injectEvent(upEvent, Device.INJECT_MODE_ASYNC)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent event = MotionEvent
|
||||
.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source,
|
||||
0);
|
||||
|
@ -6,6 +6,8 @@ public final class DeviceMessageSender {
|
||||
|
||||
private final DesktopConnection connection;
|
||||
|
||||
private Thread thread;
|
||||
|
||||
private String clipboardText;
|
||||
|
||||
private long ack;
|
||||
@ -24,7 +26,7 @@ public final class DeviceMessageSender {
|
||||
notify();
|
||||
}
|
||||
|
||||
public void loop() throws IOException, InterruptedException {
|
||||
private void loop() throws IOException, InterruptedException {
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
String text;
|
||||
long sequence;
|
||||
@ -49,4 +51,22 @@ public final class DeviceMessageSender {
|
||||
}
|
||||
}
|
||||
}
|
||||
public void start() {
|
||||
thread = new Thread(() -> {
|
||||
try {
|
||||
loop();
|
||||
} catch (IOException | InterruptedException e) {
|
||||
// this is expected on close
|
||||
Ln.d("Device message sender stopped");
|
||||
}
|
||||
});
|
||||
thread.start();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (thread != null) {
|
||||
thread.interrupt();
|
||||
thread = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,9 +5,12 @@ import android.graphics.Rect;
|
||||
import java.util.List;
|
||||
|
||||
public class Options {
|
||||
private static final String VIDEO_CODEC_H264 = "h264";
|
||||
|
||||
private Ln.Level logLevel = Ln.Level.DEBUG;
|
||||
private int uid = -1; // 31-bit non-negative value, or -1
|
||||
private int maxSize;
|
||||
private VideoCodec codec = VideoCodec.H264;
|
||||
private int bitRate = 8000000;
|
||||
private int maxFps;
|
||||
private int lockVideoOrientation = -1;
|
||||
@ -29,6 +32,7 @@ public class Options {
|
||||
private boolean sendDeviceMeta = true; // send device name and size
|
||||
private boolean sendFrameMeta = true; // send PTS so that the client may record properly
|
||||
private boolean sendDummyByte = true; // write a byte on start to detect connection issues
|
||||
private boolean sendCodecId = true; // write the codec ID (4 bytes) before the stream
|
||||
|
||||
public Ln.Level getLogLevel() {
|
||||
return logLevel;
|
||||
@ -54,6 +58,14 @@ public class Options {
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
public VideoCodec getCodec() {
|
||||
return codec;
|
||||
}
|
||||
|
||||
public void setCodec(VideoCodec codec) {
|
||||
this.codec = codec;
|
||||
}
|
||||
|
||||
public int getBitRate() {
|
||||
return bitRate;
|
||||
}
|
||||
@ -205,4 +217,12 @@ public class Options {
|
||||
public void setSendDummyByte(boolean sendDummyByte) {
|
||||
this.sendDummyByte = sendDummyByte;
|
||||
}
|
||||
|
||||
public boolean getSendCodecId() {
|
||||
return sendCodecId;
|
||||
}
|
||||
|
||||
public void setSendCodecId(boolean sendCodecId) {
|
||||
this.sendCodecId = sendCodecId;
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ import android.os.IBinder;
|
||||
import android.os.SystemClock;
|
||||
import android.view.Surface;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
@ -22,32 +21,32 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
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 REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms
|
||||
private static final String KEY_MAX_FPS_TO_ENCODER = "max-fps-to-encoder";
|
||||
|
||||
// Keep the values in descending order
|
||||
private static final int[] MAX_SIZE_FALLBACK = {2560, 1920, 1600, 1280, 1024, 800};
|
||||
|
||||
private static final long PACKET_FLAG_CONFIG = 1L << 63;
|
||||
private static final long PACKET_FLAG_KEY_FRAME = 1L << 62;
|
||||
private static final int MAX_CONSECUTIVE_ERRORS = 3;
|
||||
|
||||
private final AtomicBoolean rotationChanged = new AtomicBoolean();
|
||||
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
|
||||
|
||||
private final String videoMimeType;
|
||||
private final String encoderName;
|
||||
private final List<CodecOption> codecOptions;
|
||||
private final int bitRate;
|
||||
private final int maxFps;
|
||||
private final boolean sendFrameMeta;
|
||||
private final boolean downsizeOnError;
|
||||
private long ptsOrigin;
|
||||
|
||||
private boolean firstFrameSent;
|
||||
private int consecutiveErrors;
|
||||
|
||||
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List<CodecOption> codecOptions, String encoderName,
|
||||
boolean downsizeOnError) {
|
||||
this.sendFrameMeta = sendFrameMeta;
|
||||
public ScreenEncoder(String videoMimeType, int bitRate, int maxFps, List<CodecOption> codecOptions, String encoderName, boolean downsizeOnError) {
|
||||
this.videoMimeType = videoMimeType;
|
||||
this.bitRate = bitRate;
|
||||
this.maxFps = maxFps;
|
||||
this.codecOptions = codecOptions;
|
||||
@ -64,20 +63,9 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
return rotationChanged.getAndSet(false);
|
||||
}
|
||||
|
||||
public void streamScreen(Device device, FileDescriptor fd) throws IOException {
|
||||
Workarounds.prepareMainLooper();
|
||||
if (Build.BRAND.equalsIgnoreCase("meizu")) {
|
||||
// <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 {
|
||||
MediaCodec codec = createCodec(encoderName);
|
||||
MediaFormat format = createFormat(bitRate, maxFps, codecOptions);
|
||||
public void streamScreen(Device device, Callbacks callbacks) throws IOException {
|
||||
MediaCodec codec = createCodec(videoMimeType, encoderName);
|
||||
MediaFormat format = createFormat(videoMimeType, bitRate, maxFps, codecOptions);
|
||||
IBinder display = createDisplay();
|
||||
device.setRotationListener(this);
|
||||
boolean alive;
|
||||
@ -104,25 +92,15 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
|
||||
codec.start();
|
||||
|
||||
alive = encode(codec, fd);
|
||||
alive = encode(codec, callbacks);
|
||||
// do not call stop() on exception, it would trigger an IllegalStateException
|
||||
codec.stop();
|
||||
} catch (MediaCodec.CodecException e) {
|
||||
Ln.e("Codec error: " + e.getMessage());
|
||||
// <https://developer.android.com/reference/android/media/MediaCodec#error-handling>
|
||||
// For simplicity, handle isTransient() like isRecoverable()
|
||||
if (e.isRecoverable() || e.isTransient()) {
|
||||
// Avoid busy-loop if too many errors are generated
|
||||
SystemClock.sleep(50);
|
||||
} else if (!prepareDownsizeRetry(device, screenInfo)) {
|
||||
throw e;
|
||||
}
|
||||
alive = true;
|
||||
} catch (IllegalStateException | IllegalArgumentException e) {
|
||||
Ln.e("Encoding error: " + e.getClass().getName() + ": " + e.getMessage());
|
||||
if (!prepareDownsizeRetry(device, screenInfo)) {
|
||||
if (!prepareRetry(device, screenInfo)) {
|
||||
throw e;
|
||||
}
|
||||
Ln.i("Retrying...");
|
||||
alive = true;
|
||||
} finally {
|
||||
codec.reset();
|
||||
@ -138,13 +116,26 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean prepareDownsizeRetry(Device device, ScreenInfo screenInfo) {
|
||||
if (!downsizeOnError || firstFrameSent) {
|
||||
Ln.i("#1 " + downsizeOnError + " " + firstFrameSent);
|
||||
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) {
|
||||
@ -170,30 +161,30 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
return 0;
|
||||
}
|
||||
|
||||
private boolean encode(MediaCodec codec, FileDescriptor fd) throws IOException {
|
||||
private boolean encode(MediaCodec codec, Callbacks callbacks) throws IOException {
|
||||
boolean eof = false;
|
||||
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
||||
|
||||
while (!consumeRotationChange() && !eof) {
|
||||
int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1);
|
||||
eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
|
||||
try {
|
||||
if (consumeRotationChange()) {
|
||||
// must restart encoding with new size
|
||||
break;
|
||||
}
|
||||
|
||||
eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
|
||||
if (outputBufferId >= 0) {
|
||||
ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId);
|
||||
|
||||
if (sendFrameMeta) {
|
||||
writeFrameMeta(fd, bufferInfo, codecBuffer.remaining());
|
||||
}
|
||||
|
||||
IO.writeFully(fd, codecBuffer);
|
||||
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) {
|
||||
boolean isConfig = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0;
|
||||
if (!isConfig) {
|
||||
// If this is not a config packet, then it contains a frame
|
||||
firstFrameSent = true;
|
||||
consecutiveErrors = 0;
|
||||
}
|
||||
|
||||
callbacks.onPacket(codecBuffer, bufferInfo);
|
||||
}
|
||||
} finally {
|
||||
if (outputBufferId >= 0) {
|
||||
@ -205,50 +196,28 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
return !eof;
|
||||
}
|
||||
|
||||
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() {
|
||||
private static MediaCodecInfo[] listEncoders(String videoMimeType) {
|
||||
List<MediaCodecInfo> result = new ArrayList<>();
|
||||
MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
|
||||
for (MediaCodecInfo codecInfo : list.getCodecInfos()) {
|
||||
if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(MediaFormat.MIMETYPE_VIDEO_AVC)) {
|
||||
if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(videoMimeType)) {
|
||||
result.add(codecInfo);
|
||||
}
|
||||
}
|
||||
return result.toArray(new MediaCodecInfo[result.size()]);
|
||||
}
|
||||
|
||||
private static MediaCodec createCodec(String encoderName) throws IOException {
|
||||
private static MediaCodec createCodec(String videoMimeType, String encoderName) throws IOException {
|
||||
if (encoderName != null) {
|
||||
Ln.d("Creating encoder by name: '" + encoderName + "'");
|
||||
try {
|
||||
return MediaCodec.createByCodecName(encoderName);
|
||||
} catch (IllegalArgumentException e) {
|
||||
MediaCodecInfo[] encoders = listEncoders();
|
||||
MediaCodecInfo[] encoders = listEncoders(videoMimeType);
|
||||
throw new InvalidEncoderException(encoderName, encoders);
|
||||
}
|
||||
}
|
||||
MediaCodec codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
|
||||
MediaCodec codec = MediaCodec.createEncoderByType(videoMimeType);
|
||||
Ln.d("Using encoder: '" + codec.getName() + "'");
|
||||
return codec;
|
||||
}
|
||||
@ -270,9 +239,9 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
Ln.d("Codec option set: " + key + " (" + value.getClass().getSimpleName() + ") = " + value);
|
||||
}
|
||||
|
||||
private static MediaFormat createFormat(int bitRate, int maxFps, List<CodecOption> codecOptions) {
|
||||
private static MediaFormat createFormat(String videoMimeType, int bitRate, int maxFps, List<CodecOption> codecOptions) {
|
||||
MediaFormat format = new MediaFormat();
|
||||
format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC);
|
||||
format.setString(MediaFormat.KEY_MIME, videoMimeType);
|
||||
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
|
||||
format.setInteger(MediaFormat.KEY_FRAME_RATE, 60);
|
||||
|
@ -71,39 +71,51 @@ public final class Server {
|
||||
boolean control = options.getControl();
|
||||
boolean sendDummyByte = options.getSendDummyByte();
|
||||
|
||||
Workarounds.prepareMainLooper();
|
||||
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()) {
|
||||
Size videoSize = device.getScreenInfo().getVideoSize();
|
||||
connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
|
||||
}
|
||||
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions,
|
||||
ScreenEncoder screenEncoder = new ScreenEncoder(codec.getMimeType(), options.getBitRate(), options.getMaxFps(), codecOptions,
|
||||
options.getEncoderName(), options.getDownsizeOnError());
|
||||
|
||||
Thread controllerThread = null;
|
||||
Thread deviceMessageSenderThread = null;
|
||||
Controller controller = null;
|
||||
if (control) {
|
||||
final Controller controller = new Controller(device, connection, options.getClipboardAutosync(), options.getPowerOn());
|
||||
controller = new Controller(device, connection, options.getClipboardAutosync(), options.getPowerOn());
|
||||
controller.start();
|
||||
|
||||
// asynchronous
|
||||
controllerThread = startController(controller);
|
||||
deviceMessageSenderThread = startDeviceMessageSender(controller.getSender());
|
||||
|
||||
device.setClipboardListener(text -> controller.getSender().pushClipboardText(text));
|
||||
final Controller controllerRef = controller;
|
||||
device.setClipboardListener(text -> controllerRef.getSender().pushClipboardText(text));
|
||||
}
|
||||
|
||||
try {
|
||||
// synchronous
|
||||
screenEncoder.streamScreen(device, connection.getVideoFd());
|
||||
VideoStreamer videoStreamer = new VideoStreamer(connection.getVideoFd(), options.getSendFrameMeta());
|
||||
if (options.getSendCodecId()) {
|
||||
videoStreamer.writeHeader(codec.getId());
|
||||
}
|
||||
screenEncoder.streamScreen(device, videoStreamer);
|
||||
} catch (IOException e) {
|
||||
// this is expected on close
|
||||
Ln.d("Screen streaming stopped");
|
||||
} finally {
|
||||
initThread.interrupt();
|
||||
if (controllerThread != null) {
|
||||
controllerThread.interrupt();
|
||||
}
|
||||
if (deviceMessageSenderThread != null) {
|
||||
deviceMessageSenderThread.interrupt();
|
||||
if (controller != null) {
|
||||
controller.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -115,32 +127,6 @@ public final class Server {
|
||||
return thread;
|
||||
}
|
||||
|
||||
private static Thread startController(final Controller controller) {
|
||||
Thread thread = new Thread(() -> {
|
||||
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(() -> {
|
||||
try {
|
||||
sender.loop();
|
||||
} catch (IOException | InterruptedException e) {
|
||||
// this is expected on close
|
||||
Ln.d("Device message sender stopped");
|
||||
}
|
||||
});
|
||||
thread.start();
|
||||
return thread;
|
||||
}
|
||||
|
||||
private static Options createOptions(String... args) {
|
||||
if (args.length < 1) {
|
||||
throw new IllegalArgumentException("Missing client version");
|
||||
@ -174,6 +160,13 @@ public final class Server {
|
||||
Ln.Level level = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH));
|
||||
options.setLogLevel(level);
|
||||
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":
|
||||
int maxSize = Integer.parseInt(value) & ~7; // multiple of 8
|
||||
options.setMaxSize(maxSize);
|
||||
@ -255,12 +248,17 @@ public final class Server {
|
||||
boolean sendDummyByte = Boolean.parseBoolean(value);
|
||||
options.setSendDummyByte(sendDummyByte);
|
||||
break;
|
||||
case "send_codec_id":
|
||||
boolean sendCodecId = Boolean.parseBoolean(value);
|
||||
options.setSendCodecId(sendCodecId);
|
||||
break;
|
||||
case "raw_video_stream":
|
||||
boolean rawVideoStream = Boolean.parseBoolean(value);
|
||||
if (rawVideoStream) {
|
||||
options.setSendDeviceMeta(false);
|
||||
options.setSendFrameMeta(false);
|
||||
options.setSendDummyByte(false);
|
||||
options.setSendCodecId(false);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
36
server/src/main/java/com/genymobile/scrcpy/VideoCodec.java
Normal file
36
server/src/main/java/com/genymobile/scrcpy/VideoCodec.java
Normal file
@ -0,0 +1,36 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ package com.genymobile.scrcpy.wrappers;
|
||||
import com.genymobile.scrcpy.Ln;
|
||||
|
||||
import android.view.InputEvent;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
@ -17,6 +18,7 @@ public final class InputManager {
|
||||
private Method injectInputEventMethod;
|
||||
|
||||
private static Method setDisplayIdMethod;
|
||||
private static Method setActionButtonMethod;
|
||||
|
||||
public InputManager(android.hardware.input.InputManager manager) {
|
||||
this.manager = manager;
|
||||
@ -56,4 +58,22 @@ public final class InputManager {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static Method getSetActionButtonMethod() throws NoSuchMethodException {
|
||||
if (setActionButtonMethod == null) {
|
||||
setActionButtonMethod = MotionEvent.class.getMethod("setActionButton", int.class);
|
||||
}
|
||||
return setActionButtonMethod;
|
||||
}
|
||||
|
||||
public static boolean setActionButton(MotionEvent motionEvent, int actionButton) {
|
||||
try {
|
||||
Method method = getSetActionButtonMethod();
|
||||
method.invoke(motionEvent, actionButton);
|
||||
return true;
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Cannot set action button on MotionEvent", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -94,7 +94,8 @@ public class ControlMessageReaderTest {
|
||||
dos.writeShort(1080);
|
||||
dos.writeShort(1920);
|
||||
dos.writeShort(0xffff); // pressure
|
||||
dos.writeInt(MotionEvent.BUTTON_PRIMARY);
|
||||
dos.writeInt(MotionEvent.BUTTON_PRIMARY); // action button
|
||||
dos.writeInt(MotionEvent.BUTTON_PRIMARY); // buttons
|
||||
|
||||
byte[] packet = bos.toByteArray();
|
||||
|
||||
@ -112,6 +113,7 @@ public class ControlMessageReaderTest {
|
||||
Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth());
|
||||
Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight());
|
||||
Assert.assertEquals(1f, event.getPressure(), 0f); // must be exact
|
||||
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getActionButton());
|
||||
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getButtons());
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user