diff --git a/README.md b/README.md index 997ae89686..25d600c36e 100644 --- a/README.md +++ b/README.md @@ -251,6 +251,11 @@ _scrcpy_ window. There is no visual feedback, a log is printed to the console. +The target directory can be changed on start: + +```bash +scrcpy --push-target /sdcard/foo/bar/ +``` ### Read-only @@ -289,6 +294,14 @@ latency), use: scrcpy --render-expired-frames ``` +### Custom window title + +By default, the window title is the device model. It can be changed: + +```bash +scrcpy --window-title 'My device' +``` + ### Forward audio diff --git a/app/src/device.h b/app/src/device.h index f3449e5ed7..828443d739 100644 --- a/app/src/device.h +++ b/app/src/device.h @@ -7,7 +7,6 @@ #include "net.h" #define DEVICE_NAME_FIELD_LENGTH 64 -#define DEVICE_SDCARD_PATH "/sdcard/" // name must be at least DEVICE_NAME_FIELD_LENGTH bytes bool diff --git a/app/src/file_handler.c b/app/src/file_handler.c index ec53faaeb4..e02ca2a9db 100644 --- a/app/src/file_handler.c +++ b/app/src/file_handler.c @@ -5,17 +5,19 @@ #include "config.h" #include "command.h" -#include "device.h" #include "lock_util.h" #include "log.h" +#define DEFAULT_PUSH_TARGET "/sdcard/" + static void file_handler_request_destroy(struct file_handler_request *req) { SDL_free(req->file); } bool -file_handler_init(struct file_handler *file_handler, const char *serial) { +file_handler_init(struct file_handler *file_handler, const char *serial, + const char *push_target) { cbuf_init(&file_handler->queue); @@ -46,6 +48,8 @@ file_handler_init(struct file_handler *file_handler, const char *serial) { file_handler->stopped = false; file_handler->current_process = PROCESS_NONE; + file_handler->push_target = push_target ? push_target : DEFAULT_PUSH_TARGET; + return true; } @@ -67,8 +71,8 @@ install_apk(const char *serial, const char *file) { } static process_t -push_file(const char *serial, const char *file) { - return adb_push(serial, file, DEVICE_SDCARD_PATH); +push_file(const char *serial, const char *file, const char *push_target) { + return adb_push(serial, file, push_target); } bool @@ -124,7 +128,8 @@ run_file_handler(void *data) { process = install_apk(file_handler->serial, req.file); } else { LOGI("Pushing %s...", req.file); - process = push_file(file_handler->serial, req.file); + process = push_file(file_handler->serial, req.file, + file_handler->push_target); } file_handler->current_process = process; mutex_unlock(file_handler->mutex); @@ -137,9 +142,11 @@ run_file_handler(void *data) { } } else { if (process_check_success(process, "adb push")) { - LOGI("%s successfully pushed to /sdcard/", req.file); + LOGI("%s successfully pushed to %s", req.file, + file_handler->push_target); } else { - LOGE("Failed to push %s to /sdcard/", req.file); + LOGE("Failed to push %s to %s", req.file, + file_handler->push_target); } } diff --git a/app/src/file_handler.h b/app/src/file_handler.h index 222451057e..3418c5328a 100644 --- a/app/src/file_handler.h +++ b/app/src/file_handler.h @@ -22,6 +22,7 @@ struct file_handler_request_queue CBUF(struct file_handler_request, 16); struct file_handler { char *serial; + const char *push_target; SDL_Thread *thread; SDL_mutex *mutex; SDL_cond *event_cond; @@ -32,7 +33,8 @@ struct file_handler { }; bool -file_handler_init(struct file_handler *file_handler, const char *serial); +file_handler_init(struct file_handler *file_handler, const char *serial, + const char *push_target); void file_handler_destroy(struct file_handler *file_handler); diff --git a/app/src/main.c b/app/src/main.c index b4f8953da0..7a1cb9dbba 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -17,6 +17,8 @@ struct args { const char *serial; const char *crop; const char *record_filename; + const char *window_title; + const char *push_target; enum recorder_format record_format; bool fullscreen; bool no_control; @@ -75,6 +77,11 @@ static void usage(const char *arg0) { " Set the TCP port the client listens on.\n" " Default is %d.\n" "\n" + " --push-target path\n" + " Set the target directory for pushing files to the device by\n" + " drag & drop. It is passed as-is to \"adb push\".\n" + " Default is \"/sdcard/\".\n" + "\n" " -r, --record file.mp4\n" " Record screen to file.\n" " The format is determined by the -F/--record-format option if\n" @@ -103,6 +110,9 @@ static void usage(const char *arg0) { " -v, --version\n" " Print the version of scrcpy.\n" "\n" + " --window-title text\n" + " Set a custom window title.\n" + "\n" "Shortcuts:\n" "\n" " Ctrl+f\n" @@ -295,6 +305,8 @@ guess_record_format(const char *filename) { } #define OPT_RENDER_EXPIRED_FRAMES 1000 +#define OPT_WINDOW_TITLE 1001 +#define OPT_PUSH_TARGET 1002 static bool parse_args(struct args *args, int argc, char *argv[]) { @@ -308,6 +320,8 @@ parse_args(struct args *args, int argc, char *argv[]) { {"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, 'f'}, {"render-expired-frames", no_argument, NULL, @@ -316,6 +330,8 @@ parse_args(struct args *args, int argc, char *argv[]) { {"show-touches", no_argument, NULL, 't'}, {"turn-screen-off", no_argument, NULL, 'S'}, {"version", no_argument, NULL, 'v'}, + {"window-title", required_argument, NULL, + OPT_WINDOW_TITLE}, {NULL, 0, NULL, 0 }, }; int c; @@ -378,6 +394,12 @@ parse_args(struct args *args, int argc, char *argv[]) { case OPT_RENDER_EXPIRED_FRAMES: args->render_expired_frames = true; break; + case OPT_WINDOW_TITLE: + args->window_title = optarg; + break; + case OPT_PUSH_TARGET: + args->push_target = optarg; + break; default: // getopt prints the error message on stderr return false; @@ -434,6 +456,8 @@ main(int argc, char *argv[]) { .serial = NULL, .crop = NULL, .record_filename = NULL, + .window_title = NULL, + .push_target = NULL, .record_format = 0, .help = false, .version = false, @@ -478,6 +502,8 @@ main(int argc, char *argv[]) { .crop = args.crop, .port = args.port, .record_filename = args.record_filename, + .window_title = args.window_title, + .push_target = args.push_target, .record_format = args.record_format, .max_size = args.max_size, .bit_rate = args.bit_rate, diff --git a/app/src/recorder.c b/app/src/recorder.c index 3de8257a0a..c14394a348 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -166,11 +166,21 @@ recorder_rescale_packet(struct recorder *recorder, AVPacket *packet) { bool recorder_write(struct recorder *recorder, AVPacket *packet) { if (!recorder->header_written) { + if (packet->pts != AV_NOPTS_VALUE) { + LOGE("The first packet is not a config packet"); + return false; + } bool ok = recorder_write_header(recorder, packet); if (!ok) { return false; } recorder->header_written = true; + return true; + } + + if (packet->pts == AV_NOPTS_VALUE) { + // ignore config packets + return true; } recorder_rescale_packet(recorder, packet); diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index ede34dd7ad..ed9887789b 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -277,7 +277,6 @@ scrcpy(const struct scrcpy_options *options) { .local_port = options->port, .max_size = options->max_size, .bit_rate = options->bit_rate, - .send_frame_meta = record, .control = options->control, }; if (!server_start(&server, options->serial, ¶ms)) { @@ -334,7 +333,8 @@ scrcpy(const struct scrcpy_options *options) { video_buffer_initialized = true; if (options->control) { - if (!file_handler_init(&file_handler, server.serial)) { + if (!file_handler_init(&file_handler, server.serial, + options->push_target)) { goto end; } file_handler_initialized = true; @@ -380,7 +380,10 @@ scrcpy(const struct scrcpy_options *options) { controller_started = true; } - if (!screen_init_rendering(&screen, device_name, frame_size, + const char *window_title = + options->window_title ? options->window_title : device_name; + + if (!screen_init_rendering(&screen, window_title, frame_size, options->always_on_top)) { goto end; } diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index d705d2db11..faeb246f1c 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -9,6 +9,8 @@ struct scrcpy_options { const char *serial; const char *crop; const char *record_filename; + const char *window_title; + const char *push_target; enum recorder_format record_format; uint16_t port; uint16_t max_size; diff --git a/app/src/screen.c b/app/src/screen.c index 159d6a47cd..18d24dda79 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -134,7 +134,7 @@ create_texture(SDL_Renderer *renderer, struct size frame_size) { } bool -screen_init_rendering(struct screen *screen, const char *device_name, +screen_init_rendering(struct screen *screen, const char *window_title, struct size frame_size, bool always_on_top) { screen->frame_size = frame_size; @@ -152,7 +152,7 @@ screen_init_rendering(struct screen *screen, const char *device_name, #endif } - screen->window = SDL_CreateWindow(device_name, SDL_WINDOWPOS_UNDEFINED, + screen->window = SDL_CreateWindow(window_title, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, window_size.width, window_size.height, window_flags); diff --git a/app/src/screen.h b/app/src/screen.h index 5734fdc20f..63da6aa5d2 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -44,7 +44,7 @@ screen_init(struct screen *screen); // initialize screen, create window, renderer and texture (window is hidden) bool -screen_init_rendering(struct screen *screen, const char *device_name, +screen_init_rendering(struct screen *screen, const char *window_title, struct size frame_size, bool always_on_top); // show the window diff --git a/app/src/server.c b/app/src/server.c index 5b593c477e..85b1b6b835 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -130,7 +130,7 @@ execute_server(struct server *server, const struct server_params *params) { bit_rate_string, server->tunnel_forward ? "true" : "false", params->crop ? params->crop : "-", - params->send_frame_meta ? "true" : "false", + "true", // always send frame meta (packet boundaries + timestamp) params->control ? "true" : "false", }; return adb_execute(server->serial, cmd, sizeof(cmd) / sizeof(cmd[0])); diff --git a/app/src/server.h b/app/src/server.h index 74a6cac8e2..4970d64ec6 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -34,7 +34,6 @@ struct server_params { uint16_t local_port; uint16_t max_size; uint32_t bit_rate; - bool send_frame_meta; bool control; }; diff --git a/app/src/stream.c b/app/src/stream.c index e85834c173..0396bf6041 100644 --- a/app/src/stream.c +++ b/app/src/stream.c @@ -22,54 +22,8 @@ #define HEADER_SIZE 12 #define NO_PTS UINT64_C(-1) -static struct frame_meta * -frame_meta_new(uint64_t pts) { - struct frame_meta *meta = SDL_malloc(sizeof(*meta)); - if (!meta) { - return meta; - } - meta->pts = pts; - meta->next = NULL; - return meta; -} - -static void -frame_meta_delete(struct frame_meta *frame_meta) { - SDL_free(frame_meta); -} - static bool -receiver_state_push_meta(struct receiver_state *state, uint64_t pts) { - struct frame_meta *frame_meta = frame_meta_new(pts); - if (!frame_meta) { - return false; - } - - // append to the list - // (iterate to find the last item, in practice the list should be tiny) - struct frame_meta **p = &state->frame_meta_queue; - while (*p) { - p = &(*p)->next; - } - *p = frame_meta; - return true; -} - -static uint64_t -receiver_state_take_meta(struct receiver_state *state) { - struct frame_meta *frame_meta = state->frame_meta_queue; // first item - SDL_assert(frame_meta); // must not be empty - uint64_t pts = frame_meta->pts; - state->frame_meta_queue = frame_meta->next; // remove the item - frame_meta_delete(frame_meta); - return pts; -} - -static int -read_packet_with_meta(void *opaque, uint8_t *buf, int buf_size) { - struct stream *stream = opaque; - struct receiver_state *state = &stream->receiver_state; - +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. @@ -82,60 +36,30 @@ read_packet_with_meta(void *opaque, uint8_t *buf, int buf_size) { // // It is followed by bytes containing the packet/frame. - if (!state->remaining) { -#define HEADER_SIZE 12 - uint8_t header[HEADER_SIZE]; - ssize_t r = net_recv_all(stream->socket, header, HEADER_SIZE); - if (r == -1) { - return AVERROR(errno); - } - if (r == 0) { - return AVERROR_EOF; - } - // no partial read (net_recv_all()) - SDL_assert_release(r == HEADER_SIZE); - - uint64_t pts = buffer_read64be(header); - state->remaining = buffer_read32be(&header[8]); - - if (pts != NO_PTS && !receiver_state_push_meta(state, pts)) { - LOGE("Could not store PTS for recording"); - // we could not save the PTS, the recording would be broken - return AVERROR(ENOMEM); - } + uint8_t header[HEADER_SIZE]; + ssize_t r = net_recv_all(stream->socket, header, HEADER_SIZE); + if (r < HEADER_SIZE) { + return false; } - SDL_assert(state->remaining); + uint64_t pts = buffer_read64be(header); + uint32_t len = buffer_read32be(&header[8]); + SDL_assert(len); - if (buf_size > state->remaining) { - buf_size = state->remaining; + if (av_new_packet(packet, len)) { + LOGE("Could not allocate packet"); + return false; } - ssize_t r = net_recv(stream->socket, buf, buf_size); - if (r == -1) { - return errno ? AVERROR(errno) : AVERROR_EOF; - } - if (r == 0) { - return AVERROR_EOF; + r = net_recv_all(stream->socket, packet->data, len); + if (r < len) { + av_packet_unref(packet); + return false; } - SDL_assert(state->remaining >= r); - state->remaining -= r; + packet->pts = pts != NO_PTS ? pts : AV_NOPTS_VALUE; - return r; -} - -static int -read_raw_packet(void *opaque, uint8_t *buf, int buf_size) { - struct stream *stream = opaque; - ssize_t r = net_recv(stream->socket, buf, buf_size); - if (r == -1) { - return errno ? AVERROR(errno) : AVERROR_EOF; - } - if (r == 0) { - return AVERROR_EOF; - } - return r; + return true; } static void @@ -145,55 +69,136 @@ notify_stopped(void) { SDL_PushEvent(&stop_event); } -static int -run_stream(void *data) { - struct stream *stream = data; +static bool +process_config_packet(struct stream *stream, AVPacket *packet) { + if (stream->recorder && !recorder_write(stream->recorder, packet)) { + LOGE("Could not send config packet to recorder"); + return false; + } + return true; +} - AVFormatContext *format_ctx = avformat_alloc_context(); - if (!format_ctx) { - LOGC("Could not allocate format context"); - goto end; +static bool +process_frame(struct stream *stream, AVPacket *packet) { + if (stream->decoder && !decoder_push(stream->decoder, packet)) { + return false; } - unsigned char *buffer = av_malloc(BUFSIZE); - if (!buffer) { - LOGC("Could not allocate buffer"); - goto finally_free_format_ctx; + if (stream->recorder) { + packet->dts = packet->pts; + + if (!recorder_write(stream->recorder, packet)) { + LOGE("Could not write frame to output file"); + return false; + } } - // initialize the receiver state - stream->receiver_state.frame_meta_queue = NULL; - stream->receiver_state.remaining = 0; - - // if recording is enabled, a "header" is sent between raw packets - int (*read_packet)(void *, uint8_t *, int) = - stream->recorder ? read_packet_with_meta : read_raw_packet; - AVIOContext *avio_ctx = avio_alloc_context(buffer, BUFSIZE, 0, stream, - read_packet, NULL, NULL); - if (!avio_ctx) { - LOGC("Could not allocate avio context"); - // avformat_open_input takes ownership of 'buffer' - // so only free the buffer before avformat_open_input() - av_free(buffer); - goto finally_free_format_ctx; + 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 + SDL_assert(r == in_len); + SDL_assert(out_len == in_len); + + if (stream->parser->key_frame == 1) { + packet->flags |= AV_PKT_FLAG_KEY; } - format_ctx->pb = avio_ctx; + 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; + } - if (avformat_open_input(&format_ctx, NULL, NULL, NULL) < 0) { - LOGE("Could not open video stream"); - goto finally_free_avio_ctx; + 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_close_input; + goto finally_free_codec_ctx; } if (stream->recorder && !recorder_open(stream->recorder, codec)) { @@ -201,50 +206,40 @@ run_stream(void *data) { goto finally_close_decoder; } - AVPacket packet; - av_init_packet(&packet); - packet.data = NULL; - packet.size = 0; - - while (!av_read_frame(format_ctx, &packet)) { - if (SDL_AtomicGet(&stream->stopped)) { - // if the stream is stopped, the socket had been shutdown, so the - // last packet is probably corrupted (but not detected as such by - // FFmpeg) and will not be decoded correctly - av_packet_unref(&packet); - goto quit; - } - if (stream->decoder && !decoder_push(stream->decoder, &packet)) { - av_packet_unref(&packet); - goto quit; - } + stream->parser = av_parser_init(AV_CODEC_ID_H264); + if (!stream->parser) { + LOGE("Could not initialize parser"); + goto finally_close_recorder; + } - if (stream->recorder) { - // we retrieve the PTS in order they were received, so they will - // be assigned to the correct frame - uint64_t pts = receiver_state_take_meta(&stream->receiver_state); - packet.pts = pts; - packet.dts = pts; - - // no need to rescale with av_packet_rescale_ts(), the timestamps - // are in microseconds both in input and output - if (!recorder_write(stream->recorder, &packet)) { - LOGE("Could not write frame to output file"); - av_packet_unref(&packet); - goto quit; - } + // 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 (avio_ctx->eof_reached) { + if (!ok) { + // cannot process packet (error already logged) break; } } LOGD("End of frames"); -quit: + if (stream->has_pending) { + av_packet_unref(&stream->pending); + } + + av_parser_close(stream->parser); +finally_close_recorder: if (stream->recorder) { recorder_close(stream->recorder); } @@ -252,13 +247,8 @@ run_stream(void *data) { if (stream->decoder) { decoder_close(stream->decoder); } -finally_close_input: - avformat_close_input(&format_ctx); -finally_free_avio_ctx: - av_free(avio_ctx->buffer); - av_free(avio_ctx); -finally_free_format_ctx: - avformat_free_context(format_ctx); +finally_free_codec_ctx: + avcodec_free_context(&stream->codec_ctx); end: notify_stopped(); return 0; @@ -270,7 +260,7 @@ stream_init(struct stream *stream, socket_t socket, stream->socket = socket; stream->decoder = decoder, stream->recorder = recorder; - SDL_AtomicSet(&stream->stopped, 0); + stream->has_pending = false; } bool @@ -287,7 +277,6 @@ stream_start(struct stream *stream) { void stream_stop(struct stream *stream) { - SDL_AtomicSet(&stream->stopped, 1); if (stream->decoder) { decoder_interrupt(stream->decoder); } diff --git a/app/src/stream.h b/app/src/stream.h index 1ebff1a07a..160ed7f5d4 100644 --- a/app/src/stream.h +++ b/app/src/stream.h @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -10,23 +11,18 @@ struct video_buffer; -struct frame_meta { - uint64_t pts; - struct frame_meta *next; -}; - struct stream { socket_t socket; struct video_buffer *video_buffer; SDL_Thread *thread; - SDL_atomic_t stopped; struct decoder *decoder; struct recorder *recorder; - struct receiver_state { - // meta (in order) for frames not consumed yet - struct frame_meta *frame_meta_queue; - size_t remaining; // remaining bytes to receive for the current frame - } receiver_state; + AVCodecContext *codec_ctx; + AVCodecParserContext *parser; + // successive packets may need to be concatenated, until a non-config + // packet is available + bool has_pending; + AVPacket pending; }; void