Compare commits
11 Commits
fakecontex
...
codec
Author | SHA1 | Date | |
---|---|---|---|
d2dce51038 | |||
4342c5637d | |||
3e517cd40e | |||
f70f6cdd3e | |||
87972e2022 | |||
3aac74e9e9 | |||
1c82c3923d | |||
36d656e91f | |||
bdbf1f4eb7 | |||
4177de5880 | |||
6a07e3d470 |
21
README.md
21
README.md
@ -252,10 +252,22 @@ This affects recording orientation.
|
|||||||
The [window may also be rotated](#rotation) independently.
|
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
|
The video codec can be selected. The possible values are `h264` (default),
|
||||||
crash. It is possible to select a different encoder:
|
`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
|
```bash
|
||||||
scrcpy --encoder=OMX.qcom.video.encoder.avc
|
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:
|
error will give the available encoders:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
scrcpy --encoder=_
|
scrcpy --encoder=_ # for the default codec
|
||||||
|
scrcpy --codec=h265 --encoder=_ # for a specific codec
|
||||||
```
|
```
|
||||||
|
|
||||||
### Capture
|
### 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.
|
Default is 8000000.
|
||||||
|
|
||||||
.TP
|
.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.
|
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'.
|
||||||
@ -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.
|
This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically.
|
||||||
|
|
||||||
.TP
|
.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.
|
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".
|
||||||
@ -199,7 +203,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[:port]
|
.BI "\-p, \-\-port " port\fR[:\fIport\fR]
|
||||||
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.
|
||||||
@ -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.
|
The device serial number. Mandatory only if several devices are connected to adb.
|
||||||
|
|
||||||
.TP
|
.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".
|
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 ','.
|
||||||
@ -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).
|
Default is "lalt,lsuper" (left-Alt or left-Super).
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-tcpip[=ip[:port]]
|
.BI "\-\-tcpip\fR[=\fIip\fR[:\fIport\fR]]
|
||||||
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).
|
||||||
|
@ -57,6 +57,7 @@
|
|||||||
#define OPT_NO_CLEANUP 1037
|
#define OPT_NO_CLEANUP 1037
|
||||||
#define OPT_PRINT_FPS 1038
|
#define OPT_PRINT_FPS 1038
|
||||||
#define OPT_NO_POWER_ON 1039
|
#define OPT_NO_POWER_ON 1039
|
||||||
|
#define OPT_CODEC 1040
|
||||||
|
|
||||||
struct sc_option {
|
struct sc_option {
|
||||||
char shortopt;
|
char shortopt;
|
||||||
@ -105,6 +106,12 @@ 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",
|
||||||
@ -1377,6 +1384,24 @@ 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) {
|
||||||
@ -1610,6 +1635,11 @@ 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;
|
||||||
@ -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->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");
|
||||||
|
@ -17,6 +17,31 @@
|
|||||||
|
|
||||||
#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
|
||||||
@ -171,7 +196,13 @@ static int
|
|||||||
run_demuxer(void *data) {
|
run_demuxer(void *data) {
|
||||||
struct sc_demuxer *demuxer = 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) {
|
if (!codec) {
|
||||||
LOGE("H.264 decoder not found");
|
LOGE("H.264 decoder not found");
|
||||||
goto end;
|
goto end;
|
||||||
@ -188,7 +219,7 @@ run_demuxer(void *data) {
|
|||||||
goto finally_free_codec_ctx;
|
goto finally_free_codec_ctx;
|
||||||
}
|
}
|
||||||
|
|
||||||
demuxer->parser = av_parser_init(AV_CODEC_ID_H264);
|
demuxer->parser = av_parser_init(codec_id);
|
||||||
if (!demuxer->parser) {
|
if (!demuxer->parser) {
|
||||||
LOGE("Could not initialize parser");
|
LOGE("Could not initialize parser");
|
||||||
goto finally_close_sinks;
|
goto finally_close_sinks;
|
||||||
|
@ -13,6 +13,7 @@ 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 = {
|
||||||
|
@ -23,6 +23,12 @@ 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
|
||||||
@ -93,6 +99,7 @@ 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;
|
||||||
|
@ -11,6 +11,8 @@
|
|||||||
/** 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 *
|
||||||
@ -169,6 +171,18 @@ 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;
|
||||||
@ -243,6 +257,7 @@ 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);
|
||||||
|
@ -28,6 +28,8 @@ 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;
|
||||||
|
@ -315,6 +315,7 @@ 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,
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
#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"
|
||||||
@ -155,6 +156,20 @@ 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) {
|
||||||
@ -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("log_level=%s", log_level_to_server_string(params->log_level));
|
||||||
ADD_PARAM("bit_rate=%" PRIu32, params->bit_rate);
|
ADD_PARAM("bit_rate=%" PRIu32, params->bit_rate);
|
||||||
|
|
||||||
|
if (params->codec != SC_CODEC_H264) {
|
||||||
|
ADD_PARAM("codec=%s", sc_server_get_codec_name(params->codec));
|
||||||
|
}
|
||||||
if (params->max_size) {
|
if (params->max_size) {
|
||||||
ADD_PARAM("max_size=%" PRIu16, params->max_size);
|
ADD_PARAM("max_size=%" PRIu16, params->max_size);
|
||||||
}
|
}
|
||||||
@ -398,10 +416,9 @@ 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));
|
||||||
|
|
||||||
info->frame_size.width = (buf[SC_DEVICE_NAME_FIELD_LENGTH] << 8)
|
unsigned char *fields = &buf[SC_DEVICE_NAME_FIELD_LENGTH];
|
||||||
| buf[SC_DEVICE_NAME_FIELD_LENGTH + 1];
|
info->frame_size.width = sc_read16be(fields);
|
||||||
info->frame_size.height = (buf[SC_DEVICE_NAME_FIELD_LENGTH + 2] << 8)
|
info->frame_size.height = sc_read16be(&fields[2]);
|
||||||
| buf[SC_DEVICE_NAME_FIELD_LENGTH + 3];
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ 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;
|
||||||
|
@ -24,6 +24,8 @@ 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;
|
||||||
@ -62,7 +64,7 @@ public class Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void control() throws IOException {
|
private 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);
|
||||||
@ -82,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() {
|
public DeviceMessageSender getSender() {
|
||||||
return sender;
|
return sender;
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,8 @@ 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;
|
||||||
@ -24,7 +26,7 @@ public final class DeviceMessageSender {
|
|||||||
notify();
|
notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void loop() throws IOException, InterruptedException {
|
private void loop() throws IOException, InterruptedException {
|
||||||
while (!Thread.currentThread().isInterrupted()) {
|
while (!Thread.currentThread().isInterrupted()) {
|
||||||
String text;
|
String text;
|
||||||
long sequence;
|
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;
|
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 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;
|
||||||
@ -29,6 +32,7 @@ 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;
|
||||||
@ -54,6 +58,14 @@ 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;
|
||||||
}
|
}
|
||||||
@ -205,4 +217,12 @@ 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,6 @@ 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;
|
||||||
@ -22,6 +21,10 @@ 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";
|
||||||
@ -30,26 +33,20 @@ 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 static final long PACKET_FLAG_CONFIG = 1L << 63;
|
|
||||||
private static final long PACKET_FLAG_KEY_FRAME = 1L << 62;
|
|
||||||
|
|
||||||
private final AtomicBoolean rotationChanged = new AtomicBoolean();
|
private final AtomicBoolean rotationChanged = new AtomicBoolean();
|
||||||
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
|
|
||||||
|
|
||||||
|
private final String videoMimeType;
|
||||||
private final String encoderName;
|
private final String encoderName;
|
||||||
private final List<CodecOption> codecOptions;
|
private final List<CodecOption> codecOptions;
|
||||||
private final int bitRate;
|
private final int bitRate;
|
||||||
private final int maxFps;
|
private final int maxFps;
|
||||||
private final boolean sendFrameMeta;
|
|
||||||
private final boolean downsizeOnError;
|
private final boolean downsizeOnError;
|
||||||
private long ptsOrigin;
|
|
||||||
|
|
||||||
private boolean firstFrameSent;
|
private boolean firstFrameSent;
|
||||||
private int consecutiveErrors;
|
private int consecutiveErrors;
|
||||||
|
|
||||||
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List<CodecOption> codecOptions, String encoderName,
|
public ScreenEncoder(String videoMimeType, int bitRate, int maxFps, List<CodecOption> codecOptions, String encoderName, boolean downsizeOnError) {
|
||||||
boolean downsizeOnError) {
|
this.videoMimeType = videoMimeType;
|
||||||
this.sendFrameMeta = sendFrameMeta;
|
|
||||||
this.bitRate = bitRate;
|
this.bitRate = bitRate;
|
||||||
this.maxFps = maxFps;
|
this.maxFps = maxFps;
|
||||||
this.codecOptions = codecOptions;
|
this.codecOptions = codecOptions;
|
||||||
@ -66,20 +63,9 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
return rotationChanged.getAndSet(false);
|
return rotationChanged.getAndSet(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void streamScreen(Device device, FileDescriptor fd) throws IOException {
|
public void streamScreen(Device device, Callbacks callbacks) throws IOException {
|
||||||
Workarounds.prepareMainLooper();
|
MediaCodec codec = createCodec(videoMimeType, encoderName);
|
||||||
if (Build.BRAND.equalsIgnoreCase("meizu")) {
|
MediaFormat format = createFormat(videoMimeType, bitRate, maxFps, codecOptions);
|
||||||
// <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);
|
|
||||||
IBinder display = createDisplay();
|
IBinder display = createDisplay();
|
||||||
device.setRotationListener(this);
|
device.setRotationListener(this);
|
||||||
boolean alive;
|
boolean alive;
|
||||||
@ -106,7 +92,7 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
|
|
||||||
codec.start();
|
codec.start();
|
||||||
|
|
||||||
alive = encode(codec, fd);
|
alive = encode(codec, callbacks);
|
||||||
// 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) {
|
||||||
@ -175,31 +161,30 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean encode(MediaCodec codec, FileDescriptor fd) throws IOException {
|
private boolean encode(MediaCodec codec, Callbacks callbacks) 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);
|
||||||
|
|
||||||
if (sendFrameMeta) {
|
boolean isConfig = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0;
|
||||||
writeFrameMeta(fd, bufferInfo, codecBuffer.remaining());
|
if (!isConfig) {
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
||||||
@ -211,50 +196,28 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
return !eof;
|
return !eof;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void writeFrameMeta(FileDescriptor fd, MediaCodec.BufferInfo bufferInfo, int packetSize) throws IOException {
|
private static MediaCodecInfo[] listEncoders(String videoMimeType) {
|
||||||
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(MediaFormat.MIMETYPE_VIDEO_AVC)) {
|
if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(videoMimeType)) {
|
||||||
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 encoderName) throws IOException {
|
private static MediaCodec createCodec(String videoMimeType, 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();
|
MediaCodecInfo[] encoders = listEncoders(videoMimeType);
|
||||||
throw new InvalidEncoderException(encoderName, encoders);
|
throw new InvalidEncoderException(encoderName, encoders);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MediaCodec codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
|
MediaCodec codec = MediaCodec.createEncoderByType(videoMimeType);
|
||||||
Ln.d("Using encoder: '" + codec.getName() + "'");
|
Ln.d("Using encoder: '" + codec.getName() + "'");
|
||||||
return codec;
|
return codec;
|
||||||
}
|
}
|
||||||
@ -276,9 +239,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(int bitRate, int maxFps, List<CodecOption> codecOptions) {
|
private static MediaFormat createFormat(String videoMimeType, int bitRate, int maxFps, List<CodecOption> codecOptions) {
|
||||||
MediaFormat format = new MediaFormat();
|
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);
|
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);
|
||||||
|
@ -71,39 +71,51 @@ public final class Server {
|
|||||||
boolean control = options.getControl();
|
boolean control = options.getControl();
|
||||||
boolean sendDummyByte = options.getSendDummyByte();
|
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)) {
|
try (DesktopConnection connection = DesktopConnection.open(uid, tunnelForward, control, sendDummyByte)) {
|
||||||
|
VideoCodec codec = options.getCodec();
|
||||||
if (options.getSendDeviceMeta()) {
|
if (options.getSendDeviceMeta()) {
|
||||||
Size videoSize = device.getScreenInfo().getVideoSize();
|
Size videoSize = device.getScreenInfo().getVideoSize();
|
||||||
connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
|
connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
|
||||||
}
|
}
|
||||||
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions,
|
ScreenEncoder screenEncoder = new ScreenEncoder(codec.getMimeType(), options.getBitRate(), options.getMaxFps(), codecOptions,
|
||||||
options.getEncoderName(), options.getDownsizeOnError());
|
options.getEncoderName(), options.getDownsizeOnError());
|
||||||
|
|
||||||
Thread controllerThread = null;
|
Controller controller = null;
|
||||||
Thread deviceMessageSenderThread = null;
|
|
||||||
if (control) {
|
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
|
final Controller controllerRef = controller;
|
||||||
controllerThread = startController(controller);
|
device.setClipboardListener(text -> controllerRef.getSender().pushClipboardText(text));
|
||||||
deviceMessageSenderThread = startDeviceMessageSender(controller.getSender());
|
|
||||||
|
|
||||||
device.setClipboardListener(text -> controller.getSender().pushClipboardText(text));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// synchronous
|
// 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) {
|
} 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 (controllerThread != null) {
|
if (controller != null) {
|
||||||
controllerThread.interrupt();
|
controller.stop();
|
||||||
}
|
|
||||||
if (deviceMessageSenderThread != null) {
|
|
||||||
deviceMessageSenderThread.interrupt();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -115,32 +127,6 @@ 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");
|
||||||
@ -174,6 +160,13 @@ 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 "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);
|
||||||
@ -255,12 +248,17 @@ 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:
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user