Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e93fd94842 | ||
|
|
e131b9c667 | ||
|
|
e1a009de5d | ||
|
|
7d4aedc29a | ||
|
|
fa1178195e | ||
|
|
5dcce5f241 | ||
|
|
3f58dd22dc | ||
|
|
cbd75ff1fb | ||
|
|
39cdf68150 | ||
|
|
3492c5c963 | ||
|
|
35f4040a78 | ||
|
|
fd7d6f3822 | ||
|
|
6c3e7f6b45 | ||
|
|
94d90927ad | ||
|
|
93504f5f34 | ||
|
|
a52b1f025d | ||
|
|
46759d3f63 | ||
|
|
e37f9a4d7c | ||
|
|
842fdccaa9 | ||
|
|
8cdae30b01 | ||
|
|
30b8d140e8 | ||
|
|
4f986d4bbb | ||
|
|
6524e90c68 | ||
|
|
f2dee20a20 | ||
|
|
d2dce51038 | ||
|
|
4342c5637d | ||
|
|
3e517cd40e |
31
README.md
31
README.md
@@ -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):
|
||||||
@@ -264,7 +252,19 @@ This affects recording orientation.
|
|||||||
The [window may also be rotated](#rotation) independently.
|
The [window may also be rotated](#rotation) independently.
|
||||||
|
|
||||||
|
|
||||||
#### Encoder
|
#### 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
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
##### Encoder
|
||||||
|
|
||||||
Some devices have more than one encoder for a specific codec, and some of them
|
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:
|
may cause issues or crash. It is possible to select a different encoder:
|
||||||
@@ -277,11 +277,10 @@ 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
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that you can also select a different [codec](#select-codec).
|
|
||||||
|
|
||||||
### Capture
|
### Capture
|
||||||
|
|
||||||
#### Recording
|
#### Recording
|
||||||
|
|||||||
@@ -58,6 +58,7 @@
|
|||||||
#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_CODEC 1040
|
||||||
|
#define OPT_NO_AUDIO 1041
|
||||||
|
|
||||||
struct sc_option {
|
struct sc_option {
|
||||||
char shortopt;
|
char shortopt;
|
||||||
@@ -298,6 +299,11 @@ 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",
|
||||||
@@ -1626,6 +1632,9 @@ 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;
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ sc_demuxer_recv_codec_id(struct sc_demuxer *demuxer) {
|
|||||||
#define SC_CODEC_ID_H264 UINT32_C(0x68323634) // "h264" in ASCII
|
#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_H265 UINT32_C(0x68323635) // "h265" in ASCII
|
||||||
#define SC_CODEC_ID_AV1 UINT32_C(0x00617631) // "av1" in ASCII
|
#define SC_CODEC_ID_AV1 UINT32_C(0x00617631) // "av1" in ASCII
|
||||||
|
#define SC_CODEC_ID_OPUS UINT32_C(0x6f707573) // "opus" in ASCII
|
||||||
uint32_t codec_id = sc_read32be(data);
|
uint32_t codec_id = sc_read32be(data);
|
||||||
switch (codec_id) {
|
switch (codec_id) {
|
||||||
case SC_CODEC_ID_H264:
|
case SC_CODEC_ID_H264:
|
||||||
@@ -36,6 +37,8 @@ sc_demuxer_recv_codec_id(struct sc_demuxer *demuxer) {
|
|||||||
return AV_CODEC_ID_HEVC;
|
return AV_CODEC_ID_HEVC;
|
||||||
case SC_CODEC_ID_AV1:
|
case SC_CODEC_ID_AV1:
|
||||||
return AV_CODEC_ID_AV1;
|
return AV_CODEC_ID_AV1;
|
||||||
|
case SC_CODEC_ID_OPUS:
|
||||||
|
return AV_CODEC_ID_OPUS;
|
||||||
default:
|
default:
|
||||||
LOGE("Unknown codec id 0x%08" PRIx32, codec_id);
|
LOGE("Unknown codec id 0x%08" PRIx32, codec_id);
|
||||||
return AV_CODEC_ID_NONE;
|
return AV_CODEC_ID_NONE;
|
||||||
@@ -270,8 +273,12 @@ end:
|
|||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
sc_demuxer_init(struct sc_demuxer *demuxer, sc_socket socket,
|
sc_demuxer_init(struct sc_demuxer *demuxer, enum sc_stream_id stream_id,
|
||||||
const struct sc_demuxer_callbacks *cbs, void *cbs_userdata) {
|
sc_socket socket, const struct sc_demuxer_callbacks *cbs,
|
||||||
|
void *cbs_userdata) {
|
||||||
|
assert(socket != SC_SOCKET_NONE);
|
||||||
|
|
||||||
|
demuxer->stream_id = stream_id;
|
||||||
demuxer->socket = socket;
|
demuxer->socket = socket;
|
||||||
demuxer->pending = NULL;
|
demuxer->pending = NULL;
|
||||||
demuxer->sink_count = 0;
|
demuxer->sink_count = 0;
|
||||||
|
|||||||
@@ -14,7 +14,13 @@
|
|||||||
|
|
||||||
#define SC_DEMUXER_MAX_SINKS 2
|
#define SC_DEMUXER_MAX_SINKS 2
|
||||||
|
|
||||||
|
enum sc_stream_id {
|
||||||
|
SC_STREAM_ID_VIDEO,
|
||||||
|
SC_STREAM_ID_AUDIO,
|
||||||
|
};
|
||||||
|
|
||||||
struct sc_demuxer {
|
struct sc_demuxer {
|
||||||
|
enum sc_stream_id stream_id;
|
||||||
sc_socket socket;
|
sc_socket socket;
|
||||||
sc_thread thread;
|
sc_thread thread;
|
||||||
|
|
||||||
@@ -36,8 +42,9 @@ struct sc_demuxer_callbacks {
|
|||||||
};
|
};
|
||||||
|
|
||||||
void
|
void
|
||||||
sc_demuxer_init(struct sc_demuxer *demuxer, sc_socket socket,
|
sc_demuxer_init(struct sc_demuxer *demuxer, enum sc_stream_id stream_id,
|
||||||
const struct sc_demuxer_callbacks *cbs, void *cbs_userdata);
|
sc_socket socket, const struct sc_demuxer_callbacks *cbs,
|
||||||
|
void *cbs_userdata);
|
||||||
|
|
||||||
void
|
void
|
||||||
sc_demuxer_add_sink(struct sc_demuxer *demuxer, struct sc_packet_sink *sink);
|
sc_demuxer_add_sink(struct sc_demuxer *demuxer, struct sc_packet_sink *sink);
|
||||||
|
|||||||
@@ -66,4 +66,5 @@ 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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ 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;
|
||||||
|
|||||||
@@ -40,7 +40,8 @@
|
|||||||
struct scrcpy {
|
struct scrcpy {
|
||||||
struct sc_server server;
|
struct sc_server server;
|
||||||
struct sc_screen screen;
|
struct sc_screen screen;
|
||||||
struct sc_demuxer demuxer;
|
struct sc_demuxer video_demuxer;
|
||||||
|
struct sc_demuxer audio_demuxer;
|
||||||
struct sc_decoder decoder;
|
struct sc_decoder decoder;
|
||||||
struct sc_recorder recorder;
|
struct sc_recorder recorder;
|
||||||
#ifdef HAVE_V4L2
|
#ifdef HAVE_V4L2
|
||||||
@@ -233,13 +234,21 @@ av_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
sc_demuxer_on_eos(struct sc_demuxer *demuxer, void *userdata) {
|
sc_video_demuxer_on_eos(struct sc_demuxer *demuxer, void *userdata) {
|
||||||
(void) demuxer;
|
(void) demuxer;
|
||||||
(void) userdata;
|
(void) userdata;
|
||||||
|
|
||||||
PUSH_EVENT(EVENT_STREAM_STOPPED);
|
PUSH_EVENT(EVENT_STREAM_STOPPED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
sc_audio_demuxer_on_eos(struct sc_demuxer *demuxer, void *userdata) {
|
||||||
|
(void) demuxer;
|
||||||
|
(void) userdata;
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
sc_server_on_connection_failed(struct sc_server *server, void *userdata) {
|
sc_server_on_connection_failed(struct sc_server *server, void *userdata) {
|
||||||
(void) server;
|
(void) server;
|
||||||
@@ -295,7 +304,8 @@ scrcpy(struct scrcpy_options *options) {
|
|||||||
#ifdef HAVE_V4L2
|
#ifdef HAVE_V4L2
|
||||||
bool v4l2_sink_initialized = false;
|
bool v4l2_sink_initialized = false;
|
||||||
#endif
|
#endif
|
||||||
bool demuxer_started = false;
|
bool video_demuxer_started = false;
|
||||||
|
bool audio_demuxer_started = false;
|
||||||
#ifdef HAVE_USB
|
#ifdef HAVE_USB
|
||||||
bool aoa_hid_initialized = false;
|
bool aoa_hid_initialized = false;
|
||||||
bool hid_keyboard_initialized = false;
|
bool hid_keyboard_initialized = false;
|
||||||
@@ -326,6 +336,7 @@ 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,
|
||||||
@@ -420,17 +431,26 @@ scrcpy(struct scrcpy_options *options) {
|
|||||||
|
|
||||||
av_log_set_callback(av_log_callback);
|
av_log_set_callback(av_log_callback);
|
||||||
|
|
||||||
static const struct sc_demuxer_callbacks demuxer_cbs = {
|
static const struct sc_demuxer_callbacks video_demuxer_cbs = {
|
||||||
.on_eos = sc_demuxer_on_eos,
|
.on_eos = sc_video_demuxer_on_eos,
|
||||||
};
|
};
|
||||||
sc_demuxer_init(&s->demuxer, s->server.video_socket, &demuxer_cbs, NULL);
|
sc_demuxer_init(&s->video_demuxer, SC_STREAM_ID_VIDEO,
|
||||||
|
s->server.video_socket, &video_demuxer_cbs, NULL);
|
||||||
|
|
||||||
|
if (options->audio) {
|
||||||
|
static const struct sc_demuxer_callbacks audio_demuxer_cbs = {
|
||||||
|
.on_eos = sc_audio_demuxer_on_eos,
|
||||||
|
};
|
||||||
|
sc_demuxer_init(&s->audio_demuxer, SC_STREAM_ID_AUDIO,
|
||||||
|
s->server.audio_socket, &audio_demuxer_cbs, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
if (dec) {
|
if (dec) {
|
||||||
sc_demuxer_add_sink(&s->demuxer, &dec->packet_sink);
|
sc_demuxer_add_sink(&s->video_demuxer, &dec->packet_sink);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rec) {
|
if (rec) {
|
||||||
sc_demuxer_add_sink(&s->demuxer, &rec->packet_sink);
|
sc_demuxer_add_sink(&s->video_demuxer, &rec->packet_sink);
|
||||||
}
|
}
|
||||||
|
|
||||||
struct sc_controller *controller = NULL;
|
struct sc_controller *controller = NULL;
|
||||||
@@ -638,17 +658,24 @@ aoa_hid_end:
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
// now we consumed the header values, the socket receives the video stream
|
// now we consumed the header values, the socket receives the video stream
|
||||||
// start the demuxer
|
// start the video demuxer
|
||||||
if (!sc_demuxer_start(&s->demuxer)) {
|
if (!sc_demuxer_start(&s->video_demuxer)) {
|
||||||
goto end;
|
goto end;
|
||||||
}
|
}
|
||||||
demuxer_started = true;
|
video_demuxer_started = true;
|
||||||
|
|
||||||
|
if (options->audio) {
|
||||||
|
if (!sc_demuxer_start(&s->audio_demuxer)) {
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
audio_demuxer_started = true;
|
||||||
|
}
|
||||||
|
|
||||||
ret = event_loop(s);
|
ret = event_loop(s);
|
||||||
LOGD("quit...");
|
LOGD("quit...");
|
||||||
|
|
||||||
// Close the window immediately on closing, because screen_destroy() may
|
// Close the window immediately on closing, because screen_destroy() may
|
||||||
// only be called once the demuxer thread is joined (it may take time)
|
// only be called once the video demuxer thread is joined (it may take time)
|
||||||
sc_screen_hide_window(&s->screen);
|
sc_screen_hide_window(&s->screen);
|
||||||
|
|
||||||
end:
|
end:
|
||||||
@@ -686,8 +713,12 @@ end:
|
|||||||
|
|
||||||
// now that the sockets are shutdown, the demuxer and controller are
|
// now that the sockets are shutdown, the demuxer and controller are
|
||||||
// interrupted, we can join them
|
// interrupted, we can join them
|
||||||
if (demuxer_started) {
|
if (video_demuxer_started) {
|
||||||
sc_demuxer_join(&s->demuxer);
|
sc_demuxer_join(&s->video_demuxer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audio_demuxer_started) {
|
||||||
|
sc_demuxer_join(&s->audio_demuxer);
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef HAVE_V4L2
|
#ifdef HAVE_V4L2
|
||||||
@@ -706,8 +737,9 @@ end:
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Destroy the screen only after the demuxer is guaranteed to be finished,
|
// Destroy the screen only after the video demuxer is guaranteed to be
|
||||||
// because otherwise the screen could receive new frames after destruction
|
// finished, because otherwise the screen could receive new frames after
|
||||||
|
// destruction
|
||||||
if (screen_initialized) {
|
if (screen_initialized) {
|
||||||
sc_screen_join(&s->screen);
|
sc_screen_join(&s->screen);
|
||||||
sc_screen_destroy(&s->screen);
|
sc_screen_destroy(&s->screen);
|
||||||
|
|||||||
@@ -217,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->audio) {
|
||||||
|
ADD_PARAM("audio=false");
|
||||||
|
}
|
||||||
if (params->codec != SC_CODEC_H264) {
|
if (params->codec != SC_CODEC_H264) {
|
||||||
ADD_PARAM("codec=%s", sc_server_get_codec_name(params->codec));
|
ADD_PARAM("codec=%s", sc_server_get_codec_name(params->codec));
|
||||||
}
|
}
|
||||||
@@ -388,6 +391,7 @@ 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);
|
||||||
@@ -431,9 +435,11 @@ 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);
|
||||||
@@ -441,6 +447,14 @@ 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);
|
||||||
@@ -467,6 +481,18 @@ 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
|
||||||
@@ -493,9 +519,11 @@ 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;
|
||||||
@@ -507,6 +535,12 @@ 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");
|
||||||
@@ -852,6 +886,11 @@ 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);
|
||||||
@@ -913,6 +952,9 @@ 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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ 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;
|
||||||
@@ -69,6 +70,7 @@ 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;
|
||||||
|
|||||||
46
server/src/main/java/com/genymobile/scrcpy/AudioCodec.java
Normal file
46
server/src/main/java/com/genymobile/scrcpy/AudioCodec.java
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
|
import android.media.MediaFormat;
|
||||||
|
|
||||||
|
public enum AudioCodec implements Codec {
|
||||||
|
OPUS(0x6f_70_75_73, "opus", MediaFormat.MIMETYPE_AUDIO_OPUS);
|
||||||
|
|
||||||
|
private final int id; // 4-byte ASCII representation of the name
|
||||||
|
private final String name;
|
||||||
|
private final String mimeType;
|
||||||
|
|
||||||
|
AudioCodec(int id, String name, String mimeType) {
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
this.mimeType = mimeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Type getType() {
|
||||||
|
return Type.VIDEO;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMimeType() {
|
||||||
|
return mimeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AudioCodec findByName(String name) {
|
||||||
|
for (AudioCodec codec : values()) {
|
||||||
|
if (codec.name.equals(name)) {
|
||||||
|
return codec;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
package com.genymobile.scrcpy;
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
import android.content.ComponentName;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.media.AudioFormat;
|
import android.media.AudioFormat;
|
||||||
import android.media.AudioRecord;
|
import android.media.AudioRecord;
|
||||||
import android.media.AudioTimestamp;
|
import android.media.AudioTimestamp;
|
||||||
@@ -19,11 +15,30 @@ import android.os.SystemClock;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.concurrent.Semaphore;
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
|
import java.util.concurrent.BlockingQueue;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
public final class AudioEncoder {
|
public final class AudioEncoder {
|
||||||
|
|
||||||
|
private static class InputTask {
|
||||||
|
final int index;
|
||||||
|
|
||||||
|
InputTask(int index) {
|
||||||
|
this.index = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class OutputTask {
|
||||||
|
final int index;
|
||||||
|
final MediaCodec.BufferInfo bufferInfo;
|
||||||
|
|
||||||
|
OutputTask(int index, MediaCodec.BufferInfo bufferInfo) {
|
||||||
|
this.index = index;
|
||||||
|
this.bufferInfo = bufferInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static final String MIMETYPE = MediaFormat.MIMETYPE_AUDIO_OPUS;
|
private static final String MIMETYPE = MediaFormat.MIMETYPE_AUDIO_OPUS;
|
||||||
private static final int SAMPLE_RATE = 48000;
|
private static final int SAMPLE_RATE = 48000;
|
||||||
private static final int CHANNELS = 2;
|
private static final int CHANNELS = 2;
|
||||||
@@ -32,11 +47,27 @@ public final class AudioEncoder {
|
|||||||
private static int BUFFER_MS = 15; // milliseconds
|
private static int BUFFER_MS = 15; // milliseconds
|
||||||
private static final int BUFFER_SIZE = SAMPLE_RATE * CHANNELS * BUFFER_MS / 1000;
|
private static final int BUFFER_SIZE = SAMPLE_RATE * CHANNELS * BUFFER_MS / 1000;
|
||||||
|
|
||||||
|
private final Streamer streamer;
|
||||||
|
|
||||||
private AudioRecord recorder;
|
private AudioRecord recorder;
|
||||||
private MediaCodec mediaCodec;
|
private MediaCodec mediaCodec;
|
||||||
private HandlerThread thread;
|
|
||||||
private final AtomicBoolean interrupted = new AtomicBoolean();
|
// Capacity of 64 is in practice "infinite" (it is limited by the number of available MediaCodec buffers, typically 4).
|
||||||
private final Semaphore endSemaphore = new Semaphore(0); // blocks until encoding is ended
|
// So many pending tasks would lead to an unacceptable delay anyway.
|
||||||
|
private final BlockingQueue<InputTask> inputTasks = new ArrayBlockingQueue<>(64);
|
||||||
|
private final BlockingQueue<OutputTask> outputTasks = new ArrayBlockingQueue<>(64);
|
||||||
|
|
||||||
|
private Thread thread;
|
||||||
|
private HandlerThread mediaCodecThread;
|
||||||
|
|
||||||
|
private Thread inputThread;
|
||||||
|
private Thread outputThread;
|
||||||
|
|
||||||
|
private boolean ended;
|
||||||
|
|
||||||
|
public AudioEncoder(Streamer streamer) {
|
||||||
|
this.streamer = streamer;
|
||||||
|
}
|
||||||
|
|
||||||
private static AudioFormat createAudioFormat() {
|
private static AudioFormat createAudioFormat() {
|
||||||
AudioFormat.Builder builder = new AudioFormat.Builder();
|
AudioFormat.Builder builder = new AudioFormat.Builder();
|
||||||
@@ -69,44 +100,19 @@ public final class AudioEncoder {
|
|||||||
return format;
|
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)
|
@TargetApi(Build.VERSION_CODES.N)
|
||||||
@Override
|
private void inputThread() throws IOException, InterruptedException {
|
||||||
public void onInputBufferAvailable(MediaCodec codec, int index) {
|
final AudioTimestamp timestamp = new AudioTimestamp();
|
||||||
if (eofSignaled) {
|
long previousPts = 0;
|
||||||
return;
|
long nextPts = 0;
|
||||||
}
|
|
||||||
|
|
||||||
ByteBuffer inputBuffer = codec.getInputBuffer(index);
|
while (!Thread.currentThread().isInterrupted()) {
|
||||||
int r = recorder.read(inputBuffer, BUFFER_SIZE);
|
InputTask task = inputTasks.take();
|
||||||
|
ByteBuffer buffer = mediaCodec.getInputBuffer(task.index);
|
||||||
|
int r = recorder.read(buffer, BUFFER_SIZE);
|
||||||
|
if (r < 0) {
|
||||||
|
throw new IOException("Could not read audio: " + r);
|
||||||
|
}
|
||||||
|
|
||||||
long pts;
|
long pts;
|
||||||
|
|
||||||
@@ -124,12 +130,6 @@ public final class AudioEncoder {
|
|||||||
long durationMs = r * 1000 / CHANNELS / SAMPLE_RATE;
|
long durationMs = r * 1000 / CHANNELS / SAMPLE_RATE;
|
||||||
nextPts = pts + durationMs;
|
nextPts = pts + durationMs;
|
||||||
|
|
||||||
int flags = 0;
|
|
||||||
if (interrupted.get()) {
|
|
||||||
flags = flags | MediaCodec.BUFFER_FLAG_END_OF_STREAM;
|
|
||||||
eofSignaled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (previousPts != 0 && pts < previousPts) {
|
if (previousPts != 0 && pts < previousPts) {
|
||||||
// Audio PTS may come from two sources:
|
// Audio PTS may come from two sources:
|
||||||
// - recorder.getTimestamp() if the call works;
|
// - recorder.getTimestamp() if the call works;
|
||||||
@@ -139,38 +139,165 @@ public final class AudioEncoder {
|
|||||||
pts = previousPts + 1;
|
pts = previousPts + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
codec.queueInputBuffer(index, 0, r, pts, flags);
|
mediaCodec.queueInputBuffer(task.index, 0, r, pts, 0);
|
||||||
|
|
||||||
previousPts = pts;
|
previousPts = pts;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void outputThread() throws IOException, InterruptedException {
|
||||||
|
streamer.writeHeader();
|
||||||
|
|
||||||
|
while (!Thread.currentThread().isInterrupted()) {
|
||||||
|
OutputTask task = outputTasks.take();
|
||||||
|
ByteBuffer buffer = mediaCodec.getOutputBuffer(task.index);
|
||||||
|
try {
|
||||||
|
streamer.writePacket(buffer, task.bufferInfo);
|
||||||
|
} finally {
|
||||||
|
mediaCodec.releaseOutputBuffer(task.index, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
thread = new Thread(() -> {
|
||||||
|
try {
|
||||||
|
encode();
|
||||||
|
} catch (IOException e) {
|
||||||
|
// this is expected on close
|
||||||
|
} finally {
|
||||||
|
Ln.d("Audio encoder stopped");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
thread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
if (thread != null) {
|
||||||
|
// Just wake up the blocking wait from the thread, so that it properly releases all its resources and terminates
|
||||||
|
end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void join() throws InterruptedException {
|
||||||
|
if (thread != null) {
|
||||||
|
thread.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void end() {
|
||||||
|
ended = true;
|
||||||
|
notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void waitEnded() {
|
||||||
|
try {
|
||||||
|
while (!ended) {
|
||||||
|
wait();
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.M)
|
||||||
|
public void encode() throws IOException {
|
||||||
|
mediaCodec = MediaCodec.createEncoderByType(MIMETYPE); // may throw IOException
|
||||||
|
|
||||||
|
try {
|
||||||
|
recorder = createAudioRecord();
|
||||||
|
|
||||||
|
mediaCodecThread = new HandlerThread("AudioEncoder");
|
||||||
|
mediaCodecThread.start();
|
||||||
|
|
||||||
|
MediaFormat format = createFormat();
|
||||||
|
mediaCodec.setCallback(new EncoderCallback(), new Handler(mediaCodecThread.getLooper()));
|
||||||
|
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
||||||
|
|
||||||
|
recorder.startRecording();
|
||||||
|
|
||||||
|
inputThread = new Thread(() -> {
|
||||||
|
try {
|
||||||
|
inputThread();
|
||||||
|
} catch (IOException | InterruptedException e) {
|
||||||
|
// this is expected on close
|
||||||
|
} finally {
|
||||||
|
end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
outputThread = new Thread(() -> {
|
||||||
|
try {
|
||||||
|
outputThread();
|
||||||
|
} catch (IOException | InterruptedException e) {
|
||||||
|
// this is expected on close
|
||||||
|
} finally {
|
||||||
|
end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mediaCodec.start();
|
||||||
|
inputThread.start();
|
||||||
|
outputThread.start();
|
||||||
|
} catch (Throwable e) {
|
||||||
|
mediaCodec.release();
|
||||||
|
if (recorder != null) {
|
||||||
|
recorder.release();
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
waitEnded();
|
||||||
|
} finally {
|
||||||
|
cleanUp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanUp() {
|
||||||
|
mediaCodecThread.getLooper().quit();
|
||||||
|
inputThread.interrupt();
|
||||||
|
outputThread.interrupt();
|
||||||
|
|
||||||
|
try {
|
||||||
|
mediaCodecThread.join();
|
||||||
|
inputThread.join();
|
||||||
|
outputThread.join();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// Should never happen
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaCodec.stop();
|
||||||
|
mediaCodec.release();
|
||||||
|
recorder.stop();
|
||||||
|
recorder.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class EncoderCallback extends MediaCodec.Callback {
|
||||||
|
@TargetApi(Build.VERSION_CODES.N)
|
||||||
|
@Override
|
||||||
|
public void onInputBufferAvailable(MediaCodec codec, int index) {
|
||||||
|
try {
|
||||||
|
inputTasks.put(new InputTask(index));
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo bufferInfo) {
|
public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo bufferInfo) {
|
||||||
if (ended) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ByteBuffer codecBuffer = codec.getOutputBuffer(index);
|
|
||||||
try {
|
try {
|
||||||
boolean isConfig = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0;
|
outputTasks.put(new OutputTask(index, bufferInfo));
|
||||||
long pts = bufferInfo.presentationTimeUs;
|
} catch (InterruptedException e) {
|
||||||
Ln.i("Audio packet: pts=" + pts + " " + codecBuffer.remaining() + " bytes");
|
end();
|
||||||
} finally {
|
|
||||||
codec.releaseOutputBuffer(index, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
|
|
||||||
if (eof) {
|
|
||||||
notifyEnded();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(MediaCodec codec, MediaCodec.CodecException e) {
|
public void onError(MediaCodec codec, MediaCodec.CodecException e) {
|
||||||
Ln.e("MediaCodec error", e);
|
Ln.e("MediaCodec error", e);
|
||||||
if (!ended) {
|
end();
|
||||||
notifyEnded();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -178,30 +305,4 @@ public final class AudioEncoder {
|
|||||||
// ignore
|
// 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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
11
server/src/main/java/com/genymobile/scrcpy/Codec.java
Normal file
11
server/src/main/java/com/genymobile/scrcpy/Codec.java
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
|
public interface Codec {
|
||||||
|
|
||||||
|
enum Type {VIDEO}
|
||||||
|
|
||||||
|
Type getType();
|
||||||
|
int getId();
|
||||||
|
String getName();
|
||||||
|
String getMimeType();
|
||||||
|
}
|
||||||
@@ -90,6 +90,7 @@ public class Controller {
|
|||||||
control();
|
control();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// this is expected on close
|
// this is expected on close
|
||||||
|
} finally {
|
||||||
Ln.d("Controller stopped");
|
Ln.d("Controller stopped");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -100,11 +101,17 @@ public class Controller {
|
|||||||
public void stop() {
|
public void stop() {
|
||||||
if (thread != null) {
|
if (thread != null) {
|
||||||
thread.interrupt();
|
thread.interrupt();
|
||||||
thread = null;
|
|
||||||
}
|
}
|
||||||
sender.stop();
|
sender.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void join() throws InterruptedException {
|
||||||
|
if (thread != null) {
|
||||||
|
thread.join();
|
||||||
|
}
|
||||||
|
sender.join();
|
||||||
|
}
|
||||||
|
|
||||||
public DeviceMessageSender getSender() {
|
public DeviceMessageSender getSender() {
|
||||||
return sender;
|
return sender;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ 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;
|
||||||
@@ -27,9 +30,10 @@ 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 controlSocket) throws IOException {
|
private DesktopConnection(LocalSocket videoSocket, LocalSocket audioSocket, 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();
|
||||||
@@ -38,6 +42,7 @@ 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 {
|
||||||
@@ -55,11 +60,13 @@ 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 control, boolean sendDummyByte) throws IOException {
|
public static DesktopConnection open(int uid, boolean tunnelForward, boolean audio, boolean control, boolean sendDummyByte) throws IOException {
|
||||||
String socketName = getSocketName(uid);
|
String socketName = getSocketName(uid);
|
||||||
|
|
||||||
LocalSocket videoSocket;
|
LocalSocket videoSocket = null;
|
||||||
|
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();
|
||||||
@@ -67,28 +74,36 @@ public final class DesktopConnection implements Closeable {
|
|||||||
// 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 (control) {
|
if (audio) {
|
||||||
try {
|
audioSocket = localServerSocket.accept();
|
||||||
controlSocket = localServerSocket.accept();
|
|
||||||
} catch (IOException | RuntimeException e) {
|
|
||||||
videoSocket.close();
|
|
||||||
throw e;
|
|
||||||
}
|
}
|
||||||
|
if (control) {
|
||||||
|
controlSocket = localServerSocket.accept();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
videoSocket = connect(socketName);
|
videoSocket = connect(socketName);
|
||||||
|
if (audio) {
|
||||||
|
audioSocket = connect(socketName);
|
||||||
|
}
|
||||||
if (control) {
|
if (control) {
|
||||||
try {
|
|
||||||
controlSocket = connect(socketName);
|
controlSocket = connect(socketName);
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (IOException | RuntimeException e) {
|
} catch (IOException | RuntimeException e) {
|
||||||
|
if (videoSocket != null) {
|
||||||
videoSocket.close();
|
videoSocket.close();
|
||||||
|
}
|
||||||
|
if (audioSocket != null) {
|
||||||
|
audioSocket.close();
|
||||||
|
}
|
||||||
|
if (controlSocket != null) {
|
||||||
|
controlSocket.close();
|
||||||
|
}
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new DesktopConnection(videoSocket, controlSocket);
|
return new DesktopConnection(videoSocket, audioSocket, controlSocket);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void close() throws IOException {
|
public void close() throws IOException {
|
||||||
@@ -121,6 +136,10 @@ 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) {
|
||||||
|
|||||||
@@ -277,6 +277,26 @@ public final class Device {
|
|||||||
* @param mode one of the {@code POWER_MODE_*} constants
|
* @param mode one of the {@code POWER_MODE_*} constants
|
||||||
*/
|
*/
|
||||||
public static boolean setScreenPowerMode(int mode) {
|
public static boolean setScreenPowerMode(int mode) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
// Change the power mode for all physical displays
|
||||||
|
long[] physicalDisplayIds = SurfaceControl.getPhysicalDisplayIds();
|
||||||
|
if (physicalDisplayIds == null) {
|
||||||
|
Ln.e("Could not get physical display ids");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean allOk = true;
|
||||||
|
for (long physicalDisplayId : physicalDisplayIds) {
|
||||||
|
IBinder binder = SurfaceControl.getPhysicalDisplayToken(physicalDisplayId);
|
||||||
|
boolean ok = SurfaceControl.setDisplayPowerMode(binder, mode);
|
||||||
|
if (!ok) {
|
||||||
|
allOk = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allOk;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Older Android versions, only 1 display
|
||||||
IBinder d = SurfaceControl.getBuiltInDisplay();
|
IBinder d = SurfaceControl.getBuiltInDisplay();
|
||||||
if (d == null) {
|
if (d == null) {
|
||||||
Ln.e("Could not get built-in display");
|
Ln.e("Could not get built-in display");
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ public final class DeviceMessageSender {
|
|||||||
loop();
|
loop();
|
||||||
} catch (IOException | InterruptedException e) {
|
} catch (IOException | InterruptedException e) {
|
||||||
// this is expected on close
|
// this is expected on close
|
||||||
|
} finally {
|
||||||
Ln.d("Device message sender stopped");
|
Ln.d("Device message sender stopped");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -66,7 +67,12 @@ public final class DeviceMessageSender {
|
|||||||
public void stop() {
|
public void stop() {
|
||||||
if (thread != null) {
|
if (thread != null) {
|
||||||
thread.interrupt();
|
thread.interrupt();
|
||||||
thread = null;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void join() throws InterruptedException {
|
||||||
|
if (thread != null) {
|
||||||
|
thread.join();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ 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 VideoCodec codec = VideoCodec.H264;
|
||||||
private int bitRate = 8000000;
|
private int bitRate = 8000000;
|
||||||
@@ -50,6 +50,14 @@ 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,10 +21,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";
|
||||||
@@ -35,7 +31,8 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
|
|
||||||
private final AtomicBoolean rotationChanged = new AtomicBoolean();
|
private final AtomicBoolean rotationChanged = new AtomicBoolean();
|
||||||
|
|
||||||
private final String videoMimeType;
|
private final Device device;
|
||||||
|
private final Streamer streamer;
|
||||||
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;
|
||||||
@@ -45,8 +42,10 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
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(Device device, Streamer streamer, int bitRate, int maxFps, List<CodecOption> codecOptions,
|
||||||
this.videoMimeType = videoMimeType;
|
String encoderName, boolean downsizeOnError) {
|
||||||
|
this.device = device;
|
||||||
|
this.streamer = streamer;
|
||||||
this.bitRate = bitRate;
|
this.bitRate = bitRate;
|
||||||
this.maxFps = maxFps;
|
this.maxFps = maxFps;
|
||||||
this.codecOptions = codecOptions;
|
this.codecOptions = codecOptions;
|
||||||
@@ -63,11 +62,15 @@ 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() throws IOException {
|
||||||
|
String videoMimeType = streamer.getCodec().getMimeType();
|
||||||
MediaCodec codec = createCodec(videoMimeType, encoderName);
|
MediaCodec codec = createCodec(videoMimeType, encoderName);
|
||||||
MediaFormat format = createFormat(videoMimeType, bitRate, maxFps, codecOptions);
|
MediaFormat format = createFormat(videoMimeType, bitRate, maxFps, codecOptions);
|
||||||
IBinder display = createDisplay();
|
IBinder display = createDisplay();
|
||||||
device.setRotationListener(this);
|
device.setRotationListener(this);
|
||||||
|
|
||||||
|
streamer.writeHeader();
|
||||||
|
|
||||||
boolean alive;
|
boolean alive;
|
||||||
try {
|
try {
|
||||||
do {
|
do {
|
||||||
@@ -92,7 +95,7 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
|
|
||||||
codec.start();
|
codec.start();
|
||||||
|
|
||||||
alive = encode(codec, callbacks);
|
alive = encode(codec, streamer);
|
||||||
// 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,7 +164,7 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean encode(MediaCodec codec, Callbacks callbacks) throws IOException {
|
private boolean encode(MediaCodec codec, Streamer streamer) throws IOException {
|
||||||
boolean eof = false;
|
boolean eof = false;
|
||||||
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
||||||
|
|
||||||
@@ -184,7 +187,7 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
consecutiveErrors = 0;
|
consecutiveErrors = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
callbacks.onPacket(codecBuffer, bufferInfo);
|
streamer.writePacket(codecBuffer, bufferInfo);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (outputBufferId >= 0) {
|
if (outputBufferId >= 0) {
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ public final class Server {
|
|||||||
int uid = options.getUid();
|
int uid = options.getUid();
|
||||||
boolean tunnelForward = options.isTunnelForward();
|
boolean tunnelForward = options.isTunnelForward();
|
||||||
boolean control = options.getControl();
|
boolean control = options.getControl();
|
||||||
boolean audio = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R; // TODO option
|
boolean audio = options.getAudio();
|
||||||
boolean sendDummyByte = options.getSendDummyByte();
|
boolean sendDummyByte = options.getSendDummyByte();
|
||||||
|
|
||||||
Workarounds.prepareMainLooper();
|
Workarounds.prepareMainLooper();
|
||||||
@@ -93,14 +93,12 @@ public final class Server {
|
|||||||
Workarounds.fillAppInfo();
|
Workarounds.fillAppInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
try (DesktopConnection connection = DesktopConnection.open(uid, tunnelForward, control, sendDummyByte)) {
|
try (DesktopConnection connection = DesktopConnection.open(uid, tunnelForward, audio, control, sendDummyByte)) {
|
||||||
VideoCodec codec = options.getCodec();
|
VideoCodec codec = options.getCodec();
|
||||||
if (options.getSendDeviceMeta()) {
|
if (options.getSendDeviceMeta()) {
|
||||||
Size videoSize = device.getScreenInfo().getVideoSize();
|
Size videoSize = device.getScreenInfo().getVideoSize();
|
||||||
connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
|
connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
|
||||||
}
|
}
|
||||||
ScreenEncoder screenEncoder = new ScreenEncoder(codec.getMimeType(), options.getBitRate(), options.getMaxFps(), codecOptions,
|
|
||||||
options.getEncoderName(), options.getDownsizeOnError());
|
|
||||||
|
|
||||||
Controller controller = null;
|
Controller controller = null;
|
||||||
if (control) {
|
if (control) {
|
||||||
@@ -113,27 +111,39 @@ public final class Server {
|
|||||||
|
|
||||||
AudioEncoder audioEncoder = null;
|
AudioEncoder audioEncoder = null;
|
||||||
if (audio) {
|
if (audio) {
|
||||||
audioEncoder = new AudioEncoder();
|
Streamer audioStreamer = new Streamer(connection.getAudioFd(), AudioCodec.OPUS, options.getSendCodecId(), options.getSendFrameMeta());
|
||||||
|
audioEncoder = new AudioEncoder(audioStreamer);
|
||||||
audioEncoder.start();
|
audioEncoder.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Streamer videoStreamer = new Streamer(connection.getVideoFd(), codec, options.getSendCodecId(), options.getSendFrameMeta());
|
||||||
|
ScreenEncoder screenEncoder = new ScreenEncoder(device, videoStreamer, options.getBitRate(), options.getMaxFps(),
|
||||||
|
codecOptions, options.getEncoderName(), options.getDownsizeOnError());
|
||||||
try {
|
try {
|
||||||
// synchronous
|
// synchronous
|
||||||
VideoStreamer videoStreamer = new VideoStreamer(connection.getVideoFd(), options.getSendFrameMeta());
|
screenEncoder.streamScreen();
|
||||||
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");
|
|
||||||
} finally {
|
} finally {
|
||||||
|
Ln.d("Screen streaming stopped");
|
||||||
initThread.interrupt();
|
initThread.interrupt();
|
||||||
|
if (audioEncoder != null) {
|
||||||
|
audioEncoder.stop();
|
||||||
|
}
|
||||||
if (controller != null) {
|
if (controller != null) {
|
||||||
controller.stop();
|
controller.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
initThread.join();
|
||||||
if (audioEncoder != null) {
|
if (audioEncoder != null) {
|
||||||
audioEncoder.stop();
|
audioEncoder.join();
|
||||||
|
}
|
||||||
|
if (controller != null) {
|
||||||
|
controller.join();
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,6 +188,10 @@ 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":
|
case "codec":
|
||||||
VideoCodec codec = VideoCodec.findByName(value);
|
VideoCodec codec = VideoCodec.findByName(value);
|
||||||
if (codec == null) {
|
if (codec == null) {
|
||||||
|
|||||||
@@ -6,30 +6,39 @@ import java.io.FileDescriptor;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
public final class VideoStreamer implements ScreenEncoder.Callbacks {
|
public final class Streamer {
|
||||||
|
|
||||||
private static final long PACKET_FLAG_CONFIG = 1L << 63;
|
private static final long PACKET_FLAG_CONFIG = 1L << 63;
|
||||||
private static final long PACKET_FLAG_KEY_FRAME = 1L << 62;
|
private static final long PACKET_FLAG_KEY_FRAME = 1L << 62;
|
||||||
|
|
||||||
private final FileDescriptor fd;
|
private final FileDescriptor fd;
|
||||||
|
private final Codec codec;
|
||||||
|
private final boolean sendCodecId;
|
||||||
private final boolean sendFrameMeta;
|
private final boolean sendFrameMeta;
|
||||||
|
|
||||||
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
|
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
|
||||||
|
|
||||||
public VideoStreamer(FileDescriptor fd, boolean sendFrameMeta) {
|
public Streamer(FileDescriptor fd, Codec codec, boolean sendCodecId, boolean sendFrameMeta) {
|
||||||
this.fd = fd;
|
this.fd = fd;
|
||||||
|
this.codec = codec;
|
||||||
|
this.sendCodecId = sendCodecId;
|
||||||
this.sendFrameMeta = sendFrameMeta;
|
this.sendFrameMeta = sendFrameMeta;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void writeHeader(int codecId) throws IOException {
|
public Codec getCodec() {
|
||||||
|
return codec;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeHeader() throws IOException {
|
||||||
|
if (sendCodecId) {
|
||||||
ByteBuffer buffer = ByteBuffer.allocate(4);
|
ByteBuffer buffer = ByteBuffer.allocate(4);
|
||||||
buffer.putInt(codecId);
|
buffer.putInt(codec.getId());
|
||||||
buffer.flip();
|
buffer.flip();
|
||||||
IO.writeFully(fd, buffer);
|
IO.writeFully(fd, buffer);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
public void writePacket(ByteBuffer codecBuffer, MediaCodec.BufferInfo bufferInfo) throws IOException {
|
||||||
public void onPacket(ByteBuffer codecBuffer, MediaCodec.BufferInfo bufferInfo) throws IOException {
|
|
||||||
if (sendFrameMeta) {
|
if (sendFrameMeta) {
|
||||||
writeFrameMeta(fd, bufferInfo, codecBuffer.remaining());
|
writeFrameMeta(fd, bufferInfo, codecBuffer.remaining());
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@ package com.genymobile.scrcpy;
|
|||||||
|
|
||||||
import android.media.MediaFormat;
|
import android.media.MediaFormat;
|
||||||
|
|
||||||
public enum VideoCodec {
|
public enum VideoCodec implements Codec {
|
||||||
H264(0x68_32_36_34, "h264", MediaFormat.MIMETYPE_VIDEO_AVC),
|
H264(0x68_32_36_34, "h264", MediaFormat.MIMETYPE_VIDEO_AVC),
|
||||||
H265(0x68_32_36_35, "h265", MediaFormat.MIMETYPE_VIDEO_HEVC),
|
H265(0x68_32_36_35, "h265", MediaFormat.MIMETYPE_VIDEO_HEVC),
|
||||||
AV1(0x00_61_76_31, "av1", MediaFormat.MIMETYPE_VIDEO_AV1);
|
AV1(0x00_61_76_31, "av1", MediaFormat.MIMETYPE_VIDEO_AV1);
|
||||||
@@ -17,10 +17,22 @@ public enum VideoCodec {
|
|||||||
this.mimeType = mimeType;
|
this.mimeType = mimeType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Type getType() {
|
||||||
|
return Type.VIDEO;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public int getId() {
|
public int getId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public String getMimeType() {
|
public String getMimeType() {
|
||||||
return mimeType;
|
return mimeType;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ public final class SurfaceControl {
|
|||||||
|
|
||||||
private static Method getBuiltInDisplayMethod;
|
private static Method getBuiltInDisplayMethod;
|
||||||
private static Method setDisplayPowerModeMethod;
|
private static Method setDisplayPowerModeMethod;
|
||||||
|
private static Method getPhysicalDisplayTokenMethod;
|
||||||
|
private static Method getPhysicalDisplayIdsMethod;
|
||||||
|
|
||||||
private SurfaceControl() {
|
private SurfaceControl() {
|
||||||
// only static methods
|
// only static methods
|
||||||
@@ -98,7 +100,6 @@ public final class SurfaceControl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static IBinder getBuiltInDisplay() {
|
public static IBinder getBuiltInDisplay() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Method method = getGetBuiltInDisplayMethod();
|
Method method = getGetBuiltInDisplayMethod();
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
@@ -114,6 +115,40 @@ public final class SurfaceControl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Method getGetPhysicalDisplayTokenMethod() throws NoSuchMethodException {
|
||||||
|
if (getPhysicalDisplayTokenMethod == null) {
|
||||||
|
getPhysicalDisplayTokenMethod = CLASS.getMethod("getPhysicalDisplayToken", long.class);
|
||||||
|
}
|
||||||
|
return getPhysicalDisplayTokenMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IBinder getPhysicalDisplayToken(long physicalDisplayId) {
|
||||||
|
try {
|
||||||
|
Method method = getGetPhysicalDisplayTokenMethod();
|
||||||
|
return (IBinder) method.invoke(null, physicalDisplayId);
|
||||||
|
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||||
|
Ln.e("Could not invoke method", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Method getGetPhysicalDisplayIdsMethod() throws NoSuchMethodException {
|
||||||
|
if (getPhysicalDisplayIdsMethod == null) {
|
||||||
|
getPhysicalDisplayIdsMethod = CLASS.getMethod("getPhysicalDisplayIds");
|
||||||
|
}
|
||||||
|
return getPhysicalDisplayIdsMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long[] getPhysicalDisplayIds() {
|
||||||
|
try {
|
||||||
|
Method method = getGetPhysicalDisplayIdsMethod();
|
||||||
|
return (long[]) method.invoke(null);
|
||||||
|
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||||
|
Ln.e("Could not invoke method", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static Method getSetDisplayPowerModeMethod() throws NoSuchMethodException {
|
private static Method getSetDisplayPowerModeMethod() throws NoSuchMethodException {
|
||||||
if (setDisplayPowerModeMethod == null) {
|
if (setDisplayPowerModeMethod == null) {
|
||||||
setDisplayPowerModeMethod = CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class);
|
setDisplayPowerModeMethod = CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class);
|
||||||
|
|||||||
Reference in New Issue
Block a user