diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index ae516c348a..cdc9270fb4 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -33,10 +33,11 @@ _scrcpy() { --no-clipboard-autosync --no-downsize-on-error -n --no-control - -N --no-display + -N --no-mirror --no-key-repeat --no-mipmaps --no-power-on + --no-video --otg -p --port= --power-off-on-close diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 97bf4f3eb5..5e40b2fb9c 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -39,10 +39,11 @@ arguments=( '--no-clipboard-autosync[Disable automatic clipboard synchronization]' '--no-downsize-on-error[Disable lowering definition on MediaCodec error]' {-n,--no-control}'[Disable device control \(mirror the device in read only\)]' - {-N,--no-display}'[Do not display device \(during screen recording or when V4L2 sink is enabled\)]' + {-N,--no-mirror}'[Do not mirror device \(only when recording or V4L2 sink is enabled\)]' '--no-key-repeat[Do not forward repeated key events when a key is held down]' '--no-mipmaps[Disable the generation of mipmaps]' '--no-power-on[Do not power on the device on start]' + '--no-video[Disable video forwarding]' '--otg[Run in OTG mode \(simulating physical keyboard and mouse\)]' {-p,--port=}'[\[port\[\:port\]\] Set the TCP port \(range\) used by the client to listen]' '--power-off-on-close[Turn the device screen off when closing scrcpy]' diff --git a/app/prebuilt-deps/prepare-ffmpeg.sh b/app/prebuilt-deps/prepare-ffmpeg.sh index b156099a45..3533ded4ba 100755 --- a/app/prebuilt-deps/prepare-ffmpeg.sh +++ b/app/prebuilt-deps/prepare-ffmpeg.sh @@ -6,11 +6,11 @@ cd "$DIR" mkdir -p "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR" -VERSION=6.0-scrcpy-2 +VERSION=6.0-scrcpy-3 DEP_DIR="ffmpeg-$VERSION" FILENAME="$DEP_DIR".7z -SHA256SUM=98ef97f8607c97a5c4f9c5a0a991b78f105d002a3619145011d16ffb92501b14 +SHA256SUM=36829d98ac4454d7092c72ddb92faa20b60450bc0fe8873076efb0858cdcbc2c if [[ -d "$DEP_DIR" ]] then diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 3749721178..29d14b58db 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -210,8 +210,8 @@ This option disables this behavior. Disable device control (mirror the device in read\-only). .TP -.B \-N, \-\-no\-display -Do not display device (only when screen recording is enabled). +.B \-N, \-\-no\-mirror +Do not mirror device video or audio on the computer (only when recording or V4L2 sink is enabled). .TP .B \-\-no\-key\-repeat @@ -225,6 +225,10 @@ If the renderer is OpenGL 3.0+ or OpenGL ES 2.0+, then mipmaps are automatically .B \-\-no\-power\-on Do not power on the device on start. +.TP +.B \-\-no\-video +Disable video forwarding. + .TP .B \-\-otg Run in OTG mode: simulate physical keyboard and mouse, as if the computer keyboard and mouse were plugged directly to the device via an OTG cable. diff --git a/app/src/cli.c b/app/src/cli.c index d6d9f41dd9..e5b18277b9 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -72,6 +72,8 @@ enum { OPT_REQUIRE_AUDIO, OPT_AUDIO_BUFFER, OPT_AUDIO_OUTPUT_BUFFER, + OPT_NO_DISPLAY, + OPT_NO_VIDEO, }; struct sc_option { @@ -380,9 +382,14 @@ static const struct sc_option options[] = { }, { .shortopt = 'N', + .longopt = "no-mirror", + .text = "Do not mirror device video or audio on the computer (only " + "when recording or V4L2 sink is enabled).", + }, + { + // deprecated + .longopt_id = OPT_NO_DISPLAY, .longopt = "no-display", - .text = "Do not display device (only when screen recording or V4L2 " - "sink is enabled).", }, { .longopt_id = OPT_NO_KEY_REPEAT, @@ -401,6 +408,11 @@ static const struct sc_option options[] = { .longopt = "no-power-on", .text = "Do not power on the device on start.", }, + { + .longopt_id = OPT_NO_VIDEO, + .longopt = "no-video", + .text = "Disable video forwarding.", + }, { .longopt_id = OPT_OTG, .longopt = "otg", @@ -1467,18 +1479,39 @@ sc_parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) { } #endif +static enum sc_record_format +get_record_format(const char *name) { + if (!strcmp(name, "mp4")) { + return SC_RECORD_FORMAT_MP4; + } + if (!strcmp(name, "mkv")) { + return SC_RECORD_FORMAT_MKV; + } + if (!strcmp(name, "m4a")) { + return SC_RECORD_FORMAT_M4A; + } + if (!strcmp(name, "mka")) { + return SC_RECORD_FORMAT_MKA; + } + if (!strcmp(name, "opus")) { + return SC_RECORD_FORMAT_OPUS; + } + if (!strcmp(name, "aac")) { + return SC_RECORD_FORMAT_AAC; + } + return 0; +} + static bool parse_record_format(const char *optarg, enum sc_record_format *format) { - if (!strcmp(optarg, "mp4")) { - *format = SC_RECORD_FORMAT_MP4; - return true; - } - if (!strcmp(optarg, "mkv")) { - *format = SC_RECORD_FORMAT_MKV; - return true; + enum sc_record_format fmt = get_record_format(optarg); + if (!fmt) { + LOGE("Unsupported format: %s (expected mp4 or mkv)", optarg); + return false; } - LOGE("Unsupported format: %s (expected mp4 or mkv)", optarg); - return false; + + *format = fmt; + return true; } static bool @@ -1498,18 +1531,13 @@ parse_port(const char *optarg, uint16_t *port) { static enum sc_record_format guess_record_format(const char *filename) { - size_t len = strlen(filename); - if (len < 4) { + const char *dot = strrchr(filename, '.'); + if (!dot) { return 0; } - const char *ext = &filename[len - 4]; - if (!strcmp(ext, ".mp4")) { - return SC_RECORD_FORMAT_MP4; - } - if (!strcmp(ext, ".mkv")) { - return SC_RECORD_FORMAT_MKV; - } - return 0; + + const char *ext = dot + 1; + return get_record_format(ext); } static bool @@ -1642,8 +1670,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case 'n': opts->control = false; break; + case OPT_NO_DISPLAY: + LOGW("--no-display is deprecated, use --no-mirror instead."); + // fall through case 'N': - opts->display = false; + opts->mirror = false; break; case 'p': if (!parse_port_range(optarg, &opts->port_range)) { @@ -1788,6 +1819,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_NO_DOWNSIZE_ON_ERROR: opts->downsize_on_error = false; break; + case OPT_NO_VIDEO: + opts->video = false; + break; case OPT_NO_AUDIO: opts->audio = false; break; @@ -1890,8 +1924,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } #ifdef HAVE_V4L2 - if (!opts->display && !opts->record_filename && !opts->v4l2_device) { - LOGE("-N/--no-display requires either screen recording (-r/--record)" + if (!opts->mirror && !opts->record_filename && !opts->v4l2_device) { + LOGE("-N/--no-mirror requires either screen recording (-r/--record)" " or sink to v4l2loopback device (--v4l2-sink)"); return false; } @@ -1915,14 +1949,14 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } #else - if (!opts->display && !opts->record_filename) { - LOGE("-N/--no-display requires screen recording (-r/--record)"); + if (!opts->mirror && !opts->record_filename) { + LOGE("-N/--no-mirror requires screen recording (-r/--record)"); return false; } #endif - if (opts->audio && !opts->display && !opts->record_filename) { - LOGI("No display and no recording: audio disabled"); + if (opts->audio && !opts->mirror && !opts->record_filename) { + LOGI("No mirror and no recording: audio disabled"); opts->audio = false; } @@ -1937,19 +1971,41 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } - if (opts->record_filename && !opts->record_format) { - opts->record_format = guess_record_format(opts->record_filename); + if (opts->record_filename) { if (!opts->record_format) { - LOGE("No format specified for \"%s\" " - "(try with --record-format=mkv)", - opts->record_filename); + opts->record_format = guess_record_format(opts->record_filename); + if (!opts->record_format) { + LOGE("No format specified for \"%s\" " + "(try with --record-format=mkv)", + opts->record_filename); + return false; + } + } + + if (opts->audio_codec == SC_CODEC_RAW) { + LOGW("Recording does not support RAW audio codec"); return false; } - } - if (opts->record_filename && opts->audio_codec == SC_CODEC_RAW) { - LOGW("Recording does not support RAW audio codec"); - return false; + if (opts->video + && sc_record_format_is_audio_only(opts->record_format)) { + LOGE("Audio container does not support video stream"); + return false; + } + + if (opts->record_format == SC_RECORD_FORMAT_OPUS + && opts->audio_codec != SC_CODEC_OPUS) { + LOGE("Recording to OPUS file requires an OPUS audio stream " + "(try with --audio-codec=opus)"); + return false; + } + + if (opts->record_format == SC_RECORD_FORMAT_AAC + && opts->audio_codec != SC_CODEC_AAC) { + LOGE("Recording to AAC file requires an AAC audio stream " + "(try with --audio-codec=aac)"); + return false; + } } if (opts->audio_codec == SC_CODEC_RAW) { @@ -2032,6 +2088,21 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } #endif +#ifdef HAVE_USB + if (!(opts->mirror && opts->video) && !opts->otg) { +#else + if (!(opts->mirror && opts->video)) { +#endif + // If video mirroring is disabled and OTG are disabled, then there is + // no way to control the device. + opts->control = false; + } + + if (!opts->video) { + // If video is disabled, then scrcpy must exit on audio failure. + opts->require_audio = true; + } + return true; } diff --git a/app/src/options.c b/app/src/options.c index 8b99f6f3ad..122839523c 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -52,7 +52,7 @@ const struct scrcpy_options scrcpy_options_default = { .fullscreen = false, .always_on_top = false, .control = true, - .display = true, + .mirror = true, .turn_screen_off = false, .key_inject_mode = SC_KEY_INJECT_MODE_MIXED, .window_borderless = false, @@ -73,6 +73,7 @@ const struct scrcpy_options scrcpy_options_default = { .cleanup = true, .start_fps_counter = false, .power_on = true, + .video = true, .audio = true, .require_audio = false, .list_encoders = false, diff --git a/app/src/options.h b/app/src/options.h index c41e275703..7c442149de 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -21,8 +21,20 @@ enum sc_record_format { SC_RECORD_FORMAT_AUTO, SC_RECORD_FORMAT_MP4, SC_RECORD_FORMAT_MKV, + SC_RECORD_FORMAT_M4A, + SC_RECORD_FORMAT_MKA, + SC_RECORD_FORMAT_OPUS, + SC_RECORD_FORMAT_AAC, }; +static inline bool +sc_record_format_is_audio_only(enum sc_record_format fmt) { + return fmt == SC_RECORD_FORMAT_M4A + || fmt == SC_RECORD_FORMAT_MKA + || fmt == SC_RECORD_FORMAT_OPUS + || fmt == SC_RECORD_FORMAT_AAC; +} + enum sc_codec { SC_CODEC_H264, SC_CODEC_H265, @@ -135,7 +147,7 @@ struct scrcpy_options { bool fullscreen; bool always_on_top; bool control; - bool display; + bool mirror; bool turn_screen_off; enum sc_key_inject_mode key_inject_mode; bool window_borderless; @@ -156,6 +168,7 @@ struct scrcpy_options { bool cleanup; bool start_fps_counter; bool power_on; + bool video; bool audio; bool require_audio; bool list_encoders; diff --git a/app/src/recorder.c b/app/src/recorder.c index 2fc95eca9d..be1cbe719b 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -60,9 +60,17 @@ sc_recorder_queue_clear(struct sc_recorder_queue *queue) { static const char * sc_recorder_get_format_name(enum sc_record_format format) { switch (format) { - case SC_RECORD_FORMAT_MP4: return "mp4"; - case SC_RECORD_FORMAT_MKV: return "matroska"; - default: return NULL; + case SC_RECORD_FORMAT_MP4: + case SC_RECORD_FORMAT_M4A: + case SC_RECORD_FORMAT_AAC: + return "mp4"; + case SC_RECORD_FORMAT_MKV: + case SC_RECORD_FORMAT_MKA: + return "matroska"; + case SC_RECORD_FORMAT_OPUS: + return "opus"; + default: + return NULL; } } @@ -152,7 +160,7 @@ sc_recorder_close_output_file(struct sc_recorder *recorder) { static inline bool sc_recorder_has_empty_queues(struct sc_recorder *recorder) { - if (sc_vecdeque_is_empty(&recorder->video_queue)) { + if (recorder->video && sc_vecdeque_is_empty(&recorder->video_queue)) { // The video queue is empty return true; } @@ -176,7 +184,7 @@ sc_recorder_process_header(struct sc_recorder *recorder) { sc_cond_wait(&recorder->stream_cond, &recorder->mutex); } - if (sc_vecdeque_is_empty(&recorder->video_queue)) { + if (recorder->video && sc_vecdeque_is_empty(&recorder->video_queue)) { assert(recorder->stopped); // If the recorder is stopped, don't process anything if there are not // at least video packets @@ -184,7 +192,11 @@ sc_recorder_process_header(struct sc_recorder *recorder) { return false; } - AVPacket *video_pkt = sc_vecdeque_pop(&recorder->video_queue); + AVPacket *video_pkt = NULL; + if (!sc_vecdeque_is_empty(&recorder->video_queue)) { + assert(recorder->video); + video_pkt = sc_vecdeque_pop(&recorder->video_queue); + } AVPacket *audio_pkt = NULL; if (!sc_vecdeque_is_empty(&recorder->audio_queue)) { @@ -196,17 +208,19 @@ sc_recorder_process_header(struct sc_recorder *recorder) { int ret = false; - if (video_pkt->pts != AV_NOPTS_VALUE) { - LOGE("The first video packet is not a config packet"); - goto end; - } + if (video_pkt) { + if (video_pkt->pts != AV_NOPTS_VALUE) { + LOGE("The first video packet is not a config packet"); + goto end; + } - assert(recorder->video_stream_index >= 0); - AVStream *video_stream = - recorder->ctx->streams[recorder->video_stream_index]; - bool ok = sc_recorder_set_extradata(video_stream, video_pkt); - if (!ok) { - goto end; + assert(recorder->video_stream_index >= 0); + AVStream *video_stream = + recorder->ctx->streams[recorder->video_stream_index]; + bool ok = sc_recorder_set_extradata(video_stream, video_pkt); + if (!ok) { + goto end; + } } if (audio_pkt) { @@ -218,13 +232,13 @@ sc_recorder_process_header(struct sc_recorder *recorder) { assert(recorder->audio_stream_index >= 0); AVStream *audio_stream = recorder->ctx->streams[recorder->audio_stream_index]; - ok = sc_recorder_set_extradata(audio_stream, audio_pkt); + bool ok = sc_recorder_set_extradata(audio_stream, audio_pkt); if (!ok) { goto end; } } - ok = avformat_write_header(recorder->ctx, NULL) >= 0; + bool ok = avformat_write_header(recorder->ctx, NULL) >= 0; if (!ok) { LOGE("Failed to write header to %s", recorder->filename); goto end; @@ -233,7 +247,9 @@ sc_recorder_process_header(struct sc_recorder *recorder) { ret = true; end: - av_packet_free(&video_pkt); + if (video_pkt) { + av_packet_free(&video_pkt); + } if (audio_pkt) { av_packet_free(&audio_pkt); } @@ -263,7 +279,8 @@ sc_recorder_process_packets(struct sc_recorder *recorder) { sc_mutex_lock(&recorder->mutex); while (!recorder->stopped) { - if (!video_pkt && !sc_vecdeque_is_empty(&recorder->video_queue)) { + if (recorder->video && !video_pkt && + !sc_vecdeque_is_empty(&recorder->video_queue)) { // A new packet may be assigned to video_pkt and be processed break; } @@ -278,6 +295,11 @@ sc_recorder_process_packets(struct sc_recorder *recorder) { // If stopped is set, continue to process the remaining events (to // finish the recording) before actually stopping. + // If there is no video, then the video_queue will remain empty forever + // and video_pkt will always be NULL. + assert(recorder->video || (!video_pkt + && sc_vecdeque_is_empty(&recorder->video_queue))); + // If there is no audio, then the audio_queue will remain empty forever // and audio_pkt will always be NULL. assert(recorder->audio || (!audio_pkt @@ -319,6 +341,9 @@ sc_recorder_process_packets(struct sc_recorder *recorder) { if (!recorder->audio) { assert(video_pkt); pts_origin = video_pkt->pts; + } else if (!recorder->video) { + assert(audio_pkt); + pts_origin = audio_pkt->pts; } else if (video_pkt && audio_pkt) { pts_origin = MIN(video_pkt->pts, audio_pkt->pts); } else if (recorder->stopped) { @@ -639,7 +664,7 @@ sc_recorder_audio_packet_sink_disable(struct sc_packet_sink *sink) { bool sc_recorder_init(struct sc_recorder *recorder, const char *filename, - enum sc_record_format format, bool audio, + enum sc_record_format format, bool video, bool audio, const struct sc_recorder_callbacks *cbs, void *cbs_userdata) { recorder->filename = strdup(filename); if (!recorder->filename) { @@ -662,6 +687,8 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename, goto error_queue_cond_destroy; } + assert(video || audio); + recorder->video = video; recorder->audio = audio; sc_vecdeque_init(&recorder->video_queue); @@ -680,13 +707,15 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename, recorder->cbs = cbs; recorder->cbs_userdata = cbs_userdata; - static const struct sc_packet_sink_ops video_ops = { - .open = sc_recorder_video_packet_sink_open, - .close = sc_recorder_video_packet_sink_close, - .push = sc_recorder_video_packet_sink_push, - }; + if (video) { + static const struct sc_packet_sink_ops video_ops = { + .open = sc_recorder_video_packet_sink_open, + .close = sc_recorder_video_packet_sink_close, + .push = sc_recorder_video_packet_sink_push, + }; - recorder->video_packet_sink.ops = &video_ops; + recorder->video_packet_sink.ops = &video_ops; + } if (audio) { static const struct sc_packet_sink_ops audio_ops = { diff --git a/app/src/recorder.h b/app/src/recorder.h index 41b8db6518..9c6cd4f9cf 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -27,6 +27,7 @@ struct sc_recorder { * may access it without data races. */ bool audio; + bool video; char *filename; enum sc_record_format format; @@ -59,7 +60,7 @@ struct sc_recorder_callbacks { bool sc_recorder_init(struct sc_recorder *recorder, const char *filename, - enum sc_record_format format, bool audio, + enum sc_record_format format, bool video, bool audio, const struct sc_recorder_callbacks *cbs, void *cbs_userdata); bool diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index efa69d31aa..39a2f1a481 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -137,7 +137,7 @@ sdl_set_hints(const char *render_driver) { } static void -sdl_configure(bool display, bool disable_screensaver) { +sdl_configure(bool mirror, bool disable_screensaver) { #ifdef _WIN32 // Clean up properly on Ctrl+C on Windows bool ok = SetConsoleCtrlHandler(windows_ctrl_handler, TRUE); @@ -146,7 +146,7 @@ sdl_configure(bool display, bool disable_screensaver) { } #endif // _WIN32 - if (!display) { + if (!mirror) { return; } @@ -345,6 +345,7 @@ scrcpy(struct scrcpy_options *options) { .lock_video_orientation = options->lock_video_orientation, .control = options->control, .display_id = options->display_id, + .video = options->video, .audio = options->audio, .show_touches = options->show_touches, .stay_awake = options->stay_awake, @@ -385,13 +386,11 @@ scrcpy(struct scrcpy_options *options) { goto end; } - if (options->display) { + if (options->mirror) { sdl_set_hints(options->render_driver); - } - // Initialize SDL video in addition if display is enabled - if (options->display) { - if (SDL_Init(SDL_INIT_VIDEO)) { + // Initialize SDL video and audio in addition if mirroring is enabled + if (options->video && SDL_Init(SDL_INIT_VIDEO)) { LOGE("Could not initialize SDL video: %s", SDL_GetError()); goto end; } @@ -402,7 +401,7 @@ scrcpy(struct scrcpy_options *options) { } } - sdl_configure(options->display, options->disable_screensaver); + sdl_configure(options->mirror, options->disable_screensaver); // Await for server without blocking Ctrl+C handling bool connected; @@ -428,7 +427,8 @@ scrcpy(struct scrcpy_options *options) { struct sc_file_pusher *fp = NULL; - if (options->display && options->control) { + assert(!options->control || options->mirror); // control implies mirror + if (options->control) { if (!sc_file_pusher_init(&s->file_pusher, serial, options->push_target)) { goto end; @@ -437,11 +437,13 @@ scrcpy(struct scrcpy_options *options) { file_pusher_initialized = true; } - static const struct sc_demuxer_callbacks video_demuxer_cbs = { - .on_ended = sc_video_demuxer_on_ended, - }; - sc_demuxer_init(&s->video_demuxer, "video", s->server.video_socket, - &video_demuxer_cbs, NULL); + if (options->video) { + static const struct sc_demuxer_callbacks video_demuxer_cbs = { + .on_ended = sc_video_demuxer_on_ended, + }; + sc_demuxer_init(&s->video_demuxer, "video", s->server.video_socket, + &video_demuxer_cbs, NULL); + } if (options->audio) { static const struct sc_demuxer_callbacks audio_demuxer_cbs = { @@ -451,8 +453,8 @@ scrcpy(struct scrcpy_options *options) { &audio_demuxer_cbs, options); } - bool needs_video_decoder = options->display; - bool needs_audio_decoder = options->audio && options->display; + bool needs_video_decoder = options->mirror && options->video; + bool needs_audio_decoder = options->mirror && options->audio; #ifdef HAVE_V4L2 needs_video_decoder |= !!options->v4l2_device; #endif @@ -472,8 +474,8 @@ scrcpy(struct scrcpy_options *options) { .on_ended = sc_recorder_on_ended, }; if (!sc_recorder_init(&s->recorder, options->record_filename, - options->record_format, options->audio, - &recorder_cbs, NULL)) { + options->record_format, options->video, + options->audio, &recorder_cbs, NULL)) { goto end; } recorder_initialized = true; @@ -483,8 +485,10 @@ scrcpy(struct scrcpy_options *options) { } recorder_started = true; - sc_packet_source_add_sink(&s->video_demuxer.packet_source, - &s->recorder.video_packet_sink); + if (options->video) { + sc_packet_source_add_sink(&s->video_demuxer.packet_source, + &s->recorder.video_packet_sink); + } if (options->audio) { sc_packet_source_add_sink(&s->audio_demuxer.packet_source, &s->recorder.audio_packet_sink); @@ -646,7 +650,7 @@ scrcpy(struct scrcpy_options *options) { // There is a controller if and only if control is enabled assert(options->control == !!controller); - if (options->display) { + if (options->mirror) { const char *window_title = options->window_title ? options->window_title : info->device_name; @@ -672,11 +676,6 @@ scrcpy(struct scrcpy_options *options) { .start_fps_counter = options->start_fps_counter, }; - if (!sc_screen_init(&s->screen, &screen_params)) { - goto end; - } - screen_initialized = true; - struct sc_frame_source *src = &s->video_decoder.frame_source; if (options->display_buffer) { sc_delay_buffer_init(&s->display_buffer, options->display_buffer, @@ -685,7 +684,14 @@ scrcpy(struct scrcpy_options *options) { src = &s->display_buffer.frame_source; } - sc_frame_source_add_sink(src, &s->screen.frame_sink); + if (options->video) { + if (!sc_screen_init(&s->screen, &screen_params)) { + goto end; + } + screen_initialized = true; + + sc_frame_source_add_sink(src, &s->screen.frame_sink); + } if (options->audio) { sc_audio_player_init(&s->audio_player, options->audio_buffer, @@ -714,12 +720,15 @@ scrcpy(struct scrcpy_options *options) { } #endif - // now we consumed the header values, the socket receives the video stream - // start the video demuxer - if (!sc_demuxer_start(&s->video_demuxer)) { - goto end; + // Now that the header values have been consumed, the socket(s) will + // receive the stream(s). Start the demuxer(s). + + if (options->video) { + if (!sc_demuxer_start(&s->video_demuxer)) { + goto end; + } + video_demuxer_started = true; } - video_demuxer_started = true; if (options->audio) { if (!sc_demuxer_start(&s->audio_demuxer)) { diff --git a/app/src/server.c b/app/src/server.c index 8c4e9a95dc..9c554760db 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -226,12 +226,16 @@ execute_server(struct sc_server *server, ADD_PARAM("scid=%08x", params->scid); ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level)); + if (!params->video) { + ADD_PARAM("video=false"); + } if (params->video_bit_rate) { ADD_PARAM("video_bit_rate=%" PRIu32, params->video_bit_rate); } if (!params->audio) { ADD_PARAM("audio=false"); - } else if (params->audio_bit_rate) { + } + if (params->audio_bit_rate) { ADD_PARAM("audio_bit_rate=%" PRIu32, params->audio_bit_rate); } if (params->video_codec != SC_CODEC_H264) { @@ -463,6 +467,7 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) { const char *serial = server->serial; assert(serial); + bool video = server->params.video; bool audio = server->params.audio; bool control = server->params.control; @@ -470,9 +475,12 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) { sc_socket audio_socket = SC_SOCKET_NONE; sc_socket control_socket = SC_SOCKET_NONE; if (!tunnel->forward) { - video_socket = net_accept_intr(&server->intr, tunnel->server_socket); - if (video_socket == SC_SOCKET_NONE) { - goto fail; + if (video) { + video_socket = + net_accept_intr(&server->intr, tunnel->server_socket); + if (video_socket == SC_SOCKET_NONE) { + goto fail; + } } if (audio) { @@ -503,35 +511,45 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) { unsigned attempts = 100; sc_tick delay = SC_TICK_FROM_MS(100); - video_socket = connect_to_server(server, attempts, delay, tunnel_host, - tunnel_port); - if (video_socket == SC_SOCKET_NONE) { + sc_socket first_socket = connect_to_server(server, attempts, delay, + tunnel_host, tunnel_port); + if (first_socket == SC_SOCKET_NONE) { goto fail; } + if (video) { + video_socket = first_socket; + } + if (audio) { - audio_socket = net_socket(); - if (audio_socket == SC_SOCKET_NONE) { - goto fail; - } - bool ok = net_connect_intr(&server->intr, audio_socket, tunnel_host, - tunnel_port); - if (!ok) { - goto fail; + if (!video) { + audio_socket = first_socket; + } else { + audio_socket = net_socket(); + if (audio_socket == SC_SOCKET_NONE) { + goto fail; + } + bool ok = net_connect_intr(&server->intr, audio_socket, tunnel_host, + tunnel_port); + if (!ok) { + goto fail; + } } } if (control) { - // we know that the device is listening, we don't need several - // attempts - control_socket = net_socket(); - if (control_socket == SC_SOCKET_NONE) { - goto fail; - } - bool ok = net_connect_intr(&server->intr, control_socket, - tunnel_host, tunnel_port); - if (!ok) { - goto fail; + if (!video && !audio) { + control_socket = first_socket; + } else { + control_socket = net_socket(); + if (control_socket == SC_SOCKET_NONE) { + goto fail; + } + bool ok = net_connect_intr(&server->intr, control_socket, + tunnel_host, tunnel_port); + if (!ok) { + goto fail; + } } } } @@ -540,13 +558,17 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) { sc_adb_tunnel_close(tunnel, &server->intr, serial, server->device_socket_name); + sc_socket first_socket = video ? video_socket + : audio ? audio_socket + : control_socket; + // The sockets will be closed on stop if device_read_info() fails - bool ok = device_read_info(&server->intr, video_socket, info); + bool ok = device_read_info(&server->intr, first_socket, info); if (!ok) { goto fail; } - assert(video_socket != SC_SOCKET_NONE); + assert(!video || video_socket != SC_SOCKET_NONE); assert(!audio || audio_socket != SC_SOCKET_NONE); assert(!control || control_socket != SC_SOCKET_NONE); @@ -930,8 +952,11 @@ run_server(void *data) { sc_mutex_unlock(&server->mutex); // Interrupt sockets to wake up socket blocking calls on the server - assert(server->video_socket != SC_SOCKET_NONE); - net_interrupt(server->video_socket); + + if (server->video_socket != SC_SOCKET_NONE) { + // There is no video_socket if --no-video is set + net_interrupt(server->video_socket); + } if (server->audio_socket != SC_SOCKET_NONE) { // There is no audio_socket if --no-audio is set diff --git a/app/src/server.h b/app/src/server.h index c425856b73..316484456a 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -41,6 +41,7 @@ struct sc_server_params { int8_t lock_video_orientation; bool control; uint32_t display_id; + bool video; bool audio; bool show_touches; bool stay_awake; diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c index 3e9a248a31..1f00cb90c1 100644 --- a/app/tests/test_cli.c +++ b/app/tests/test_cli.c @@ -53,7 +53,7 @@ static void test_options(void) { "--max-size", "1024", "--lock-video-orientation=2", // optional arguments require '=' // "--no-control" is not compatible with "--turn-screen-off" - // "--no-display" is not compatible with "--fulscreen" + // "--no-mirror" is not compatible with "--fulscreen" "--port", "1234:1236", "--push-target", "/sdcard/Movies", "--record", "file", @@ -108,8 +108,8 @@ static void test_options2(void) { char *argv[] = { "scrcpy", "--no-control", - "--no-display", - "--record", "file.mp4", // cannot enable --no-display without recording + "--no-mirror", + "--record", "file.mp4", // cannot enable --no-mirror without recording }; bool ok = scrcpy_parse_args(&args, ARRAY_LEN(argv), argv); @@ -117,7 +117,7 @@ static void test_options2(void) { const struct scrcpy_options *opts = &args.opts; assert(!opts->control); - assert(!opts->display); + assert(!opts->mirror); assert(!strcmp(opts->record_filename, "file.mp4")); assert(opts->record_format == SC_RECORD_FORMAT_MP4); } diff --git a/cross_win32.txt b/cross_win32.txt index 18834af48c..e8e8c7e8b9 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -16,6 +16,6 @@ cpu = 'i686' endian = 'little' [properties] -prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-2/win32' +prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-3/win32' prebuilt_sdl2 = 'SDL2-2.26.4/i686-w64-mingw32' prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-Win32' diff --git a/cross_win64.txt b/cross_win64.txt index 1c7c087522..d1de3c2189 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -16,6 +16,6 @@ cpu = 'x86_64' endian = 'little' [properties] -prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-2/win64' +prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-3/win64' prebuilt_sdl2 = 'SDL2-2.26.4/x86_64-w64-mingw32' prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-x64' diff --git a/doc/audio.md b/doc/audio.md index 6e97b103e6..08868eaf81 100644 --- a/doc/audio.md +++ b/doc/audio.md @@ -24,6 +24,21 @@ To disable audio: scrcpy --no-audio ``` +## Audio only + +To play audio only, disable the video: + +``` +scrcpy --no-video +``` + +Without video, the audio latency is typically not criticial, so it might be +interesting to add [buffering](#buffering) to minimize glitches: + +``` +scrcpy --no-video --audio-buffer=200 +``` + ## Codec The audio codec can be selected. The possible values are `opus` (default), `aac` diff --git a/doc/recording.md b/doc/recording.md index 4aad088c7e..7d66f2ff34 100644 --- a/doc/recording.md +++ b/doc/recording.md @@ -13,12 +13,18 @@ To record only the video: scrcpy --no-audio --record=file.mp4 ``` -_It is currently not possible to record only the audio._ +To record only the audio: + +```bash +scrcpy --no-video --record=file.opus +scrcpy --no-video --audio-codec=aac --record-file=file.aac +# .m4a/.mp4 and .mka/.mkv are also supported for both opus and aac +``` To disable mirroring while recording: ```bash -scrcpy --no-display --record=file.mp4 +scrcpy --no-mirror --record=file.mp4 scrcpy -Nr file.mkv # interrupt recording with Ctrl+C ``` diff --git a/doc/v4l2.md b/doc/v4l2.md index ea8c0eed7d..2c6e2cfcd6 100644 --- a/doc/v4l2.md +++ b/doc/v4l2.md @@ -35,7 +35,7 @@ To start `scrcpy` using a v4l2 sink: ```bash scrcpy --v4l2-sink=/dev/videoN -scrcpy --v4l2-sink=/dev/videoN --no-display # disable mirroring window +scrcpy --v4l2-sink=/dev/videoN --no-mirror # disable mirroring window ``` (replace `N` with the device ID, check with `ls /dev/video*`) diff --git a/doc/video.md b/doc/video.md index a2e9d10674..303d9048bf 100644 --- a/doc/video.md +++ b/doc/video.md @@ -159,17 +159,27 @@ scrcpy --display-buffer=50 --v4l2-buffer=300 ``` -## No display +## No mirror It is possible to capture an Android device without displaying a mirroring window. This option is available if either [recording](recording.md) or [v4l2](#video4linux) is enabled: ```bash -scrcpy --v4l2-sink=/dev/video2 --no-display -scrcpy --record=file.mkv --no-display +scrcpy --v4l2-sink=/dev/video2 --no-mirror +scrcpy --record=file.mkv --no-mirror ``` + +## No video + +To disable video forwarding completely, so that only audio is forwarded: + +``` +scrcpy --no-video +``` + + ## Video4Linux See the dedicated [Video4Linux](v4l2.md) page. diff --git a/release.mk b/release.mk index 0f5cbe2429..9fbd67c640 100644 --- a/release.mk +++ b/release.mk @@ -94,11 +94,11 @@ dist-win32: build-server build-win32 cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)" cp app/data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)" cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/avutil-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/avcodec-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/zlib1.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-3/win32/bin/avutil-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-3/win32/bin/avcodec-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-3/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-3/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-3/win32/bin/zlib1.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.1/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" @@ -113,11 +113,11 @@ dist-win64: build-server build-win64 cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)" cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)" cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/avutil-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/avcodec-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/zlib1.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-3/win64/bin/avutil-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-3/win64/bin/avcodec-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-3/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-3/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-3/win64/bin/zlib1.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.1/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" diff --git a/server/src/main/java/com/genymobile/scrcpy/AsyncProcessor.java b/server/src/main/java/com/genymobile/scrcpy/AsyncProcessor.java index cbc435b0d3..b9b6745cbb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AsyncProcessor.java +++ b/server/src/main/java/com/genymobile/scrcpy/AsyncProcessor.java @@ -1,7 +1,16 @@ package com.genymobile.scrcpy; public interface AsyncProcessor { - void start(); + interface TerminationListener { + /** + * Notify processor termination + * + * @param fatalError {@code true} if this must cause the termination of the whole scrcpy-server. + */ + void onTerminated(boolean fatalError); + } + + void start(TerminationListener listener); void stop(); void join() throws InterruptedException; } diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java index f2bba7728e..a1abd71b20 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java @@ -114,21 +114,29 @@ private void outputThread(MediaCodec mediaCodec) throws IOException, Interrupted } } - public void start() { + @Override + public void start(TerminationListener listener) { thread = new Thread(() -> { + boolean fatalError = false; try { encode(); - } catch (ConfigurationException | AudioCaptureForegroundException e) { + } catch (ConfigurationException e) { + // Do not print stack trace, a user-friendly error-message has already been logged + fatalError = true; + } catch (AudioCaptureForegroundException e) { // Do not print stack trace, a user-friendly error-message has already been logged } catch (IOException e) { Ln.e("Audio encoding error", e); + fatalError = true; } finally { Ln.d("Audio encoder stopped"); + listener.onTerminated(fatalError); } }); thread.start(); } + @Override public void stop() { if (thread != null) { // Just wake up the blocking wait from the thread, so that it properly releases all its resources and terminates @@ -136,6 +144,7 @@ public void stop() { } } + @Override public void join() throws InterruptedException { if (thread != null) { thread.join(); diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java b/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java index f98440de80..685ac3bd7c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java @@ -53,27 +53,33 @@ private void record() throws IOException, AudioCaptureForegroundException { } } - public void start() { + @Override + public void start(TerminationListener listener) { thread = new Thread(() -> { + boolean fatalError = false; try { record(); } catch (AudioCaptureForegroundException e) { // Do not print stack trace, a user-friendly error-message has already been logged } catch (IOException e) { Ln.e("Audio recording error", e); + fatalError = true; } finally { Ln.d("Audio recorder stopped"); + listener.onTerminated(fatalError); } }); thread.start(); } + @Override public void stop() { if (thread != null) { thread.interrupt(); } } + @Override public void join() throws InterruptedException { if (thread != null) { thread.join(); diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 59fae60246..9a4e275a1b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -84,7 +84,8 @@ private void control() throws IOException { } } - public void start() { + @Override + public void start(TerminationListener listener) { thread = new Thread(() -> { try { control(); @@ -92,12 +93,14 @@ public void start() { // this is expected on close } finally { Ln.d("Controller stopped"); + listener.onTerminated(true); } }); thread.start(); sender.start(); } + @Override public void stop() { if (thread != null) { thread.interrupt(); @@ -105,6 +108,7 @@ public void stop() { sender.stop(); } + @Override public void join() throws InterruptedException { if (thread != null) { thread.join(); diff --git a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java index 4bfff7260f..20ab1f9c8d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java +++ b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java @@ -41,7 +41,7 @@ private DesktopConnection(LocalSocket videoSocket, LocalSocket audioSocket, Loca controlInputStream = null; controlOutputStream = null; } - videoFd = videoSocket.getFileDescriptor(); + videoFd = videoSocket != null ? videoSocket.getFileDescriptor() : null; audioFd = audioSocket != null ? audioSocket.getFileDescriptor() : null; } @@ -60,29 +60,43 @@ private static String getSocketName(int scid) { return SOCKET_NAME_PREFIX + String.format("_%08x", scid); } - public static DesktopConnection open(int scid, boolean tunnelForward, boolean audio, boolean control, boolean sendDummyByte) throws IOException { + public static DesktopConnection open(int scid, boolean tunnelForward, boolean video, boolean audio, boolean control, boolean sendDummyByte) + throws IOException { String socketName = getSocketName(scid); + LocalSocket firstSocket = null; + LocalSocket videoSocket = null; LocalSocket audioSocket = null; LocalSocket controlSocket = null; try { if (tunnelForward) { try (LocalServerSocket localServerSocket = new LocalServerSocket(socketName)) { - videoSocket = localServerSocket.accept(); - if (sendDummyByte) { - // send one byte so the client may read() to detect a connection error - videoSocket.getOutputStream().write(0); + if (video) { + videoSocket = localServerSocket.accept(); + firstSocket = videoSocket; } if (audio) { audioSocket = localServerSocket.accept(); + if (firstSocket == null) { + firstSocket = audioSocket; + } } if (control) { controlSocket = localServerSocket.accept(); + if (firstSocket == null) { + firstSocket = controlSocket; + } + } + if (sendDummyByte) { + // send one byte so the client may read() to detect a connection error + firstSocket.getOutputStream().write(0); } } } else { - videoSocket = connect(socketName); + if (video) { + videoSocket = connect(socketName); + } if (audio) { audioSocket = connect(socketName); } @@ -106,10 +120,22 @@ public static DesktopConnection open(int scid, boolean tunnelForward, boolean au return new DesktopConnection(videoSocket, audioSocket, controlSocket); } + private LocalSocket getFirstSocket() { + if (videoSocket != null) { + return videoSocket; + } + if (audioSocket != null) { + return audioSocket; + } + return controlSocket; + } + public void close() throws IOException { - videoSocket.shutdownInput(); - videoSocket.shutdownOutput(); - videoSocket.close(); + if (videoSocket != null) { + videoSocket.shutdownInput(); + videoSocket.shutdownOutput(); + videoSocket.close(); + } if (audioSocket != null) { audioSocket.shutdownInput(); audioSocket.shutdownOutput(); @@ -130,7 +156,8 @@ public void sendDeviceMeta(String deviceName) throws IOException { System.arraycopy(deviceNameBytes, 0, buffer, 0, len); // byte[] are always 0-initialized in java, no need to set '\0' explicitly - IO.writeFully(videoFd, buffer, 0, buffer.length); + FileDescriptor fd = getFirstSocket().getFileDescriptor(); + IO.writeFully(fd, buffer, 0, buffer.length); } public FileDescriptor getVideoFd() { diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index a34eb9b573..7bd94cb39a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -9,6 +9,7 @@ public class Options { private Ln.Level logLevel = Ln.Level.DEBUG; private int scid = -1; // 31-bit non-negative value, or -1 + private boolean video = true; private boolean audio = true; private int maxSize; private VideoCodec videoCodec = VideoCodec.H264; @@ -51,6 +52,10 @@ public int getScid() { return scid; } + public boolean getVideo() { + return video; + } + public boolean getAudio() { return audio; } @@ -200,6 +205,9 @@ public static Options parse(String... args) { case "log_level": options.logLevel = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH)); break; + case "video": + options.video = Boolean.parseBoolean(value); + break; case "audio": options.audio = Boolean.parseBoolean(value); break; diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 528cd32792..901ba94c43 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -16,7 +16,7 @@ import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; -public class ScreenEncoder implements Device.RotationListener { +public class ScreenEncoder implements Device.RotationListener, AsyncProcessor { private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms @@ -39,6 +39,9 @@ public class ScreenEncoder implements Device.RotationListener { private boolean firstFrameSent; private int consecutiveErrors; + private Thread thread; + private final AtomicBoolean stopped = new AtomicBoolean(); + public ScreenEncoder(Device device, Streamer streamer, int videoBitRate, int maxFps, List codecOptions, String encoderName, boolean downsizeOnError) { this.device = device; @@ -55,11 +58,11 @@ public void onRotationChanged(int rotation) { rotationChanged.set(true); } - public boolean consumeRotationChange() { + private boolean consumeRotationChange() { return rotationChanged.getAndSet(false); } - public void streamScreen() throws IOException, ConfigurationException { + private void streamScreen() throws IOException, ConfigurationException { Codec codec = streamer.getCodec(); MediaCodec mediaCodec = createMediaCodec(codec, encoderName); MediaFormat format = createFormat(codec.getMimeType(), videoBitRate, maxFps, codecOptions); @@ -163,9 +166,14 @@ private static int chooseMaxSizeFallback(Size failedSize) { private boolean encode(MediaCodec codec, Streamer streamer) throws IOException { boolean eof = false; + boolean alive = true; MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); while (!consumeRotationChange() && !eof) { + if (stopped.get()) { + alive = false; + break; + } int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1); try { if (consumeRotationChange()) { @@ -193,7 +201,7 @@ private boolean encode(MediaCodec codec, Streamer streamer) throws IOException { } } - return !eof; + return !eof && alive; } private static MediaCodec createMediaCodec(Codec codec, String encoderName) throws IOException, ConfigurationException { @@ -267,4 +275,38 @@ private static void setDisplaySurface(IBinder display, Surface surface, int orie SurfaceControl.closeTransaction(); } } + + @Override + public void start(TerminationListener listener) { + thread = new Thread(() -> { + try { + streamScreen(); + } catch (ConfigurationException e) { + // Do not print stack trace, a user-friendly error-message has already been logged + } catch (IOException e) { + // Broken pipe is expected on close, because the socket is closed by the client + if (!IO.isBrokenPipe(e)) { + Ln.e("Video encoding error", e); + } + } finally { + Ln.d("Screen streaming stopped"); + listener.onTerminated(true); + } + }); + thread.start(); + } + + @Override + public void stop() { + if (thread != null) { + stopped.set(true); + } + } + + @Override + public void join() throws InterruptedException { + if (thread != null) { + thread.join(); + } + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 067b16709e..db99383026 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -9,6 +9,35 @@ public final class Server { + private static class Completion { + private int running; + private boolean fatalError; + + Completion(int running) { + this.running = running; + } + + synchronized void addCompleted(boolean fatalError) { + --running; + if (fatalError) { + this.fatalError = true; + } + if (running == 0 || this.fatalError) { + notify(); + } + } + + synchronized void await() { + try { + while (running > 0 && !fatalError) { + wait(); + } + } catch (InterruptedException e) { + // ignore + } + } + } + private Server() { // not instantiable } @@ -66,6 +95,7 @@ private static void scrcpy(Options options) throws IOException, ConfigurationExc int scid = options.getScid(); boolean tunnelForward = options.isTunnelForward(); boolean control = options.getControl(); + boolean video = options.getVideo(); boolean audio = options.getAudio(); boolean sendDummyByte = options.getSendDummyByte(); @@ -92,7 +122,8 @@ private static void scrcpy(Options options) throws IOException, ConfigurationExc List asyncProcessors = new ArrayList<>(); - try (DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, audio, control, sendDummyByte)) { + DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, video, audio, control, sendDummyByte); + try { if (options.getSendDeviceMeta()) { connection.sendDeviceMeta(Device.getDeviceName()); } @@ -117,26 +148,23 @@ private static void scrcpy(Options options) throws IOException, ConfigurationExc asyncProcessors.add(audioRecorder); } - Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendCodecMeta(), - options.getSendFrameMeta()); - ScreenEncoder screenEncoder = new ScreenEncoder(device, videoStreamer, options.getVideoBitRate(), options.getMaxFps(), - options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError()); + if (video) { + Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendCodecMeta(), + options.getSendFrameMeta()); + ScreenEncoder screenEncoder = new ScreenEncoder(device, videoStreamer, options.getVideoBitRate(), options.getMaxFps(), + options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError()); + asyncProcessors.add(screenEncoder); + } + Completion completion = new Completion(asyncProcessors.size()); for (AsyncProcessor asyncProcessor : asyncProcessors) { - asyncProcessor.start(); + asyncProcessor.start((fatalError) -> { + completion.addCompleted(fatalError); + }); } - try { - // synchronous - screenEncoder.streamScreen(); - } catch (IOException e) { - // Broken pipe is expected on close, because the socket is closed by the client - if (!IO.isBrokenPipe(e)) { - Ln.e("Video encoding error", e); - } - } + completion.await(); } finally { - Ln.d("Screen streaming stopped"); initThread.interrupt(); for (AsyncProcessor asyncProcessor : asyncProcessors) { asyncProcessor.stop(); @@ -150,6 +178,8 @@ private static void scrcpy(Options options) throws IOException, ConfigurationExc } catch (InterruptedException e) { // ignore } + + connection.close(); } }