Compare commits

..

7 Commits

Author SHA1 Message Date
Romain Vimont
4fc8b150f8 Move Workarounds call 2023-02-01 08:32:56 +01:00
Romain Vimont
f7cd88f717 Use FakeContext for Application object 2023-02-01 08:32:56 +01:00
Romain Vimont
b097f04459 Use shell package name 2023-02-01 08:32:56 +01:00
Romain Vimont
cbabaa223e Use PACKAGE_NAME from FakeContext 2023-02-01 08:32:56 +01:00
Romain Vimont
c91253105e Use AttributionSource from Context 2023-02-01 08:32:56 +01:00
Simon Chan
4332b53dd5 FakeContext
Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-02-01 08:32:33 +01:00
Romain Vimont
2e9c364ef3 Process.ROOT_UID 2023-01-31 21:58:58 +01:00
21 changed files with 141 additions and 707 deletions

View File

@@ -194,18 +194,6 @@ The other dimension is computed so that the Android device aspect ratio is
preserved. That way, a device in 1920×1080 will be mirrored at 1024×576. preserved. That way, a device in 1920×1080 will be mirrored at 1024×576.
#### Select codec
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
```
#### Change bit-rate #### Change bit-rate
The default bit-rate is 8 Mbps. To change the video bitrate (e.g. to 2 Mbps): The default bit-rate is 8 Mbps. To change the video bitrate (e.g. to 2 Mbps):
@@ -266,8 +254,8 @@ The [window may also be rotated](#rotation) independently.
#### Encoder #### Encoder
Some devices have more than one encoder for a specific codec, and some of them Some devices have more than one encoder, and some of them may cause issues or
may cause issues or crash. It is possible to select a different encoder: 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
@@ -280,8 +268,6 @@ error will give the available encoders:
scrcpy --encoder=_ scrcpy --encoder=_
``` ```
Note that you can also select a different [codec](#select-codec).
### Capture ### Capture
#### Recording #### Recording

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

@@ -57,8 +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
#define OPT_NO_AUDIO 1041
struct sc_option { struct sc_option {
char shortopt; char shortopt;
@@ -107,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",
@@ -299,11 +291,6 @@ static const struct sc_option options[] = {
.text = "Do not display device (only when screen recording or V4L2 " .text = "Do not display device (only when screen recording or V4L2 "
"sink is enabled).", "sink is enabled).",
}, },
{
.longopt_id = OPT_NO_AUDIO,
.longopt = "no-audio",
.text = "Disable audio forwarding.",
},
{ {
.longopt_id = OPT_NO_KEY_REPEAT, .longopt_id = OPT_NO_KEY_REPEAT,
.longopt = "no-key-repeat", .longopt = "no-key-repeat",
@@ -1390,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) {
@@ -1632,9 +1601,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
case OPT_NO_DOWNSIZE_ON_ERROR: case OPT_NO_DOWNSIZE_ON_ERROR:
opts->downsize_on_error = false; opts->downsize_on_error = false;
break; break;
case OPT_NO_AUDIO:
opts->audio = false;
break;
case OPT_NO_CLEANUP: case OPT_NO_CLEANUP:
opts->cleanup = false; opts->cleanup = false;
break; break;
@@ -1644,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;
@@ -1757,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

@@ -17,31 +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_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 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
@@ -196,13 +171,7 @@ static int
run_demuxer(void *data) { run_demuxer(void *data) {
struct sc_demuxer *demuxer = data; struct sc_demuxer *demuxer = data;
enum AVCodecID codec_id = sc_demuxer_recv_codec_id(demuxer); const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
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;
@@ -219,7 +188,7 @@ run_demuxer(void *data) {
goto finally_free_codec_ctx; goto finally_free_codec_ctx;
} }
demuxer->parser = av_parser_init(codec_id); demuxer->parser = av_parser_init(AV_CODEC_ID_H264);
if (!demuxer->parser) { if (!demuxer->parser) {
LOGE("Could not initialize parser"); LOGE("Could not initialize parser");
goto finally_close_sinks; goto finally_close_sinks;

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 = {
@@ -66,5 +65,4 @@ const struct scrcpy_options scrcpy_options_default = {
.cleanup = true, .cleanup = true,
.start_fps_counter = false, .start_fps_counter = false,
.power_on = true, .power_on = true,
.audio = true,
}; };

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;
@@ -147,7 +140,6 @@ struct scrcpy_options {
bool cleanup; bool cleanup;
bool start_fps_counter; bool start_fps_counter;
bool power_on; bool power_on;
bool audio;
}; };
extern const struct scrcpy_options scrcpy_options_default; extern const struct scrcpy_options scrcpy_options_default;

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

@@ -315,7 +315,6 @@ scrcpy(struct scrcpy_options *options) {
.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,
@@ -326,7 +325,6 @@ scrcpy(struct scrcpy_options *options) {
.lock_video_orientation = options->lock_video_orientation, .lock_video_orientation = options->lock_video_orientation,
.control = options->control, .control = options->control,
.display_id = options->display_id, .display_id = options->display_id,
.audio = options->audio,
.show_touches = options->show_touches, .show_touches = options->show_touches,
.stay_awake = options->stay_awake, .stay_awake = options->stay_awake,
.codec_options = options->codec_options, .codec_options = options->codec_options,

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"
@@ -156,20 +155,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) {
@@ -217,12 +202,6 @@ execute_server(struct sc_server *server,
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->audio) {
ADD_PARAM("audio=false");
}
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);
} }
@@ -391,7 +370,6 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params,
server->stopped = false; server->stopped = false;
server->video_socket = SC_SOCKET_NONE; server->video_socket = SC_SOCKET_NONE;
server->audio_socket = SC_SOCKET_NONE;
server->control_socket = SC_SOCKET_NONE; server->control_socket = SC_SOCKET_NONE;
sc_adb_tunnel_init(&server->tunnel); sc_adb_tunnel_init(&server->tunnel);
@@ -420,9 +398,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;
} }
@@ -435,11 +414,9 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
const char *serial = server->serial; const char *serial = server->serial;
assert(serial); assert(serial);
bool audio = server->params.audio;
bool control = server->params.control; bool control = server->params.control;
sc_socket video_socket = SC_SOCKET_NONE; sc_socket video_socket = SC_SOCKET_NONE;
sc_socket audio_socket = SC_SOCKET_NONE;
sc_socket control_socket = SC_SOCKET_NONE; sc_socket control_socket = SC_SOCKET_NONE;
if (!tunnel->forward) { if (!tunnel->forward) {
video_socket = net_accept_intr(&server->intr, tunnel->server_socket); video_socket = net_accept_intr(&server->intr, tunnel->server_socket);
@@ -447,14 +424,6 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
goto fail; goto fail;
} }
if (audio) {
audio_socket =
net_accept_intr(&server->intr, tunnel->server_socket);
if (audio_socket == SC_SOCKET_NONE) {
goto fail;
}
}
if (control) { if (control) {
control_socket = control_socket =
net_accept_intr(&server->intr, tunnel->server_socket); net_accept_intr(&server->intr, tunnel->server_socket);
@@ -481,18 +450,6 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
goto fail; goto fail;
} }
if (audio) {
audio_socket = net_socket();
if (audio_socket == SC_SOCKET_NONE) {
goto fail;
}
bool ok = net_connect_intr(&server->intr, audio_socket, tunnel_host,
tunnel_port);
if (!ok) {
goto fail;
}
}
if (control) { if (control) {
// we know that the device is listening, we don't need several // we know that the device is listening, we don't need several
// attempts // attempts
@@ -519,11 +476,9 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
} }
assert(video_socket != SC_SOCKET_NONE); assert(video_socket != SC_SOCKET_NONE);
assert(!audio || audio_socket != SC_SOCKET_NONE);
assert(!control || control_socket != SC_SOCKET_NONE); assert(!control || control_socket != SC_SOCKET_NONE);
server->video_socket = video_socket; server->video_socket = video_socket;
server->audio_socket = audio_socket;
server->control_socket = control_socket; server->control_socket = control_socket;
return true; return true;
@@ -535,12 +490,6 @@ fail:
} }
} }
if (audio_socket != SC_SOCKET_NONE) {
if (!net_close(audio_socket)) {
LOGW("Could not close audio socket");
}
}
if (control_socket != SC_SOCKET_NONE) { if (control_socket != SC_SOCKET_NONE) {
if (!net_close(control_socket)) { if (!net_close(control_socket)) {
LOGW("Could not close control socket"); LOGW("Could not close control socket");
@@ -886,11 +835,6 @@ run_server(void *data) {
assert(server->video_socket != SC_SOCKET_NONE); assert(server->video_socket != SC_SOCKET_NONE);
net_interrupt(server->video_socket); net_interrupt(server->video_socket);
if (server->audio_socket != SC_SOCKET_NONE) {
// There is no audio_socket if --no-audio is set
net_interrupt(server->audio_socket);
}
if (server->control_socket != SC_SOCKET_NONE) { if (server->control_socket != SC_SOCKET_NONE) {
// There is no control_socket if --no-control is set // There is no control_socket if --no-control is set
net_interrupt(server->control_socket); net_interrupt(server->control_socket);
@@ -952,9 +896,6 @@ sc_server_destroy(struct sc_server *server) {
if (server->video_socket != SC_SOCKET_NONE) { if (server->video_socket != SC_SOCKET_NONE) {
net_close(server->video_socket); net_close(server->video_socket);
} }
if (server->audio_socket != SC_SOCKET_NONE) {
net_close(server->audio_socket);
}
if (server->control_socket != SC_SOCKET_NONE) { if (server->control_socket != SC_SOCKET_NONE) {
net_close(server->control_socket); net_close(server->control_socket);
} }

View File

@@ -25,7 +25,6 @@ struct sc_server_params {
uint32_t uid; 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;
@@ -38,7 +37,6 @@ struct sc_server_params {
int8_t lock_video_orientation; int8_t lock_video_orientation;
bool control; bool control;
uint32_t display_id; uint32_t display_id;
bool audio;
bool show_touches; bool show_touches;
bool stay_awake; bool stay_awake;
bool force_adb_forward; bool force_adb_forward;
@@ -70,7 +68,6 @@ struct sc_server {
struct sc_adb_tunnel tunnel; struct sc_adb_tunnel tunnel;
sc_socket video_socket; sc_socket video_socket;
sc_socket audio_socket;
sc_socket control_socket; sc_socket control_socket;
const struct sc_server_callbacks *cbs; const struct sc_server_callbacks *cbs;

View File

@@ -1,207 +0,0 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.ComponentName;
import android.content.Intent;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.AudioTimestamp;
import android.media.MediaCodec;
import android.media.MediaFormat;
import android.media.MediaRecorder;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.SystemClock;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicBoolean;
public final class AudioEncoder {
private static final String MIMETYPE = MediaFormat.MIMETYPE_AUDIO_OPUS;
private static final int SAMPLE_RATE = 48000;
private static final int CHANNELS = 2;
private static final int BIT_RATE = 128000;
private static int BUFFER_MS = 15; // milliseconds
private static final int BUFFER_SIZE = SAMPLE_RATE * CHANNELS * BUFFER_MS / 1000;
private AudioRecord recorder;
private MediaCodec mediaCodec;
private HandlerThread thread;
private final AtomicBoolean interrupted = new AtomicBoolean();
private final Semaphore endSemaphore = new Semaphore(0); // blocks until encoding is ended
private static AudioFormat createAudioFormat() {
AudioFormat.Builder builder = new AudioFormat.Builder();
builder.setEncoding(AudioFormat.ENCODING_PCM_16BIT);
builder.setSampleRate(SAMPLE_RATE);
builder.setChannelMask(CHANNELS == 2 ? AudioFormat.CHANNEL_IN_STEREO : AudioFormat.CHANNEL_IN_MONO);
return builder.build();
}
@TargetApi(Build.VERSION_CODES.M)
@SuppressLint({"WrongConstant", "MissingPermission"})
private static AudioRecord createAudioRecord() {
AudioRecord.Builder builder = new AudioRecord.Builder();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// On older APIs, Workarounds.fillAppInfo() must be called beforehand
builder.setContext(FakeContext.get());
}
builder.setAudioSource(MediaRecorder.AudioSource.REMOTE_SUBMIX);
builder.setAudioFormat(createAudioFormat());
builder.setBufferSizeInBytes(1024 * 1024);
return builder.build();
}
private static MediaFormat createFormat() {
MediaFormat format = new MediaFormat();
format.setString(MediaFormat.KEY_MIME, MIMETYPE);
format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);
format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, CHANNELS);
format.setInteger(MediaFormat.KEY_SAMPLE_RATE, SAMPLE_RATE);
return format;
}
@TargetApi(Build.VERSION_CODES.M)
public void start() throws IOException {
mediaCodec = MediaCodec.createEncoderByType(MIMETYPE); // may throw IOException
recorder = createAudioRecord();
MediaFormat format = createFormat();
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
recorder.startRecording();
thread = new HandlerThread("AudioEncoder");
thread.start();
class AudioEncoderCallbacks extends MediaCodec.Callback {
private final AudioTimestamp timestamp = new AudioTimestamp();
private long previousPts;
private long nextPts;
private boolean eofSignaled;
private boolean ended;
private void notifyEnded() {
assert !ended;
ended = true;
endSemaphore.release();
}
@TargetApi(Build.VERSION_CODES.N)
@Override
public void onInputBufferAvailable(MediaCodec codec, int index) {
if (eofSignaled) {
return;
}
ByteBuffer inputBuffer = codec.getInputBuffer(index);
int r = recorder.read(inputBuffer, BUFFER_SIZE);
long pts;
int ret = recorder.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC);
if (ret == AudioRecord.SUCCESS) {
pts = timestamp.nanoTime / 1000;
} else {
if (nextPts == 0) {
Ln.w("Could not get any audio timestamp");
}
// compute from previous timestamp and packet size
pts = nextPts;
}
long durationMs = r * 1000 / CHANNELS / SAMPLE_RATE;
nextPts = pts + durationMs;
int flags = 0;
if (interrupted.get()) {
flags = flags | MediaCodec.BUFFER_FLAG_END_OF_STREAM;
eofSignaled = true;
}
if (previousPts != 0 && pts < previousPts) {
// Audio PTS may come from two sources:
// - recorder.getTimestamp() if the call works;
// - an estimation from the previous PTS and the packet size as a fallback.
//
// Therefore, the property that PTS are monotonically increasing is no guaranteed in corner cases, so enforce it.
pts = previousPts + 1;
}
codec.queueInputBuffer(index, 0, r, pts, flags);
previousPts = pts;
}
@Override
public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo bufferInfo) {
if (ended) {
return;
}
ByteBuffer codecBuffer = codec.getOutputBuffer(index);
try {
boolean isConfig = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0;
long pts = bufferInfo.presentationTimeUs;
Ln.i("Audio packet: pts=" + pts + " " + codecBuffer.remaining() + " bytes");
} finally {
codec.releaseOutputBuffer(index, false);
}
boolean eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
if (eof) {
notifyEnded();
}
}
@Override
public void onError(MediaCodec codec, MediaCodec.CodecException e) {
Ln.e("MediaCodec error", e);
if (!ended) {
notifyEnded();
}
}
@Override
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
// ignore
}
}
mediaCodec.setCallback(new AudioEncoderCallbacks(), new Handler(thread.getLooper()));
mediaCodec.start();
}
private void waitEnded() {
try {
endSemaphore.acquire();
} catch (InterruptedException e) {
// ignore
}
}
public void stop() {
Ln.i("==== STOP");
if (thread != null) {
interrupted.set(true);
waitEnded();
thread.interrupt();
thread = null;
mediaCodec.stop();
mediaCodec.release();
recorder.stop();
Ln.i("==== STOPPED");
}
}
}

View File

@@ -24,8 +24,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 +62,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);
@@ -84,27 +82,6 @@ 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() { public DeviceMessageSender getSender() {
return sender; return sender;
} }

View File

@@ -20,9 +20,6 @@ public final class DesktopConnection implements Closeable {
private final LocalSocket videoSocket; private final LocalSocket videoSocket;
private final FileDescriptor videoFd; private final FileDescriptor videoFd;
private final LocalSocket audioSocket;
private final FileDescriptor audioFd;
private final LocalSocket controlSocket; private final LocalSocket controlSocket;
private final InputStream controlInputStream; private final InputStream controlInputStream;
private final OutputStream controlOutputStream; private final OutputStream controlOutputStream;
@@ -30,10 +27,9 @@ public final class DesktopConnection implements Closeable {
private final ControlMessageReader reader = new ControlMessageReader(); private final ControlMessageReader reader = new ControlMessageReader();
private final DeviceMessageWriter writer = new DeviceMessageWriter(); private final DeviceMessageWriter writer = new DeviceMessageWriter();
private DesktopConnection(LocalSocket videoSocket, LocalSocket audioSocket, LocalSocket controlSocket) throws IOException { private DesktopConnection(LocalSocket videoSocket, LocalSocket controlSocket) throws IOException {
this.videoSocket = videoSocket; this.videoSocket = videoSocket;
this.controlSocket = controlSocket; this.controlSocket = controlSocket;
this.audioSocket = audioSocket;
if (controlSocket != null) { if (controlSocket != null) {
controlInputStream = controlSocket.getInputStream(); controlInputStream = controlSocket.getInputStream();
controlOutputStream = controlSocket.getOutputStream(); controlOutputStream = controlSocket.getOutputStream();
@@ -42,7 +38,6 @@ public final class DesktopConnection implements Closeable {
controlOutputStream = null; controlOutputStream = null;
} }
videoFd = videoSocket.getFileDescriptor(); videoFd = videoSocket.getFileDescriptor();
audioFd = audioSocket != null ? audioSocket.getFileDescriptor() : null;
} }
private static LocalSocket connect(String abstractName) throws IOException { private static LocalSocket connect(String abstractName) throws IOException {
@@ -60,47 +55,40 @@ public final class DesktopConnection implements Closeable {
return SOCKET_NAME_PREFIX + String.format("_%08x", uid); return SOCKET_NAME_PREFIX + String.format("_%08x", uid);
} }
public static DesktopConnection open(int uid, boolean tunnelForward, boolean audio, boolean control, boolean sendDummyByte) throws IOException { public static DesktopConnection open(int uid, boolean tunnelForward, boolean control, boolean sendDummyByte) throws IOException {
String socketName = getSocketName(uid); String socketName = getSocketName(uid);
LocalSocket videoSocket = null; LocalSocket videoSocket;
LocalSocket audioSocket = null;
LocalSocket controlSocket = null; LocalSocket controlSocket = null;
try { if (tunnelForward) {
if (tunnelForward) { try (LocalServerSocket localServerSocket = new LocalServerSocket(socketName)) {
try (LocalServerSocket localServerSocket = new LocalServerSocket(socketName)) { 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 videoSocket.getOutputStream().write(0);
videoSocket.getOutputStream().write(0);
}
if (audio) {
audioSocket = localServerSocket.accept();
}
if (control) {
controlSocket = localServerSocket.accept();
}
} }
} else {
videoSocket = connect(socketName);
if (control) { if (control) {
controlSocket = connect(socketName); try {
controlSocket = localServerSocket.accept();
} catch (IOException | RuntimeException e) {
videoSocket.close();
throw e;
}
} }
} }
} catch (IOException | RuntimeException e) { } else {
if (videoSocket != null) { videoSocket = connect(socketName);
videoSocket.close(); if (control) {
try {
controlSocket = connect(socketName);
} catch (IOException | RuntimeException e) {
videoSocket.close();
throw e;
}
} }
if (audioSocket != null) {
audioSocket.close();
}
if (controlSocket != null) {
controlSocket.close();
}
throw e;
} }
return new DesktopConnection(videoSocket, audioSocket, controlSocket); return new DesktopConnection(videoSocket, controlSocket);
} }
public void close() throws IOException { public void close() throws IOException {
@@ -133,10 +121,6 @@ public final class DesktopConnection implements Closeable {
return videoFd; return videoFd;
} }
public FileDescriptor getAudioFd() {
return audioFd;
}
public ControlMessage receiveControlMessage() throws IOException { public ControlMessage receiveControlMessage() throws IOException {
ControlMessage msg = reader.next(); ControlMessage msg = reader.next();
while (msg == null) { while (msg == null) {

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,7 +24,7 @@ public final class DeviceMessageSender {
notify(); notify();
} }
private void loop() throws IOException, InterruptedException { public void loop() throws IOException, InterruptedException {
while (!Thread.currentThread().isInterrupted()) { while (!Thread.currentThread().isInterrupted()) {
String text; String text;
long sequence; long sequence;
@@ -51,22 +49,4 @@ 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;
}
}
} }

View File

@@ -5,13 +5,9 @@ import android.graphics.Rect;
import java.util.List; import java.util.List;
public class Options { public class Options {
private static final String VIDEO_CODEC_H264 = "h264";
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 uid = -1; // 31-bit non-negative value, or -1
private boolean audio = true;
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;
@@ -33,7 +29,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;
@@ -51,14 +46,6 @@ public class Options {
this.uid = uid; this.uid = uid;
} }
public boolean getAudio() {
return audio;
}
public void setAudio(boolean audio) {
this.audio = audio;
}
public int getMaxSize() { public int getMaxSize() {
return maxSize; return maxSize;
} }
@@ -67,14 +54,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;
} }
@@ -226,12 +205,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

@@ -12,6 +12,7 @@ import android.os.IBinder;
import android.os.SystemClock; 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,10 +22,6 @@ 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";
@@ -33,20 +30,26 @@ public class ScreenEncoder implements Device.RotationListener {
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 int MAX_CONSECUTIVE_ERRORS = 3;
private final AtomicBoolean rotationChanged = new AtomicBoolean(); 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 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; 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,9 +66,9 @@ 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); MediaCodec codec = createCodec(encoderName);
MediaFormat format = createFormat(videoMimeType, bitRate, maxFps, codecOptions); MediaFormat format = createFormat(bitRate, maxFps, codecOptions);
IBinder display = createDisplay(); IBinder display = createDisplay();
device.setRotationListener(this); device.setRotationListener(this);
boolean alive; boolean alive;
@@ -92,7 +95,7 @@ public class ScreenEncoder implements Device.RotationListener {
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) {
@@ -161,30 +164,31 @@ 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());
}
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 // If this is not a config packet, then it contains a frame
firstFrameSent = true; firstFrameSent = true;
consecutiveErrors = 0; consecutiveErrors = 0;
} }
callbacks.onPacket(codecBuffer, bufferInfo);
} }
} finally { } finally {
if (outputBufferId >= 0) { if (outputBufferId >= 0) {
@@ -196,28 +200,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 +265,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);

View File

@@ -66,74 +66,51 @@ public final class Server {
Thread initThread = startInitThread(options); Thread initThread = startInitThread(options);
int uid = options.getUid();
boolean tunnelForward = options.isTunnelForward();
boolean control = options.getControl();
boolean audio = options.getAudio();
boolean sendDummyByte = options.getSendDummyByte();
Workarounds.prepareMainLooper(); 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/240> // <https://github.com/Genymobile/scrcpy/issues/2656>
// - <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>
boolean mustFillAppInfo = Build.BRAND.equalsIgnoreCase("meizu");
// Before Android 11, audio is not supported.
// Since Android 12, we can properly set a context on the AudioRecord.
// Only on Android 11 we must fill app info for the AudioRecord to work.
mustFillAppInfo |= audio && Build.VERSION.SDK_INT == Build.VERSION_CODES.R;
if (mustFillAppInfo) {
Workarounds.fillAppInfo(); Workarounds.fillAppInfo();
} }
try (DesktopConnection connection = DesktopConnection.open(uid, tunnelForward, audio, control, sendDummyByte)) { int uid = options.getUid();
VideoCodec codec = options.getCodec(); boolean tunnelForward = options.isTunnelForward();
boolean control = options.getControl();
boolean sendDummyByte = options.getSendDummyByte();
try (DesktopConnection connection = DesktopConnection.open(uid, tunnelForward, control, sendDummyByte)) {
if (options.getSendDeviceMeta()) { if (options.getSendDeviceMeta()) {
Size videoSize = device.getScreenInfo().getVideoSize(); Size videoSize = device.getScreenInfo().getVideoSize();
connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight()); connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
} }
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());
AudioEncoder audioEncoder = null; device.setClipboardListener(text -> controller.getSender().pushClipboardText(text));
if (audio) {
audioEncoder = new AudioEncoder();
audioEncoder.start();
} }
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
Ln.d("Screen streaming stopped"); Ln.d("Screen streaming stopped");
} finally { } finally {
initThread.interrupt(); initThread.interrupt();
if (controller != null) { if (controllerThread != null) {
controller.stop(); controllerThread.interrupt();
} }
if (audioEncoder != null) { if (deviceMessageSenderThread != null) {
audioEncoder.stop(); deviceMessageSenderThread.interrupt();
} }
} }
} }
@@ -145,6 +122,32 @@ public final class Server {
return thread; 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) { private static Options createOptions(String... args) {
if (args.length < 1) { if (args.length < 1) {
throw new IllegalArgumentException("Missing client version"); throw new IllegalArgumentException("Missing client version");
@@ -178,17 +181,6 @@ public final class Server {
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 "audio":
boolean audio = Boolean.parseBoolean(value);
options.setAudio(audio);
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);
@@ -270,17 +262,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:

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;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Application; import android.app.Application;
import android.app.Instrumentation; import android.app.Instrumentation;
import android.content.ContextWrapper;
import android.content.pm.ApplicationInfo; import android.content.pm.ApplicationInfo;
import android.os.Looper; import android.os.Looper;
@@ -61,10 +60,7 @@ public final class Workarounds {
mBoundApplicationField.setAccessible(true); mBoundApplicationField.setAccessible(true);
mBoundApplicationField.set(activityThread, appBindData); mBoundApplicationField.set(activityThread, appBindData);
Application app = Application.class.newInstance(); Application app = Instrumentation.newApplication(Application.class, FakeContext.get());
Field baseField = ContextWrapper.class.getDeclaredField("mBase");
baseField.setAccessible(true);
baseField.set(app, FakeContext.get());
// activityThread.mInitialApplication = app; // activityThread.mInitialApplication = app;
Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication"); Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");