From 81a6b8e3a356a46f7bfd2c5c7b4d7fa6fcd78592 Mon Sep 17 00:00:00 2001 From: Killian Richard Date: Mon, 24 Feb 2020 09:56:29 +0100 Subject: [PATCH 1/9] Adding --serve option on README --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 7be5f5f280..7397cc597b 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,22 @@ variation] does not impact the recorded file. [packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation +### Serve + +It is possible to forward the video stream: + +```bash +scrcpy --serve tcp:localhost:1234 +``` + +To disable mirroring while forwarding the stream: + +```bash +scrcpy --no-display --serve tcp:localhost:1234 +scrcpy -Nr --serve tcp:localhost:1234 +# interrupt recording with Ctrl+C +# Ctrl+C does not terminate properly on Windows, so disconnect the device +``` ### Connection From b4dc78af0c86f442748ea70b17c25475392d9352 Mon Sep 17 00:00:00 2001 From: Killian Richard Date: Mon, 24 Feb 2020 10:13:29 +0100 Subject: [PATCH 2/9] Adding --serve option : - Add current working files --- README.md | 4 +- app/.project | 11 ++ app/meson.build | 1 + app/src/cli.c | 173 +++++++++++++++- app/src/scrcpy.c | 16 +- app/src/scrcpy.h | 10 +- app/src/serve.c | 101 ++++++++++ app/src/serve.h | 28 +++ app/src/stream.c | 498 ++++++++++++++++++++++++----------------------- app/src/stream.h | 3 +- 10 files changed, 598 insertions(+), 247 deletions(-) create mode 100644 app/.project create mode 100644 app/src/serve.c create mode 100644 app/src/serve.h diff --git a/README.md b/README.md index 7397cc597b..6684d8caab 100644 --- a/README.md +++ b/README.md @@ -213,8 +213,8 @@ To disable mirroring while forwarding the stream: ```bash scrcpy --no-display --serve tcp:localhost:1234 -scrcpy -Nr --serve tcp:localhost:1234 -# interrupt recording with Ctrl+C +scrcpy -N --serve tcp:localhost:1234 +# interrupt serve with Ctrl+C # Ctrl+C does not terminate properly on Windows, so disconnect the device ``` diff --git a/app/.project b/app/.project new file mode 100644 index 0000000000..3256dae820 --- /dev/null +++ b/app/.project @@ -0,0 +1,11 @@ + + + app + + + + + + + + diff --git a/app/meson.build b/app/meson.build index 3bcb9bc10b..fbda134026 100644 --- a/app/meson.build +++ b/app/meson.build @@ -15,6 +15,7 @@ src = [ 'src/recorder.c', 'src/scrcpy.c', 'src/screen.c', + 'src/serve.c', 'src/server.c', 'src/stream.c', 'src/tiny_xpm.c', diff --git a/app/src/cli.c b/app/src/cli.c index d9e1013a0a..a449f9121b 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -6,9 +6,12 @@ #include "config.h" #include "recorder.h" +#include "serve.h" #include "util/log.h" #include "util/str_util.h" +#define IPV4_LOCALHOST 0x7F000001 + void scrcpy_print_usage(const char *arg0) { #ifdef __APPLE__ @@ -79,6 +82,11 @@ scrcpy_print_usage(const char *arg0) { " The format is determined by the --record-format option if\n" " set, or by the file extension (.mp4 or .mkv).\n" "\n" + " --serve tcp:localhost:1234\n" + " Open a socket to redirect video stream.\n" + " It will Wait for a client to connect before starting the mirroring,\n" + " then it would forward the video stream.\n" + "\n" " --record-format format\n" " Force recording format (either mp4 or mkv).\n" "\n" @@ -297,6 +305,7 @@ parse_port(const char *s, uint16_t *port) { return true; } + static bool parse_record_format(const char *optarg, enum recorder_format *format) { if (!strcmp(optarg, "mp4")) { @@ -311,6 +320,154 @@ parse_record_format(const char *optarg, enum recorder_format *format) { return false; } +char** +str_split(const char* a_str, const char a_delim) +{ + char** result = 0; + size_t count = 0; + char* tmp = (char*)a_str; + char str[100]; + strncpy(str, a_str, sizeof(str)); + char* last_comma = 0; + char delim[2]; + delim[0] = a_delim; + delim[1] = 0; + + /* Count how many elements will be extracted. */ + while (*tmp) + { + if (a_delim == *tmp) + { + count++; + last_comma = tmp; + } + tmp++; + } + + /* Add space for trailing token. */ + count += last_comma < (str + strlen(str) - 1); + + /* Add space for terminating null string so caller + knows where the list of returned strings ends. */ + count++; + + result = malloc(sizeof(char*) * count); + + if (result) + { + size_t idx = 0; + char* token = strtok(str, delim); + + while (token) + { + assert(idx < count); + *(result + idx++) = strdup(token); + token = strtok(0, delim); + } + assert(idx == count - 1); + *(result + idx) = 0; + } + + return result; +} + +int +validate_ip(char* ip) { //check whether the IP is valid or not + int num, dots = 0; + char* ptr; + if (ip == NULL) + return 0; + ptr = strtok(ip, "."); //cut the string using dor delimiter + if (ptr == NULL) + return 0; + while (ptr) { + long value; + if (!parse_integer(ptr, &value)) //check whether the sub string is holding only number or not + return 0; + num = atoi(ptr); //convert substring to number + if (num >= 0 && num <= 255) { + ptr = strtok(NULL, "."); //cut the next part of the string + if (ptr != NULL) + dots++; //increase the dot count + } + else + return 0; + } + if (dots != 3) //if the number of dots are not 3, return false + return 0; + return 1; +} + +static bool +parse_serve_args(const char *optarg, char **s_protocol, uint32_t *s_ip, uint16_t *s_port) { + bool protocol_valid = false; + bool ip_valid = false; + bool port_valid = false; + + char* protocol = NULL; + char* ip = NULL; + uint32_t ip_value; + char* port = NULL; + char** values; + + values = str_split(optarg, ':'); + + if (values) + { + protocol = *values; + ip = *(values + 1); + port = *(values + 2); + } + + free(values); + + if (!strcmp(protocol, "tcp")) + { + //protocol = "tcp"; + protocol_valid = true; + } else if (!strcmp(protocol, "udp")) + { + //protocol = "udp"; + protocol_valid = true; + } + else { + LOGE("Unexpected protocol: %s (expected tcp or udp)", protocol); + return false; + } + + //Allowing to write localhost or the IP address + if (!strcmp(ip, "localhost")) + { + ip_value = IPV4_LOCALHOST; + ip_valid = true; + } else if (validate_ip(ip)) { + ip_valid = true; + } + else { + LOGE("Unexpected ip address (expected \"localhost\" or 255.255.255.255)"); + return false; + } + + long port_value = 0; + port_valid = parse_integer_arg(port, &port_value, false, 0, 0xFFFF, "port"); + + //Check if everything is valid + if (!protocol_valid || !ip_valid || !port_valid) { + LOGE("Unexpected argument format: %s (expected [tcp/udp]:[ip or \"localhost\"]:[port])", optarg); + return false; + } + + /*LOGI("%s", protocol); + LOGI("%d", ip_value); + LOGI("%ld", port_value);*/ + + *s_protocol = protocol; + *s_ip = (uint32_t)ip_value; + *s_port = (uint16_t)port_value; + + return true; +} + static enum recorder_format guess_record_format(const char *filename) { size_t len = strlen(filename); @@ -340,6 +497,7 @@ guess_record_format(const char *filename) { #define OPT_WINDOW_HEIGHT 1010 #define OPT_WINDOW_BORDERLESS 1011 #define OPT_MAX_FPS 1012 +#define OPT_SERVE 1013 bool scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { @@ -360,6 +518,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { {"render-expired-frames", no_argument, NULL, OPT_RENDER_EXPIRED_FRAMES}, {"serial", required_argument, NULL, 's'}, + {"serve", required_argument, NULL, OPT_SERVE}, {"show-touches", no_argument, NULL, 't'}, {"turn-screen-off", no_argument, NULL, 'S'}, {"prefer-text", no_argument, NULL, OPT_PREFER_TEXT}, @@ -434,6 +593,16 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case 's': opts->serial = optarg; break; + case OPT_SERVE: + if (!parse_serve_args(optarg, &opts->serve_protocol, &opts->serve_ip, &opts->serve_port)) { + return false; + } else { + opts->serve = true; + } + /*LOGI("protocol is %s", opts->serve_protocol); + LOGI("ip value is %d", opts->serve_ip); + LOGI("port is %d", opts->serve_port);*/ + break; case 'S': opts->turn_screen_off = true; break; @@ -490,8 +659,8 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { } } - if (!opts->display && !opts->record_filename) { - LOGE("-N/--no-display requires screen recording (-r/--record)"); + if (!opts->display && !opts->record_filename && !opts->serve) { + LOGE("-N/--no-display requires screen recording (-r/--record) or to serve to another client (--serve)"); return false; } diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 17be1ed415..b7138fa5d9 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -21,6 +21,7 @@ #include "recorder.h" #include "screen.h" #include "server.h" +#include "serve.h" #include "stream.h" #include "tiny_xpm.h" #include "video_buffer.h" @@ -35,6 +36,7 @@ static struct video_buffer video_buffer; static struct stream stream; static struct decoder decoder; static struct recorder recorder; +static struct serve serve; static struct controller controller; static struct file_handler file_handler; @@ -307,6 +309,7 @@ scrcpy(const struct scrcpy_options *options) { bool stream_started = false; bool controller_initialized = false; bool controller_started = false; + //bool serve_initialized = false; if (!sdl_init_and_configure(options->display)) { goto end; @@ -365,7 +368,18 @@ scrcpy(const struct scrcpy_options *options) { av_log_set_callback(av_log_callback); - stream_init(&stream, server.video_socket, dec, rec); + struct serve *serv = NULL; + if (options->serve) + { + serve_init(&serve, options->serve_protocol, options->serve_ip, options->serve_port); + + if (!serve_start(&serve)) { + goto end; + } + serv = &serve; + } + + stream_init(&stream, server.video_socket, dec, rec, serv); // now we consumed the header values, the socket receives the video stream // start the stream diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 75de8717f3..6908cdf870 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -13,7 +13,7 @@ struct scrcpy_options { const char *crop; const char *record_filename; const char *window_title; - const char *push_target; + const char *push_target; enum recorder_format record_format; uint16_t port; uint16_t max_size; @@ -32,6 +32,10 @@ struct scrcpy_options { bool render_expired_frames; bool prefer_text; bool window_borderless; + char *serve_protocol; + uint32_t serve_ip; + uint16_t serve_port; + bool serve; }; #define SCRCPY_OPTIONS_DEFAULT { \ @@ -58,6 +62,10 @@ struct scrcpy_options { .render_expired_frames = false, \ .prefer_text = false, \ .window_borderless = false, \ + .serve_protocol = NULL, \ + .serve_ip = 0, \ + .serve_port = 0, \ + .serve = false, \ } bool diff --git a/app/src/serve.c b/app/src/serve.c new file mode 100644 index 0000000000..8d40290e59 --- /dev/null +++ b/app/src/serve.c @@ -0,0 +1,101 @@ +#include "serve.h" + +#include +#include +#include + +#include "config.h" +#include "events.h" +#include "util/log.h" +#include "util/net.h" + +# define SOCKET_ERROR -1 + +void +serve_init(struct serve* serve, char *protocol, uint32_t ip, uint16_t port) { + serve->protocol = protocol; + serve->ip = ip; + serve->port = port; +} + +//static int +//run_serve(void *data) { +// struct serve* serve = data; +// +// socket_t Listensocket; +// socket_t ClientSocket; +// +// Listensocket = net_listen(serve->ip, serve->port, 1); +// if (Listensocket == INVALID_SOCKET) { +// LOGI("Listen Error"); +// net_close(Listensocket); +// return 0; +// } +// +// for (;;) { +// ClientSocket = net_accept(Listensocket); +// if (ClientSocket == INVALID_SOCKET) { +// LOGI("Client Error"); +// net_close(Listensocket); +// return 0; +// } +// LOGI("Client found"); +// +// net_close(Listensocket); +// +// serve->socket = ClientSocket; +// +// if (serve->stopped) +// { +// break; +// } +// } +// +// LOGD("Serve thread ended"); +// return 0; +//} + +bool +serve_start(struct serve* serve) { + LOGD("Starting serve thread"); + + socket_t Listensocket; + socket_t ClientSocket; + + Listensocket = net_listen(serve->ip, serve->port, 1); + if (Listensocket == INVALID_SOCKET) { + LOGI("Listen Error"); + net_close(Listensocket); + return 0; + } + + ClientSocket = net_accept(Listensocket); + if (ClientSocket == INVALID_SOCKET) { + LOGI("Client Error"); + net_close(Listensocket); + return 0; + } + LOGI("Client found"); + + net_close(Listensocket); + + serve->socket = ClientSocket; + + /*serve->thread = SDL_CreateThread(run_serve, "serve", serve); + if (!serve->thread) { + LOGC("Could not start stream thread"); + return false; + }*/ + return true; +} + +bool +serve_push(struct serve* serve, const AVPacket packet) { + if (net_send(serve->socket, packet.data, packet.size) == SOCKET_ERROR) + { + LOGI("Client lost"); + net_close(serve->socket); + return false; + } + return true; +} diff --git a/app/src/serve.h b/app/src/serve.h new file mode 100644 index 0000000000..44ee7583e5 --- /dev/null +++ b/app/src/serve.h @@ -0,0 +1,28 @@ +#ifndef SERVE_H +#define SERVE_H + +#include +#include +#include +#include +#include + +#include "config.h" +#include "util/net.h" + +struct serve { + socket_t socket; + char *protocol; + uint32_t ip; + uint16_t port; +}; + +void +serve_init(struct serve* serve, char* protocol, uint32_t ip, uint16_t port); + +bool +serve_start(struct serve* serve); + +bool +serve_push(struct serve* serve, const AVPacket packet); +#endif \ No newline at end of file diff --git a/app/src/stream.c b/app/src/stream.c index dd2dbd763e..b91fb2fec9 100644 --- a/app/src/stream.c +++ b/app/src/stream.c @@ -7,14 +7,18 @@ #include #include #include +#include #include "config.h" #include "compat.h" #include "decoder.h" #include "events.h" #include "recorder.h" +#include "serve.h" +#include "stream.h" #include "util/buffer_util.h" #include "util/log.h" +#include "util/net.h" #define BUFSIZE 0x10000 @@ -22,281 +26,295 @@ #define NO_PTS UINT64_C(-1) static bool -stream_recv_packet(struct stream *stream, AVPacket *packet) { - // The video stream contains raw packets, without time information. When we - // record, we retrieve the timestamps separately, from a "meta" header - // added by the server before each raw packet. - // - // The "meta" header length is 12 bytes: - // [. . . . . . . .|. . . .]. . . . . . . . . . . . . . . ... - // <-------------> <-----> <-----------------------------... - // PTS packet raw packet - // size - // - // It is followed by bytes containing the packet/frame. - - uint8_t header[HEADER_SIZE]; - ssize_t r = net_recv_all(stream->socket, header, HEADER_SIZE); - if (r < HEADER_SIZE) { - return false; - } - - uint64_t pts = buffer_read64be(header); - uint32_t len = buffer_read32be(&header[8]); - assert(pts == NO_PTS || (pts & 0x8000000000000000) == 0); - assert(len); - - if (av_new_packet(packet, len)) { - LOGE("Could not allocate packet"); - return false; - } - - r = net_recv_all(stream->socket, packet->data, len); - if (r < 0 || ((uint32_t) r) < len) { - av_packet_unref(packet); - return false; - } - - packet->pts = pts != NO_PTS ? (int64_t) pts : AV_NOPTS_VALUE; - - return true; +stream_recv_packet(struct stream* stream, AVPacket* packet) { + // The video stream contains raw packets, without time information. When we + // record, we retrieve the timestamps separately, from a "meta" header + // added by the server before each raw packet. + // + // The "meta" header length is 12 bytes: + // [. . . . . . . .|. . . .]. . . . . . . . . . . . . . . ... + // <-------------> <-----> <-----------------------------... + // PTS packet raw packet + // size + // + // It is followed by bytes containing the packet/frame. + + uint8_t header[HEADER_SIZE]; + ssize_t r = net_recv_all(stream->socket, header, HEADER_SIZE); + if (r < HEADER_SIZE) { + return false; + } + + uint64_t pts = buffer_read64be(header); + uint32_t len = buffer_read32be(&header[8]); + assert(pts == NO_PTS || (pts & 0x8000000000000000) == 0); + assert(len); + + if (av_new_packet(packet, len)) { + LOGE("Could not allocate packet"); + return false; + } + + r = net_recv_all(stream->socket, packet->data, len); + if (r < 0 || ((uint32_t)r) < len) { + av_packet_unref(packet); + return false; + } + + packet->pts = pts != NO_PTS ? (int64_t)pts : AV_NOPTS_VALUE; + + return true; } static void notify_stopped(void) { - SDL_Event stop_event; - stop_event.type = EVENT_STREAM_STOPPED; - SDL_PushEvent(&stop_event); + SDL_Event stop_event; + stop_event.type = EVENT_STREAM_STOPPED; + SDL_PushEvent(&stop_event); } static bool -process_config_packet(struct stream *stream, AVPacket *packet) { - if (stream->recorder && !recorder_push(stream->recorder, packet)) { - LOGE("Could not send config packet to recorder"); - return false; - } - return true; +process_config_packet(struct stream* stream, AVPacket* packet) { + if (stream->recorder && !recorder_push(stream->recorder, packet)) { + LOGE("Could not send config packet to recorder"); + return false; + } + return true; } -static bool -process_frame(struct stream *stream, AVPacket *packet) { - if (stream->decoder && !decoder_push(stream->decoder, packet)) { - return false; - } - - if (stream->recorder) { - packet->dts = packet->pts; - if (!recorder_push(stream->recorder, packet)) { - LOGE("Could not send packet to recorder"); - return false; - } - } - return true; +static bool +process_frame(struct stream* stream, AVPacket* packet) { + if (stream->decoder && !decoder_push(stream->decoder, packet)) { + return false; + } + + if (stream->recorder) { + packet->dts = packet->pts; + + if (!recorder_push(stream->recorder, packet)) { + LOGE("Could not send packet to recorder"); + return false; + } + } + + if (stream->serve && !serve_push(stream->serve, *packet)) { + LOGE("Could not serve packet"); + return false; + } + + return true; } static bool -stream_parse(struct stream *stream, AVPacket *packet) { - uint8_t *in_data = packet->data; - int in_len = packet->size; - uint8_t *out_data = NULL; - int out_len = 0; - int r = av_parser_parse2(stream->parser, stream->codec_ctx, - &out_data, &out_len, in_data, in_len, - AV_NOPTS_VALUE, AV_NOPTS_VALUE, -1); - - // PARSER_FLAG_COMPLETE_FRAMES is set - assert(r == in_len); - (void) r; - assert(out_len == in_len); - - if (stream->parser->key_frame == 1) { - packet->flags |= AV_PKT_FLAG_KEY; - } - - bool ok = process_frame(stream, packet); - if (!ok) { - LOGE("Could not process frame"); - return false; - } - - return true; +stream_parse(struct stream* stream, AVPacket* packet) { + uint8_t* in_data = packet->data; + int in_len = packet->size; + uint8_t* out_data = NULL; + int out_len = 0; + int r = av_parser_parse2(stream->parser, stream->codec_ctx, + &out_data, &out_len, in_data, in_len, + AV_NOPTS_VALUE, AV_NOPTS_VALUE, -1); + + // PARSER_FLAG_COMPLETE_FRAMES is set + assert(r == in_len); + (void)r; + assert(out_len == in_len); + + if (stream->parser->key_frame == 1) { + packet->flags |= AV_PKT_FLAG_KEY; + } + + bool ok = process_frame(stream, packet); + if (!ok) { + LOGE("Could not process frame"); + return false; + } + + return true; } static bool -stream_push_packet(struct stream *stream, AVPacket *packet) { - bool is_config = packet->pts == AV_NOPTS_VALUE; - - // A config packet must not be decoded immetiately (it contains no - // frame); instead, it must be concatenated with the future data packet. - if (stream->has_pending || is_config) { - size_t offset; - if (stream->has_pending) { - offset = stream->pending.size; - if (av_grow_packet(&stream->pending, packet->size)) { - LOGE("Could not grow packet"); - return false; - } - } else { - offset = 0; - if (av_new_packet(&stream->pending, packet->size)) { - LOGE("Could not create packet"); - return false; - } - stream->has_pending = true; - } - - memcpy(stream->pending.data + offset, packet->data, packet->size); - - if (!is_config) { - // prepare the concat packet to send to the decoder - stream->pending.pts = packet->pts; - stream->pending.dts = packet->dts; - stream->pending.flags = packet->flags; - packet = &stream->pending; - } - } - - if (is_config) { - // config packet - bool ok = process_config_packet(stream, packet); - if (!ok) { - return false; - } - } else { - // data packet - bool ok = stream_parse(stream, packet); - - if (stream->has_pending) { - // the pending packet must be discarded (consumed or error) - stream->has_pending = false; - av_packet_unref(&stream->pending); - } - - if (!ok) { - return false; - } - } - return true; +stream_push_packet(struct stream* stream, AVPacket* packet) { + bool is_config = packet->pts == AV_NOPTS_VALUE; + + // A config packet must not be decoded immetiately (it contains no + // frame); instead, it must be concatenated with the future data packet. + if (stream->has_pending || is_config) { + size_t offset; + if (stream->has_pending) { + offset = stream->pending.size; + if (av_grow_packet(&stream->pending, packet->size)) { + LOGE("Could not grow packet"); + return false; + } + } + else { + offset = 0; + if (av_new_packet(&stream->pending, packet->size)) { + LOGE("Could not create packet"); + return false; + } + stream->has_pending = true; + } + + memcpy(stream->pending.data + offset, packet->data, packet->size); + + if (!is_config) { + // prepare the concat packet to send to the decoder + stream->pending.pts = packet->pts; + stream->pending.dts = packet->dts; + stream->pending.flags = packet->flags; + packet = &stream->pending; + } + } + + if (is_config) { + // config packet + bool ok = process_config_packet(stream, packet); + if (!ok) { + return false; + } + } + else { + // data packet + bool ok = stream_parse(stream, packet); + + if (stream->has_pending) { + // the pending packet must be discarded (consumed or error) + stream->has_pending = false; + av_packet_unref(&stream->pending); + } + + if (!ok) { + return false; + } + } + return true; } static int -run_stream(void *data) { - struct stream *stream = data; - - AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264); - if (!codec) { - LOGE("H.264 decoder not found"); - goto end; - } - - stream->codec_ctx = avcodec_alloc_context3(codec); - if (!stream->codec_ctx) { - LOGC("Could not allocate codec context"); - goto end; - } - - if (stream->decoder && !decoder_open(stream->decoder, codec)) { - LOGE("Could not open decoder"); - goto finally_free_codec_ctx; - } - - if (stream->recorder) { - if (!recorder_open(stream->recorder, codec)) { - LOGE("Could not open recorder"); - goto finally_close_decoder; - } - - if (!recorder_start(stream->recorder)) { - LOGE("Could not start recorder"); - goto finally_close_recorder; - } - } - - stream->parser = av_parser_init(AV_CODEC_ID_H264); - if (!stream->parser) { - LOGE("Could not initialize parser"); - goto finally_stop_and_join_recorder; - } - - // We must only pass complete frames to av_parser_parse2()! - // It's more complicated, but this allows to reduce the latency by 1 frame! - stream->parser->flags |= PARSER_FLAG_COMPLETE_FRAMES; - - for (;;) { - AVPacket packet; - bool ok = stream_recv_packet(stream, &packet); - if (!ok) { - // end of stream - break; - } - - ok = stream_push_packet(stream, &packet); - av_packet_unref(&packet); - if (!ok) { - // cannot process packet (error already logged) - break; - } - } - - LOGD("End of frames"); - - if (stream->has_pending) { - av_packet_unref(&stream->pending); - } - - av_parser_close(stream->parser); +run_stream(void* data) { + struct stream* stream = data; + + AVCodec* codec = avcodec_find_decoder(AV_CODEC_ID_H264); + if (!codec) { + LOGE("H.264 decoder not found"); + goto end; + } + + stream->codec_ctx = avcodec_alloc_context3(codec); + if (!stream->codec_ctx) { + LOGC("Could not allocate codec context"); + goto end; + } + + if (stream->decoder && !decoder_open(stream->decoder, codec)) { + LOGE("Could not open decoder"); + goto finally_free_codec_ctx; + } + + if (stream->recorder) { + if (!recorder_open(stream->recorder, codec)) { + LOGE("Could not open recorder"); + goto finally_close_decoder; + } + + if (!recorder_start(stream->recorder)) { + LOGE("Could not start recorder"); + goto finally_close_recorder; + } + } + + stream->parser = av_parser_init(AV_CODEC_ID_H264); + if (!stream->parser) { + LOGE("Could not initialize parser"); + goto finally_stop_and_join_recorder; + } + + // We must only pass complete frames to av_parser_parse2()! + // It's more complicated, but this allows to reduce the latency by 1 frame! + stream->parser->flags |= PARSER_FLAG_COMPLETE_FRAMES; + + for (;;) { + AVPacket packet; + bool ok = stream_recv_packet(stream, &packet); + + if (!ok) { + // end of stream + break; + } + + ok = stream_push_packet(stream, &packet); + + av_packet_unref(&packet); + + if (!ok) { + // cannot process packet (error already logged) + break; + } + } + + LOGD("End of frames"); + + if (stream->has_pending) { + av_packet_unref(&stream->pending); + } + + av_parser_close(stream->parser); finally_stop_and_join_recorder: - if (stream->recorder) { - recorder_stop(stream->recorder); - LOGI("Finishing recording..."); - recorder_join(stream->recorder); - } + if (stream->recorder) { + recorder_stop(stream->recorder); + LOGI("Finishing recording..."); + recorder_join(stream->recorder); + } finally_close_recorder: - if (stream->recorder) { - recorder_close(stream->recorder); - } + if (stream->recorder) { + recorder_close(stream->recorder); + } finally_close_decoder: - if (stream->decoder) { - decoder_close(stream->decoder); - } + if (stream->decoder) { + decoder_close(stream->decoder); + } finally_free_codec_ctx: - avcodec_free_context(&stream->codec_ctx); + avcodec_free_context(&stream->codec_ctx); end: - notify_stopped(); - return 0; + notify_stopped(); + return 0; } void -stream_init(struct stream *stream, socket_t socket, - struct decoder *decoder, struct recorder *recorder) { - stream->socket = socket; - stream->decoder = decoder, - stream->recorder = recorder; - stream->has_pending = false; +stream_init(struct stream* stream, socket_t socket, + struct decoder* decoder, struct recorder* recorder, struct serve* serve) { + stream->socket = socket; + stream->decoder = decoder; + stream->recorder = recorder; + stream->serve = serve; + stream->has_pending = false; } bool -stream_start(struct stream *stream) { - LOGD("Starting stream thread"); - - stream->thread = SDL_CreateThread(run_stream, "stream", stream); - if (!stream->thread) { - LOGC("Could not start stream thread"); - return false; - } - return true; +stream_start(struct stream* stream) { + LOGD("Starting stream thread"); + + stream->thread = SDL_CreateThread(run_stream, "stream", stream); + if (!stream->thread) { + LOGC("Could not start stream thread"); + return false; + } + return true; } void -stream_stop(struct stream *stream) { - if (stream->decoder) { - decoder_interrupt(stream->decoder); - } +stream_stop(struct stream* stream) { + if (stream->decoder) { + decoder_interrupt(stream->decoder); + } } void -stream_join(struct stream *stream) { - SDL_WaitThread(stream->thread, NULL); +stream_join(struct stream* stream) { + SDL_WaitThread(stream->thread, NULL); } + diff --git a/app/src/stream.h b/app/src/stream.h index f7c5e475e2..fb82f8028b 100644 --- a/app/src/stream.h +++ b/app/src/stream.h @@ -18,6 +18,7 @@ struct stream { SDL_Thread *thread; struct decoder *decoder; struct recorder *recorder; + struct serve *serve; AVCodecContext *codec_ctx; AVCodecParserContext *parser; // successive packets may need to be concatenated, until a non-config @@ -28,7 +29,7 @@ struct stream { void stream_init(struct stream *stream, socket_t socket, - struct decoder *decoder, struct recorder *recorder); + struct decoder *decoder, struct recorder *recorder, struct serve *serve); bool stream_start(struct stream *stream); From 17947e175c2fc79c8a6e1746d9e684af852f4155 Mon Sep 17 00:00:00 2001 From: Killian Richard Date: Fri, 22 May 2020 12:12:05 +0200 Subject: [PATCH 3/9] Update --serve option with scrcpy v1.13 + Cleaning code. --- .github/ISSUE_TEMPLATE/bug_report.md | 29 + .github/ISSUE_TEMPLATE/feature_request.md | 22 + .gitignore | 2 + BUILD.md | 25 +- DEVELOP.md | 9 + FAQ.md | 163 +++++- Makefile.CrossWindows | 24 +- README.ko.md | 2 + README.md | 118 +++- README.pt-br.md | 531 ++++++++++++++++++ app/meson.build | 18 +- app/scrcpy.1 | 77 ++- app/src/cli.c | 469 ++++++++++------ app/src/command.c | 41 +- app/src/command.h | 5 + app/src/common.h | 5 + app/src/control_msg.c | 5 +- app/src/control_msg.h | 2 +- app/src/event_converter.c | 17 + app/src/fps_counter.c | 26 +- app/src/fps_counter.h | 4 +- app/src/input_manager.c | 44 +- app/src/opengl.c | 56 ++ app/src/opengl.h | 36 ++ app/src/scrcpy.c | 38 +- app/src/scrcpy.h | 40 +- app/src/screen.c | 256 +++++++-- app/src/screen.h | 32 +- app/src/serve.c | 102 +--- app/src/server.c | 272 ++++++--- app/src/server.h | 34 +- app/src/stream.c | 509 +++++++++-------- app/src/sys/unix/command.c | 50 ++ app/src/sys/win/command.c | 21 + app/src/util/net.c | 30 + app/src/util/str_util.c | 29 + app/src/util/str_util.h | 5 + app/tests/test_cli.c | 7 +- app/tests/test_control_msg_serialize.c | 10 +- app/tests/test_strutil.c | 50 ++ build.gradle | 2 +- config/checkstyle/checkstyle.xml | 5 - cross_win32.txt | 6 +- cross_win64.txt | 6 +- gradle/wrapper/gradle-wrapper.jar | Bin 55616 -> 58694 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 33 +- gradlew.bat | 3 + meson.build | 4 +- meson_options.txt | 1 + prebuilt-deps/Makefile | 30 +- server/build.gradle | 4 +- server/build_without_gradle.sh | 2 +- server/meson.build | 4 +- .../scrcpy/ControlMessageReader.java | 14 +- .../com/genymobile/scrcpy/Controller.java | 25 +- .../genymobile/scrcpy/DesktopConnection.java | 1 - .../java/com/genymobile/scrcpy/Device.java | 144 ++--- .../scrcpy/DeviceMessageWriter.java | 1 - .../com/genymobile/scrcpy/DisplayInfo.java | 22 +- .../scrcpy/InvalidDisplayIdException.java | 21 + .../java/com/genymobile/scrcpy/Options.java | 18 + .../java/com/genymobile/scrcpy/Position.java | 13 + .../com/genymobile/scrcpy/ScreenEncoder.java | 45 +- .../com/genymobile/scrcpy/ScreenInfo.java | 152 ++++- .../java/com/genymobile/scrcpy/Server.java | 34 +- .../com/genymobile/scrcpy/StringUtils.java | 1 - .../com/genymobile/scrcpy/Workarounds.java | 4 +- .../scrcpy/wrappers/ClipboardManager.java | 4 +- .../scrcpy/wrappers/DisplayManager.java | 19 +- .../scrcpy/wrappers/InputManager.java | 20 + .../scrcpy/wrappers/ServiceManager.java | 2 +- .../scrcpy/wrappers/SurfaceControl.java | 4 +- .../scrcpy/wrappers/WindowManager.java | 4 +- .../scrcpy/ControlMessageReaderTest.java | 16 +- .../genymobile/scrcpy/StringUtilsTest.java | 1 - 76 files changed, 2899 insertions(+), 983 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 README.pt-br.md create mode 100644 app/src/opengl.c create mode 100644 app/src/opengl.h create mode 100644 server/src/main/java/com/genymobile/scrcpy/InvalidDisplayIdException.java diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..1c04da7f61 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + + - [ ] I have read the [FAQ](https://github.com/Genymobile/scrcpy/blob/master/FAQ.md). + - [ ] I have searched in existing [issues](https://github.com/Genymobile/scrcpy/issues). + +**Environment** + - OS: [e.g. Debian, Windows, macOS...] + - scrcpy version: [e.g. 1.12.1] + - installation method: [e.g. manual build, apt, snap, brew, Windows release...] + - device model: + - Android version: [e.g. 10] + +**Describe the bug** +A clear and concise description of what the bug is. + +On errors, please provide the output of the console (and `adb logcat` if relevant). + +``` +Please paste terminal output in a code block. +``` + +Please do not post screenshots of your terminal, just post the content as text instead. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..524c370f9a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,22 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + + - [ ] I have checked that a similar [feature request](https://github.com/Genymobile/scrcpy/issues?q=is%3Aopen+is%3Aissue+label%3A%22feature+request%22) does not already exist. + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.gitignore b/.gitignore index 59bc840d54..f152b4b84a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ build/ /dist/ .idea/ .gradle/ +.vs/ +/x/ diff --git a/BUILD.md b/BUILD.md index e35d07d0ea..e4f175efc3 100644 --- a/BUILD.md +++ b/BUILD.md @@ -8,6 +8,22 @@ case, use the [prebuilt server] (so you will not need Java or the Android SDK). [prebuilt server]: #prebuilt-server +## Branches + +### `master` + +The `master` branch concerns the latest release, and is the home page of the +project on Github. + + +### `dev` + +`dev` is the current development branch. Every commit present in `dev` will be +in the next release. + +If you want to contribute code, please base your commits on the latest `dev` +branch. + ## Requirements @@ -233,10 +249,10 @@ You can then [run](README.md#run) _scrcpy_. ## Prebuilt server - - [`scrcpy-server-v1.12.1`][direct-scrcpy-server] - _(SHA-256: 63e569c8a1d0c1df31d48c4214871c479a601782945fed50c1e61167d78266ea)_ + - [`scrcpy-server-v1.13`][direct-scrcpy-server] + _(SHA-256: 5fee64ca1ccdc2f38550f31f5353c66de3de30c2e929a964e30fa2d005d5f885)_ -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.12.1/scrcpy-server-v1.12.1 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.13/scrcpy-server-v1.13 Download the prebuilt server somewhere, and specify its path during the Meson configuration: @@ -247,3 +263,6 @@ meson x --buildtype release --strip -Db_lto=true \ ninja -Cx sudo ninja -Cx install ``` + +The server only works with a matching client version (this server works with the +`master` branch). diff --git a/DEVELOP.md b/DEVELOP.md index 0258782fb1..4d8acc59e5 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -282,6 +282,15 @@ meson x -Dserver_debugger=true meson configure x -Dserver_debugger=true ``` +If your device runs Android 8 or below, set the `server_debugger_method` to +`old` in addition: + +```bash +meson x -Dserver_debugger=true -Dserver_debugger_method=old +# or, if x is already configured +meson configure x -Dserver_debugger=true -Dserver_debugger_method=old +``` + Then recompile. When you start scrcpy, it will start a debugger on port 5005 on the device. diff --git a/FAQ.md b/FAQ.md index 49382471e5..871ae8f002 100644 --- a/FAQ.md +++ b/FAQ.md @@ -3,19 +3,102 @@ Here are the common reported problems and their status. -### On Windows, my device is not detected +## `adb` issues -The most common is your device not being detected by `adb`, or is unauthorized. -Check everything is ok by calling: +`scrcpy` execute `adb` commands to initialize the connection with the device. If +`adb` fails, then scrcpy will not work. - adb devices +In that case, it will print this error: -Windows may need some [drivers] to detect your device. +> ERROR: "adb push" returned with value 1 + +This is typically not a bug in _scrcpy_, but a problem in your environment. + +To find out the cause, execute: + +```bash +adb devices +``` + +### `adb` not found + +You need `adb` accessible from your `PATH`. + +On Windows, the current directory is in your `PATH`, and `adb.exe` is included +in the release, so it should work out-of-the-box. + + +### Device unauthorized + +Check [stackoverflow][device-unauthorized]. + +[device-unauthorized]: https://stackoverflow.com/questions/23081263/adb-android-device-unauthorized + + +### Device not detected + +If your device is not detected, you may need some [drivers] (on Windows). [drivers]: https://developer.android.com/studio/run/oem-usb.html -### I can only mirror, I cannot interact with the device +### Several devices connected + +If several devices are connected, you will encounter this error: + +> adb: error: failed to get feature set: more than one device/emulator + +the identifier of the device you want to mirror must be provided: + +```bash +scrcpy -s 01234567890abcdef +``` + +Note that if your device is connected over TCP/IP, you'll get this message: + +> adb: error: more than one device/emulator +> ERROR: "adb reverse" returned with value 1 +> WARN: 'adb reverse' failed, fallback to 'adb forward' + +This is expected (due to a bug on old Android versions, see [#5]), but in that +case, scrcpy fallbacks to a different method, which should work. + +[#5]: https://github.com/Genymobile/scrcpy/issues/5 + + +### Conflicts between adb versions + +> adb server version (41) doesn't match this client (39); killing... + +This error occurs when you use several `adb` versions simultaneously. You must +find the program using a different `adb` version, and use the same `adb` version +everywhere. + +You could overwrite the `adb` binary in the other program, or ask _scrcpy_ to +use a specific `adb` binary, by setting the `ADB` environment variable: + +```bash +set ADB=/path/to/your/adb +scrcpy +``` + + +### Device disconnected + +If _scrcpy_ stops itself with the warning "Device disconnected", then the +`adb` connection has been closed. + +Try with another USB cable or plug it into another USB port. See [#281] and +[#283]. + +[#281]: https://github.com/Genymobile/scrcpy/issues/281 +[#283]: https://github.com/Genymobile/scrcpy/issues/283 + + + +## Control issues + +### Mouse and keyboard do not work On some devices, you may need to enable an option to allow [simulating input]. In developer options, enable: @@ -29,22 +112,43 @@ In developer options, enable: ### Mouse clicks at wrong location On MacOS, with HiDPI support and multiple screens, input location are wrongly -scaled. See [issue 15]. +scaled. See [#15]. -[issue 15]: https://github.com/Genymobile/scrcpy/issues/15 +[#15]: https://github.com/Genymobile/scrcpy/issues/15 -A workaround is to build with HiDPI support disabled: +Open _scrcpy_ directly on the monitor you use it. -```bash -meson x --buildtype release -Dhidpi_support=false -``` -However, the video will be displayed at lower resolution. +### Special characters do not work +Injecting text input is [limited to ASCII characters][text-input]. A trick +allows to also inject some [accented characters][accented-characters], but +that's all. See [#37]. -### The quality is low on HiDPI display +[text-input]: https://github.com/Genymobile/scrcpy/issues?q=is%3Aopen+is%3Aissue+label%3Aunicode +[accented-characters]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-accented-characters +[#37]: https://github.com/Genymobile/scrcpy/issues/37 -On Windows, you may need to configure the [scaling behavior]. + +## Client issues + +### The quality is low + +If the definition of your client window is smaller than that of your device +screen, then you might get poor quality, especially visible on text (see [#40]). + +[#40]: https://github.com/Genymobile/scrcpy/issues/40 + +To improve downscaling quality, trilinear filtering is enabled automatically +if the renderer is OpenGL and if it supports mipmapping. + +On Windows, you might want to force OpenGL: + +``` +scrcpy --render-driver=opengl +``` + +You may also need to configure the [scaling behavior]: > `scrcpy.exe` > Properties > Compatibility > Change high DPI settings > > Override high DPI scaling behavior > Scaling performed by: _Application_. @@ -52,6 +156,7 @@ On Windows, you may need to configure the [scaling behavior]. [scaling behavior]: https://github.com/Genymobile/scrcpy/issues/40#issuecomment-424466723 + ### KWin compositor crashes On Plasma Desktop, compositor is disabled while _scrcpy_ is running. @@ -61,19 +166,29 @@ As a workaround, [disable "Block compositing"][kwin]. [kwin]: https://github.com/Genymobile/scrcpy/issues/114#issuecomment-378778613 -### I get an error "Could not open video stream" +## Crashes + +### Exception There may be many reasons. One common cause is that the hardware encoder of your device is not able to encode at the given definition: -``` -ERROR: Exception on thread Thread[main,5,main] -android.media.MediaCodec$CodecException: Error 0xfffffc0e -... -Exit due to uncaughtException in main thread: -ERROR: Could not open video stream -INFO: Initial texture: 1080x2336 -``` +> ``` +> ERROR: Exception on thread Thread[main,5,main] +> android.media.MediaCodec$CodecException: Error 0xfffffc0e +> ... +> Exit due to uncaughtException in main thread: +> ERROR: Could not open video stream +> INFO: Initial texture: 1080x2336 +> ``` + +or + +> ``` +> ERROR: Exception on thread Thread[main,5,main] +> java.lang.IllegalStateException +> at android.media.MediaCodec.native_dequeueOutputBuffer(Native Method) +> ``` Just try with a lower definition: diff --git a/Makefile.CrossWindows b/Makefile.CrossWindows index 2b30dcb5a7..3aed2019e3 100644 --- a/Makefile.CrossWindows +++ b/Makefile.CrossWindows @@ -100,30 +100,30 @@ dist-win32: build-server build-win32 build-win32-noconsole cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN32_TARGET_DIR)/" cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp "$(WIN32_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/scrcpy-noconsole.exe" - cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/SDL2-2.0.10/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/SDL2-2.0.12/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" dist-win64: build-server build-win64 build-win64-noconsole mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)" cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN64_TARGET_DIR)/" cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp "$(WIN64_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/scrcpy-noconsole.exe" - cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/SDL2-2.0.10/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/SDL2-2.0.12/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" zip-win32: dist-win32 cd "$(DIST)/$(WIN32_TARGET_DIR)"; \ diff --git a/README.ko.md b/README.ko.md index b232accd9a..4e6d8fc58b 100644 --- a/README.ko.md +++ b/README.ko.md @@ -1,3 +1,5 @@ +_Only the original [README](README.md) is guaranteed to be up-to-date._ + # scrcpy (v1.11) This document will be updated frequently along with the original Readme file diff --git a/README.md b/README.md index 6684d8caab..04b644a443 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# scrcpy (v1.12.1) +# scrcpy (v1.13) This application provides display and control of Android devices connected on USB (or [over TCP/IP][article-tcpip]). It does not require any _root_ access. @@ -37,7 +37,7 @@ control it using keyboard and mouse. ### Linux -In Debian (_testing_ and _sid_ for now): +On Debian (_testing_ and _sid_ for now) and Ubuntu (20.04): ``` apt install scrcpy @@ -66,16 +66,13 @@ hard). ### Windows -For Windows, for simplicity, prebuilt archives with all the dependencies -(including `adb`) are available: +For Windows, for simplicity, a prebuilt archive with all the dependencies +(including `adb`) is available: - - [`scrcpy-win32-v1.12.1.zip`][direct-win32] - _(SHA-256: 0f4b3b063536b50a2df05dc42c760f9cc0093a9a26dbdf02d8232c74dab43480)_ - - [`scrcpy-win64-v1.12.1.zip`][direct-win64] - _(SHA-256: 57d34b6d16cfd9fe169bc37c4df58ebd256d05c1ea3febc63d9cb0a027ab47c9)_ + - [`scrcpy-win64-v1.13.zip`][direct-win64] + _(SHA-256: 806aafc00d4db01513193addaa24f47858893ba5efe75770bfef6ae1ea987d27)_ -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.12.1/scrcpy-win32-v1.12.1.zip -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.12.1/scrcpy-win64-v1.12.1.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.13/scrcpy-win64-v1.13.zip It is also available in [Chocolatey]: @@ -83,14 +80,18 @@ It is also available in [Chocolatey]: ```bash choco install scrcpy +choco install adb # if you don't have it yet ``` -You need `adb`, accessible from your `PATH`. If you don't have it yet: +And in [Scoop]: ```bash -choco install adb +scoop install scrcpy +scoop install adb # if you don't have it yet ``` +[Scoop]: https://scoop.sh + You can also [build the app manually][BUILD]. @@ -158,12 +159,14 @@ scrcpy -b 2M # short version #### Limit frame rate -On devices with Android >= 10, the capture frame rate can be limited: +The capture frame rate can be limited: ```bash scrcpy --max-fps 15 ``` +This is officially supported since Android 10, but may work on earlier versions. + #### Crop The device screen may be cropped to mirror only part of the screen. @@ -177,6 +180,21 @@ scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0) If `--max-size` is also specified, resizing is applied after cropping. +#### Lock video orientation + + +To lock the orientation of the mirroring: + +```bash +scrcpy --lock-video-orientation 0 # natural orientation +scrcpy --lock-video-orientation 1 # 90° counterclockwise +scrcpy --lock-video-orientation 2 # 180° +scrcpy --lock-video-orientation 3 # 90° clockwise +``` + +This affects recording orientation. + + ### Recording It is possible to record the screen while mirroring: @@ -201,22 +219,6 @@ variation] does not impact the recorded file. [packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation -### Serve - -It is possible to forward the video stream: - -```bash -scrcpy --serve tcp:localhost:1234 -``` - -To disable mirroring while forwarding the stream: - -```bash -scrcpy --no-display --serve tcp:localhost:1234 -scrcpy -N --serve tcp:localhost:1234 -# interrupt serve with Ctrl+C -# Ctrl+C does not terminate properly on Windows, so disconnect the device -``` ### Connection @@ -260,6 +262,16 @@ scrcpy -s 192.168.0.1:5555 # short version You can start several instances of _scrcpy_ for several devices. +#### Autostart on device connection + +You could use [AutoAdb]: + +```bash +autoadb scrcpy -s '{}' +``` + +[AutoAdb]: https://github.com/rom1v/autoadb + #### SSH tunnel To connect to a remote device, it is possible to connect a local `adb` client to @@ -329,6 +341,33 @@ scrcpy -f # short version Fullscreen can then be toggled dynamically with `Ctrl`+`f`. +#### Rotation + +The window may be rotated: + +```bash +scrcpy --rotation 1 +``` + +Possibles values are: + - `0`: no rotation + - `1`: 90 degrees counterclockwise + - `2`: 180 degrees + - `3`: 90 degrees clockwise + +The rotation can also be changed dynamically with `Ctrl`+`←` _(left)_ and +`Ctrl`+`→` _(right)_. + +Note that _scrcpy_ manages 3 different rotations: + - `Ctrl`+`r` requests the device to switch between portrait and landscape (the + current running app may refuse, if it does support the requested + orientation). + - `--lock-video-orientation` changes the mirroring orientation (the orientation + of the video sent from the device to the computer). This affects the + recording. + - `--rotation` (or `Ctrl`+`←`/`Ctrl`+`→`) rotates only the window content. This + affects only the display, not the recording. + ### Other mirroring options @@ -342,6 +381,25 @@ scrcpy --no-control scrcpy -n ``` +#### Display + +If several displays are available, it is possible to select the display to +mirror: + +```bash +scrcpy --display 1 +``` + +The list of display ids can be retrieved by: + +``` +adb shell dumpsys display # search "mDisplayId=" in the output +``` + +The secondary display may only be controlled if the device runs at least Android +10 (otherwise it is mirrored in read-only). + + #### Turn screen off It is possible to turn the device screen off while mirroring on start with a @@ -465,6 +523,8 @@ Also see [issue #14]. | Action | Shortcut | Shortcut (macOS) | -------------------------------------- |:----------------------------- |:----------------------------- | Switch fullscreen mode | `Ctrl`+`f` | `Cmd`+`f` + | Rotate display left | `Ctrl`+`←` _(left)_ | `Cmd`+`←` _(left)_ + | Rotate display right | `Ctrl`+`→` _(right)_ | `Cmd`+`→` _(right)_ | Resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` | `Cmd`+`g` | Resize window to remove black borders | `Ctrl`+`x` \| _Double-click¹_ | `Cmd`+`x` \| _Double-click¹_ | Click on `HOME` | `Ctrl`+`h` \| _Middle-click_ | `Ctrl`+`h` \| _Middle-click_ diff --git a/README.pt-br.md b/README.pt-br.md new file mode 100644 index 0000000000..654f62cb29 --- /dev/null +++ b/README.pt-br.md @@ -0,0 +1,531 @@ +_Only the original [README](README.md) is guaranteed to be up-to-date._ + +# scrcpy (v1.12.1) + +Esta aplicação fornece visualização e controle de dispositivos Android conectados via +USB (ou [via TCP/IP][article-tcpip]). Não requer nenhum acesso root. +Funciona em _GNU/Linux_, _Windows_ e _macOS_. + +![screenshot](assets/screenshot-debian-600.jpg) + +Foco em: + + - **leveza** (Nativo, mostra apenas a tela do dispositivo) + - **performance** (30~60fps) + - **qualidade** (1920×1080 ou acima) + - **baixa latência** ([35~70ms][lowlatency]) + - **baixo tempo de inicialização** (~1 segundo para mostrar a primeira imagem) + - **não intrusivo** (nada é deixado instalado no dispositivo) + +[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 + + +## Requisitos + +O Dispositivo Android requer pelo menos a API 21 (Android 5.0). + + +Tenha certeza de ter [ativado a depuração USB][enable-adb] no(s) seu(s) dispositivo(s). + +[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling + + +Em alguns dispositivos, você também precisará ativar [uma opção adicional][control] para controlá-lo usando o teclado e mouse. + +[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 + + +## Obtendo o app + + +### Linux + +No Debian (_em testes_ e _sid_ por enquanto): + +``` +apt install scrcpy +``` + +O pacote [Snap] está disponível: [`scrcpy`][snap-link]. + +[snap-link]: https://snapstats.org/snaps/scrcpy + +[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager) + +Para Arch Linux, um pacote [AUR] está disponível: [`scrcpy`][aur-link]. + +[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository +[aur-link]: https://aur.archlinux.org/packages/scrcpy/ + +Para Gentoo, uma [Ebuild] está disponível: [`scrcpy/`][ebuild-link]. + +[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild +[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy + + +Você também pode [compilar a aplicação manualmente][BUILD] (não se preocupe, não é tão difícil). + + +### Windows + +Para Windows, para simplicidade, um arquivo pré-compilado com todas as dependências +(incluindo `adb`) está disponível: + + - [`scrcpy-win64-v1.12.1.zip`][direct-win64] + _(SHA-256: 57d34b6d16cfd9fe169bc37c4df58ebd256d05c1ea3febc63d9cb0a027ab47c9)_ + +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.12.1/scrcpy-win64-v1.12.1.zip + +Também disponível em [Chocolatey]: + +[Chocolatey]: https://chocolatey.org/ + +```bash +choco install scrcpy +choco install adb # se você ainda não o tem +``` + +E no [Scoop]: + +```bash +scoop install scrcpy +scoop install adb # se você ainda não o tem +``` + +[Scoop]: https://scoop.sh + +Você também pode [compilar a aplicação manualmente][BUILD]. + + +### macOS + +A aplicação está disponível em [Homebrew]. Apenas a instale: + +[Homebrew]: https://brew.sh/ + +```bash +brew install scrcpy +``` + +Você precisa do `adb`, acessível através do seu `PATH`. Se você ainda não o tem: + +```bash +brew cask install android-platform-tools +``` + +Você também pode [compilar a aplicação manualmente][BUILD]. + + +## Executar + +Plugue um dispositivo Android e execute: + +```bash +scrcpy +``` + +Também aceita argumentos de linha de comando, listados por: + +```bash +scrcpy --help +``` + +## Funcionalidades + +### Configuração de captura + +#### Redução de tamanho + +Algumas vezes, é útil espelhar um dispositivo Android em uma resolução menor para +aumentar performance. + +Para limitar ambos(largura e altura) para algum valor (ex: 1024): + +```bash +scrcpy --max-size 1024 +scrcpy -m 1024 # versão reduzida +``` + +A outra dimensão é calculada para que a proporção do dispositivo seja preservada. +Dessa forma, um dispositivo em 1920x1080 será espelhado em 1024x576. + + +#### Mudanças no bit-rate + +O Padrão de bit-rate é 8 mbps. Para mudar o bitrate do vídeo (ex: para 2 Mbps): + +```bash +scrcpy --bit-rate 2M +scrcpy -b 2M # versão reduzida +``` + +#### Limitar frame rates + +Em dispositivos com Android >= 10, a captura de frame rate pode ser limitada: + +```bash +scrcpy --max-fps 15 +``` + +#### Cortar + +A tela do dispositivo pode ser cortada para espelhar apenas uma parte da tela. + +Isso é útil por exemplo, ao espelhar apenas um olho do Oculus Go: + +```bash +scrcpy --crop 1224:1440:0:0 # 1224x1440 no deslocamento (0,0) +``` + +Se `--max-size` também for especificado, redimensionar é aplicado após os cortes. + + +### Gravando + +É possível gravar a tela enquanto ocorre o espelhamento: + +```bash +scrcpy --record file.mp4 +scrcpy -r file.mkv +``` + +Para desativar o espelhamento durante a gravação: + +```bash +scrcpy --no-display --record file.mp4 +scrcpy -Nr file.mkv +# interrompe a gravação com Ctrl+C +# Ctrl+C não encerrar propriamente no Windows, então desconecte o dispositivo +``` + +"Frames pulados" são gravados, mesmo que não sejam mostrado em tempo real (por motivos de performance). +Frames tem seu _horário_ _carimbado_ no dispositivo, então [Variação de atraso nos pacotes] não impacta na gravação do arquivo. + +[Variação de atraso de pacote]: https://en.wikipedia.org/wiki/Packet_delay_variation + + +### Conexão + +#### Wireless/Sem fio + +_Scrcpy_ usa `adb` para se comunicar com o dispositivo, e `adb` pode [conectar-se] à um dispositivo via TCP/IP: + +1. Conecte o dispositivo a mesma rede Wi-Fi do seu computador. +2. Pegue o endereço de IP do seu dispositivo (Em Configurações → Sobre o Telefone → Status). +3. Ative o adb via TCP/IP no seu dispositivo: `adb tcpip 5555`. +4. Desplugue seu dispositivo. +5. Conecte-se ao seu dispositivo: `adb connect DEVICE_IP:5555` _(substitua o `DEVICE_IP`)_. +6. Execute `scrcpy` como de costume. + +Pode ser útil diminuir o bit-rate e a resolução: + +```bash +scrcpy --bit-rate 2M --max-size 800 +scrcpy -b2M -m800 # versão reduzida +``` + +[conectar-se]: https://developer.android.com/studio/command-line/adb.html#wireless + + +#### N-dispositivos + +Se alguns dispositivos estão listados em `adb devices`, você precisa especificar o _serial_: + +```bash +scrcpy --serial 0123456789abcdef +scrcpy -s 0123456789abcdef # versão reduzida +``` + +Se o dispositivo está conectado via TCP/IP: + +```bash +scrcpy --serial 192.168.0.1:5555 +scrcpy -s 192.168.0.1:5555 # versão reduzida +``` + +Você pode iniciar algumas instâncias do _scrcpy_ para alguns dispositivos. + +#### Conexão via SSH + +Para conectar-se à um dispositivo remoto, é possível se conectar um cliente local `adb` à um servidor `adb` remoto (contanto que eles usem a mesma versão do protocolo _adb_): + +```bash +adb kill-server # encerra o servidor local na 5037 +ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer +# mantém isso aberto +``` + +De outro terminal: + +```bash +scrcpy +``` + +Igual para conexões sem fio, pode ser útil reduzir a qualidade: + +``` +scrcpy -b2M -m800 --max-fps 15 +``` + +### Configurações de Janela + +#### Título + +Por padrão, o título da janela é o modelo do dispositivo. Isto pode ser mudado: + +```bash +scrcpy --window-title 'Meu dispositivo' +``` + +#### Posição e tamanho + +A posição e tamanho iniciais da janela podem ser especificados: + +```bash +scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600 +``` + +#### Sem bordas + +Para desativar decorações da janela: + +```bash +scrcpy --window-borderless +``` + +#### Sempre visível + +Para manter a janela do scrcpy sempre visível: + +```bash +scrcpy --always-on-top +``` + +#### Tela cheia + +A aplicação pode ser iniciada diretamente em tela cheia: + +```bash +scrcpy --fullscreen +scrcpy -f # versão reduzida +``` + +Tela cheia pode ser alternada dinamicamente com `Ctrl`+`f`. + + +### Outras opções de espelhamento + +#### Apenas leitura + +Para desativar controles (tudo que possa interagir com o dispositivo: teclas de entrada, eventos de mouse, arrastar e soltar arquivos): + +```bash +scrcpy --no-control +scrcpy -n +``` + +#### Desligar a tela + +É possível desligar a tela do dispositivo durante o início do espelhamento com uma opção de linha de comando: + +```bash +scrcpy --turn-screen-off +scrcpy -S +``` + +Ou apertando `Ctrl`+`o` durante qualquer momento. + +Para ligar novamente, pressione `POWER` (ou `Ctrl`+`p`). + +#### Frames expirados de renderização + +Por padrão, para minimizar a latência, _scrcpy_ sempre renderiza o último frame decodificado disponível e descarta o anterior. + +Para forçar a renderização de todos os frames ( com o custo de aumento de latência), use: + +```bash +scrcpy --render-expired-frames +``` + +#### Mostrar toques + +Para apresentações, pode ser útil mostrar toques físicos(dispositivo físico). + +Android fornece esta funcionalidade nas _Opções do Desenvolvedor_. + +_Scrcpy_ fornece esta opção de ativar esta funcionalidade no início e desativar no encerramento: + +```bash +scrcpy --show-touches +scrcpy -t +``` + +Note que isto mostra apenas toques _físicos_ (com o dedo no dispositivo). + + +### Controle de entrada + +#### Rotacionar a tela do dispositivo + +Pressione `Ctrl`+`r` para mudar entre os modos Retrato e Paisagem. + +Note que só será rotacionado se a aplicação em primeiro plano tiver suporte para o modo requisitado. + +#### Copiar-Colar + +É possível sincronizar áreas de transferência entre computador e o dispositivo, +para ambas direções: + + - `Ctrl`+`c` copia a área de transferência do dispositivo para a área de trasferência do computador; + - `Ctrl`+`Shift`+`v` copia a área de transferência do computador para a área de transferência do dispositivo; + - `Ctrl`+`v` _cola_ a área de transferência do computador como uma sequência de eventos de texto (mas + quebra caracteres não-ASCII). + +#### Preferências de injeção de texto + +Existe dois tipos de [eventos][textevents] gerados ao digitar um texto: + - _eventos de teclas_, sinalizando que a tecla foi pressionada ou solta; + - _eventos de texto_, sinalizando que o texto foi inserido. + +Por padrão, letras são injetadas usando eventos de teclas, assim teclados comportam-se +como esperado em jogos (normalmente para tecladas WASD) + +Mas isto pode [causar problemas][prefertext]. Se você encontrar tal problema, +pode evitá-lo usando: + +```bash +scrcpy --prefer-text +``` + +(mas isto vai quebrar o comportamento do teclado em jogos) + +[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input +[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 + + +### Transferência de arquivo + +#### Instalar APK + +Para instalar um APK, arraste e solte o arquivo APK(com extensão `.apk`) na janela _scrcpy_. + +Não existe feedback visual, um log é imprimido no console. + + +#### Enviar arquivo para o dispositivo + +Para enviar um arquivo para o diretório `/sdcard/` no dispositivo, arraste e solte um arquivo não APK para a janela do +_scrcpy_. + +Não existe feedback visual, um log é imprimido no console. + +O diretório alvo pode ser mudado ao iniciar: + +```bash +scrcpy --push-target /sdcard/foo/bar/ +``` + + +### Encaminhamento de áudio + +Áudio não é encaminhando pelo _scrcpy_. Use [USBaudio] (Apenas linux). + +Também veja [issue #14]. + +[USBaudio]: https://github.com/rom1v/usbaudio +[issue #14]: https://github.com/Genymobile/scrcpy/issues/14 + + +## Atalhos + + | Ação | Atalho | Atalho (macOS) + | ------------------------------------------------------------- |:------------------------------- |:----------------------------- + | Alternar para modo de tela cheia | `Ctrl`+`f` | `Cmd`+`f` + | Redimensionar janela para pixel-perfect(Escala 1:1) | `Ctrl`+`g` | `Cmd`+`g` + | Redimensionar janela para tirar as bordas pretas | `Ctrl`+`x` \| _Clique-duplo¹_ | `Cmd`+`x` \| _Clique-duplo¹_ + | Clicar em `HOME` | `Ctrl`+`h` \| _Clique-central_ | `Ctrl`+`h` \| _Clique-central_ + | Clicar em `BACK` | `Ctrl`+`b` \| _Clique-direito²_ | `Cmd`+`b` \| _Clique-direito²_ + | Clicar em `APP_SWITCH` | `Ctrl`+`s` | `Cmd`+`s` + | Clicar em `MENU` | `Ctrl`+`m` | `Ctrl`+`m` + | Clicar em `VOLUME_UP` | `Ctrl`+`↑` _(cima)_ | `Cmd`+`↑` _(cima)_ + | Clicar em `VOLUME_DOWN` | `Ctrl`+`↓` _(baixo)_ | `Cmd`+`↓` _(baixo)_ + | Clicar em `POWER` | `Ctrl`+`p` | `Cmd`+`p` + | Ligar | _Clique-direito²_ | _Clique-direito²_ + | Desligar a tela do dispositivo | `Ctrl`+`o` | `Cmd`+`o` + | Rotacionar tela do dispositivo | `Ctrl`+`r` | `Cmd`+`r` + | Expandir painel de notificação | `Ctrl`+`n` | `Cmd`+`n` + | Esconder painel de notificação | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n` + | Copiar área de transferência do dispositivo para o computador | `Ctrl`+`c` | `Cmd`+`c` + | Colar área de transferência do computador para o dispositivo | `Ctrl`+`v` | `Cmd`+`v` + | Copiar área de transferência do computador para dispositivo | `Ctrl`+`Shift`+`v` | `Cmd`+`Shift`+`v` + | Ativar/desativar contador de FPS(Frames por segundo) | `Ctrl`+`i` | `Cmd`+`i` + +_¹Clique-duplo em bordas pretas para removê-las._ +_²Botão direito liga a tela se ela estiver desligada, clique BACK para o contrário._ + + +## Caminhos personalizados + +Para usar um binário específico _adb_, configure seu caminho na variável de ambiente `ADB`: + + ADB=/caminho/para/adb scrcpy + +Para sobrepor o caminho do arquivo `scrcpy-server`, configure seu caminho em +`SCRCPY_SERVER_PATH`. + +[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345 + + +## Por quê _scrcpy_? + +Um colega me desafiou a encontrar um nome impronunciável como [gnirehtet]. + +[`strcpy`] copia uma **str**ing; `scrcpy` copia uma **scr**een. + +[gnirehtet]: https://github.com/Genymobile/gnirehtet +[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html + + +## Como compilar? + +Veja [BUILD]. + +[BUILD]: BUILD.md + + +## Problemas comuns + +Veja [FAQ](FAQ.md). + + +## Desenvolvedores + +Leia a [developers page]. + +[developers page]: DEVELOP.md + + +## Licença + + Copyright (C) 2018 Genymobile + Copyright (C) 2018-2020 Romain Vimont + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +## Artigos + +- [Introducing scrcpy][article-intro] +- [Scrcpy now works wirelessly][article-tcpip] + +[article-intro]: https://blog.rom1v.com/2018/03/introducing-scrcpy/ +[article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/ diff --git a/app/meson.build b/app/meson.build index fbda134026..5d2b4caac3 100644 --- a/app/meson.build +++ b/app/meson.build @@ -11,11 +11,11 @@ src = [ 'src/file_handler.c', 'src/fps_counter.c', 'src/input_manager.c', + 'src/opengl.c', 'src/receiver.c', 'src/recorder.c', 'src/scrcpy.c', 'src/screen.c', - 'src/serve.c', 'src/server.c', 'src/stream.c', 'src/tiny_xpm.c', @@ -77,11 +77,9 @@ cc = meson.get_compiler('c') if host_machine.system() == 'windows' src += [ 'src/sys/win/command.c' ] - src += [ 'src/sys/win/net.c' ] dependencies += cc.find_library('ws2_32') else src += [ 'src/sys/unix/command.c' ] - src += [ 'src/sys/unix/net.c' ] endif conf = configuration_data() @@ -99,14 +97,21 @@ conf.set_quoted('PREFIX', get_option('prefix')) # directory as the executable) conf.set('PORTABLE', get_option('portable')) -# the default client TCP port for the "adb reverse" tunnel +# the default client TCP port range for the "adb reverse" tunnel # overridden by option --port -conf.set('DEFAULT_LOCAL_PORT', '27183') +conf.set('DEFAULT_LOCAL_PORT_RANGE_FIRST', '27183') +conf.set('DEFAULT_LOCAL_PORT_RANGE_LAST', '27199') # the default max video size for both dimensions, in pixels # overridden by option --max-size conf.set('DEFAULT_MAX_SIZE', '0') # 0: unlimited +# the default video orientation +# natural device orientation is 0 and each increment adds 90 degrees +# counterclockwise +# overridden by option --lock-video-orientation +conf.set('DEFAULT_LOCK_VIDEO_ORIENTATION', '-1') # -1: unlocked + # the default video bitrate, in bits/second # overridden by option --bit-rate conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps @@ -120,6 +125,9 @@ conf.set('WINDOWS_NOCONSOLE', get_option('windows_noconsole')) # run a server debugger and wait for a client to be attached conf.set('SERVER_DEBUGGER', get_option('server_debugger')) +# select the debugger method ('old' for Android < 9, 'new' for Android >= 9) +conf.set('SERVER_DEBUGGER_METHOD_NEW', get_option('server_debugger_method') == 'new') + configure_file(configuration: conf, output: 'config.h') src_dir = include_directories('src') diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 9560df1c75..673cd11d55 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -33,6 +33,15 @@ The values are expressed in the device natural orientation (typically, portrait .B \-\-max\-size value is computed on the cropped size. +.TP +.BI "\-\-display " id +Specify the display id to mirror. + +The list of possible display ids can be listed by "adb shell dumpsys display" +(search "mDisplayId=" in the output). + +Default is 0. + .TP .B \-f, \-\-fullscreen Start in fullscreen. @@ -41,9 +50,15 @@ Start in fullscreen. .B \-h, \-\-help Print this help. +.TP +.BI "\-\-lock\-video\-orientation " value +Lock video orientation to \fIvalue\fR. Possible values are -1 (unlocked), 0, 1, 2 and 3. Natural device orientation is 0, and each increment adds a 90 degrees otation counterclockwise. + +Default is -1 (unlocked). + .TP .BI "\-\-max\-fps " value -Limit the framerate of screen capture (only supported on devices with Android >= 10). +Limit the framerate of screen capture (officially supported since Android 10, but may work on earlier versions). .TP .BI "\-m, \-\-max\-size " value @@ -60,10 +75,14 @@ Disable device control (mirror the device in read\-only). Do not display device (only when screen recording is enabled). .TP -.BI "\-p, \-\-port " port -Set the TCP port the client listens on. +.B \-\-no\-mipmaps +If the renderer is OpenGL 3.0+ or OpenGL ES 2.0+, then mipmaps are automatically generated to improve downscaling quality. This option disables the generation of mipmaps. -Default is 27183. +.TP +.BI "\-p, \-\-port " port[:port] +Set the TCP port (range) used by the client to listen. + +Default is 27183:27199. .TP .B \-\-prefer\-text @@ -91,10 +110,22 @@ option if set, or by the file extension (.mp4 or .mkv). .BI "\-\-record\-format " format Force recording format (either mp4 or mkv). +.TP +.BI "\-\-render\-driver " name +Request SDL to use the given render driver (this is just a hint). + +Supported names are currently "direct3d", "opengl", "opengles2", "opengles", "metal" and "software". +.UR https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER +.UE + .TP .B \-\-render\-expired\-frames By default, to minimize latency, scrcpy always renders the last available decoded frame, and drops any previous ones. This flag forces to render all frames, at a cost of a possible increased latency. +.TP +.BI "\-\-rotation " value +Set the initial display rotation. Possibles values are 0, 1, 2 and 3. Each increment adds a 90 degrees rotation counterclockwise. + .TP .BI "\-s, \-\-serial " number The device serial number. Mandatory only if several devices are connected to adb. @@ -125,13 +156,13 @@ Set a custom window title. .BI "\-\-window\-x " value Set the initial window horizontal position. -Default is -1 (automatic).\n +Default is "auto".\n .TP .BI "\-\-window\-y " value Set the initial window vertical position. -Default is -1 (automatic).\n +Default is "auto".\n .TP .BI "\-\-window\-width " value @@ -149,15 +180,23 @@ Default is 0 (automatic).\n .TP .B Ctrl+f -switch fullscreen mode +Switch fullscreen mode + +.TP +.B Ctrl+Left +Rotate display left + +.TP +.B Ctrl+Right +Rotate display right .TP .B Ctrl+g -resize window to 1:1 (pixel\-perfect) +Resize window to 1:1 (pixel\-perfect) .TP .B Ctrl+x, Double\-click on black borders -resize window to remove black borders +Resize window to remove black borders .TP .B Ctrl+h, Home, Middle\-click @@ -189,43 +228,43 @@ Click on POWER (turn screen on/off) .TP .B Right\-click (when screen is off) -turn screen on +Turn screen on .TP .B Ctrl+o -turn device screen off (keep mirroring) +Turn device screen off (keep mirroring) .TP .B Ctrl+r -rotate device screen +Rotate device screen .TP .B Ctrl+n -expand notification panel +Expand notification panel .TP .B Ctrl+Shift+n -collapse notification panel +Collapse notification panel .TP .B Ctrl+c -copy device clipboard to computer +Copy device clipboard to computer .TP .B Ctrl+v -paste computer clipboard to device +Paste computer clipboard to device .TP .B Ctrl+Shift+v -copy computer clipboard to device +Copy computer clipboard to device .TP .B Ctrl+i -enable/disable FPS counter (print frames/second in logs) +Enable/disable FPS counter (print frames/second in logs) .TP .B Drag & drop APK file -install APK from computer +Install APK from computer .SH Environment variables diff --git a/app/src/cli.c b/app/src/cli.c index a449f9121b..69b89899b8 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1,5 +1,6 @@ #include "cli.h" +#include #include #include #include @@ -10,8 +11,6 @@ #include "util/log.h" #include "util/str_util.h" -#define IPV4_LOCALHOST 0x7F000001 - void scrcpy_print_usage(const char *arg0) { #ifdef __APPLE__ @@ -38,15 +37,31 @@ scrcpy_print_usage(const char *arg0) { " (typically, portrait for a phone, landscape for a tablet).\n" " Any --max-size value is computed on the cropped size.\n" "\n" + " --display id\n" + " Specify the display id to mirror.\n" + "\n" + " The list of possible display ids can be listed by:\n" + " adb shell dumpsys display\n" + " (search \"mDisplayId=\" in the output)\n" + "\n" + " Default is 0.\n" + "\n" " -f, --fullscreen\n" " Start in fullscreen.\n" "\n" " -h, --help\n" " Print this help.\n" "\n" + " --lock-video-orientation value\n" + " Lock video orientation to value.\n" + " Possible values are -1 (unlocked), 0, 1, 2 and 3.\n" + " Natural device orientation is 0, and each increment adds a\n" + " 90 degrees rotation counterclockwise.\n" + " Default is %d%s.\n" + "\n" " --max-fps value\n" - " Limit the frame rate of screen capture (only supported on\n" - " devices with Android >= 10).\n" + " Limit the frame rate of screen capture (officially supported\n" + " since Android 10, but may work on earlier versions).\n" "\n" " -m, --max-size value\n" " Limit both the width and height of the video to value. The\n" @@ -61,9 +76,14 @@ scrcpy_print_usage(const char *arg0) { " Do not display device (only when screen recording is\n" " enabled).\n" "\n" - " -p, --port port\n" - " Set the TCP port the client listens on.\n" - " Default is %d.\n" + " --no-mipmaps\n" + " If the renderer is OpenGL 3.0+ or OpenGL ES 2.0+, then\n" + " mipmaps are automatically generated to improve downscaling\n" + " quality. This option disables the generation of mipmaps.\n" + "\n" + " -p, --port port[:port]\n" + " Set the TCP port (range) used by the client to listen.\n" + " Default is %d:%d.\n" "\n" " --prefer-text\n" " Inject alpha characters and space as text events instead of\n" @@ -82,13 +102,20 @@ scrcpy_print_usage(const char *arg0) { " The format is determined by the --record-format option if\n" " set, or by the file extension (.mp4 or .mkv).\n" "\n" + " --record-format format\n" + " Force recording format (either mp4 or mkv).\n" + "\n" " --serve tcp:localhost:1234\n" " Open a socket to redirect video stream.\n" - " It will Wait for a client to connect before starting the mirroring,\n" + " It will wait for a client to connect before starting the mirroring,\n" " then it would forward the video stream.\n" "\n" - " --record-format format\n" - " Force recording format (either mp4 or mkv).\n" + " --render-driver name\n" + " Request SDL to use the given render driver (this is just a\n" + " hint).\n" + " Supported names are currently \"direct3d\", \"opengl\",\n" + " \"opengles2\", \"opengles\", \"metal\" and \"software\".\n" + " \n" "\n" " --render-expired-frames\n" " By default, to minimize latency, scrcpy always renders the\n" @@ -96,6 +123,11 @@ scrcpy_print_usage(const char *arg0) { " This flag forces to render all frames, at a cost of a\n" " possible increased latency.\n" "\n" + " --rotation value\n" + " Set the initial display rotation.\n" + " Possibles values are 0, 1, 2 and 3. Each increment adds a 90\n" + " degrees rotation counterclockwise.\n" + "\n" " -s, --serial serial\n" " The device serial number. Mandatory only if several devices\n" " are connected to adb.\n" @@ -118,11 +150,11 @@ scrcpy_print_usage(const char *arg0) { "\n" " --window-x value\n" " Set the initial window horizontal position.\n" - " Default is -1 (automatic).\n" + " Default is \"auto\".\n" "\n" " --window-y value\n" " Set the initial window vertical position.\n" - " Default is -1 (automatic).\n" + " Default is \"auto\".\n" "\n" " --window-width value\n" " Set the initial window width.\n" @@ -135,73 +167,80 @@ scrcpy_print_usage(const char *arg0) { "Shortcuts:\n" "\n" " " CTRL_OR_CMD "+f\n" - " switch fullscreen mode\n" + " Switch fullscreen mode\n" + "\n" + " " CTRL_OR_CMD "+Left\n" + " Rotate display left\n" + "\n" + " " CTRL_OR_CMD "+Right\n" + " Rotate display right\n" "\n" " " CTRL_OR_CMD "+g\n" - " resize window to 1:1 (pixel-perfect)\n" + " Resize window to 1:1 (pixel-perfect)\n" "\n" " " CTRL_OR_CMD "+x\n" " Double-click on black borders\n" - " resize window to remove black borders\n" + " Resize window to remove black borders\n" "\n" " Ctrl+h\n" " Middle-click\n" - " click on HOME\n" + " Click on HOME\n" "\n" " " CTRL_OR_CMD "+b\n" " " CTRL_OR_CMD "+Backspace\n" " Right-click (when screen is on)\n" - " click on BACK\n" + " Click on BACK\n" "\n" " " CTRL_OR_CMD "+s\n" - " click on APP_SWITCH\n" + " Click on APP_SWITCH\n" "\n" " Ctrl+m\n" - " click on MENU\n" + " Click on MENU\n" "\n" " " CTRL_OR_CMD "+Up\n" - " click on VOLUME_UP\n" + " Click on VOLUME_UP\n" "\n" " " CTRL_OR_CMD "+Down\n" - " click on VOLUME_DOWN\n" + " Click on VOLUME_DOWN\n" "\n" " " CTRL_OR_CMD "+p\n" - " click on POWER (turn screen on/off)\n" + " Click on POWER (turn screen on/off)\n" "\n" " Right-click (when screen is off)\n" - " power on\n" + " Power on\n" "\n" " " CTRL_OR_CMD "+o\n" - " turn device screen off (keep mirroring)\n" + " Turn device screen off (keep mirroring)\n" "\n" " " CTRL_OR_CMD "+r\n" - " rotate device screen\n" + " Rotate device screen\n" "\n" " " CTRL_OR_CMD "+n\n" - " expand notification panel\n" + " Expand notification panel\n" "\n" " " CTRL_OR_CMD "+Shift+n\n" - " collapse notification panel\n" + " Collapse notification panel\n" "\n" " " CTRL_OR_CMD "+c\n" - " copy device clipboard to computer\n" + " Copy device clipboard to computer\n" "\n" " " CTRL_OR_CMD "+v\n" - " paste computer clipboard to device\n" + " Paste computer clipboard to device\n" "\n" " " CTRL_OR_CMD "+Shift+v\n" - " copy computer clipboard to device\n" + " Copy computer clipboard to device\n" "\n" " " CTRL_OR_CMD "+i\n" - " enable/disable FPS counter (print frames/second in logs)\n" + " Enable/disable FPS counter (print frames/second in logs)\n" "\n" " Drag & drop APK file\n" - " install APK from computer\n" + " Install APK from computer\n" "\n", arg0, DEFAULT_BIT_RATE, + DEFAULT_LOCK_VIDEO_ORIENTATION, DEFAULT_LOCK_VIDEO_ORIENTATION >= 0 ? "" : " (unlocked)", DEFAULT_MAX_SIZE, DEFAULT_MAX_SIZE ? "" : " (unlimited)", - DEFAULT_LOCAL_PORT); + DEFAULT_LOCAL_PORT_RANGE_FIRST, DEFAULT_LOCAL_PORT_RANGE_LAST); } static bool @@ -229,6 +268,27 @@ parse_integer_arg(const char *s, long *out, bool accept_suffix, long min, return true; } +static size_t +parse_integers_arg(const char *s, size_t max_items, long *out, long min, + long max, const char *name) { + size_t count = parse_integers(s, ':', max_items, out); + if (!count) { + LOGE("Could not parse %s: %s", name, s); + return 0; + } + + for (size_t i = 0; i < count; ++i) { + long value = out[i]; + if (value < min || value > max) { + LOGE("Could not parse %s: value (%ld) out-of-range (%ld; %ld)", + name, value, min, max); + return 0; + } + } + + return count; +} + static bool parse_bit_rate(const char *s, uint32_t *bit_rate) { long value; @@ -267,10 +327,43 @@ parse_max_fps(const char *s, uint16_t *max_fps) { return true; } +static bool +parse_lock_video_orientation(const char *s, int8_t *lock_video_orientation) { + long value; + bool ok = parse_integer_arg(s, &value, false, -1, 3, + "lock video orientation"); + if (!ok) { + return false; + } + + *lock_video_orientation = (int8_t) value; + return true; +} + +static bool +parse_rotation(const char *s, uint8_t *rotation) { + long value; + bool ok = parse_integer_arg(s, &value, false, 0, 3, "rotation"); + if (!ok) { + return false; + } + + *rotation = (uint8_t) value; + return true; +} + static bool parse_window_position(const char *s, int16_t *position) { + // special value for "auto" + static_assert(WINDOW_POSITION_UNDEFINED == -0x8000, "unexpected value"); + + if (!strcmp(s, "auto")) { + *position = WINDOW_POSITION_UNDEFINED; + return true; + } + long value; - bool ok = parse_integer_arg(s, &value, false, -1, 0x7FFF, + bool ok = parse_integer_arg(s, &value, false, -0x7FFF, 0x7FFF, "window position"); if (!ok) { return false; @@ -294,18 +387,45 @@ parse_window_dimension(const char *s, uint16_t *dimension) { } static bool -parse_port(const char *s, uint16_t *port) { +parse_port_range(const char *s, struct port_range *port_range) { + long values[2]; + size_t count = parse_integers_arg(s, 2, values, 0, 0xFFFF, "port"); + if (!count) { + return false; + } + + uint16_t v0 = (uint16_t) values[0]; + if (count == 1) { + port_range->first = v0; + port_range->last = v0; + return true; + } + + assert(count == 2); + uint16_t v1 = (uint16_t) values[1]; + if (v0 < v1) { + port_range->first = v0; + port_range->last = v1; + } else { + port_range->first = v1; + port_range->last = v0; + } + + return true; +} + +static bool +parse_display_id(const char *s, uint16_t *display_id) { long value; - bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "port"); + bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "display id"); if (!ok) { return false; } - *port = (uint16_t) value; + *display_id = (uint16_t) value; return true; } - static bool parse_record_format(const char *optarg, enum recorder_format *format) { if (!strcmp(optarg, "mp4")) { @@ -320,9 +440,24 @@ parse_record_format(const char *optarg, enum recorder_format *format) { return false; } -char** -str_split(const char* a_str, const char a_delim) -{ +static enum recorder_format +guess_record_format(const char *filename) { + size_t len = strlen(filename); + if (len < 4) { + return 0; + } + const char *ext = &filename[len - 4]; + if (!strcmp(ext, ".mp4")) { + return RECORDER_FORMAT_MP4; + } + if (!strcmp(ext, ".mkv")) { + return RECORDER_FORMAT_MKV; + } + return 0; +} + +char** +str_split(const char *a_str, const char a_delim) { char** result = 0; size_t count = 0; char* tmp = (char*)a_str; @@ -334,10 +469,8 @@ str_split(const char* a_str, const char a_delim) delim[1] = 0; /* Count how many elements will be extracted. */ - while (*tmp) - { - if (a_delim == *tmp) - { + while (*tmp) { + if (a_delim == *tmp) { count++; last_comma = tmp; } @@ -347,23 +480,22 @@ str_split(const char* a_str, const char a_delim) /* Add space for trailing token. */ count += last_comma < (str + strlen(str) - 1); - /* Add space for terminating null string so caller - knows where the list of returned strings ends. */ + /* Add space for terminating null string so caller + knows where the list of returned strings ends. */ count++; result = malloc(sizeof(char*) * count); - if (result) - { + if (result) { size_t idx = 0; char* token = strtok(str, delim); - while (token) - { + while (token) { assert(idx < count); *(result + idx++) = strdup(token); token = strtok(0, delim); } + assert(idx == count - 1); *(result + idx) = 0; } @@ -371,31 +503,37 @@ str_split(const char* a_str, const char a_delim) return result; } -int -validate_ip(char* ip) { //check whether the IP is valid or not +bool +check_if_ip_valid(char *ip) { int num, dots = 0; char* ptr; + if (ip == NULL) return 0; - ptr = strtok(ip, "."); //cut the string using dor delimiter + + ptr = strtok(ip, "."); //Cut the string using dot as delimiter + if (ptr == NULL) - return 0; + return false; + while (ptr) { - long value; - if (!parse_integer(ptr, &value)) //check whether the sub string is holding only number or not - return 0; - num = atoi(ptr); //convert substring to number + long value: + if (!parse_integer(ptr, &value)) //Check whether the substring is holding only number or not + return false; + num = atoi(ptr); //Convert substring to number if (num >= 0 && num <= 255) { - ptr = strtok(NULL, "."); //cut the next part of the string + ptr = strtok(NULL, "."); //Cut the next part of the string if (ptr != NULL) - dots++; //increase the dot count + dots++; //Increase the dot count + } else { + return false; } - else - return 0; } - if (dots != 3) //if the number of dots are not 3, return false - return 0; - return 1; + + if (dots != 3) + return false; + + return true; } static bool @@ -408,12 +546,11 @@ parse_serve_args(const char *optarg, char **s_protocol, uint32_t *s_ip, uint16_t char* ip = NULL; uint32_t ip_value; char* port = NULL; + char** values; - values = str_split(optarg, ':'); - if (values) - { + if (values) { protocol = *values; ip = *(values + 1); port = *(values + 2); @@ -421,46 +558,35 @@ parse_serve_args(const char *optarg, char **s_protocol, uint32_t *s_ip, uint16_t free(values); - if (!strcmp(protocol, "tcp")) - { - //protocol = "tcp"; + //Check if the choosen protocol is allowed + if (!strcmp(protocol, "tcp")) { protocol_valid = true; - } else if (!strcmp(protocol, "udp")) - { - //protocol = "udp"; - protocol_valid = true; - } - else { - LOGE("Unexpected protocol: %s (expected tcp or udp)", protocol); + } else { + LOGE("Unexpected protocol: $s (expected tcp)", protocol); return false; } - - //Allowing to write localhost or the IP address - if (!strcmp(ip, "localhost")) - { - ip_value = IPV4_LOCALHOST; + + //Check if the choosen ip is valid + if (!strcmp(ip, "localhost")) { + ip_value = 0x7F000001; ip_valid = true; - } else if (validate_ip(ip)) { + } else if (check_if_ip_valid(ip)) { ip_valid = true; - } - else { - LOGE("Unexpected ip address (expected \"localhost\" or 255.255.255.255)"); + } else { + LOGE("Unexpected ip address (expected \"localhost\" or 255.255.255.255 format)"); return false; } + //Check if the choosen port is valid long port_value = 0; - port_valid = parse_integer_arg(port, &port_value, false, 0, 0xFFFF, "port"); - + port_valid = parse_interger_arg(port, &port_value, false, 0, 0xFFFF, "port"); + //Check if everything is valid if (!protocol_valid || !ip_valid || !port_valid) { - LOGE("Unexpected argument format: %s (expected [tcp/udp]:[ip or \"localhost\"]:[port])", optarg); + LOGE("Unexpected argument format: $s (expected [tcp]:[ip or \"localhost\"]:[port])", optarg); return false; } - /*LOGI("%s", protocol); - LOGI("%d", ip_value); - LOGI("%ld", port_value);*/ - *s_protocol = protocol; *s_ip = (uint32_t)ip_value; *s_port = (uint16_t)port_value; @@ -468,69 +594,64 @@ parse_serve_args(const char *optarg, char **s_protocol, uint32_t *s_ip, uint16_t return true; } -static enum recorder_format -guess_record_format(const char *filename) { - size_t len = strlen(filename); - if (len < 4) { - return 0; - } - const char *ext = &filename[len - 4]; - if (!strcmp(ext, ".mp4")) { - return RECORDER_FORMAT_MP4; - } - if (!strcmp(ext, ".mkv")) { - return RECORDER_FORMAT_MKV; - } - return 0; -} - -#define OPT_RENDER_EXPIRED_FRAMES 1000 -#define OPT_WINDOW_TITLE 1001 -#define OPT_PUSH_TARGET 1002 -#define OPT_ALWAYS_ON_TOP 1003 -#define OPT_CROP 1004 -#define OPT_RECORD_FORMAT 1005 -#define OPT_PREFER_TEXT 1006 -#define OPT_WINDOW_X 1007 -#define OPT_WINDOW_Y 1008 -#define OPT_WINDOW_WIDTH 1009 -#define OPT_WINDOW_HEIGHT 1010 -#define OPT_WINDOW_BORDERLESS 1011 -#define OPT_MAX_FPS 1012 -#define OPT_SERVE 1013 +#define OPT_RENDER_EXPIRED_FRAMES 1000 +#define OPT_WINDOW_TITLE 1001 +#define OPT_PUSH_TARGET 1002 +#define OPT_ALWAYS_ON_TOP 1003 +#define OPT_CROP 1004 +#define OPT_RECORD_FORMAT 1005 +#define OPT_PREFER_TEXT 1006 +#define OPT_WINDOW_X 1007 +#define OPT_WINDOW_Y 1008 +#define OPT_WINDOW_WIDTH 1009 +#define OPT_WINDOW_HEIGHT 1010 +#define OPT_WINDOW_BORDERLESS 1011 +#define OPT_MAX_FPS 1012 +#define OPT_LOCK_VIDEO_ORIENTATION 1013 +#define OPT_DISPLAY_ID 1014 +#define OPT_ROTATION 1015 +#define OPT_RENDER_DRIVER 1016 +#define OPT_NO_MIPMAPS 1017 +#define OPT_SERVE 1018 bool scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { static const struct option long_options[] = { - {"always-on-top", no_argument, NULL, OPT_ALWAYS_ON_TOP}, - {"bit-rate", required_argument, NULL, 'b'}, - {"crop", required_argument, NULL, OPT_CROP}, - {"fullscreen", no_argument, NULL, 'f'}, - {"help", no_argument, NULL, 'h'}, - {"max-fps", required_argument, NULL, OPT_MAX_FPS}, - {"max-size", required_argument, NULL, 'm'}, - {"no-control", no_argument, NULL, 'n'}, - {"no-display", no_argument, NULL, 'N'}, - {"port", required_argument, NULL, 'p'}, - {"push-target", required_argument, NULL, OPT_PUSH_TARGET}, - {"record", required_argument, NULL, 'r'}, - {"record-format", required_argument, NULL, OPT_RECORD_FORMAT}, - {"render-expired-frames", no_argument, NULL, - OPT_RENDER_EXPIRED_FRAMES}, - {"serial", required_argument, NULL, 's'}, - {"serve", required_argument, NULL, OPT_SERVE}, - {"show-touches", no_argument, NULL, 't'}, - {"turn-screen-off", no_argument, NULL, 'S'}, - {"prefer-text", no_argument, NULL, OPT_PREFER_TEXT}, - {"version", no_argument, NULL, 'v'}, - {"window-title", required_argument, NULL, OPT_WINDOW_TITLE}, - {"window-x", required_argument, NULL, OPT_WINDOW_X}, - {"window-y", required_argument, NULL, OPT_WINDOW_Y}, - {"window-width", required_argument, NULL, OPT_WINDOW_WIDTH}, - {"window-height", required_argument, NULL, OPT_WINDOW_HEIGHT}, - {"window-borderless", no_argument, NULL, - OPT_WINDOW_BORDERLESS}, - {NULL, 0, NULL, 0 }, + {"always-on-top", no_argument, NULL, OPT_ALWAYS_ON_TOP}, + {"bit-rate", required_argument, NULL, 'b'}, + {"crop", required_argument, NULL, OPT_CROP}, + {"display", required_argument, NULL, OPT_DISPLAY_ID}, + {"fullscreen", no_argument, NULL, 'f'}, + {"help", no_argument, NULL, 'h'}, + {"lock-video-orientation", required_argument, NULL, + OPT_LOCK_VIDEO_ORIENTATION}, + {"max-fps", required_argument, NULL, OPT_MAX_FPS}, + {"max-size", required_argument, NULL, 'm'}, + {"no-control", no_argument, NULL, 'n'}, + {"no-display", no_argument, NULL, 'N'}, + {"no-mipmaps", no_argument, NULL, OPT_NO_MIPMAPS}, + {"port", required_argument, NULL, 'p'}, + {"push-target", required_argument, NULL, OPT_PUSH_TARGET}, + {"record", required_argument, NULL, 'r'}, + {"record-format", required_argument, NULL, OPT_RECORD_FORMAT}, + {"render-driver", required_argument, NULL, OPT_RENDER_DRIVER}, + {"render-expired-frames", no_argument, NULL, + OPT_RENDER_EXPIRED_FRAMES}, + {"rotation", required_argument, NULL, OPT_ROTATION}, + {"serial", required_argument, NULL, 's'}, + {"serve", required_argument, NULL, OPT_SERVE} + {"show-touches", no_argument, NULL, 't'}, + {"turn-screen-off", no_argument, NULL, 'S'}, + {"prefer-text", no_argument, NULL, OPT_PREFER_TEXT}, + {"version", no_argument, NULL, 'v'}, + {"window-title", required_argument, NULL, OPT_WINDOW_TITLE}, + {"window-x", required_argument, NULL, OPT_WINDOW_X}, + {"window-y", required_argument, NULL, OPT_WINDOW_Y}, + {"window-width", required_argument, NULL, OPT_WINDOW_WIDTH}, + {"window-height", required_argument, NULL, OPT_WINDOW_HEIGHT}, + {"window-borderless", no_argument, NULL, + OPT_WINDOW_BORDERLESS}, + {NULL, 0, NULL, 0 }, }; struct scrcpy_options *opts = &args->opts; @@ -552,6 +673,11 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case OPT_CROP: opts->crop = optarg; break; + case OPT_DISPLAY_ID: + if (!parse_display_id(optarg, &opts->display_id)) { + return false; + } + break; case 'f': opts->fullscreen = true; break; @@ -576,6 +702,11 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { return false; } break; + case OPT_LOCK_VIDEO_ORIENTATION: + if (!parse_lock_video_orientation(optarg, &opts->lock_video_orientation)) { + return false; + } + break; case 'n': opts->control = false; break; @@ -583,7 +714,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { opts->display = false; break; case 'p': - if (!parse_port(optarg, &opts->port)) { + if (!parse_port_range(optarg, &opts->port_range)) { return false; } break; @@ -593,18 +724,16 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case 's': opts->serial = optarg; break; + case 'S': + opts->turn_screen_off = true; + break; case OPT_SERVE: if (!parse_serve_args(optarg, &opts->serve_protocol, &opts->serve_ip, &opts->serve_port)) { return false; - } else { + } + else { opts->serve = true; } - /*LOGI("protocol is %s", opts->serve_protocol); - LOGI("ip value is %d", opts->serve_ip); - LOGI("port is %d", opts->serve_port);*/ - break; - case 'S': - opts->turn_screen_off = true; break; case 't': opts->show_touches = true; @@ -653,6 +782,17 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case OPT_PREFER_TEXT: opts->prefer_text = true; break; + case OPT_ROTATION: + if (!parse_rotation(optarg, &opts->rotation)) { + return false; + } + break; + case OPT_RENDER_DRIVER: + opts->render_driver = optarg; + break; + case OPT_NO_MIPMAPS: + opts->mipmaps = false; + break; default: // getopt prints the error message on stderr return false; @@ -664,11 +804,6 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { return false; } - if (!opts->display && opts->fullscreen) { - LOGE("-f/--fullscreen-window is incompatible with -N/--no-display"); - return false; - } - int index = optind; if (index < argc) { LOGE("Unexpected additional argument: %s", argv[index]); diff --git a/app/src/command.c b/app/src/command.c index 63afccb41c..81047b7a32 100644 --- a/app/src/command.c +++ b/app/src/command.c @@ -4,9 +4,6 @@ #include #include #include -#include -#include -#include #include "config.h" #include "common.h" @@ -58,6 +55,32 @@ argv_to_string(const char *const *argv, char *buf, size_t bufsize) { return idx; } +static void +show_adb_installation_msg() { +#ifndef __WINDOWS__ + static const struct { + const char *binary; + const char *command; + } pkg_managers[] = { + {"apt", "apt install adb"}, + {"apt-get", "apt-get install adb"}, + {"brew", "brew cask install android-platform-tools"}, + {"dnf", "dnf install android-tools"}, + {"emerge", "emerge dev-util/android-tools"}, + {"pacman", "pacman -S android-tools"}, + }; + for (size_t i = 0; i < ARRAY_LEN(pkg_managers); ++i) { + if (cmd_search(pkg_managers[i].binary)) { + LOGI("You may install 'adb' by \"%s\"", pkg_managers[i].command); + return; + } + } +#endif + + LOGI("You may download and install 'adb' from " + "https://developer.android.com/studio/releases/platform-tools"); +} + static void show_adb_err_msg(enum process_result err, const char *const argv[]) { char buf[512]; @@ -71,6 +94,7 @@ show_adb_err_msg(enum process_result err, const char *const argv[]) { LOGE("Command not found: %s", buf); LOGE("(make 'adb' accessible from your PATH or define its full" "path in the ADB environment variable)"); + show_adb_installation_msg(); break; case PROCESS_SUCCESS: // do nothing @@ -205,14 +229,3 @@ process_check_success(process_t proc, const char *name) { } return true; } - -bool -is_regular_file(const char *path) { - struct stat path_stat; - int r = stat(path, &path_stat); - if (r) { - perror("stat"); - return false; - } - return S_ISREG(path_stat.st_mode); -} diff --git a/app/src/command.h b/app/src/command.h index 9fc81c1ce8..28f9fbcf7e 100644 --- a/app/src/command.h +++ b/app/src/command.h @@ -43,6 +43,11 @@ enum process_result { PROCESS_ERROR_MISSING_BINARY, }; +#ifndef __WINDOWS__ +bool +cmd_search(const char *file); +#endif + enum process_result cmd_execute(const char *const argv[], process_t *process); diff --git a/app/src/common.h b/app/src/common.h index e5cbe95326..4cbf1d7494 100644 --- a/app/src/common.h +++ b/app/src/common.h @@ -27,4 +27,9 @@ struct position { struct point point; }; +struct port_range { + uint16_t first; + uint16_t last; +}; + #endif diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 4511313917..252a34252a 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -45,8 +45,9 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { buffer_write32be(&buf[6], msg->inject_keycode.metastate); return 10; case CONTROL_MSG_TYPE_INJECT_TEXT: { - size_t len = write_string(msg->inject_text.text, - CONTROL_MSG_TEXT_MAX_LENGTH, &buf[1]); + size_t len = + write_string(msg->inject_text.text, + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH, &buf[1]); return 1 + len; } case CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT: diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 49a159a65c..e132fc6b00 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -10,7 +10,7 @@ #include "android/keycodes.h" #include "common.h" -#define CONTROL_MSG_TEXT_MAX_LENGTH 300 +#define CONTROL_MSG_INJECT_TEXT_MAX_LENGTH 300 #define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH 4093 #define CONTROL_MSG_SERIALIZED_MAX_SIZE \ (3 + CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH) diff --git a/app/src/event_converter.c b/app/src/event_converter.c index 80ead6153d..1054dcf9ec 100644 --- a/app/src/event_converter.c +++ b/app/src/event_converter.c @@ -94,6 +94,23 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod, MAP(SDLK_UP, AKEYCODE_DPAD_UP); } + if (!(mod & (KMOD_NUM | KMOD_SHIFT))) { + // Handle Numpad events when Num Lock is disabled + // If SHIFT is pressed, a text event will be sent instead + switch(from) { + MAP(SDLK_KP_0, AKEYCODE_INSERT); + MAP(SDLK_KP_1, AKEYCODE_MOVE_END); + MAP(SDLK_KP_2, AKEYCODE_DPAD_DOWN); + MAP(SDLK_KP_3, AKEYCODE_PAGE_DOWN); + MAP(SDLK_KP_4, AKEYCODE_DPAD_LEFT); + MAP(SDLK_KP_6, AKEYCODE_DPAD_RIGHT); + MAP(SDLK_KP_7, AKEYCODE_MOVE_HOME); + MAP(SDLK_KP_8, AKEYCODE_DPAD_UP); + MAP(SDLK_KP_9, AKEYCODE_PAGE_UP); + MAP(SDLK_KP_PERIOD, AKEYCODE_FORWARD_DEL); + } + } + if (prefer_text) { // do not forward alpha and space key events return false; diff --git a/app/src/fps_counter.c b/app/src/fps_counter.c index 58c62d5511..b4dd8b9bc1 100644 --- a/app/src/fps_counter.c +++ b/app/src/fps_counter.c @@ -23,7 +23,7 @@ fps_counter_init(struct fps_counter *counter) { } counter->thread = NULL; - SDL_AtomicSet(&counter->started, 0); + atomic_init(&counter->started, 0); // no need to initialize the other fields, they are unused until started return true; @@ -35,6 +35,16 @@ fps_counter_destroy(struct fps_counter *counter) { SDL_DestroyMutex(counter->mutex); } +static inline bool +is_started(struct fps_counter *counter) { + return atomic_load_explicit(&counter->started, memory_order_acquire); +} + +static inline void +set_started(struct fps_counter *counter, bool started) { + atomic_store_explicit(&counter->started, started, memory_order_release); +} + // must be called with mutex locked static void display_fps(struct fps_counter *counter) { @@ -70,10 +80,10 @@ run_fps_counter(void *data) { mutex_lock(counter->mutex); while (!counter->interrupted) { - while (!counter->interrupted && !SDL_AtomicGet(&counter->started)) { + while (!counter->interrupted && !is_started(counter)) { cond_wait(counter->state_cond, counter->mutex); } - while (!counter->interrupted && SDL_AtomicGet(&counter->started)) { + while (!counter->interrupted && is_started(counter)) { uint32_t now = SDL_GetTicks(); check_interval_expired(counter, now); @@ -96,7 +106,7 @@ fps_counter_start(struct fps_counter *counter) { counter->nr_skipped = 0; mutex_unlock(counter->mutex); - SDL_AtomicSet(&counter->started, 1); + set_started(counter, true); cond_signal(counter->state_cond); // counter->thread is always accessed from the same thread, no need to lock @@ -114,13 +124,13 @@ fps_counter_start(struct fps_counter *counter) { void fps_counter_stop(struct fps_counter *counter) { - SDL_AtomicSet(&counter->started, 0); + set_started(counter, false); cond_signal(counter->state_cond); } bool fps_counter_is_started(struct fps_counter *counter) { - return SDL_AtomicGet(&counter->started); + return is_started(counter); } void @@ -145,7 +155,7 @@ fps_counter_join(struct fps_counter *counter) { void fps_counter_add_rendered_frame(struct fps_counter *counter) { - if (!SDL_AtomicGet(&counter->started)) { + if (!is_started(counter)) { return; } @@ -158,7 +168,7 @@ fps_counter_add_rendered_frame(struct fps_counter *counter) { void fps_counter_add_skipped_frame(struct fps_counter *counter) { - if (!SDL_AtomicGet(&counter->started)) { + if (!is_started(counter)) { return; } diff --git a/app/src/fps_counter.h b/app/src/fps_counter.h index 1c56bb01f4..52157172ac 100644 --- a/app/src/fps_counter.h +++ b/app/src/fps_counter.h @@ -1,9 +1,9 @@ #ifndef FPSCOUNTER_H #define FPSCOUNTER_H +#include #include #include -#include #include #include @@ -16,7 +16,7 @@ struct fps_counter { // atomic so that we can check without locking the mutex // if the FPS counter is disabled, we don't want to lock unnecessarily - SDL_atomic_t started; + atomic_bool started; // the following fields are protected by the mutex bool interrupted; diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 8c4c230ab1..14ee7e400b 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -221,6 +221,18 @@ rotate_device(struct controller *controller) { } } +static void +rotate_client_left(struct screen *screen) { + unsigned new_rotation = (screen->rotation + 1) % 4; + screen_set_rotation(screen, new_rotation); +} + +static void +rotate_client_right(struct screen *screen) { + unsigned new_rotation = (screen->rotation + 3) % 4; + screen_set_rotation(screen, new_rotation); +} + void input_manager_process_text_input(struct input_manager *im, const SDL_TextInputEvent *event) { @@ -351,6 +363,16 @@ input_manager_process_key(struct input_manager *im, action_volume_up(controller, action); } return; + case SDLK_LEFT: + if (cmd && !shift && down) { + rotate_client_left(im->screen); + } + return; + case SDLK_RIGHT: + if (cmd && !shift && down) { + rotate_client_right(im->screen); + } + return; case SDLK_c: if (control && cmd && !shift && !repeat && down) { request_device_clipboard(controller); @@ -427,8 +449,8 @@ convert_mouse_motion(const SDL_MouseMotionEvent *from, struct screen *screen, to->inject_touch_event.action = AMOTION_EVENT_ACTION_MOVE; to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; to->inject_touch_event.position.screen_size = screen->frame_size; - to->inject_touch_event.position.point.x = from->x; - to->inject_touch_event.position.point.y = from->y; + to->inject_touch_event.position.point = + screen_convert_to_frame_coords(screen, from->x, from->y); to->inject_touch_event.pressure = 1.f; to->inject_touch_event.buttons = convert_mouse_buttons(from->state); @@ -463,13 +485,13 @@ convert_touch(const SDL_TouchFingerEvent *from, struct screen *screen, return false; } - struct size frame_size = screen->frame_size; - to->inject_touch_event.pointer_id = from->fingerId; - to->inject_touch_event.position.screen_size = frame_size; + to->inject_touch_event.position.screen_size = screen->frame_size; // SDL touch event coordinates are normalized in the range [0; 1] - to->inject_touch_event.position.point.x = from->x * frame_size.width; - to->inject_touch_event.position.point.y = from->y * frame_size.height; + float x = from->x * screen->content_size.width; + float y = from->y * screen->content_size.height; + to->inject_touch_event.position.point = + screen_convert_to_frame_coords(screen, x, y); to->inject_touch_event.pressure = from->pressure; to->inject_touch_event.buttons = 0; return true; @@ -489,8 +511,8 @@ input_manager_process_touch(struct input_manager *im, static bool is_outside_device_screen(struct input_manager *im, int x, int y) { - return x < 0 || x >= im->screen->frame_size.width || - y < 0 || y >= im->screen->frame_size.height; + return x < 0 || x >= im->screen->content_size.width || + y < 0 || y >= im->screen->content_size.height; } static bool @@ -504,8 +526,8 @@ convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen, to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; to->inject_touch_event.position.screen_size = screen->frame_size; - to->inject_touch_event.position.point.x = from->x; - to->inject_touch_event.position.point.y = from->y; + to->inject_touch_event.position.point = + screen_convert_to_frame_coords(screen, from->x, from->y); to->inject_touch_event.pressure = 1.f; to->inject_touch_event.buttons = convert_mouse_buttons(SDL_BUTTON(from->button)); diff --git a/app/src/opengl.c b/app/src/opengl.c new file mode 100644 index 0000000000..da05c0821a --- /dev/null +++ b/app/src/opengl.c @@ -0,0 +1,56 @@ +#include "opengl.h" + +#include +#include +#include "SDL2/SDL.h" + +void +sc_opengl_init(struct sc_opengl *gl) { + gl->GetString = SDL_GL_GetProcAddress("glGetString"); + assert(gl->GetString); + + gl->TexParameterf = SDL_GL_GetProcAddress("glTexParameterf"); + assert(gl->TexParameterf); + + gl->TexParameteri = SDL_GL_GetProcAddress("glTexParameteri"); + assert(gl->TexParameteri); + + // optional + gl->GenerateMipmap = SDL_GL_GetProcAddress("glGenerateMipmap"); + + const char *version = (const char *) gl->GetString(GL_VERSION); + assert(version); + gl->version = version; + +#define OPENGL_ES_PREFIX "OpenGL ES " + /* starts with "OpenGL ES " */ + gl->is_opengles = !strncmp(gl->version, OPENGL_ES_PREFIX, + sizeof(OPENGL_ES_PREFIX) - 1); + if (gl->is_opengles) { + /* skip the prefix */ + version += sizeof(PREFIX) - 1; + } + + int r = sscanf(version, "%d.%d", &gl->version_major, &gl->version_minor); + if (r != 2) { + // failed to parse the version + gl->version_major = 0; + gl->version_minor = 0; + } +} + +bool +sc_opengl_version_at_least(struct sc_opengl *gl, + int minver_major, int minver_minor, + int minver_es_major, int minver_es_minor) +{ + if (gl->is_opengles) { + return gl->version_major > minver_es_major + || (gl->version_major == minver_es_major + && gl->version_minor >= minver_es_minor); + } + + return gl->version_major > minver_major + || (gl->version_major == minver_major + && gl->version_minor >= minver_minor); +} diff --git a/app/src/opengl.h b/app/src/opengl.h new file mode 100644 index 0000000000..f0a89a14ce --- /dev/null +++ b/app/src/opengl.h @@ -0,0 +1,36 @@ +#ifndef SC_OPENGL_H +#define SC_OPENGL_H + +#include +#include + +#include "config.h" + +struct sc_opengl { + const char *version; + bool is_opengles; + int version_major; + int version_minor; + + const GLubyte * + (*GetString)(GLenum name); + + void + (*TexParameterf)(GLenum target, GLenum pname, GLfloat param); + + void + (*TexParameteri)(GLenum target, GLenum pname, GLint param); + + void + (*GenerateMipmap)(GLenum target); +}; + +void +sc_opengl_init(struct sc_opengl *gl); + +bool +sc_opengl_version_at_least(struct sc_opengl *gl, + int minver_major, int minver_minor, + int minver_es_major, int minver_es_minor); + +#endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index b7138fa5d9..54d3133b46 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -49,7 +49,7 @@ static struct input_manager input_manager = { // init SDL and set appropriate hints static bool -sdl_init_and_configure(bool display) { +sdl_init_and_configure(bool display, const char *render_driver) { uint32_t flags = display ? SDL_INIT_VIDEO : SDL_INIT_EVENTS; if (SDL_Init(flags)) { LOGC("Could not initialize SDL: %s", SDL_GetError()); @@ -62,9 +62,13 @@ sdl_init_and_configure(bool display) { return true; } - // Use the best available scale quality - if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "2")) { - LOGW("Could not enable bilinear filtering"); + if (render_driver && !SDL_SetHint(SDL_HINT_RENDER_DRIVER, render_driver)) { + LOGW("Could not set render driver"); + } + + // Linear filtering + if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1")) { + LOGW("Could not enable linear filtering"); } #ifdef SCRCPY_SDL_HAS_HINT_MOUSE_FOCUS_CLICKTHROUGH @@ -107,9 +111,10 @@ static int event_watcher(void *data, SDL_Event *event) { (void) data; if (event->type == SDL_WINDOWEVENT - && event->window.event == SDL_WINDOWEVENT_RESIZED) { - // called from another thread, not very safe, but it's a workaround! - screen_render(&screen); + && event->window.event == SDL_WINDOWEVENT_SIZE_CHANGED) { + // In practice, it seems to always be called from the same thread in + // that specific case. Anyway, it's just a workaround. + screen_handle_window_event(&screen, &event->window); } return 0; } @@ -282,11 +287,13 @@ scrcpy(const struct scrcpy_options *options) { bool record = !!options->record_filename; struct server_params params = { .crop = options->crop, - .local_port = options->port, + .port_range = options->port_range, .max_size = options->max_size, .bit_rate = options->bit_rate, .max_fps = options->max_fps, + .lock_video_orientation = options->lock_video_orientation, .control = options->control, + .display_id = options->display_id, }; if (!server_start(&server, options->serial, ¶ms)) { return false; @@ -309,9 +316,8 @@ scrcpy(const struct scrcpy_options *options) { bool stream_started = false; bool controller_initialized = false; bool controller_started = false; - //bool serve_initialized = false; - if (!sdl_init_and_configure(options->display)) { + if (!sdl_init_and_configure(options->display, options->render_driver)) { goto end; } @@ -366,11 +372,8 @@ scrcpy(const struct scrcpy_options *options) { recorder_initialized = true; } - av_log_set_callback(av_log_callback); - - struct serve *serv = NULL; - if (options->serve) - { + struct serve* serv = NULL; + if (options->serve) { serve_init(&serve, options->serve_protocol, options->serve_ip, options->serve_port); if (!serve_start(&serve)) { @@ -379,6 +382,8 @@ scrcpy(const struct scrcpy_options *options) { serv = &serve; } + av_log_set_callback(av_log_callback); + stream_init(&stream, server.video_socket, dec, rec, serv); // now we consumed the header values, the socket receives the video stream @@ -408,7 +413,8 @@ scrcpy(const struct scrcpy_options *options) { options->always_on_top, options->window_x, options->window_y, options->window_width, options->window_height, - options->window_borderless)) { + options->window_borderless, + options->rotation, options-> mipmaps)) { goto end; } diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 6908cdf870..61c372e505 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -5,6 +5,7 @@ #include #include "config.h" +#include "common.h" #include "input_manager.h" #include "recorder.h" @@ -13,16 +14,23 @@ struct scrcpy_options { const char *crop; const char *record_filename; const char *window_title; - const char *push_target; + const char *push_target; + const char *render_driver; + const char *serve_protocol; enum recorder_format record_format; - uint16_t port; + struct port_range port_range; uint16_t max_size; uint32_t bit_rate; uint16_t max_fps; - int16_t window_x; - int16_t window_y; + int8_t lock_video_orientation; + uint8_t rotation; + int16_t window_x; // WINDOW_POSITION_UNDEFINED for "auto" + int16_t window_y; // WINDOW_POSITION_UNDEFINED for "auto" uint16_t window_width; uint16_t window_height; + uint16_t display_id; + uint32_t serve_ip; + uint16_t serve_port; bool show_touches; bool fullscreen; bool always_on_top; @@ -32,9 +40,7 @@ struct scrcpy_options { bool render_expired_frames; bool prefer_text; bool window_borderless; - char *serve_protocol; - uint32_t serve_ip; - uint16_t serve_port; + bool mipmaps; bool serve; }; @@ -44,15 +50,25 @@ struct scrcpy_options { .record_filename = NULL, \ .window_title = NULL, \ .push_target = NULL, \ + .render_driver = NULL, \ + .serve_protocol = NULL, \ .record_format = RECORDER_FORMAT_AUTO, \ - .port = DEFAULT_LOCAL_PORT, \ + .port_range = { \ + .first = DEFAULT_LOCAL_PORT_RANGE_FIRST, \ + .last = DEFAULT_LOCAL_PORT_RANGE_LAST, \ + }, \ .max_size = DEFAULT_MAX_SIZE, \ .bit_rate = DEFAULT_BIT_RATE, \ .max_fps = 0, \ - .window_x = -1, \ - .window_y = -1, \ + .lock_video_orientation = DEFAULT_LOCK_VIDEO_ORIENTATION, \ + .rotation = 0, \ + .window_x = WINDOW_POSITION_UNDEFINED, \ + .window_y = WINDOW_POSITION_UNDEFINED, \ .window_width = 0, \ .window_height = 0, \ + .display_id = 0, \ + .serve_ip = 0, \ + .serve_port = 0, \ .show_touches = false, \ .fullscreen = false, \ .always_on_top = false, \ @@ -62,9 +78,7 @@ struct scrcpy_options { .render_expired_frames = false, \ .prefer_text = false, \ .window_borderless = false, \ - .serve_protocol = NULL, \ - .serve_ip = 0, \ - .serve_port = 0, \ + .mipmaps = true, \ .serve = false, \ } diff --git a/app/src/screen.c b/app/src/screen.c index beb1075499..0af8de835f 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -15,6 +15,19 @@ #define DISPLAY_MARGINS 96 +static inline struct size +get_rotated_size(struct size size, int rotation) { + struct size rotated_size; + if (rotation & 1) { + rotated_size.width = size.height; + rotated_size.height = size.width; + } else { + rotated_size.width = size.width; + rotated_size.height = size.height; + } + return rotated_size; +} + // get the window size in a struct size static struct size get_window_size(SDL_Window *window) { @@ -80,8 +93,8 @@ get_preferred_display_bounds(struct size *bounds) { // - it keeps the aspect ratio // - it scales down to make it fit in the display_size static struct size -get_optimal_size(struct size current_size, struct size frame_size) { - if (frame_size.width == 0 || frame_size.height == 0) { +get_optimal_size(struct size current_size, struct size content_size) { + if (content_size.width == 0 || content_size.height == 0) { // avoid division by 0 return current_size; } @@ -100,14 +113,21 @@ get_optimal_size(struct size current_size, struct size frame_size) { h = MIN(current_size.height, display_size.height); } - bool keep_width = frame_size.width * h > frame_size.height * w; + if (h == w * content_size.height / content_size.width + || w == h * content_size.width / content_size.height) { + // The size is already optimal, if we ignore rounding errors due to + // integer window dimensions + return (struct size) {w, h}; + } + + bool keep_width = content_size.width * h > content_size.height * w; if (keep_width) { // remove black borders on top and bottom - h = frame_size.height * w / frame_size.width; + h = content_size.height * w / content_size.width; } else { // remove black borders on left and right (or none at all if it already // fits) - w = frame_size.width * h / frame_size.height; + w = content_size.width * h / content_size.height; } // w and h must fit into 16 bits @@ -117,33 +137,33 @@ get_optimal_size(struct size current_size, struct size frame_size) { // same as get_optimal_size(), but read the current size from the window static inline struct size -get_optimal_window_size(const struct screen *screen, struct size frame_size) { +get_optimal_window_size(const struct screen *screen, struct size content_size) { struct size windowed_size = get_windowed_window_size(screen); - return get_optimal_size(windowed_size, frame_size); + return get_optimal_size(windowed_size, content_size); } // initially, there is no current size, so use the frame size as current size // req_width and req_height, if not 0, are the sizes requested by the user static inline struct size -get_initial_optimal_size(struct size frame_size, uint16_t req_width, +get_initial_optimal_size(struct size content_size, uint16_t req_width, uint16_t req_height) { struct size window_size; if (!req_width && !req_height) { - window_size = get_optimal_size(frame_size, frame_size); + window_size = get_optimal_size(content_size, content_size); } else { if (req_width) { window_size.width = req_width; } else { // compute from the requested height - window_size.width = (uint32_t) req_height * frame_size.width - / frame_size.height; + window_size.width = (uint32_t) req_height * content_size.width + / content_size.height; } if (req_height) { window_size.height = req_height; } else { // compute from the requested width - window_size.height = (uint32_t) req_width * frame_size.height - / frame_size.width; + window_size.height = (uint32_t) req_width * content_size.height + / content_size.width; } } return window_size; @@ -155,21 +175,48 @@ screen_init(struct screen *screen) { } static inline SDL_Texture * -create_texture(SDL_Renderer *renderer, struct size frame_size) { - return SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12, - SDL_TEXTUREACCESS_STREAMING, - frame_size.width, frame_size.height); +create_texture(struct screen *screen) { + SDL_Renderer *renderer = screen->renderer; + struct size size = screen->frame_size; + SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12, + SDL_TEXTUREACCESS_STREAMING, + size.width, size.height); + if (!texture) { + return NULL; + } + + if (screen->mipmaps) { + struct sc_opengl *gl = &screen->gl; + + SDL_GL_BindTexture(texture, NULL, NULL); + + // Enable trilinear filtering for downscaling + gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, + GL_LINEAR_MIPMAP_LINEAR); + gl->TexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -.5f); + + SDL_GL_UnbindTexture(texture); + } + + return texture; } bool screen_init_rendering(struct screen *screen, const char *window_title, struct size frame_size, bool always_on_top, int16_t window_x, int16_t window_y, uint16_t window_width, - uint16_t window_height, bool window_borderless) { + uint16_t window_height, bool window_borderless, + uint8_t rotation, bool mipmaps) { screen->frame_size = frame_size; + screen->rotation = rotation; + if (rotation) { + LOGI("Initial display rotation set to %u", rotation); + } + struct size content_size = get_rotated_size(frame_size, screen->rotation); + screen->content_size = content_size; struct size window_size = - get_initial_optimal_size(frame_size, window_width, window_height); + get_initial_optimal_size(content_size, window_width, window_height); uint32_t window_flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE; #ifdef HIDPI_SUPPORT window_flags |= SDL_WINDOW_ALLOW_HIGHDPI; @@ -186,8 +233,10 @@ screen_init_rendering(struct screen *screen, const char *window_title, window_flags |= SDL_WINDOW_BORDERLESS; } - int x = window_x != -1 ? window_x : (int) SDL_WINDOWPOS_UNDEFINED; - int y = window_y != -1 ? window_y : (int) SDL_WINDOWPOS_UNDEFINED; + int x = window_x != WINDOW_POSITION_UNDEFINED + ? window_x : (int) SDL_WINDOWPOS_UNDEFINED; + int y = window_y != WINDOW_POSITION_UNDEFINED + ? window_y : (int) SDL_WINDOWPOS_UNDEFINED; screen->window = SDL_CreateWindow(window_title, x, y, window_size.width, window_size.height, window_flags); @@ -204,13 +253,44 @@ screen_init_rendering(struct screen *screen, const char *window_title, return false; } - if (SDL_RenderSetLogicalSize(screen->renderer, frame_size.width, - frame_size.height)) { + SDL_RendererInfo renderer_info; + int r = SDL_GetRendererInfo(screen->renderer, &renderer_info); + const char *renderer_name = r ? NULL : renderer_info.name; + LOGI("Renderer: %s", renderer_name ? renderer_name : "(unknown)"); + + if (SDL_RenderSetLogicalSize(screen->renderer, content_size.width, + content_size.height)) { LOGE("Could not set renderer logical size: %s", SDL_GetError()); screen_destroy(screen); return false; } + // starts with "opengl" + screen->use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6); + if (screen->use_opengl) { + struct sc_opengl *gl = &screen->gl; + sc_opengl_init(gl); + + LOGI("OpenGL version: %s", gl->version); + + if (mipmaps) { + bool supports_mipmaps = + sc_opengl_version_at_least(gl, 3, 0, /* OpenGL 3.0+ */ + 2, 0 /* OpenGL ES 2.0+ */); + if (supports_mipmaps) { + LOGI("Trilinear filtering enabled"); + screen->mipmaps = true; + } else { + LOGW("Trilinear filtering disabled " + "(OpenGL 3.0+ or ES 2.0+ required)"); + } + } else { + LOGI("Trilinear filtering disabled"); + } + } else { + LOGW("Trilinear filtering disabled (not an OpenGL renderer)"); + } + SDL_Surface *icon = read_xpm(icon_xpm); if (icon) { SDL_SetWindowIcon(screen->window, icon); @@ -221,7 +301,7 @@ screen_init_rendering(struct screen *screen, const char *window_title, LOGI("Initial texture: %" PRIu16 "x%" PRIu16, frame_size.width, frame_size.height); - screen->texture = create_texture(screen->renderer, frame_size); + screen->texture = create_texture(screen); if (!screen->texture) { LOGC("Could not create texture: %s", SDL_GetError()); screen_destroy(screen); @@ -251,13 +331,51 @@ screen_destroy(struct screen *screen) { } } +void +screen_set_rotation(struct screen *screen, unsigned rotation) { + assert(rotation < 4); + if (rotation == screen->rotation) { + return; + } + + struct size old_content_size = screen->content_size; + struct size new_content_size = + get_rotated_size(screen->frame_size, rotation); + + if (SDL_RenderSetLogicalSize(screen->renderer, + new_content_size.width, + new_content_size.height)) { + LOGE("Could not set renderer logical size: %s", SDL_GetError()); + return; + } + + struct size windowed_size = get_windowed_window_size(screen); + struct size target_size = { + .width = (uint32_t) windowed_size.width * new_content_size.width + / old_content_size.width, + .height = (uint32_t) windowed_size.height * new_content_size.height + / old_content_size.height, + }; + target_size = get_optimal_size(target_size, new_content_size); + set_window_size(screen, target_size); + + screen->content_size = new_content_size; + screen->rotation = rotation; + LOGI("Display rotation set to %u", rotation); + + screen_render(screen); +} + // recreate the texture and resize the window if the frame size has changed static bool prepare_for_frame(struct screen *screen, struct size new_frame_size) { if (screen->frame_size.width != new_frame_size.width || screen->frame_size.height != new_frame_size.height) { - if (SDL_RenderSetLogicalSize(screen->renderer, new_frame_size.width, - new_frame_size.height)) { + struct size new_content_size = + get_rotated_size(new_frame_size, screen->rotation); + if (SDL_RenderSetLogicalSize(screen->renderer, + new_content_size.width, + new_content_size.height)) { LOGE("Could not set renderer logical size: %s", SDL_GetError()); return false; } @@ -265,21 +383,23 @@ prepare_for_frame(struct screen *screen, struct size new_frame_size) { // frame dimension changed, destroy texture SDL_DestroyTexture(screen->texture); + struct size content_size = screen->content_size; struct size windowed_size = get_windowed_window_size(screen); struct size target_size = { - (uint32_t) windowed_size.width * new_frame_size.width - / screen->frame_size.width, - (uint32_t) windowed_size.height * new_frame_size.height - / screen->frame_size.height, + (uint32_t) windowed_size.width * new_content_size.width + / content_size.width, + (uint32_t) windowed_size.height * new_content_size.height + / content_size.height, }; - target_size = get_optimal_size(target_size, new_frame_size); + target_size = get_optimal_size(target_size, new_content_size); set_window_size(screen, target_size); screen->frame_size = new_frame_size; + screen->content_size = new_content_size; LOGI("New texture: %" PRIu16 "x%" PRIu16, screen->frame_size.width, screen->frame_size.height); - screen->texture = create_texture(screen->renderer, new_frame_size); + screen->texture = create_texture(screen); if (!screen->texture) { LOGC("Could not create texture: %s", SDL_GetError()); return false; @@ -296,6 +416,13 @@ update_texture(struct screen *screen, const AVFrame *frame) { frame->data[0], frame->linesize[0], frame->data[1], frame->linesize[1], frame->data[2], frame->linesize[2]); + + if (screen->mipmaps) { + assert(screen->use_opengl); + SDL_GL_BindTexture(screen->texture, NULL, NULL); + screen->gl.GenerateMipmap(GL_TEXTURE_2D); + SDL_GL_UnbindTexture(screen->texture); + } } bool @@ -317,7 +444,28 @@ screen_update_frame(struct screen *screen, struct video_buffer *vb) { void screen_render(struct screen *screen) { SDL_RenderClear(screen->renderer); - SDL_RenderCopy(screen->renderer, screen->texture, NULL, NULL); + if (screen->rotation == 0) { + SDL_RenderCopy(screen->renderer, screen->texture, NULL, NULL); + } else { + // rotation in RenderCopyEx() is clockwise, while screen->rotation is + // counterclockwise (to be consistent with --lock-video-orientation) + int cw_rotation = (4 - screen->rotation) % 4; + double angle = 90 * cw_rotation; + + SDL_Rect *dstrect = NULL; + SDL_Rect rect; + if (screen->rotation & 1) { + struct size size = screen->content_size; + rect.x = (size.width - size.height) / 2; + rect.y = (size.height - size.width) / 2; + rect.w = size.height; + rect.h = size.width; + dstrect = ▭ + } + + SDL_RenderCopyEx(screen->renderer, screen->texture, NULL, dstrect, + angle, NULL, 0); + } SDL_RenderPresent(screen->renderer); } @@ -348,9 +496,10 @@ screen_resize_to_fit(struct screen *screen) { } struct size optimal_size = - get_optimal_window_size(screen, screen->frame_size); + get_optimal_window_size(screen, screen->content_size); SDL_SetWindowSize(screen->window, optimal_size.width, optimal_size.height); - LOGD("Resized to optimal size"); + LOGD("Resized to optimal size: %ux%u", optimal_size.width, + optimal_size.height); } void @@ -364,9 +513,10 @@ screen_resize_to_pixel_perfect(struct screen *screen) { screen->maximized = false; } - SDL_SetWindowSize(screen->window, screen->frame_size.width, - screen->frame_size.height); - LOGD("Resized to pixel-perfect"); + struct size content_size = screen->content_size; + SDL_SetWindowSize(screen->window, content_size.width, content_size.height); + LOGD("Resized to pixel-perfect: %ux%u", content_size.width, + content_size.height); } void @@ -410,3 +560,33 @@ screen_handle_window_event(struct screen *screen, break; } } + +struct point +screen_convert_to_frame_coords(struct screen *screen, int32_t x, int32_t y) { + unsigned rotation = screen->rotation; + assert(rotation < 4); + + int32_t w = screen->content_size.width; + int32_t h = screen->content_size.height; + struct point result; + switch (rotation) { + case 0: + result.x = x; + result.y = y; + break; + case 1: + result.x = h - y; + result.y = x; + break; + case 2: + result.x = w - x; + result.y = h - y; + break; + default: + assert(rotation == 3); + result.x = y; + result.y = w - x; + break; + } + return result; +} diff --git a/app/src/screen.h b/app/src/screen.h index 2346ff152f..85514279c8 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -7,6 +7,9 @@ #include "config.h" #include "common.h" +#include "opengl.h" + +#define WINDOW_POSITION_UNDEFINED (-0x8000) struct video_buffer; @@ -14,24 +17,36 @@ struct screen { SDL_Window *window; SDL_Renderer *renderer; SDL_Texture *texture; + bool use_opengl; + struct sc_opengl gl; struct size frame_size; + struct size content_size; // rotated frame_size // The window size the last time it was not maximized or fullscreen. struct size windowed_window_size; // Since we receive the event SIZE_CHANGED before MAXIMIZED, we must be // able to revert the size to its non-maximized value. struct size windowed_window_size_backup; + // client rotation: 0, 1, 2 or 3 (x90 degrees counterclockwise) + unsigned rotation; bool has_frame; bool fullscreen; bool maximized; bool no_window; + bool mipmaps; }; #define SCREEN_INITIALIZER { \ .window = NULL, \ .renderer = NULL, \ .texture = NULL, \ + .use_opengl = false, \ + .gl = {0}, \ .frame_size = { \ - .width = 0, \ + .width = 0, \ + .height = 0, \ + }, \ + .content_size = { \ + .width = 0, \ .height = 0, \ }, \ .windowed_window_size = { \ @@ -42,10 +57,12 @@ struct screen { .width = 0, \ .height = 0, \ }, \ + .rotation = 0, \ .has_frame = false, \ .fullscreen = false, \ .maximized = false, \ .no_window = false, \ + .mipmaps = false, \ } // initialize default values @@ -53,11 +70,13 @@ void screen_init(struct screen *screen); // initialize screen, create window, renderer and texture (window is hidden) +// window_x and window_y accept WINDOW_POSITION_UNDEFINED bool screen_init_rendering(struct screen *screen, const char *window_title, struct size frame_size, bool always_on_top, int16_t window_x, int16_t window_y, uint16_t window_width, - uint16_t window_height, bool window_borderless); + uint16_t window_height, bool window_borderless, + uint8_t rotation, bool mipmaps); // show the window void @@ -87,8 +106,17 @@ screen_resize_to_fit(struct screen *screen); void screen_resize_to_pixel_perfect(struct screen *screen); +// set the display rotation (0, 1, 2 or 3, x90 degrees counterclockwise) +void +screen_set_rotation(struct screen *screen, unsigned rotation); + // react to window events void screen_handle_window_event(struct screen *screen, const SDL_WindowEvent *event); +// convert point from window coordinates to frame coordinates +// x and y are expressed in pixels +struct point +screen_convert_to_frame_coords(struct screen *screen, int32_t x, int32_t y); + #endif diff --git a/app/src/serve.c b/app/src/serve.c index 8d40290e59..5de0c40f03 100644 --- a/app/src/serve.c +++ b/app/src/serve.c @@ -13,89 +13,47 @@ void serve_init(struct serve* serve, char *protocol, uint32_t ip, uint16_t port) { - serve->protocol = protocol; - serve->ip = ip; - serve->port = port; + serve->protocol = protocol; + serve->ip = ip; + serve->port = port; } -//static int -//run_serve(void *data) { -// struct serve* serve = data; -// -// socket_t Listensocket; -// socket_t ClientSocket; -// -// Listensocket = net_listen(serve->ip, serve->port, 1); -// if (Listensocket == INVALID_SOCKET) { -// LOGI("Listen Error"); -// net_close(Listensocket); -// return 0; -// } -// -// for (;;) { -// ClientSocket = net_accept(Listensocket); -// if (ClientSocket == INVALID_SOCKET) { -// LOGI("Client Error"); -// net_close(Listensocket); -// return 0; -// } -// LOGI("Client found"); -// -// net_close(Listensocket); -// -// serve->socket = ClientSocket; -// -// if (serve->stopped) -// { -// break; -// } -// } -// -// LOGD("Serve thread ended"); -// return 0; -//} - bool serve_start(struct serve* serve) { - LOGD("Starting serve thread"); + LOGD("Starting serve thread"); + + socket_t Listensocket; + socket_t ClientSocket; - socket_t Listensocket; - socket_t ClientSocket; + Listensocket = net_listen(serve->ip, serve->port, 1); + if (Listensocket == INVALID_SOCKET) { + LOGI("Listen error"); + net_close(Listensocket); + return 0; + } - Listensocket = net_listen(serve->ip, serve->port, 1); - if (Listensocket == INVALID_SOCKET) { - LOGI("Listen Error"); - net_close(Listensocket); - return 0; - } + ClientSocket = net_accept(Listensocket); + if (ClientSocket == INVALID_SOCKET) { + LOGI("Client error"); + net_close(Listensocket); + return 0; + } - ClientSocket = net_accept(Listensocket); - if (ClientSocket == INVALID_SOCKET) { - LOGI("Client Error"); - net_close(Listensocket); - return 0; - } - LOGI("Client found"); + LOGI("Client found"); - net_close(Listensocket); + net_close(Listensocket); - serve->socket = ClientSocket; + serve->socket = ClientSocket; - /*serve->thread = SDL_CreateThread(run_serve, "serve", serve); - if (!serve->thread) { - LOGC("Could not start stream thread"); - return false; - }*/ - return true; + return true; } bool serve_push(struct serve* serve, const AVPacket packet) { - if (net_send(serve->socket, packet.data, packet.size) == SOCKET_ERROR) - { - LOGI("Client lost"); - net_close(serve->socket); - return false; - } - return true; -} + if (net_send(serve->socket, packet.data, packet.size) == SOCKET_ERROR) { + LOGI("Client lost"); + net_close(serve->socket); + return false; + } + return true; +} \ No newline at end of file diff --git a/app/src/server.c b/app/src/server.c index ff167aebcc..b102f0c27d 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -5,12 +5,15 @@ #include #include #include +#include #include +#include #include "config.h" #include "command.h" #include "util/log.h" #include "util/net.h" +#include "util/str_util.h" #define SOCKET_NAME "scrcpy" #define SERVER_FILENAME "scrcpy-server" @@ -18,20 +21,39 @@ #define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FILENAME #define DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar" -static const char * +static char * get_server_path(void) { +#ifdef __WINDOWS__ + const wchar_t *server_path_env = _wgetenv(L"SCRCPY_SERVER_PATH"); +#else const char *server_path_env = getenv("SCRCPY_SERVER_PATH"); +#endif if (server_path_env) { - LOGD("Using SCRCPY_SERVER_PATH: %s", server_path_env); // if the envvar is set, use it - return server_path_env; +#ifdef __WINDOWS__ + char *server_path = utf8_from_wide_char(server_path_env); +#else + char *server_path = SDL_strdup(server_path_env); +#endif + if (!server_path) { + LOGE("Could not allocate memory"); + return NULL; + } + LOGD("Using SCRCPY_SERVER_PATH: %s", server_path); + return server_path; } #ifndef PORTABLE LOGD("Using server: " DEFAULT_SERVER_PATH); + char *server_path = SDL_strdup(DEFAULT_SERVER_PATH); + if (!server_path) { + LOGE("Could not allocate memory"); + return NULL; + } // the absolute path is hardcoded - return DEFAULT_SERVER_PATH; + return server_path; #else + // use scrcpy-server in the same directory as the executable char *executable_path = get_executable_path(); if (!executable_path) { @@ -67,12 +89,17 @@ get_server_path(void) { static bool push_server(const char *serial) { - const char *server_path = get_server_path(); + char *server_path = get_server_path(); + if (!server_path) { + return false; + } if (!is_regular_file(server_path)) { LOGE("'%s' does not exist or is not a regular file\n", server_path); + SDL_free(server_path); return false; } process_t process = adb_push(serial, server_path, DEVICE_SERVER_PATH); + SDL_free(server_path); return process_check_success(process, "adb push"); } @@ -101,22 +128,105 @@ disable_tunnel_forward(const char *serial, uint16_t local_port) { } static bool -enable_tunnel(struct server *server) { - if (enable_tunnel_reverse(server->serial, server->local_port)) { - return true; +disable_tunnel(struct server *server) { + if (server->tunnel_forward) { + return disable_tunnel_forward(server->serial, server->local_port); } + return disable_tunnel_reverse(server->serial); +} - LOGW("'adb reverse' failed, fallback to 'adb forward'"); +static socket_t +listen_on_port(uint16_t port) { +#define IPV4_LOCALHOST 0x7F000001 + return net_listen(IPV4_LOCALHOST, port, 1); +} + +static bool +enable_tunnel_reverse_any_port(struct server *server, + struct port_range port_range) { + uint16_t port = port_range.first; + for (;;) { + if (!enable_tunnel_reverse(server->serial, port)) { + // the command itself failed, it will fail on any port + return false; + } + + // At the application level, the device part is "the server" because it + // serves video stream and control. However, at the network level, the + // client listens and the server connects to the client. That way, the + // client can listen before starting the server app, so there is no + // need to try to connect until the server socket is listening on the + // device. + server->server_socket = listen_on_port(port); + if (server->server_socket != INVALID_SOCKET) { + // success + server->local_port = port; + return true; + } + + // failure, disable tunnel and try another port + if (!disable_tunnel_reverse(server->serial)) { + LOGW("Could not remove reverse tunnel on port %" PRIu16, port); + } + + // check before incrementing to avoid overflow on port 65535 + if (port < port_range.last) { + LOGW("Could not listen on port %" PRIu16", retrying on %" PRIu16, + port, (uint16_t) (port + 1)); + port++; + continue; + } + + if (port_range.first == port_range.last) { + LOGE("Could not listen on port %" PRIu16, port_range.first); + } else { + LOGE("Could not listen on any port in range %" PRIu16 ":%" PRIu16, + port_range.first, port_range.last); + } + return false; + } +} + +static bool +enable_tunnel_forward_any_port(struct server *server, + struct port_range port_range) { server->tunnel_forward = true; - return enable_tunnel_forward(server->serial, server->local_port); + uint16_t port = port_range.first; + for (;;) { + if (enable_tunnel_forward(server->serial, port)) { + // success + server->local_port = port; + return true; + } + + if (port < port_range.last) { + LOGW("Could not forward port %" PRIu16", retrying on %" PRIu16, + port, port + 1); + port++; + continue; + } + + if (port_range.first == port_range.last) { + LOGE("Could not forward port %" PRIu16, port_range.first); + } else { + LOGE("Could not forward any port in range %" PRIu16 ":%" PRIu16, + port_range.first, port_range.last); + } + return false; + } } static bool -disable_tunnel(struct server *server) { - if (server->tunnel_forward) { - return disable_tunnel_forward(server->serial, server->local_port); +enable_tunnel_any_port(struct server *server, struct port_range port_range) { + if (enable_tunnel_reverse_any_port(server, port_range)) { + return true; } - return disable_tunnel_reverse(server->serial); + + // if "adb reverse" does not work (e.g. over "adb connect"), it fallbacks to + // "adb forward", so the app socket is the client + + LOGW("'adb reverse' failed, fallback to 'adb forward'"); + return enable_tunnel_forward_any_port(server, port_range); } static process_t @@ -124,16 +234,26 @@ execute_server(struct server *server, const struct server_params *params) { char max_size_string[6]; char bit_rate_string[11]; char max_fps_string[6]; + char lock_video_orientation_string[3]; + char display_id_string[6]; sprintf(max_size_string, "%"PRIu16, params->max_size); sprintf(bit_rate_string, "%"PRIu32, params->bit_rate); sprintf(max_fps_string, "%"PRIu16, params->max_fps); + sprintf(lock_video_orientation_string, "%"PRIi8, params->lock_video_orientation); + sprintf(display_id_string, "%"PRIu16, params->display_id); const char *const cmd[] = { "shell", "CLASSPATH=" DEVICE_SERVER_PATH, "app_process", #ifdef SERVER_DEBUGGER # define SERVER_DEBUGGER_PORT "5005" +# ifdef SERVER_DEBUGGER_METHOD_NEW + /* Android 9 and above */ + "-XjdwpProvider:internal -XjdwpOptions:transport=dt_socket,suspend=y,server=y,address=" +# else + /* Android 8 and below */ "-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address=" +# endif SERVER_DEBUGGER_PORT, #endif "/", // unused @@ -142,10 +262,12 @@ execute_server(struct server *server, const struct server_params *params) { max_size_string, bit_rate_string, max_fps_string, + lock_video_orientation_string, server->tunnel_forward ? "true" : "false", params->crop ? params->crop : "-", "true", // always send frame meta (packet boundaries + timestamp) params->control ? "true" : "false", + display_id_string, }; #ifdef SERVER_DEBUGGER LOGI("Server debugger waiting for a client on device port " @@ -161,13 +283,6 @@ execute_server(struct server *server, const struct server_params *params) { return adb_execute(server->serial, cmd, sizeof(cmd) / sizeof(cmd[0])); } -#define IPV4_LOCALHOST 0x7F000001 - -static socket_t -listen_on_port(uint16_t port) { - return net_listen(IPV4_LOCALHOST, port, 1); -} - static socket_t connect_and_read_byte(uint16_t port) { socket_t socket = net_connect(IPV4_LOCALHOST, port); @@ -203,14 +318,12 @@ connect_to_server(uint16_t port, uint32_t attempts, uint32_t delay) { } static void -close_socket(socket_t *socket) { - assert(*socket != INVALID_SOCKET); - net_shutdown(*socket, SHUT_RDWR); - if (!net_close(*socket)) { +close_socket(socket_t socket) { + assert(socket != INVALID_SOCKET); + net_shutdown(socket, SHUT_RDWR); + if (!net_close(socket)) { LOGW("Could not close socket"); - return; } - *socket = INVALID_SOCKET; } void @@ -218,10 +331,26 @@ server_init(struct server *server) { *server = (struct server) SERVER_INITIALIZER; } +static int +run_wait_server(void *data) { + struct server *server = data; + cmd_simple_wait(server->process, NULL); // ignore exit code + // no need for synchronization, server_socket is initialized before this + // thread was created + if (server->server_socket != INVALID_SOCKET + && !atomic_flag_test_and_set(&server->server_socket_closed)) { + // On Linux, accept() is unblocked by shutdown(), but on Windows, it is + // unblocked by closesocket(). Therefore, call both (close_socket()). + close_socket(server->server_socket); + } + LOGD("Server terminated"); + return 0; +} + bool server_start(struct server *server, const char *serial, const struct server_params *params) { - server->local_port = params->local_port; + server->port_range = params->port_range; if (serial) { server->serial = SDL_strdup(serial); @@ -231,49 +360,50 @@ server_start(struct server *server, const char *serial, } if (!push_server(serial)) { - SDL_free(server->serial); - return false; - } - - if (!enable_tunnel(server)) { - SDL_free(server->serial); - return false; + goto error1; } - // if "adb reverse" does not work (e.g. over "adb connect"), it fallbacks to - // "adb forward", so the app socket is the client - if (!server->tunnel_forward) { - // At the application level, the device part is "the server" because it - // serves video stream and control. However, at the network level, the - // client listens and the server connects to the client. That way, the - // client can listen before starting the server app, so there is no - // need to try to connect until the server socket is listening on the - // device. - - server->server_socket = listen_on_port(params->local_port); - if (server->server_socket == INVALID_SOCKET) { - LOGE("Could not listen on port %" PRIu16, params->local_port); - disable_tunnel(server); - SDL_free(server->serial); - return false; - } + if (!enable_tunnel_any_port(server, params->port_range)) { + goto error1; } // server will connect to our server socket server->process = execute_server(server, params); - if (server->process == PROCESS_NONE) { - if (!server->tunnel_forward) { - close_socket(&server->server_socket); - } - disable_tunnel(server); - SDL_free(server->serial); - return false; + goto error2; + } + + // If the server process dies before connecting to the server socket, then + // the client will be stuck forever on accept(). To avoid the problem, we + // must be able to wake up the accept() call when the server dies. To keep + // things simple and multiplatform, just spawn a new thread waiting for the + // server process and calling shutdown()/close() on the server socket if + // necessary to wake up any accept() blocking call. + server->wait_server_thread = + SDL_CreateThread(run_wait_server, "wait-server", server); + if (!server->wait_server_thread) { + cmd_terminate(server->process); + cmd_simple_wait(server->process, NULL); // ignore exit code + goto error2; } server->tunnel_enabled = true; return true; + +error2: + if (!server->tunnel_forward) { + bool was_closed = + atomic_flag_test_and_set(&server->server_socket_closed); + // the thread is not started, the flag could not be already set + assert(!was_closed); + (void) was_closed; + close_socket(server->server_socket); + } + disable_tunnel(server); +error1: + SDL_free(server->serial); + return false; } bool @@ -291,7 +421,11 @@ server_connect_to(struct server *server) { } // we don't need the server socket anymore - close_socket(&server->server_socket); + if (!atomic_flag_test_and_set(&server->server_socket_closed)) { + // close it from here + close_socket(server->server_socket); + // otherwise, it is closed by run_wait_server() + } } else { uint32_t attempts = 100; uint32_t delay = 100; // ms @@ -318,29 +452,27 @@ server_connect_to(struct server *server) { void server_stop(struct server *server) { - if (server->server_socket != INVALID_SOCKET) { - close_socket(&server->server_socket); + if (server->server_socket != INVALID_SOCKET + && !atomic_flag_test_and_set(&server->server_socket_closed)) { + close_socket(server->server_socket); } if (server->video_socket != INVALID_SOCKET) { - close_socket(&server->video_socket); + close_socket(server->video_socket); } if (server->control_socket != INVALID_SOCKET) { - close_socket(&server->control_socket); + close_socket(server->control_socket); } assert(server->process != PROCESS_NONE); - if (!cmd_terminate(server->process)) { - LOGW("Could not terminate server"); - } - - cmd_simple_wait(server->process, NULL); // ignore exit code - LOGD("Server terminated"); + cmd_terminate(server->process); if (server->tunnel_enabled) { // ignore failure disable_tunnel(server); } + + SDL_WaitThread(server->wait_server_thread, NULL); } void diff --git a/app/src/server.h b/app/src/server.h index 0cb1ab3a6d..a2ecdefcda 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -1,42 +1,56 @@ #ifndef SERVER_H #define SERVER_H +#include #include #include +#include #include "config.h" #include "command.h" +#include "common.h" #include "util/net.h" struct server { char *serial; process_t process; + SDL_Thread *wait_server_thread; + atomic_flag server_socket_closed; socket_t server_socket; // only used if !tunnel_forward socket_t video_socket; socket_t control_socket; - uint16_t local_port; + struct port_range port_range; + uint16_t local_port; // selected from port_range bool tunnel_enabled; bool tunnel_forward; // use "adb forward" instead of "adb reverse" }; -#define SERVER_INITIALIZER { \ - .serial = NULL, \ - .process = PROCESS_NONE, \ - .server_socket = INVALID_SOCKET, \ - .video_socket = INVALID_SOCKET, \ +#define SERVER_INITIALIZER { \ + .serial = NULL, \ + .process = PROCESS_NONE, \ + .wait_server_thread = NULL, \ + .server_socket_closed = ATOMIC_FLAG_INIT, \ + .server_socket = INVALID_SOCKET, \ + .video_socket = INVALID_SOCKET, \ .control_socket = INVALID_SOCKET, \ - .local_port = 0, \ - .tunnel_enabled = false, \ - .tunnel_forward = false, \ + .port_range = { \ + .first = 0, \ + .last = 0, \ + }, \ + .local_port = 0, \ + .tunnel_enabled = false, \ + .tunnel_forward = false, \ } struct server_params { const char *crop; - uint16_t local_port; + struct port_range port_range; uint16_t max_size; uint32_t bit_rate; uint16_t max_fps; + int8_t lock_video_orientation; bool control; + uint16_t display_id; }; // init default values diff --git a/app/src/stream.c b/app/src/stream.c index b91fb2fec9..a7cee43698 100644 --- a/app/src/stream.c +++ b/app/src/stream.c @@ -7,7 +7,6 @@ #include #include #include -#include #include "config.h" #include "compat.h" @@ -15,10 +14,8 @@ #include "events.h" #include "recorder.h" #include "serve.h" -#include "stream.h" #include "util/buffer_util.h" #include "util/log.h" -#include "util/net.h" #define BUFSIZE 0x10000 @@ -26,295 +23,291 @@ #define NO_PTS UINT64_C(-1) static bool -stream_recv_packet(struct stream* stream, AVPacket* packet) { - // The video stream contains raw packets, without time information. When we - // record, we retrieve the timestamps separately, from a "meta" header - // added by the server before each raw packet. - // - // The "meta" header length is 12 bytes: - // [. . . . . . . .|. . . .]. . . . . . . . . . . . . . . ... - // <-------------> <-----> <-----------------------------... - // PTS packet raw packet - // size - // - // It is followed by bytes containing the packet/frame. - - uint8_t header[HEADER_SIZE]; - ssize_t r = net_recv_all(stream->socket, header, HEADER_SIZE); - if (r < HEADER_SIZE) { - return false; - } - - uint64_t pts = buffer_read64be(header); - uint32_t len = buffer_read32be(&header[8]); - assert(pts == NO_PTS || (pts & 0x8000000000000000) == 0); - assert(len); - - if (av_new_packet(packet, len)) { - LOGE("Could not allocate packet"); - return false; - } - - r = net_recv_all(stream->socket, packet->data, len); - if (r < 0 || ((uint32_t)r) < len) { - av_packet_unref(packet); - return false; - } - - packet->pts = pts != NO_PTS ? (int64_t)pts : AV_NOPTS_VALUE; - - return true; +stream_recv_packet(struct stream *stream, AVPacket *packet) { + // The video stream contains raw packets, without time information. When we + // record, we retrieve the timestamps separately, from a "meta" header + // added by the server before each raw packet. + // + // The "meta" header length is 12 bytes: + // [. . . . . . . .|. . . .]. . . . . . . . . . . . . . . ... + // <-------------> <-----> <-----------------------------... + // PTS packet raw packet + // size + // + // It is followed by bytes containing the packet/frame. + + uint8_t header[HEADER_SIZE]; + ssize_t r = net_recv_all(stream->socket, header, HEADER_SIZE); + if (r < HEADER_SIZE) { + return false; + } + + uint64_t pts = buffer_read64be(header); + uint32_t len = buffer_read32be(&header[8]); + assert(pts == NO_PTS || (pts & 0x8000000000000000) == 0); + assert(len); + + if (av_new_packet(packet, len)) { + LOGE("Could not allocate packet"); + return false; + } + + r = net_recv_all(stream->socket, packet->data, len); + if (r < 0 || ((uint32_t) r) < len) { + av_packet_unref(packet); + return false; + } + + packet->pts = pts != NO_PTS ? (int64_t) pts : AV_NOPTS_VALUE; + + return true; } static void notify_stopped(void) { - SDL_Event stop_event; - stop_event.type = EVENT_STREAM_STOPPED; - SDL_PushEvent(&stop_event); + SDL_Event stop_event; + stop_event.type = EVENT_STREAM_STOPPED; + SDL_PushEvent(&stop_event); } static bool -process_config_packet(struct stream* stream, AVPacket* packet) { - if (stream->recorder && !recorder_push(stream->recorder, packet)) { - LOGE("Could not send config packet to recorder"); - return false; - } - return true; +process_config_packet(struct stream *stream, AVPacket *packet) { + if (stream->recorder && !recorder_push(stream->recorder, packet)) { + LOGE("Could not send config packet to recorder"); + return false; + } + return true; } - - static bool -process_frame(struct stream* stream, AVPacket* packet) { - if (stream->decoder && !decoder_push(stream->decoder, packet)) { - return false; - } - - if (stream->recorder) { - packet->dts = packet->pts; - - if (!recorder_push(stream->recorder, packet)) { - LOGE("Could not send packet to recorder"); - return false; - } - } - - if (stream->serve && !serve_push(stream->serve, *packet)) { - LOGE("Could not serve packet"); - return false; - } - - return true; +process_frame(struct stream *stream, AVPacket *packet) { + if (stream->decoder && !decoder_push(stream->decoder, packet)) { + return false; + } + + if (stream->recorder) { + packet->dts = packet->pts; + + if (!recorder_push(stream->recorder, packet)) { + LOGE("Could not send packet to recorder"); + return false; + } + } + + if (stream->serve) { + packet->dts = packet->pts; + + if (!serve_push(stream->serve, packet) { + LOGE("Could not serve packet"); + return false; + } + } + + return true; } static bool -stream_parse(struct stream* stream, AVPacket* packet) { - uint8_t* in_data = packet->data; - int in_len = packet->size; - uint8_t* out_data = NULL; - int out_len = 0; - int r = av_parser_parse2(stream->parser, stream->codec_ctx, - &out_data, &out_len, in_data, in_len, - AV_NOPTS_VALUE, AV_NOPTS_VALUE, -1); - - // PARSER_FLAG_COMPLETE_FRAMES is set - assert(r == in_len); - (void)r; - assert(out_len == in_len); - - if (stream->parser->key_frame == 1) { - packet->flags |= AV_PKT_FLAG_KEY; - } - - bool ok = process_frame(stream, packet); - if (!ok) { - LOGE("Could not process frame"); - return false; - } - - return true; +stream_parse(struct stream *stream, AVPacket *packet) { + uint8_t *in_data = packet->data; + int in_len = packet->size; + uint8_t *out_data = NULL; + int out_len = 0; + int r = av_parser_parse2(stream->parser, stream->codec_ctx, + &out_data, &out_len, in_data, in_len, + AV_NOPTS_VALUE, AV_NOPTS_VALUE, -1); + + // PARSER_FLAG_COMPLETE_FRAMES is set + assert(r == in_len); + (void) r; + assert(out_len == in_len); + + if (stream->parser->key_frame == 1) { + packet->flags |= AV_PKT_FLAG_KEY; + } + + bool ok = process_frame(stream, packet); + if (!ok) { + LOGE("Could not process frame"); + return false; + } + + return true; } static bool -stream_push_packet(struct stream* stream, AVPacket* packet) { - bool is_config = packet->pts == AV_NOPTS_VALUE; - - // A config packet must not be decoded immetiately (it contains no - // frame); instead, it must be concatenated with the future data packet. - if (stream->has_pending || is_config) { - size_t offset; - if (stream->has_pending) { - offset = stream->pending.size; - if (av_grow_packet(&stream->pending, packet->size)) { - LOGE("Could not grow packet"); - return false; - } - } - else { - offset = 0; - if (av_new_packet(&stream->pending, packet->size)) { - LOGE("Could not create packet"); - return false; - } - stream->has_pending = true; - } - - memcpy(stream->pending.data + offset, packet->data, packet->size); - - if (!is_config) { - // prepare the concat packet to send to the decoder - stream->pending.pts = packet->pts; - stream->pending.dts = packet->dts; - stream->pending.flags = packet->flags; - packet = &stream->pending; - } - } - - if (is_config) { - // config packet - bool ok = process_config_packet(stream, packet); - if (!ok) { - return false; - } - } - else { - // data packet - bool ok = stream_parse(stream, packet); - - if (stream->has_pending) { - // the pending packet must be discarded (consumed or error) - stream->has_pending = false; - av_packet_unref(&stream->pending); - } - - if (!ok) { - return false; - } - } - return true; +stream_push_packet(struct stream *stream, AVPacket *packet) { + bool is_config = packet->pts == AV_NOPTS_VALUE; + + // A config packet must not be decoded immetiately (it contains no + // frame); instead, it must be concatenated with the future data packet. + if (stream->has_pending || is_config) { + size_t offset; + if (stream->has_pending) { + offset = stream->pending.size; + if (av_grow_packet(&stream->pending, packet->size)) { + LOGE("Could not grow packet"); + return false; + } + } else { + offset = 0; + if (av_new_packet(&stream->pending, packet->size)) { + LOGE("Could not create packet"); + return false; + } + stream->has_pending = true; + } + + memcpy(stream->pending.data + offset, packet->data, packet->size); + + if (!is_config) { + // prepare the concat packet to send to the decoder + stream->pending.pts = packet->pts; + stream->pending.dts = packet->dts; + stream->pending.flags = packet->flags; + packet = &stream->pending; + } + } + + if (is_config) { + // config packet + bool ok = process_config_packet(stream, packet); + if (!ok) { + return false; + } + } else { + // data packet + bool ok = stream_parse(stream, packet); + + if (stream->has_pending) { + // the pending packet must be discarded (consumed or error) + stream->has_pending = false; + av_packet_unref(&stream->pending); + } + + if (!ok) { + return false; + } + } + return true; } static int -run_stream(void* data) { - struct stream* stream = data; - - AVCodec* codec = avcodec_find_decoder(AV_CODEC_ID_H264); - if (!codec) { - LOGE("H.264 decoder not found"); - goto end; - } - - stream->codec_ctx = avcodec_alloc_context3(codec); - if (!stream->codec_ctx) { - LOGC("Could not allocate codec context"); - goto end; - } - - if (stream->decoder && !decoder_open(stream->decoder, codec)) { - LOGE("Could not open decoder"); - goto finally_free_codec_ctx; - } - - if (stream->recorder) { - if (!recorder_open(stream->recorder, codec)) { - LOGE("Could not open recorder"); - goto finally_close_decoder; - } - - if (!recorder_start(stream->recorder)) { - LOGE("Could not start recorder"); - goto finally_close_recorder; - } - } - - stream->parser = av_parser_init(AV_CODEC_ID_H264); - if (!stream->parser) { - LOGE("Could not initialize parser"); - goto finally_stop_and_join_recorder; - } - - // We must only pass complete frames to av_parser_parse2()! - // It's more complicated, but this allows to reduce the latency by 1 frame! - stream->parser->flags |= PARSER_FLAG_COMPLETE_FRAMES; - - for (;;) { - AVPacket packet; - bool ok = stream_recv_packet(stream, &packet); - - if (!ok) { - // end of stream - break; - } - - ok = stream_push_packet(stream, &packet); - - av_packet_unref(&packet); - - if (!ok) { - // cannot process packet (error already logged) - break; - } - } - - LOGD("End of frames"); - - if (stream->has_pending) { - av_packet_unref(&stream->pending); - } - - av_parser_close(stream->parser); +run_stream(void *data) { + struct stream *stream = data; + + AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264); + if (!codec) { + LOGE("H.264 decoder not found"); + goto end; + } + + stream->codec_ctx = avcodec_alloc_context3(codec); + if (!stream->codec_ctx) { + LOGC("Could not allocate codec context"); + goto end; + } + + if (stream->decoder && !decoder_open(stream->decoder, codec)) { + LOGE("Could not open decoder"); + goto finally_free_codec_ctx; + } + + if (stream->recorder) { + if (!recorder_open(stream->recorder, codec)) { + LOGE("Could not open recorder"); + goto finally_close_decoder; + } + + if (!recorder_start(stream->recorder)) { + LOGE("Could not start recorder"); + goto finally_close_recorder; + } + } + + stream->parser = av_parser_init(AV_CODEC_ID_H264); + if (!stream->parser) { + LOGE("Could not initialize parser"); + goto finally_stop_and_join_recorder; + } + + // We must only pass complete frames to av_parser_parse2()! + // It's more complicated, but this allows to reduce the latency by 1 frame! + stream->parser->flags |= PARSER_FLAG_COMPLETE_FRAMES; + + for (;;) { + AVPacket packet; + bool ok = stream_recv_packet(stream, &packet); + if (!ok) { + // end of stream + break; + } + + ok = stream_push_packet(stream, &packet); + av_packet_unref(&packet); + if (!ok) { + // cannot process packet (error already logged) + break; + } + } + + LOGD("End of frames"); + + if (stream->has_pending) { + av_packet_unref(&stream->pending); + } + + av_parser_close(stream->parser); finally_stop_and_join_recorder: - if (stream->recorder) { - recorder_stop(stream->recorder); - LOGI("Finishing recording..."); - recorder_join(stream->recorder); - } + if (stream->recorder) { + recorder_stop(stream->recorder); + LOGI("Finishing recording..."); + recorder_join(stream->recorder); + } finally_close_recorder: - if (stream->recorder) { - recorder_close(stream->recorder); - } + if (stream->recorder) { + recorder_close(stream->recorder); + } finally_close_decoder: - if (stream->decoder) { - decoder_close(stream->decoder); - } + if (stream->decoder) { + decoder_close(stream->decoder); + } finally_free_codec_ctx: - avcodec_free_context(&stream->codec_ctx); + avcodec_free_context(&stream->codec_ctx); end: - notify_stopped(); - return 0; + notify_stopped(); + return 0; } void -stream_init(struct stream* stream, socket_t socket, - struct decoder* decoder, struct recorder* recorder, struct serve* serve) { - stream->socket = socket; - stream->decoder = decoder; - stream->recorder = recorder; - stream->serve = serve; - stream->has_pending = false; +stream_init(struct stream *stream, socket_t socket, + struct decoder *decoder, struct recorder *recorder, struct serve *serve) { + stream->socket = socket; + stream->decoder = decoder, + stream->recorder = recorder; + stream->serve = serve; + stream->has_pending = false; } bool -stream_start(struct stream* stream) { - LOGD("Starting stream thread"); - - stream->thread = SDL_CreateThread(run_stream, "stream", stream); - if (!stream->thread) { - LOGC("Could not start stream thread"); - return false; - } - return true; +stream_start(struct stream *stream) { + LOGD("Starting stream thread"); + + stream->thread = SDL_CreateThread(run_stream, "stream", stream); + if (!stream->thread) { + LOGC("Could not start stream thread"); + return false; + } + return true; } void -stream_stop(struct stream* stream) { - if (stream->decoder) { - decoder_interrupt(stream->decoder); - } +stream_stop(struct stream *stream) { + if (stream->decoder) { + decoder_interrupt(stream->decoder); + } } void -stream_join(struct stream* stream) { - SDL_WaitThread(stream->thread, NULL); +stream_join(struct stream *stream) { + SDL_WaitThread(stream->thread, NULL); } - diff --git a/app/src/sys/unix/command.c b/app/src/sys/unix/command.c index fbcf2355cd..64a54e7105 100644 --- a/app/src/sys/unix/command.c +++ b/app/src/sys/unix/command.c @@ -14,12 +14,51 @@ #include #include #include +#include +#include #include +#include #include #include #include "util/log.h" +bool +cmd_search(const char *file) { + char *path = getenv("PATH"); + if (!path) + return false; + path = strdup(path); + if (!path) + return false; + + bool ret = false; + size_t file_len = strlen(file); + char *saveptr; + for (char *dir = strtok_r(path, ":", &saveptr); dir; + dir = strtok_r(NULL, ":", &saveptr)) { + size_t dir_len = strlen(dir); + char *fullpath = malloc(dir_len + file_len + 2); + if (!fullpath) + continue; + memcpy(fullpath, dir, dir_len); + fullpath[dir_len] = '/'; + memcpy(fullpath + dir_len + 1, file, file_len + 1); + + struct stat sb; + bool fullpath_executable = stat(fullpath, &sb) == 0 && + sb.st_mode & S_IXUSR; + free(fullpath); + if (fullpath_executable) { + ret = true; + break; + } + } + + free(path); + return ret; +} + enum process_result cmd_execute(const char *const argv[], pid_t *pid) { int fd[2]; @@ -127,3 +166,14 @@ get_executable_path(void) { return NULL; #endif } + +bool +is_regular_file(const char *path) { + struct stat path_stat; + + if (stat(path, &path_stat)) { + perror("stat"); + return false; + } + return S_ISREG(path_stat.st_mode); +} diff --git a/app/src/sys/win/command.c b/app/src/sys/win/command.c index 55edaf8f6e..105234b034 100644 --- a/app/src/sys/win/command.c +++ b/app/src/sys/win/command.c @@ -1,5 +1,7 @@ #include "command.h" +#include + #include "config.h" #include "util/log.h" #include "util/str_util.h" @@ -90,3 +92,22 @@ get_executable_path(void) { buf[len] = '\0'; return utf8_from_wide_char(buf); } + +bool +is_regular_file(const char *path) { + wchar_t *wide_path = utf8_to_wide_char(path); + if (!wide_path) { + LOGC("Could not allocate wide char string"); + return false; + } + + struct _stat path_stat; + int r = _wstat(wide_path, &path_stat); + SDL_free(wide_path); + + if (r) { + perror("stat"); + return false; + } + return S_ISREG(path_stat.st_mode); +} diff --git a/app/src/util/net.c b/app/src/util/net.c index bf4389dd4e..efce6fa9b9 100644 --- a/app/src/util/net.c +++ b/app/src/util/net.c @@ -1,6 +1,7 @@ #include "net.h" #include +#include #include "config.h" #include "log.h" @@ -115,3 +116,32 @@ bool net_shutdown(socket_t socket, int how) { return !shutdown(socket, how); } + +bool +net_init(void) { +#ifdef __WINDOWS__ + WSADATA wsa; + int res = WSAStartup(MAKEWORD(2, 2), &wsa) < 0; + if (res < 0) { + LOGC("WSAStartup failed with error %d", res); + return false; + } +#endif + return true; +} + +void +net_cleanup(void) { +#ifdef __WINDOWS__ + WSACleanup(); +#endif +} + +bool +net_close(socket_t socket) { +#ifdef __WINDOWS__ + return !closesocket(socket); +#else + return !close(socket); +#endif +} diff --git a/app/src/util/str_util.c b/app/src/util/str_util.c index 4d17540718..ce0498a537 100644 --- a/app/src/util/str_util.c +++ b/app/src/util/str_util.c @@ -81,6 +81,35 @@ parse_integer(const char *s, long *out) { return true; } +size_t +parse_integers(const char *s, const char sep, size_t max_items, long *out) { + size_t count = 0; + char *endptr; + do { + errno = 0; + long value = strtol(s, &endptr, 0); + if (errno == ERANGE) { + return 0; + } + + if (endptr == s || (*endptr != sep && *endptr != '\0')) { + return 0; + } + + out[count++] = value; + if (*endptr == sep) { + if (count >= max_items) { + // max items already reached, could not accept a new item + return 0; + } + // parse the next token during the next iteration + s = endptr + 1; + } + } while (*endptr != '\0'); + + return count; +} + bool parse_integer_with_suffix(const char *s, long *out) { char *endptr; diff --git a/app/src/util/str_util.h b/app/src/util/str_util.h index 8d9b990ced..c7f26cdb65 100644 --- a/app/src/util/str_util.h +++ b/app/src/util/str_util.h @@ -31,6 +31,11 @@ strquote(const char *src); bool parse_integer(const char *s, long *out); +// parse s as integers separated by sep (for example '1234:2000') +// returns the number of integers on success, 0 on failure +size_t +parse_integers(const char *s, const char sep, size_t max_items, long *out); + // parse s as an integer into value // like parse_integer(), but accept 'k'/'K' (x1000) and 'm'/'M' (x1000000) as // suffix diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c index 539c3c94cf..c5d956336b 100644 --- a/app/tests/test_cli.c +++ b/app/tests/test_cli.c @@ -48,9 +48,10 @@ static void test_options(void) { "--fullscreen", "--max-fps", "30", "--max-size", "1024", + "--lock-video-orientation", "2", // "--no-control" is not compatible with "--turn-screen-off" // "--no-display" is not compatible with "--fulscreen" - "--port", "1234", + "--port", "1234:1236", "--push-target", "/sdcard/Movies", "--record", "file", "--record-format", "mkv", @@ -78,7 +79,9 @@ static void test_options(void) { assert(opts->fullscreen); assert(opts->max_fps == 30); assert(opts->max_size == 1024); - assert(opts->port == 1234); + assert(opts->lock_video_orientation == 2); + assert(opts->port_range.first == 1234); + assert(opts->port_range.last == 1236); assert(!strcmp(opts->push_target, "/sdcard/Movies")); assert(!strcmp(opts->record_filename, "file")); assert(opts->record_format == RECORDER_FORMAT_MKV); diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index d6f556f3f5..4dc790186d 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -49,20 +49,20 @@ static void test_serialize_inject_text(void) { static void test_serialize_inject_text_long(void) { struct control_msg msg; msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; - char text[CONTROL_MSG_TEXT_MAX_LENGTH + 1]; + char text[CONTROL_MSG_INJECT_TEXT_MAX_LENGTH + 1]; memset(text, 'a', sizeof(text)); - text[CONTROL_MSG_TEXT_MAX_LENGTH] = '\0'; + text[CONTROL_MSG_INJECT_TEXT_MAX_LENGTH] = '\0'; msg.inject_text.text = text; unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; int size = control_msg_serialize(&msg, buf); - assert(size == 3 + CONTROL_MSG_TEXT_MAX_LENGTH); + assert(size == 3 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); - unsigned char expected[3 + CONTROL_MSG_TEXT_MAX_LENGTH]; + unsigned char expected[3 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH]; expected[0] = CONTROL_MSG_TYPE_INJECT_TEXT; expected[1] = 0x01; expected[2] = 0x2c; // text length (16 bits) - memset(&expected[3], 'a', CONTROL_MSG_TEXT_MAX_LENGTH); + memset(&expected[3], 'a', CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); assert(!memcmp(buf, expected, sizeof(expected))); } diff --git a/app/tests/test_strutil.c b/app/tests/test_strutil.c index 200e0f6306..a88bca0eec 100644 --- a/app/tests/test_strutil.c +++ b/app/tests/test_strutil.c @@ -187,6 +187,55 @@ static void test_parse_integer(void) { assert(!ok); // out-of-range } +static void test_parse_integers(void) { + long values[5]; + + size_t count = parse_integers("1234", ':', 5, values); + assert(count == 1); + assert(values[0] == 1234); + + count = parse_integers("1234:5678", ':', 5, values); + assert(count == 2); + assert(values[0] == 1234); + assert(values[1] == 5678); + + count = parse_integers("1234:5678", ':', 2, values); + assert(count == 2); + assert(values[0] == 1234); + assert(values[1] == 5678); + + count = parse_integers("1234:-5678", ':', 2, values); + assert(count == 2); + assert(values[0] == 1234); + assert(values[1] == -5678); + + count = parse_integers("1:2:3:4:5", ':', 5, values); + assert(count == 5); + assert(values[0] == 1); + assert(values[1] == 2); + assert(values[2] == 3); + assert(values[3] == 4); + assert(values[4] == 5); + + count = parse_integers("1234:5678", ':', 1, values); + assert(count == 0); // max_items == 1 + + count = parse_integers("1:2:3:4:5", ':', 3, values); + assert(count == 0); // max_items == 3 + + count = parse_integers(":1234", ':', 5, values); + assert(count == 0); // invalid + + count = parse_integers("1234:", ':', 5, values); + assert(count == 0); // invalid + + count = parse_integers("1234:", ':', 1, values); + assert(count == 0); // invalid, even when max_items == 1 + + count = parse_integers("1234::5678", ':', 5, values); + assert(count == 0); // invalid +} + static void test_parse_integer_with_suffix(void) { long value; bool ok = parse_integer_with_suffix("1234", &value); @@ -249,6 +298,7 @@ int main(void) { test_strquote(); test_utf8_truncate(); test_parse_integer(); + test_parse_integers(); test_parse_integer_with_suffix(); return 0; } diff --git a/build.gradle b/build.gradle index b6ec625d9d..94862d2ecf 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.4.2' + classpath 'com.android.tools.build:gradle:3.6.2' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 798814d9d6..812d060be3 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -129,11 +129,6 @@ page at http://checkstyle.sourceforge.net/config.html --> - - - - - diff --git a/cross_win32.txt b/cross_win32.txt index d13af0e240..7b420690a4 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -15,6 +15,6 @@ cpu = 'i686' endian = 'little' [properties] -prebuilt_ffmpeg_shared = 'ffmpeg-4.2.1-win32-shared' -prebuilt_ffmpeg_dev = 'ffmpeg-4.2.1-win32-dev' -prebuilt_sdl2 = 'SDL2-2.0.10/i686-w64-mingw32' +prebuilt_ffmpeg_shared = 'ffmpeg-4.2.2-win32-shared' +prebuilt_ffmpeg_dev = 'ffmpeg-4.2.2-win32-dev' +prebuilt_sdl2 = 'SDL2-2.0.12/i686-w64-mingw32' diff --git a/cross_win64.txt b/cross_win64.txt index 09f387e1c1..cb9e095495 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -15,6 +15,6 @@ cpu = 'x86_64' endian = 'little' [properties] -prebuilt_ffmpeg_shared = 'ffmpeg-4.2.1-win64-shared' -prebuilt_ffmpeg_dev = 'ffmpeg-4.2.1-win64-dev' -prebuilt_sdl2 = 'SDL2-2.0.10/x86_64-w64-mingw32' +prebuilt_ffmpeg_shared = 'ffmpeg-4.2.2-win64-shared' +prebuilt_ffmpeg_dev = 'ffmpeg-4.2.2-win64-dev' +prebuilt_sdl2 = 'SDL2-2.0.12/x86_64-w64-mingw32' diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 5c2d1cf016b3885f6930543d57b744ea8c220a1a..490fda8577df6c95960ba7077c43220e5bb2c0d9 100644 GIT binary patch delta 22806 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 Z!$zsDgoO%|(a?lZ=Qe6kxv{^r{{!uYR<-~D delta 19980 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 Z5H)%VJiSWIAOn$sbP+#;Q{V4m{s)AkOUM8K diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 430dfabc5c..a4b4429748 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.5.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 8e25e6c19d..2fe81a7d95 100755 --- a/gradlew +++ b/gradlew @@ -125,8 +125,8 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` @@ -154,19 +154,19 @@ if $cygwin ; then else eval `echo args$i`="\"$arg\"" fi - i=$((i+1)) + i=`expr $i + 1` done case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi @@ -175,14 +175,9 @@ save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } -APP_ARGS=$(save "$@") +APP_ARGS=`save "$@"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 24467a141f..9109989e3c 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -29,6 +29,9 @@ if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" diff --git a/meson.build b/meson.build index 412c9c51cf..49093453ad 100644 --- a/meson.build +++ b/meson.build @@ -1,6 +1,6 @@ project('scrcpy', 'c', - version: '1.12.1', - meson_version: '>= 0.37', + version: '1.13', + meson_version: '>= 0.48', default_options: [ 'c_std=c11', 'warning_level=2', diff --git a/meson_options.txt b/meson_options.txt index 4cf4a8bfb8..c213e7dd43 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -6,3 +6,4 @@ option('prebuilt_server', type: 'string', description: 'Path of the prebuilt ser option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server from the same directory as the scrcpy executable') option('hidpi_support', type: 'boolean', value: true, description: 'Enable High DPI support') option('server_debugger', type: 'boolean', value: false, description: 'Run a server debugger and wait for a client to be attached') +option('server_debugger_method', type: 'combo', choices: ['old', 'new'], value: 'new', description: 'Select the debugger method (Android < 9: "old", Android >= 9: "new")') diff --git a/prebuilt-deps/Makefile b/prebuilt-deps/Makefile index 892af6c7f6..088cee92c5 100644 --- a/prebuilt-deps/Makefile +++ b/prebuilt-deps/Makefile @@ -10,29 +10,29 @@ prepare-win32: prepare-sdl2 prepare-ffmpeg-shared-win32 prepare-ffmpeg-dev-win32 prepare-win64: prepare-sdl2 prepare-ffmpeg-shared-win64 prepare-ffmpeg-dev-win64 prepare-adb prepare-ffmpeg-shared-win32: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.2.1-win32-shared.zip \ - 9208255f409410d95147151d7e829b5699bf8d91bfe1e81c3f470f47c2fa66d2 \ - ffmpeg-4.2.1-win32-shared + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.2.2-win32-shared.zip \ + ab5d603aaa54de360db2c2ffe378c82376b9343ea1175421dd644639aa07ee31 \ + ffmpeg-4.2.2-win32-shared prepare-ffmpeg-dev-win32: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.2.1-win32-dev.zip \ - c3469e6c5f031cbcc8cba88dee92d6548c5c6b6ff14f4097f18f72a92d0d70c4 \ - ffmpeg-4.2.1-win32-dev + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.2.2-win32-dev.zip \ + 8d224be567a2950cad4be86f4aabdd045bfa52ad758e87c72cedd278613bc6c8 \ + ffmpeg-4.2.2-win32-dev prepare-ffmpeg-shared-win64: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.2.1-win64-shared.zip \ - 55063d3cf750a75485c7bf196031773d81a1b25d0980c7db48ecfc7701a42331 \ - ffmpeg-4.2.1-win64-shared + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.2.2-win64-shared.zip \ + 5aedf268952b7d9f6541dbfcb47cd86a7e7881a3b7ba482fd3bc4ca33bda7bf5 \ + ffmpeg-4.2.2-win64-shared prepare-ffmpeg-dev-win64: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.2.1-win64-dev.zip \ - 5af393be5f25c0a71aa29efce768e477c35347f7f8e0d9696767d5b9d405b74e \ - ffmpeg-4.2.1-win64-dev + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.2.2-win64-dev.zip \ + f4885f859c5b0d6663c2a0a4c1cf035b1c60b146402790b796bd3ad84f4f3ca2 \ + ffmpeg-4.2.2-win64-dev prepare-sdl2: - @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.10-mingw.tar.gz \ - a90a7cddaec4996f4d7be6d80c57ec69b062e132bffc513965f99217f603274a \ - SDL2-2.0.10 + @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.12-mingw.tar.gz \ + e614a60f797e35ef9f3f96aef3dc6a1d786de3cc7ca6216f97e435c0b6aafc46 \ + SDL2-2.0.12 prepare-adb: @./prepare-dep https://dl.google.com/android/repository/platform-tools_r29.0.5-windows.zip \ diff --git a/server/build.gradle b/server/build.gradle index 539a97b8aa..3fa1651930 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -6,8 +6,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 29 - versionCode 14 - versionName "1.12.1" + versionCode 15 + versionName "1.13" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index c117d89c95..06fc0d75a2 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -12,7 +12,7 @@ set -e SCRCPY_DEBUG=false -SCRCPY_VERSION_NAME=1.12.1 +SCRCPY_VERSION_NAME=1.13 PLATFORM=${ANDROID_PLATFORM:-29} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2} diff --git a/server/meson.build b/server/meson.build index 4ba481d5fc..984daf3b2b 100644 --- a/server/meson.build +++ b/server/meson.build @@ -3,7 +3,9 @@ prebuilt_server = get_option('prebuilt_server') if prebuilt_server == '' custom_target('scrcpy-server', - build_always: true, # gradle is responsible for tracking source changes + # gradle is responsible for tracking source changes + build_by_default: true, + build_always_stale: true, output: 'scrcpy-server', command: [find_program('./scripts/build-wrapper.sh'), meson.current_source_dir(), '@OUTPUT@', get_option('buildtype')], console: true, diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index 726b565989..1c0810580e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -8,14 +8,13 @@ public class ControlMessageReader { - private static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 9; - private static final int INJECT_MOUSE_EVENT_PAYLOAD_LENGTH = 17; - private static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 21; - private static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; - private static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; + static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 9; + static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27; + static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; + static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; - public static final int TEXT_MAX_LENGTH = 300; public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093; + public static final int INJECT_TEXT_MAX_LENGTH = 300; private static final int RAW_BUFFER_SIZE = 1024; private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE]; @@ -122,7 +121,6 @@ private ControlMessage parseInjectText() { return ControlMessage.createInjectText(text); } - @SuppressWarnings("checkstyle:MagicNumber") private ControlMessage parseInjectTouchEvent() { if (buffer.remaining() < INJECT_TOUCH_EVENT_PAYLOAD_LENGTH) { return null; @@ -172,12 +170,10 @@ private static Position readPosition(ByteBuffer buffer) { return new Position(x, y, screenWidth, screenHeight); } - @SuppressWarnings("checkstyle:MagicNumber") private static int toUnsigned(short value) { return value & 0xffff; } - @SuppressWarnings("checkstyle:MagicNumber") private static int toUnsigned(byte value) { return value & 0xff; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index dc0fa67bc1..32a18e16af 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -47,7 +47,6 @@ private void initPointers() { } } - @SuppressWarnings("checkstyle:MagicNumber") public void control() throws IOException { // on start, power on the device if (!device.isScreenOn()) { @@ -76,19 +75,29 @@ private void handleEvent() throws IOException { ControlMessage msg = connection.receiveControlMessage(); switch (msg.getType()) { case ControlMessage.TYPE_INJECT_KEYCODE: - injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState()); + if (device.supportsInputEvents()) { + injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState()); + } break; case ControlMessage.TYPE_INJECT_TEXT: - injectText(msg.getText()); + if (device.supportsInputEvents()) { + injectText(msg.getText()); + } break; case ControlMessage.TYPE_INJECT_TOUCH_EVENT: - injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons()); + if (device.supportsInputEvents()) { + injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons()); + } break; case ControlMessage.TYPE_INJECT_SCROLL_EVENT: - injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll()); + if (device.supportsInputEvents()) { + injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll()); + } break; case ControlMessage.TYPE_BACK_OR_SCREEN_ON: - pressBackOrTurnScreenOn(); + if (device.supportsInputEvents()) { + pressBackOrTurnScreenOn(); + } break; case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: device.expandNotificationPanel(); @@ -104,7 +113,9 @@ private void handleEvent() throws IOException { device.setClipboardText(msg.getText()); break; case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: - device.setScreenPowerMode(msg.getAction()); + if (device.supportsInputEvents()) { + device.setScreenPowerMode(msg.getAction()); + } break; case ControlMessage.TYPE_ROTATE_DEVICE: device.rotateDevice(); diff --git a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java index a725d83d59..0ec430401f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java +++ b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java @@ -84,7 +84,6 @@ public void close() throws IOException { controlSocket.close(); } - @SuppressWarnings("checkstyle:MagicNumber") private void send(String deviceName, int width, int height) throws IOException { byte[] buffer = new byte[DEVICE_NAME_FIELD_LENGTH + 4]; diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 9448098a5b..0c43dd340e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy; +import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.SurfaceControl; import com.genymobile.scrcpy.wrappers.WindowManager; @@ -25,13 +26,36 @@ public interface RotationListener { private ScreenInfo screenInfo; private RotationListener rotationListener; + /** + * Logical display identifier + */ + private final int displayId; + + /** + * The surface flinger layer stack associated with this logical display + */ + private final int layerStack; + + private final boolean supportsInputEvents; + public Device(Options options) { - screenInfo = computeScreenInfo(options.getCrop(), options.getMaxSize()); + displayId = options.getDisplayId(); + DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(displayId); + if (displayInfo == null) { + int[] displayIds = serviceManager.getDisplayManager().getDisplayIds(); + throw new InvalidDisplayIdException(displayId, displayIds); + } + + int displayInfoFlags = displayInfo.getFlags(); + + screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize(), options.getLockedVideoOrientation()); + layerStack = displayInfo.getLayerStack(); + registerRotationWatcher(new IRotationWatcher.Stub() { @Override public void onRotationChanged(int rotation) throws RemoteException { synchronized (Device.this) { - screenInfo = screenInfo.withRotation(rotation); + screenInfo = screenInfo.withDeviceRotation(rotation); // notify if (rotationListener != null) { @@ -39,89 +63,69 @@ public void onRotationChanged(int rotation) throws RemoteException { } } } - }); - } + }, displayId); - public synchronized ScreenInfo getScreenInfo() { - return screenInfo; - } - - private ScreenInfo computeScreenInfo(Rect crop, int maxSize) { - DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(); - boolean rotated = (displayInfo.getRotation() & 1) != 0; - Size deviceSize = displayInfo.getSize(); - Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight()); - if (crop != null) { - if (rotated) { - // the crop (provided by the user) is expressed in the natural orientation - crop = flipRect(crop); - } - if (!contentRect.intersect(crop)) { - // intersect() changes contentRect so that it is intersected with crop - Ln.w("Crop rectangle (" + formatCrop(crop) + ") does not intersect device screen (" + formatCrop(deviceSize.toRect()) + ")"); - contentRect = new Rect(); // empty - } + if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) { + Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted"); } - Size videoSize = computeVideoSize(contentRect.width(), contentRect.height(), maxSize); - return new ScreenInfo(contentRect, videoSize, rotated); + // main display or any display on Android >= Q + supportsInputEvents = displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; + if (!supportsInputEvents) { + Ln.w("Input events are not supported for secondary displays before Android 10"); + } } - private static String formatCrop(Rect rect) { - return rect.width() + ":" + rect.height() + ":" + rect.left + ":" + rect.top; + public synchronized ScreenInfo getScreenInfo() { + return screenInfo; } - @SuppressWarnings("checkstyle:MagicNumber") - private static Size computeVideoSize(int w, int h, int maxSize) { - // Compute the video size and the padding of the content inside this video. - // Principle: - // - scale down the great side of the screen to maxSize (if necessary); - // - scale down the other side so that the aspect ratio is preserved; - // - round this value to the nearest multiple of 8 (H.264 only accepts multiples of 8) - w &= ~7; // in case it's not a multiple of 8 - h &= ~7; - if (maxSize > 0) { - if (BuildConfig.DEBUG && maxSize % 8 != 0) { - throw new AssertionError("Max size must be a multiple of 8"); - } - boolean portrait = h > w; - int major = portrait ? h : w; - int minor = portrait ? w : h; - if (major > maxSize) { - int minorExact = minor * maxSize / major; - // +4 to round the value to the nearest multiple of 8 - minor = (minorExact + 4) & ~7; - major = maxSize; - } - w = portrait ? minor : major; - h = portrait ? major : minor; - } - return new Size(w, h); + public int getLayerStack() { + return layerStack; } public Point getPhysicalPoint(Position position) { // it hides the field on purpose, to read it with a lock @SuppressWarnings("checkstyle:HiddenField") ScreenInfo screenInfo = getScreenInfo(); // read with synchronization - Size videoSize = screenInfo.getVideoSize(); - Size clientVideoSize = position.getScreenSize(); - if (!videoSize.equals(clientVideoSize)) { + + // ignore the locked video orientation, the events will apply in coordinates considered in the physical device orientation + Size unlockedVideoSize = screenInfo.getUnlockedVideoSize(); + + int reverseVideoRotation = screenInfo.getReverseVideoRotation(); + // reverse the video rotation to apply the events + Position devicePosition = position.rotate(reverseVideoRotation); + + Size clientVideoSize = devicePosition.getScreenSize(); + if (!unlockedVideoSize.equals(clientVideoSize)) { // The client sends a click relative to a video with wrong dimensions, // the device may have been rotated since the event was generated, so ignore the event return null; } Rect contentRect = screenInfo.getContentRect(); - Point point = position.getPoint(); - int scaledX = contentRect.left + point.getX() * contentRect.width() / videoSize.getWidth(); - int scaledY = contentRect.top + point.getY() * contentRect.height() / videoSize.getHeight(); - return new Point(scaledX, scaledY); + Point point = devicePosition.getPoint(); + int convertedX = contentRect.left + point.getX() * contentRect.width() / unlockedVideoSize.getWidth(); + int convertedY = contentRect.top + point.getY() * contentRect.height() / unlockedVideoSize.getHeight(); + return new Point(convertedX, convertedY); } public static String getDeviceName() { return Build.MODEL; } + public boolean supportsInputEvents() { + return supportsInputEvents; + } + public boolean injectInputEvent(InputEvent inputEvent, int mode) { + if (!supportsInputEvents()) { + throw new AssertionError("Could not inject input event if !supportsInputEvents()"); + } + + if (displayId != 0 && !InputManager.setDisplayId(inputEvent, displayId)) { + return false; + } + return serviceManager.getInputManager().injectInputEvent(inputEvent, mode); } @@ -129,8 +133,8 @@ public boolean isScreenOn() { return serviceManager.getPowerManager().isScreenOn(); } - public void registerRotationWatcher(IRotationWatcher rotationWatcher) { - serviceManager.getWindowManager().registerRotationWatcher(rotationWatcher); + public void registerRotationWatcher(IRotationWatcher rotationWatcher, int displayId) { + serviceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId); } public synchronized void setRotationListener(RotationListener rotationListener) { @@ -154,8 +158,10 @@ public String getClipboardText() { } public void setClipboardText(String text) { - serviceManager.getClipboardManager().setText(text); - Ln.i("Device clipboard set"); + boolean ok = serviceManager.getClipboardManager().setText(text); + if (ok) { + Ln.i("Device clipboard set"); + } } /** @@ -167,8 +173,10 @@ public void setScreenPowerMode(int mode) { Ln.e("Could not get built-in display"); return; } - SurfaceControl.setDisplayPowerMode(d, mode); - Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on")); + boolean ok = SurfaceControl.setDisplayPowerMode(d, mode); + if (ok) { + Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on")); + } } /** @@ -191,8 +199,4 @@ public void rotateDevice() { wm.thawRotation(); } } - - static Rect flipRect(Rect crop) { - return new Rect(crop.top, crop.left, crop.bottom, crop.right); - } } diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java index e2a3a1a217..6c7f36343e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java @@ -13,7 +13,6 @@ public class DeviceMessageWriter { private final byte[] rawBuffer = new byte[MAX_EVENT_SIZE]; private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); - @SuppressWarnings("checkstyle:MagicNumber") public void writeTo(DeviceMessage msg, OutputStream output) throws IOException { buffer.clear(); buffer.put((byte) DeviceMessage.TYPE_CLIPBOARD); diff --git a/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java b/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java index 639869b514..4b8036f85e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java @@ -1,12 +1,24 @@ package com.genymobile.scrcpy; public final class DisplayInfo { + private final int displayId; private final Size size; private final int rotation; + private final int layerStack; + private final int flags; - public DisplayInfo(Size size, int rotation) { + public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 0x00000001; + + public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags) { + this.displayId = displayId; this.size = size; this.rotation = rotation; + this.layerStack = layerStack; + this.flags = flags; + } + + public int getDisplayId() { + return displayId; } public Size getSize() { @@ -16,5 +28,13 @@ public Size getSize() { public int getRotation() { return rotation; } + + public int getLayerStack() { + return layerStack; + } + + public int getFlags() { + return flags; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/InvalidDisplayIdException.java b/server/src/main/java/com/genymobile/scrcpy/InvalidDisplayIdException.java new file mode 100644 index 0000000000..81e3b90377 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/InvalidDisplayIdException.java @@ -0,0 +1,21 @@ +package com.genymobile.scrcpy; + +public class InvalidDisplayIdException extends RuntimeException { + + private final int displayId; + private final int[] availableDisplayIds; + + public InvalidDisplayIdException(int displayId, int[] availableDisplayIds) { + super("There is no display having id " + displayId); + this.displayId = displayId; + this.availableDisplayIds = availableDisplayIds; + } + + public int getDisplayId() { + return displayId; + } + + public int[] getAvailableDisplayIds() { + return availableDisplayIds; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 5b993f3085..2a4bba3374 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -6,10 +6,12 @@ public class Options { private int maxSize; private int bitRate; private int maxFps; + private int lockedVideoOrientation; private boolean tunnelForward; private Rect crop; private boolean sendFrameMeta; // send PTS so that the client may record properly private boolean control; + private int displayId; public int getMaxSize() { return maxSize; @@ -35,6 +37,14 @@ public void setMaxFps(int maxFps) { this.maxFps = maxFps; } + public int getLockedVideoOrientation() { + return lockedVideoOrientation; + } + + public void setLockedVideoOrientation(int lockedVideoOrientation) { + this.lockedVideoOrientation = lockedVideoOrientation; + } + public boolean isTunnelForward() { return tunnelForward; } @@ -66,4 +76,12 @@ public boolean getControl() { public void setControl(boolean control) { this.control = control; } + + public int getDisplayId() { + return displayId; + } + + public void setDisplayId(int displayId) { + this.displayId = displayId; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Position.java b/server/src/main/java/com/genymobile/scrcpy/Position.java index b46d2f7364..e9b6d8a276 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Position.java +++ b/server/src/main/java/com/genymobile/scrcpy/Position.java @@ -23,6 +23,19 @@ public Size getScreenSize() { return screenSize; } + public Position rotate(int rotation) { + switch (rotation) { + case 1: + return new Position(new Point(screenSize.getHeight() - point.getY(), point.getX()), screenSize.rotate()); + case 2: + return new Position(new Point(screenSize.getWidth() - point.getX(), screenSize.getHeight() - point.getY()), screenSize); + case 3: + return new Position(new Point(point.getY(), screenSize.getWidth() - point.getX()), screenSize.rotate()); + default: + return this; + } + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index c9a37f847b..fc1a25b125 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -6,7 +6,6 @@ import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaFormat; -import android.os.Build; import android.os.IBinder; import android.view.Surface; @@ -19,6 +18,7 @@ public class ScreenEncoder implements Device.RotationListener { private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms + private static final String KEY_MAX_FPS_TO_ENCODER = "max-fps-to-encoder"; private static final int NO_PTS = -1; @@ -27,19 +27,13 @@ public class ScreenEncoder implements Device.RotationListener { private int bitRate; private int maxFps; - private int iFrameInterval; private boolean sendFrameMeta; private long ptsOrigin; - public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int iFrameInterval) { + public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps) { this.sendFrameMeta = sendFrameMeta; this.bitRate = bitRate; this.maxFps = maxFps; - this.iFrameInterval = iFrameInterval; - } - - public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps) { - this(sendFrameMeta, bitRate, maxFps, DEFAULT_I_FRAME_INTERVAL); } @Override @@ -55,19 +49,26 @@ public void streamScreen(Device device, FileDescriptor fd) throws IOException { Workarounds.prepareMainLooper(); Workarounds.fillAppInfo(); - MediaFormat format = createFormat(bitRate, maxFps, iFrameInterval); + MediaFormat format = createFormat(bitRate, maxFps, DEFAULT_I_FRAME_INTERVAL); device.setRotationListener(this); boolean alive; try { do { MediaCodec codec = createCodec(); IBinder display = createDisplay(); - Rect contentRect = device.getScreenInfo().getContentRect(); - Rect videoRect = device.getScreenInfo().getVideoSize().toRect(); + ScreenInfo screenInfo = device.getScreenInfo(); + Rect contentRect = screenInfo.getContentRect(); + // include the locked video orientation + Rect videoRect = screenInfo.getVideoSize().toRect(); + // does not include the locked video orientation + Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect(); + int videoRotation = screenInfo.getVideoRotation(); + int layerStack = device.getLayerStack(); + setSize(format, videoRect.width(), videoRect.height()); configure(codec, format); Surface surface = codec.createInputSurface(); - setDisplaySurface(display, surface, contentRect, videoRect); + setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack); codec.start(); try { alive = encode(codec, fd); @@ -135,13 +136,12 @@ private void writeFrameMeta(FileDescriptor fd, MediaCodec.BufferInfo bufferInfo, } private static MediaCodec createCodec() throws IOException { - return MediaCodec.createEncoderByType("video/avc"); + return MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC); } - @SuppressWarnings("checkstyle:MagicNumber") private static MediaFormat createFormat(int bitRate, int maxFps, int iFrameInterval) { MediaFormat format = new MediaFormat(); - format.setString(MediaFormat.KEY_MIME, "video/avc"); + format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC); format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); // must be present to configure the encoder, but does not impact the actual frame rate, which is variable format.setInteger(MediaFormat.KEY_FRAME_RATE, 60); @@ -150,11 +150,10 @@ private static MediaFormat createFormat(int bitRate, int maxFps, int iFrameInter // display the very first frame, and recover from bad quality when no new frames format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, REPEAT_FRAME_DELAY_US); // µs if (maxFps > 0) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - format.setFloat(MediaFormat.KEY_MAX_FPS_TO_ENCODER, maxFps); - } else { - Ln.w("Max FPS is only supported since Android 10, the option has been ignored"); - } + // The key existed privately before Android 10: + // + // + format.setFloat(KEY_MAX_FPS_TO_ENCODER, maxFps); } return format; } @@ -172,12 +171,12 @@ private static void setSize(MediaFormat format, int width, int height) { format.setInteger(MediaFormat.KEY_HEIGHT, height); } - private static void setDisplaySurface(IBinder display, Surface surface, Rect deviceRect, Rect displayRect) { + private static void setDisplaySurface(IBinder display, Surface surface, int orientation, Rect deviceRect, Rect displayRect, int layerStack) { SurfaceControl.openTransaction(); try { SurfaceControl.setDisplaySurface(display, surface); - SurfaceControl.setDisplayProjection(display, 0, deviceRect, displayRect); - SurfaceControl.setDisplayLayerStack(display, 0); + SurfaceControl.setDisplayProjection(display, orientation, deviceRect, displayRect); + SurfaceControl.setDisplayLayerStack(display, layerStack); } finally { SurfaceControl.closeTransaction(); } diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java b/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java index f2fce1d66b..10acfb5061 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java @@ -3,29 +3,161 @@ import android.graphics.Rect; public final class ScreenInfo { + /** + * Device (physical) size, possibly cropped + */ private final Rect contentRect; // device size, possibly cropped - private final Size videoSize; - private final boolean rotated; - public ScreenInfo(Rect contentRect, Size videoSize, boolean rotated) { + /** + * Video size, possibly smaller than the device size, already taking the device rotation and crop into account. + *

+ * However, it does not include the locked video orientation. + */ + private final Size unlockedVideoSize; + + /** + * Device rotation, related to the natural device orientation (0, 1, 2 or 3) + */ + private final int deviceRotation; + + /** + * The locked video orientation (-1: disabled, 0: normal, 1: 90° CCW, 2: 180°, 3: 90° CW) + */ + private final int lockedVideoOrientation; + + public ScreenInfo(Rect contentRect, Size unlockedVideoSize, int deviceRotation, int lockedVideoOrientation) { this.contentRect = contentRect; - this.videoSize = videoSize; - this.rotated = rotated; + this.unlockedVideoSize = unlockedVideoSize; + this.deviceRotation = deviceRotation; + this.lockedVideoOrientation = lockedVideoOrientation; } public Rect getContentRect() { return contentRect; } + /** + * Return the video size as if locked video orientation was not set. + * + * @return the unlocked video size + */ + public Size getUnlockedVideoSize() { + return unlockedVideoSize; + } + + /** + * Return the actual video size if locked video orientation is set. + * + * @return the actual video size + */ public Size getVideoSize() { - return videoSize; + if (getVideoRotation() % 2 == 0) { + return unlockedVideoSize; + } + + return unlockedVideoSize.rotate(); } - public ScreenInfo withRotation(int rotation) { - boolean newRotated = (rotation & 1) != 0; - if (rotated == newRotated) { + public int getDeviceRotation() { + return deviceRotation; + } + + public ScreenInfo withDeviceRotation(int newDeviceRotation) { + if (newDeviceRotation == deviceRotation) { return this; } - return new ScreenInfo(Device.flipRect(contentRect), videoSize.rotate(), newRotated); + // true if changed between portrait and landscape + boolean orientationChanged = (deviceRotation + newDeviceRotation) % 2 != 0; + Rect newContentRect; + Size newUnlockedVideoSize; + if (orientationChanged) { + newContentRect = flipRect(contentRect); + newUnlockedVideoSize = unlockedVideoSize.rotate(); + } else { + newContentRect = contentRect; + newUnlockedVideoSize = unlockedVideoSize; + } + return new ScreenInfo(newContentRect, newUnlockedVideoSize, newDeviceRotation, lockedVideoOrientation); + } + + public static ScreenInfo computeScreenInfo(DisplayInfo displayInfo, Rect crop, int maxSize, int lockedVideoOrientation) { + int rotation = displayInfo.getRotation(); + Size deviceSize = displayInfo.getSize(); + Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight()); + if (crop != null) { + if (rotation % 2 != 0) { // 180s preserve dimensions + // the crop (provided by the user) is expressed in the natural orientation + crop = flipRect(crop); + } + if (!contentRect.intersect(crop)) { + // intersect() changes contentRect so that it is intersected with crop + Ln.w("Crop rectangle (" + formatCrop(crop) + ") does not intersect device screen (" + formatCrop(deviceSize.toRect()) + ")"); + contentRect = new Rect(); // empty + } + } + + Size videoSize = computeVideoSize(contentRect.width(), contentRect.height(), maxSize); + return new ScreenInfo(contentRect, videoSize, rotation, lockedVideoOrientation); + } + + private static String formatCrop(Rect rect) { + return rect.width() + ":" + rect.height() + ":" + rect.left + ":" + rect.top; + } + + private static Size computeVideoSize(int w, int h, int maxSize) { + // Compute the video size and the padding of the content inside this video. + // Principle: + // - scale down the great side of the screen to maxSize (if necessary); + // - scale down the other side so that the aspect ratio is preserved; + // - round this value to the nearest multiple of 8 (H.264 only accepts multiples of 8) + w &= ~7; // in case it's not a multiple of 8 + h &= ~7; + if (maxSize > 0) { + if (BuildConfig.DEBUG && maxSize % 8 != 0) { + throw new AssertionError("Max size must be a multiple of 8"); + } + boolean portrait = h > w; + int major = portrait ? h : w; + int minor = portrait ? w : h; + if (major > maxSize) { + int minorExact = minor * maxSize / major; + // +4 to round the value to the nearest multiple of 8 + minor = (minorExact + 4) & ~7; + major = maxSize; + } + w = portrait ? minor : major; + h = portrait ? major : minor; + } + return new Size(w, h); + } + + private static Rect flipRect(Rect crop) { + return new Rect(crop.top, crop.left, crop.bottom, crop.right); + } + + /** + * Return the rotation to apply to the device rotation to get the requested locked video orientation + * + * @return the rotation offset + */ + public int getVideoRotation() { + if (lockedVideoOrientation == -1) { + // no offset + return 0; + } + return (deviceRotation + 4 - lockedVideoOrientation) % 4; + } + + /** + * Return the rotation to apply to the requested locked video orientation to get the device rotation + * + * @return the (reverse) rotation offset + */ + public int getReverseVideoRotation() { + if (lockedVideoOrientation == -1) { + // no offset + return 0; + } + return (lockedVideoOrientation + 4 - deviceRotation) % 4; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 56b738fbc7..a6f7a78c3a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -16,6 +16,7 @@ private Server() { } private static void scrcpy(Options options) throws IOException { + Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")"); final Device device = new Device(options); boolean tunnelForward = options.isTunnelForward(); try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) { @@ -67,7 +68,6 @@ public void run() { }).start(); } - @SuppressWarnings("checkstyle:MagicNumber") private static Options createOptions(String... args) { if (args.length < 1) { throw new IllegalArgumentException("Missing client version"); @@ -76,11 +76,11 @@ private static Options createOptions(String... args) { String clientVersion = args[0]; if (!clientVersion.equals(BuildConfig.VERSION_NAME)) { throw new IllegalArgumentException( - "The server version (" + clientVersion + ") does not match the client " + "(" + BuildConfig.VERSION_NAME + ")"); + "The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")"); } - if (args.length != 8) { - throw new IllegalArgumentException("Expecting 8 parameters"); + if (args.length != 10) { + throw new IllegalArgumentException("Expecting 10 parameters"); } Options options = new Options(); @@ -94,23 +94,28 @@ private static Options createOptions(String... args) { int maxFps = Integer.parseInt(args[3]); options.setMaxFps(maxFps); + int lockedVideoOrientation = Integer.parseInt(args[4]); + options.setLockedVideoOrientation(lockedVideoOrientation); + // use "adb forward" instead of "adb tunnel"? (so the server must listen) - boolean tunnelForward = Boolean.parseBoolean(args[4]); + boolean tunnelForward = Boolean.parseBoolean(args[5]); options.setTunnelForward(tunnelForward); - Rect crop = parseCrop(args[5]); + Rect crop = parseCrop(args[6]); options.setCrop(crop); - boolean sendFrameMeta = Boolean.parseBoolean(args[6]); + boolean sendFrameMeta = Boolean.parseBoolean(args[7]); options.setSendFrameMeta(sendFrameMeta); - boolean control = Boolean.parseBoolean(args[7]); + boolean control = Boolean.parseBoolean(args[8]); options.setControl(control); + int displayId = Integer.parseInt(args[9]); + options.setDisplayId(displayId); + return options; } - @SuppressWarnings("checkstyle:MagicNumber") private static Rect parseCrop(String crop) { if ("-".equals(crop)) { return null; @@ -135,7 +140,6 @@ private static void unlinkSelf() { } } - @SuppressWarnings("checkstyle:MagicNumber") private static void suggestFix(Throwable e) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (e instanceof MediaCodec.CodecException) { @@ -147,6 +151,16 @@ private static void suggestFix(Throwable e) { } } } + if (e instanceof InvalidDisplayIdException) { + InvalidDisplayIdException idie = (InvalidDisplayIdException) e; + int[] displayIds = idie.getAvailableDisplayIds(); + if (displayIds != null && displayIds.length > 0) { + Ln.e("Try to use one of the available display ids:"); + for (int id : displayIds) { + Ln.e(" scrcpy --display " + id); + } + } + } } public static void main(String... args) throws Exception { diff --git a/server/src/main/java/com/genymobile/scrcpy/StringUtils.java b/server/src/main/java/com/genymobile/scrcpy/StringUtils.java index 199fc8c1cd..dac05466c3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/StringUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/StringUtils.java @@ -5,7 +5,6 @@ private StringUtils() { // not instantiable } - @SuppressWarnings("checkstyle:MagicNumber") public static int getUtf8TruncationIndex(byte[] utf8, int maxLength) { int len = utf8.length; if (len <= maxLength) { diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index b1b8190326..351cc574ce 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -28,7 +28,7 @@ public static void prepareMainLooper() { Looper.prepareMainLooper(); } - @SuppressLint("PrivateApi") + @SuppressLint("PrivateApi,DiscouragedPrivateApi") public static void fillAppInfo() { try { // ActivityThread activityThread = new ActivityThread(); @@ -73,7 +73,7 @@ public static void fillAppInfo() { mInitialApplicationField.set(activityThread, app); } catch (Throwable throwable) { // this is a workaround, so failing is not an error - Ln.w("Could not fill app info: " + throwable.getMessage()); + Ln.d("Could not fill app info: " + throwable.getMessage()); } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java index 592bdf6be0..ade2303285 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -74,13 +74,15 @@ public CharSequence getText() { } } - public void setText(CharSequence text) { + public boolean setText(CharSequence text) { try { Method method = getSetPrimaryClipMethod(); ClipData clipData = ClipData.newPlainText(null, text); setPrimaryClip(method, manager, clipData); + return true; } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { Ln.e("Could not invoke method", e); + return false; } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java index 568afacd6f..cedb3f47ed 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -12,15 +12,28 @@ public DisplayManager(IInterface manager) { this.manager = manager; } - public DisplayInfo getDisplayInfo() { + public DisplayInfo getDisplayInfo(int displayId) { try { - Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, 0); + Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, displayId); + if (displayInfo == null) { + return null; + } Class cls = displayInfo.getClass(); // width and height already take the rotation into account int width = cls.getDeclaredField("logicalWidth").getInt(displayInfo); int height = cls.getDeclaredField("logicalHeight").getInt(displayInfo); int rotation = cls.getDeclaredField("rotation").getInt(displayInfo); - return new DisplayInfo(new Size(width, height), rotation); + int layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo); + int flags = cls.getDeclaredField("flags").getInt(displayInfo); + return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags); + } catch (Exception e) { + throw new AssertionError(e); + } + } + + public int[] getDisplayIds() { + try { + return (int[]) manager.getClass().getMethod("getDisplayIds").invoke(manager); } catch (Exception e) { throw new AssertionError(e); } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java index 44fa613b2a..e17b5a178d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java @@ -17,6 +17,8 @@ public final class InputManager { private final IInterface manager; private Method injectInputEventMethod; + private static Method setDisplayIdMethod; + public InputManager(IInterface manager) { this.manager = manager; } @@ -37,4 +39,22 @@ public boolean injectInputEvent(InputEvent inputEvent, int mode) { return false; } } + + private static Method getSetDisplayIdMethod() throws NoSuchMethodException { + if (setDisplayIdMethod == null) { + setDisplayIdMethod = InputEvent.class.getMethod("setDisplayId", int.class); + } + return setDisplayIdMethod; + } + + public static boolean setDisplayId(InputEvent inputEvent, int displayId) { + try { + Method method = getSetDisplayIdMethod(); + method.invoke(inputEvent, displayId); + return true; + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Cannot associate a display id to the input event", e); + return false; + } + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java index 0b625c9270..cc56783792 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java @@ -6,7 +6,7 @@ import java.lang.reflect.Method; -@SuppressLint("PrivateApi") +@SuppressLint("PrivateApi,DiscouragedPrivateApi") public final class ServiceManager { private final Method getServiceMethod; diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java index 227bbc854c..8fbb860b97 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -121,12 +121,14 @@ private static Method getSetDisplayPowerModeMethod() throws NoSuchMethodExceptio return setDisplayPowerModeMethod; } - public static void setDisplayPowerMode(IBinder displayToken, int mode) { + public static boolean setDisplayPowerMode(IBinder displayToken, int mode) { try { Method method = getSetDisplayPowerModeMethod(); method.invoke(null, displayToken, mode); + return true; } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { Ln.e("Could not invoke method", e); + return false; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java index cc687cd5e3..faa366a5d1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -93,13 +93,13 @@ public void thawRotation() { } } - public void registerRotationWatcher(IRotationWatcher rotationWatcher) { + public void registerRotationWatcher(IRotationWatcher rotationWatcher, int displayId) { try { Class cls = manager.getClass(); try { // display parameter added since this commit: // https://android.googlesource.com/platform/frameworks/base/+/35fa3c26adcb5f6577849fd0df5228b1f67cf2c6%5E%21/#F1 - cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, 0); + cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, displayId); } catch (NoSuchMethodException e) { // old version cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher); diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index 5e663bb9fb..202126a5d2 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -28,6 +28,9 @@ public void testParseKeycodeEvent() throws IOException { dos.writeInt(KeyEvent.META_CTRL_ON); byte[] packet = bos.toByteArray(); + // The message type (1 byte) does not count + Assert.assertEquals(ControlMessageReader.INJECT_KEYCODE_PAYLOAD_LENGTH, packet.length - 1); + reader.readFrom(new ByteArrayInputStream(packet)); ControlMessage event = reader.next(); @@ -63,7 +66,7 @@ public void testParseLongTextEvent() throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_INJECT_TEXT); - byte[] text = new byte[ControlMessageReader.TEXT_MAX_LENGTH]; + byte[] text = new byte[ControlMessageReader.INJECT_TEXT_MAX_LENGTH]; Arrays.fill(text, (byte) 'a'); dos.writeShort(text.length); dos.write(text); @@ -77,7 +80,6 @@ public void testParseLongTextEvent() throws IOException { } @Test - @SuppressWarnings("checkstyle:MagicNumber") public void testParseTouchEvent() throws IOException { ControlMessageReader reader = new ControlMessageReader(); @@ -95,6 +97,9 @@ public void testParseTouchEvent() throws IOException { byte[] packet = bos.toByteArray(); + // The message type (1 byte) does not count + Assert.assertEquals(ControlMessageReader.INJECT_TOUCH_EVENT_PAYLOAD_LENGTH, packet.length - 1); + reader.readFrom(new ByteArrayInputStream(packet)); ControlMessage event = reader.next(); @@ -110,7 +115,6 @@ public void testParseTouchEvent() throws IOException { } @Test - @SuppressWarnings("checkstyle:MagicNumber") public void testParseScrollEvent() throws IOException { ControlMessageReader reader = new ControlMessageReader(); @@ -126,6 +130,9 @@ public void testParseScrollEvent() throws IOException { byte[] packet = bos.toByteArray(); + // The message type (1 byte) does not count + Assert.assertEquals(ControlMessageReader.INJECT_SCROLL_EVENT_PAYLOAD_LENGTH, packet.length - 1); + reader.readFrom(new ByteArrayInputStream(packet)); ControlMessage event = reader.next(); @@ -233,6 +240,9 @@ public void testParseSetScreenPowerMode() throws IOException { byte[] packet = bos.toByteArray(); + // The message type (1 byte) does not count + Assert.assertEquals(ControlMessageReader.SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH, packet.length - 1); + reader.readFrom(new ByteArrayInputStream(packet)); ControlMessage event = reader.next(); diff --git a/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java b/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java index 7d89ee64e8..89799c5ecd 100644 --- a/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java @@ -8,7 +8,6 @@ public class StringUtilsTest { @Test - @SuppressWarnings("checkstyle:MagicNumber") public void testUtf8Truncate() { String s = "aÉbÔc"; byte[] utf8 = s.getBytes(StandardCharsets.UTF_8); From 4078f7b9e6bb2e8ba4b91d27d171c3af8302e310 Mon Sep 17 00:00:00 2001 From: Killian Richard Date: Fri, 22 May 2020 15:22:51 +0200 Subject: [PATCH 4/9] Fix missing char and error --- app/meson.build | 1 + app/src/cli.c | 14 +++++++------- app/src/serve.c | 8 +++++--- app/src/serve.h | 2 +- app/src/stream.c | 2 +- 5 files changed, 15 insertions(+), 12 deletions(-) diff --git a/app/meson.build b/app/meson.build index 5d2b4caac3..0e6ef15e26 100644 --- a/app/meson.build +++ b/app/meson.build @@ -17,6 +17,7 @@ src = [ 'src/scrcpy.c', 'src/screen.c', 'src/server.c', + 'src/serve.c', 'src/stream.c', 'src/tiny_xpm.c', 'src/video_buffer.c', diff --git a/app/src/cli.c b/app/src/cli.c index 69b89899b8..a405f093ad 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -461,7 +461,7 @@ str_split(const char *a_str, const char a_delim) { char** result = 0; size_t count = 0; char* tmp = (char*)a_str; - char str[100]; + char str[50]; strncpy(str, a_str, sizeof(str)); char* last_comma = 0; char delim[2]; @@ -517,7 +517,7 @@ check_if_ip_valid(char *ip) { return false; while (ptr) { - long value: + long value; if (!parse_integer(ptr, &value)) //Check whether the substring is holding only number or not return false; num = atoi(ptr); //Convert substring to number @@ -537,7 +537,7 @@ check_if_ip_valid(char *ip) { } static bool -parse_serve_args(const char *optarg, char **s_protocol, uint32_t *s_ip, uint16_t *s_port) { +parse_serve_args(const char *optarg, const char **s_protocol, uint32_t *s_ip, uint16_t *s_port) { bool protocol_valid = false; bool ip_valid = false; bool port_valid = false; @@ -562,7 +562,7 @@ parse_serve_args(const char *optarg, char **s_protocol, uint32_t *s_ip, uint16_t if (!strcmp(protocol, "tcp")) { protocol_valid = true; } else { - LOGE("Unexpected protocol: $s (expected tcp)", protocol); + LOGE("Unexpected protocol (expected tcp)"); return false; } @@ -579,11 +579,11 @@ parse_serve_args(const char *optarg, char **s_protocol, uint32_t *s_ip, uint16_t //Check if the choosen port is valid long port_value = 0; - port_valid = parse_interger_arg(port, &port_value, false, 0, 0xFFFF, "port"); + port_valid = parse_integer_arg(port, &port_value, false, 0, 0xFFFF, "port"); //Check if everything is valid if (!protocol_valid || !ip_valid || !port_valid) { - LOGE("Unexpected argument format: $s (expected [tcp]:[ip or \"localhost\"]:[port])", optarg); + LOGE("Unexpected argument format (expected [tcp]:[ip or \"localhost\"]:[port])"); return false; } @@ -639,7 +639,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { OPT_RENDER_EXPIRED_FRAMES}, {"rotation", required_argument, NULL, OPT_ROTATION}, {"serial", required_argument, NULL, 's'}, - {"serve", required_argument, NULL, OPT_SERVE} + {"serve", required_argument, NULL, OPT_SERVE}, {"show-touches", no_argument, NULL, 't'}, {"turn-screen-off", no_argument, NULL, 'S'}, {"prefer-text", no_argument, NULL, OPT_PREFER_TEXT}, diff --git a/app/src/serve.c b/app/src/serve.c index 5de0c40f03..b969dc8d81 100644 --- a/app/src/serve.c +++ b/app/src/serve.c @@ -9,7 +9,7 @@ #include "util/log.h" #include "util/net.h" -# define SOCKET_ERROR -1 +# define SOCKET_ERROR (-1) void serve_init(struct serve* serve, char *protocol, uint32_t ip, uint16_t port) { @@ -32,6 +32,8 @@ serve_start(struct serve* serve) { return 0; } + LOGI("Waiting for a client to connect"); + ClientSocket = net_accept(Listensocket); if (ClientSocket == INVALID_SOCKET) { LOGI("Client error"); @@ -49,8 +51,8 @@ serve_start(struct serve* serve) { } bool -serve_push(struct serve* serve, const AVPacket packet) { - if (net_send(serve->socket, packet.data, packet.size) == SOCKET_ERROR) { +serve_push(struct serve* serve, const AVPacket *packet) { + if (net_send(serve->socket, packet->data, packet->size) == SOCKET_ERROR) { LOGI("Client lost"); net_close(serve->socket); return false; diff --git a/app/src/serve.h b/app/src/serve.h index 44ee7583e5..885fcef347 100644 --- a/app/src/serve.h +++ b/app/src/serve.h @@ -24,5 +24,5 @@ bool serve_start(struct serve* serve); bool -serve_push(struct serve* serve, const AVPacket packet); +serve_push(struct serve* serve, const AVPacket *packet); #endif \ No newline at end of file diff --git a/app/src/stream.c b/app/src/stream.c index a7cee43698..557cff068e 100644 --- a/app/src/stream.c +++ b/app/src/stream.c @@ -97,7 +97,7 @@ process_frame(struct stream *stream, AVPacket *packet) { if (stream->serve) { packet->dts = packet->pts; - if (!serve_push(stream->serve, packet) { + if (!serve_push(stream->serve, packet)) { LOGE("Could not serve packet"); return false; } From b666c207b961d2ab47c1e1896cddcfe04fee4d49 Mon Sep 17 00:00:00 2001 From: Killian Richard Date: Fri, 22 May 2020 15:56:03 +0200 Subject: [PATCH 5/9] Allowing other ip address by converting it to hex --- app/src/cli.c | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/app/src/cli.c b/app/src/cli.c index a405f093ad..c65fb884a3 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -536,6 +536,30 @@ check_if_ip_valid(char *ip) { return true; } +uint32_t +convert_ip_to_int(char* ip_string) { + int num, dots = 0; + char* ptr; + + char* ip = "0x"; + + ptr = strtok(ip_string, "."); //Cut the string using dot as delimiter + + while (ptr) { + num = atoi(ptr); //Convert substring to number + if (num >= 0 && num <= 255) { + char hex[3]; + sprintf(hex, "%X", num); + strcat(ip, hex); + ptr = strtok(NULL, "."); //Cut the next part of the string + if (ptr != NULL) + dots++; //Increase the dot count + } + } + + return atoi(ip); +} + static bool parse_serve_args(const char *optarg, const char **s_protocol, uint32_t *s_ip, uint16_t *s_port) { bool protocol_valid = false; @@ -571,6 +595,7 @@ parse_serve_args(const char *optarg, const char **s_protocol, uint32_t *s_ip, ui ip_value = 0x7F000001; ip_valid = true; } else if (check_if_ip_valid(ip)) { + ip_value = convert_ip_to_int(ip); ip_valid = true; } else { LOGE("Unexpected ip address (expected \"localhost\" or 255.255.255.255 format)"); From 556e3bb8c3e1f82ce27b305a181acdb0efab16d5 Mon Sep 17 00:00:00 2001 From: Killian Richard Date: Tue, 16 Jun 2020 16:33:26 +0200 Subject: [PATCH 6/9] Change to run server only when serve is ready to forward the stream --- app/src/cli.c | 4 ++-- app/src/scrcpy.c | 32 ++++++++++++++++++++------------ app/src/scrcpy.h | 2 +- app/src/serve.c | 32 ++++++++++++++++++++++---------- app/src/serve.h | 4 ++++ app/src/server.c | 24 ++++++++++++++++++------ app/src/server.h | 3 ++- app/src/stream.c | 11 +++++++---- 8 files changed, 76 insertions(+), 36 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index c65fb884a3..f3b4d42724 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -461,7 +461,7 @@ str_split(const char *a_str, const char a_delim) { char** result = 0; size_t count = 0; char* tmp = (char*)a_str; - char str[50]; + char str[100]; strncpy(str, a_str, sizeof(str)); char* last_comma = 0; char delim[2]; @@ -561,7 +561,7 @@ convert_ip_to_int(char* ip_string) { } static bool -parse_serve_args(const char *optarg, const char **s_protocol, uint32_t *s_ip, uint16_t *s_port) { +parse_serve_args(const char *optarg, char **s_protocol, uint32_t *s_ip, uint16_t *s_port) { bool protocol_valid = false; bool ip_valid = false; bool port_valid = false; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 54d3133b46..f46366c8ec 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -295,7 +295,21 @@ scrcpy(const struct scrcpy_options *options) { .control = options->control, .display_id = options->display_id, }; - if (!server_start(&server, options->serial, ¶ms)) { + + struct serve* serv = NULL; + if (options->serve) { + serve_init(&serve, options->serve_protocol, options->serve_ip, options->serve_port); + + serv = &serve; + } + + if (options->serve) { + if (!serve_start(&serve)) { + goto end; + } + } + + if (!server_start(&server, options->serial, ¶ms, serv)) { return false; } @@ -316,6 +330,7 @@ scrcpy(const struct scrcpy_options *options) { bool stream_started = false; bool controller_initialized = false; bool controller_started = false; + bool serve_started = false; if (!sdl_init_and_configure(options->display, options->render_driver)) { goto end; @@ -372,16 +387,6 @@ scrcpy(const struct scrcpy_options *options) { recorder_initialized = true; } - struct serve* serv = NULL; - if (options->serve) { - serve_init(&serve, options->serve_protocol, options->serve_ip, options->serve_port); - - if (!serve_start(&serve)) { - goto end; - } - serv = &serve; - } - av_log_set_callback(av_log_callback); stream_init(&stream, server.video_socket, dec, rec, serv); @@ -414,7 +419,7 @@ scrcpy(const struct scrcpy_options *options) { options->window_y, options->window_width, options->window_height, options->window_borderless, - options->rotation, options-> mipmaps)) { + options->rotation, options->mipmaps)) { goto end; } @@ -460,6 +465,9 @@ scrcpy(const struct scrcpy_options *options) { if (fps_counter_initialized) { fps_counter_interrupt(&fps_counter); } + if (serve_started) { + serve_stop(&serve); + } // shutdown the sockets and kill the server server_stop(&server); diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 61c372e505..8a44a3e15a 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -16,7 +16,7 @@ struct scrcpy_options { const char *window_title; const char *push_target; const char *render_driver; - const char *serve_protocol; + char *serve_protocol; enum recorder_format record_format; struct port_range port_range; uint16_t max_size; diff --git a/app/src/serve.c b/app/src/serve.c index b969dc8d81..2acabae76c 100644 --- a/app/src/serve.c +++ b/app/src/serve.c @@ -6,6 +6,7 @@ #include "config.h" #include "events.h" +#include #include "util/log.h" #include "util/net.h" @@ -16,6 +17,7 @@ serve_init(struct serve* serve, char *protocol, uint32_t ip, uint16_t port) { serve->protocol = protocol; serve->ip = ip; serve->port = port; + serve->isServeReady = false; } bool @@ -27,35 +29,45 @@ serve_start(struct serve* serve) { Listensocket = net_listen(serve->ip, serve->port, 1); if (Listensocket == INVALID_SOCKET) { - LOGI("Listen error"); + LOGI("Listen socket error"); net_close(Listensocket); return 0; } - LOGI("Waiting for a client to connect"); - ClientSocket = net_accept(Listensocket); if (ClientSocket == INVALID_SOCKET) { - LOGI("Client error"); + LOGI("Client socket error"); net_close(Listensocket); return 0; } LOGI("Client found"); - net_close(Listensocket); serve->socket = ClientSocket; + serve->isServeReady = true; + LOGI("Serve is ready to forward the stream"); + return true; } bool serve_push(struct serve* serve, const AVPacket *packet) { - if (net_send(serve->socket, packet->data, packet->size) == SOCKET_ERROR) { - LOGI("Client lost"); - net_close(serve->socket); - return false; + if (serve->isServeReady) + { + if (net_send(serve->socket, packet->data, packet->size) == SOCKET_ERROR) { + LOGI("Client lost"); + serve->isServeReady = false; + net_close(serve->socket); + return false; + } + return true; } - return true; + return false; +} + +void +serve_stop(struct serve* serve) { + net_close(serve->socket); } \ No newline at end of file diff --git a/app/src/serve.h b/app/src/serve.h index 885fcef347..ef3b57e3be 100644 --- a/app/src/serve.h +++ b/app/src/serve.h @@ -15,6 +15,7 @@ struct serve { char *protocol; uint32_t ip; uint16_t port; + bool isServeReady; }; void @@ -25,4 +26,7 @@ serve_start(struct serve* serve); bool serve_push(struct serve* serve, const AVPacket *packet); + +void +serve_stop(struct serve* serve); #endif \ No newline at end of file diff --git a/app/src/server.c b/app/src/server.c index b102f0c27d..1672a23f47 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -11,6 +11,7 @@ #include "config.h" #include "command.h" +#include "serve.h" #include "util/log.h" #include "util/net.h" #include "util/str_util.h" @@ -349,7 +350,7 @@ run_wait_server(void *data) { bool server_start(struct server *server, const char *serial, - const struct server_params *params) { + const struct server_params *params, struct serve* serve) { server->port_range = params->port_range; if (serial) { @@ -367,12 +368,23 @@ server_start(struct server *server, const char *serial, goto error1; } - // server will connect to our server socket - server->process = execute_server(server, params); - if (server->process == PROCESS_NONE) { - goto error2; + /*server->serve = serve; + if (server->serve) { + if (server->serve->isServeReady == true) { + server->process = execute_server(server, params); + if (server->process == PROCESS_NONE) { + goto error2; + } + } } - + else {*/ + // server will connect to our server socket + server->process = execute_server(server, params); + if (server->process == PROCESS_NONE) { + goto error2; + } + //} + // If the server process dies before connecting to the server socket, then // the client will be stuck forever on accept(). To avoid the problem, we // must be able to wake up the accept() call when the server dies. To keep diff --git a/app/src/server.h b/app/src/server.h index a2ecdefcda..c9b21340e8 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -23,6 +23,7 @@ struct server { uint16_t local_port; // selected from port_range bool tunnel_enabled; bool tunnel_forward; // use "adb forward" instead of "adb reverse" + struct serve* serve; }; #define SERVER_INITIALIZER { \ @@ -60,7 +61,7 @@ server_init(struct server *server); // push, enable tunnel et start the server bool server_start(struct server *server, const char *serial, - const struct server_params *params); + const struct server_params *params, struct serve* serve); // block until the communication with the server is established bool diff --git a/app/src/stream.c b/app/src/stream.c index 557cff068e..782b02a176 100644 --- a/app/src/stream.c +++ b/app/src/stream.c @@ -95,11 +95,14 @@ process_frame(struct stream *stream, AVPacket *packet) { } if (stream->serve) { - packet->dts = packet->pts; + if (stream->serve->isServeReady == true) { + //LOGI("Serve is processing"); + packet->dts = packet->pts; - if (!serve_push(stream->serve, packet)) { - LOGE("Could not serve packet"); - return false; + if (!serve_push(stream->serve, packet)) { + LOGE("Could not serve packet"); + return false; + } } } From f1fafa506d429bf64f6796e68fbe6ff2b440e5b2 Mon Sep 17 00:00:00 2001 From: Killian Richard Date: Tue, 16 Jun 2020 16:50:45 +0200 Subject: [PATCH 7/9] Add meson configuration folder in git ignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f152b4b84a..4c08b81363 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ build/ .gradle/ .vs/ /x/ +configuration/ From 1e607c6445b28609a228fab631e6e7d0802449ab Mon Sep 17 00:00:00 2001 From: Killian Richard Date: Fri, 3 Jul 2020 11:29:28 +0200 Subject: [PATCH 8/9] Cleaning code for pull request --- app/src/cli.c | 27 +++++++++++++-------------- app/src/scrcpy.c | 2 +- app/src/serve.c | 8 ++++---- app/src/serve.h | 8 ++++---- app/src/server.c | 20 ++++---------------- app/src/server.h | 2 +- 6 files changed, 27 insertions(+), 40 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index f3b4d42724..6dc1e9fe1c 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -458,9 +458,9 @@ guess_record_format(const char *filename) { char** str_split(const char *a_str, const char a_delim) { - char** result = 0; + char **result = 0; size_t count = 0; - char* tmp = (char*)a_str; + char *tmp = (char*)a_str; char str[100]; strncpy(str, a_str, sizeof(str)); char* last_comma = 0; @@ -480,8 +480,8 @@ str_split(const char *a_str, const char a_delim) { /* Add space for trailing token. */ count += last_comma < (str + strlen(str) - 1); - /* Add space for terminating null string so caller - knows where the list of returned strings ends. */ + /* Add space for terminating null string so caller + knows where the list of returned strings ends. */ count++; result = malloc(sizeof(char*) * count); @@ -506,7 +506,7 @@ str_split(const char *a_str, const char a_delim) { bool check_if_ip_valid(char *ip) { int num, dots = 0; - char* ptr; + char *ptr; if (ip == NULL) return 0; @@ -539,9 +539,9 @@ check_if_ip_valid(char *ip) { uint32_t convert_ip_to_int(char* ip_string) { int num, dots = 0; - char* ptr; + char *ptr; - char* ip = "0x"; + char *ip = "0x"; ptr = strtok(ip_string, "."); //Cut the string using dot as delimiter @@ -566,12 +566,12 @@ parse_serve_args(const char *optarg, char **s_protocol, uint32_t *s_ip, uint16_t bool ip_valid = false; bool port_valid = false; - char* protocol = NULL; - char* ip = NULL; + char *protocol = NULL; + char *ip = NULL; uint32_t ip_value; - char* port = NULL; - - char** values; + char *port = NULL; + char **values; + values = str_split(optarg, ':'); if (values) { @@ -755,8 +755,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case OPT_SERVE: if (!parse_serve_args(optarg, &opts->serve_protocol, &opts->serve_ip, &opts->serve_port)) { return false; - } - else { + } else { opts->serve = true; } break; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index f46366c8ec..f27d733bcf 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -309,7 +309,7 @@ scrcpy(const struct scrcpy_options *options) { } } - if (!server_start(&server, options->serial, ¶ms, serv)) { + if (!server_start(&server, options->serial, ¶ms)) { return false; } diff --git a/app/src/serve.c b/app/src/serve.c index 2acabae76c..50b457750a 100644 --- a/app/src/serve.c +++ b/app/src/serve.c @@ -13,7 +13,7 @@ # define SOCKET_ERROR (-1) void -serve_init(struct serve* serve, char *protocol, uint32_t ip, uint16_t port) { +serve_init(struct serve *serve, char *protocol, uint32_t ip, uint16_t port) { serve->protocol = protocol; serve->ip = ip; serve->port = port; @@ -21,7 +21,7 @@ serve_init(struct serve* serve, char *protocol, uint32_t ip, uint16_t port) { } bool -serve_start(struct serve* serve) { +serve_start(struct serve *serve) { LOGD("Starting serve thread"); socket_t Listensocket; @@ -53,7 +53,7 @@ serve_start(struct serve* serve) { } bool -serve_push(struct serve* serve, const AVPacket *packet) { +serve_push(struct serve *serve, const AVPacket *packet) { if (serve->isServeReady) { if (net_send(serve->socket, packet->data, packet->size) == SOCKET_ERROR) { @@ -68,6 +68,6 @@ serve_push(struct serve* serve, const AVPacket *packet) { } void -serve_stop(struct serve* serve) { +serve_stop(struct serve *serve) { net_close(serve->socket); } \ No newline at end of file diff --git a/app/src/serve.h b/app/src/serve.h index ef3b57e3be..5643e89034 100644 --- a/app/src/serve.h +++ b/app/src/serve.h @@ -19,14 +19,14 @@ struct serve { }; void -serve_init(struct serve* serve, char* protocol, uint32_t ip, uint16_t port); +serve_init(struct serve *serve, char *protocol, uint32_t ip, uint16_t port); bool -serve_start(struct serve* serve); +serve_start(struct serve *serve); bool -serve_push(struct serve* serve, const AVPacket *packet); +serve_push(struct serve *serve, const AVPacket *packet); void -serve_stop(struct serve* serve); +serve_stop(struct serve *serve); #endif \ No newline at end of file diff --git a/app/src/server.c b/app/src/server.c index 1672a23f47..64f71ae2e5 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -350,7 +350,7 @@ run_wait_server(void *data) { bool server_start(struct server *server, const char *serial, - const struct server_params *params, struct serve* serve) { + const struct server_params *params) { server->port_range = params->port_range; if (serial) { @@ -368,22 +368,10 @@ server_start(struct server *server, const char *serial, goto error1; } - /*server->serve = serve; - if (server->serve) { - if (server->serve->isServeReady == true) { - server->process = execute_server(server, params); - if (server->process == PROCESS_NONE) { - goto error2; - } - } + server->process = execute_server(server, params); + if (server->process == PROCESS_NONE) { + goto error2; } - else {*/ - // server will connect to our server socket - server->process = execute_server(server, params); - if (server->process == PROCESS_NONE) { - goto error2; - } - //} // If the server process dies before connecting to the server socket, then // the client will be stuck forever on accept(). To avoid the problem, we diff --git a/app/src/server.h b/app/src/server.h index c9b21340e8..5b7ce9b1d9 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -61,7 +61,7 @@ server_init(struct server *server); // push, enable tunnel et start the server bool server_start(struct server *server, const char *serial, - const struct server_params *params, struct serve* serve); + const struct server_params *params); // block until the communication with the server is established bool From 97f5ae91c8097ca0aa3b12b960bd4e5501c51dad Mon Sep 17 00:00:00 2001 From: Killian Date: Fri, 3 Jul 2020 11:39:55 +0200 Subject: [PATCH 9/9] Delete .gitignore --- .gitignore | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 .gitignore diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 4c08b81363..0000000000 --- a/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -build/ -/dist/ -.idea/ -.gradle/ -.vs/ -/x/ -configuration/