From 6928acdeac29eee404a7c7014654965ef5128b88 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 5 May 2023 23:43:14 +0200 Subject: [PATCH 01/14] Rename --no-display to --no-mirror The option impacts both video and audio playback, so "no display" is not an appropriate name. PR #3978 --- app/data/bash-completion/scrcpy | 2 +- app/data/zsh-completion/_scrcpy | 2 +- app/scrcpy.1 | 4 ++-- app/src/cli.c | 27 ++++++++++++++++++--------- app/src/options.c | 2 +- app/src/options.h | 2 +- app/src/scrcpy.c | 20 +++++++++----------- app/tests/test_cli.c | 8 ++++---- doc/recording.md | 2 +- doc/v4l2.md | 2 +- doc/video.md | 6 +++--- 11 files changed, 42 insertions(+), 35 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index ae516c348a..a0fca23db8 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -33,7 +33,7 @@ _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 diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 97bf4f3eb5..ccb51a2c5d 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -39,7 +39,7 @@ 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]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 3749721178..6ef0168076 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 diff --git a/app/src/cli.c b/app/src/cli.c index d6d9f41dd9..1ba7fe1f28 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -72,6 +72,7 @@ enum { OPT_REQUIRE_AUDIO, OPT_AUDIO_BUFFER, OPT_AUDIO_OUTPUT_BUFFER, + OPT_NO_DISPLAY, }; struct sc_option { @@ -380,9 +381,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, @@ -1642,8 +1648,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)) { @@ -1890,8 +1899,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 +1924,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; } diff --git a/app/src/options.c b/app/src/options.c index 8b99f6f3ad..eec8171600 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, diff --git a/app/src/options.h b/app/src/options.h index c41e275703..3bb0c91ef2 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -135,7 +135,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; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index efa69d31aa..2c7fbf30a3 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; } @@ -385,12 +385,10 @@ 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) { + // Initialize SDL video and audio in addition if mirroring is enabled if (SDL_Init(SDL_INIT_VIDEO)) { LOGE("Could not initialize SDL video: %s", SDL_GetError()); goto end; @@ -402,7 +400,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 +426,7 @@ scrcpy(struct scrcpy_options *options) { struct sc_file_pusher *fp = NULL; - if (options->display && options->control) { + if (options->mirror && options->control) { if (!sc_file_pusher_init(&s->file_pusher, serial, options->push_target)) { goto end; @@ -451,8 +449,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; + bool needs_audio_decoder = options->mirror && options->audio; #ifdef HAVE_V4L2 needs_video_decoder |= !!options->v4l2_device; #endif @@ -646,7 +644,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; 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/doc/recording.md b/doc/recording.md index 4aad088c7e..9455a6fc91 100644 --- a/doc/recording.md +++ b/doc/recording.md @@ -18,7 +18,7 @@ _It is currently not possible to record only the audio._ 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..58aa502201 100644 --- a/doc/video.md +++ b/doc/video.md @@ -159,15 +159,15 @@ 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 ``` ## Video4Linux From 92483fe11b6fd6bae5ef775ccaff78fefa92aad4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 5 May 2023 23:45:55 +0200 Subject: [PATCH 02/14] Disable controls on --no-mirror If mirroring is disabled, control must also be disabled. PR #3978 --- app/src/cli.c | 9 +++++++++ app/src/scrcpy.c | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/src/cli.c b/app/src/cli.c index 1ba7fe1f28..066f46fc82 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -2041,6 +2041,15 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } #endif +#ifdef HAVE_USB + if (!opts->mirror && opts->control && !opts->otg) { +#else + if (!opts->mirror && opts->control) { +#endif + LOGD("Mirroring is disabled, force --no-control"); + opts->control = false; + } + return true; } diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 2c7fbf30a3..03b643f6af 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -426,7 +426,8 @@ scrcpy(struct scrcpy_options *options) { struct sc_file_pusher *fp = NULL; - if (options->mirror && 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; From 9c08eb79cb7941848882cb908cefee9933450de5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 9 Apr 2023 19:12:39 +0200 Subject: [PATCH 03/14] Close connection at the end of finally-block The async processors use the socket file descriptors from the connection. Therefore, the connection must not be closed before all async processor threads are joined. PR #3978 --- server/src/main/java/com/genymobile/scrcpy/Server.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 067b16709e..fade721474 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -92,7 +92,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, audio, control, sendDummyByte); + try { if (options.getSendDeviceMeta()) { connection.sendDeviceMeta(Device.getDeviceName()); } @@ -150,6 +151,8 @@ private static void scrcpy(Options options) throws IOException, ConfigurationExc } catch (InterruptedException e) { // ignore } + + connection.close(); } } From 751a3653a0ab36b98d29da689f26d315c8426701 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 9 Apr 2023 15:18:31 +0200 Subject: [PATCH 04/14] Add missing @Override annotations PR #3978 --- server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java | 3 +++ .../src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java | 3 +++ server/src/main/java/com/genymobile/scrcpy/Controller.java | 3 +++ 3 files changed, 9 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java index f2bba7728e..ac2f0a31ca 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java @@ -114,6 +114,7 @@ private void outputThread(MediaCodec mediaCodec) throws IOException, Interrupted } } + @Override public void start() { thread = new Thread(() -> { try { @@ -129,6 +130,7 @@ public void start() { 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 +138,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..32efc35419 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java @@ -53,6 +53,7 @@ private void record() throws IOException, AudioCaptureForegroundException { } } + @Override public void start() { thread = new Thread(() -> { try { @@ -68,12 +69,14 @@ public void start() { 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..ab09c33640 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -84,6 +84,7 @@ private void control() throws IOException { } } + @Override public void start() { thread = new Thread(() -> { try { @@ -98,6 +99,7 @@ public void start() { sender.start(); } + @Override public void stop() { if (thread != null) { thread.interrupt(); @@ -105,6 +107,7 @@ public void stop() { sender.stop(); } + @Override public void join() throws InterruptedException { if (thread != null) { thread.join(); From feab87053abcceded41342d9d856763dedc09187 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 9 Apr 2023 15:17:54 +0200 Subject: [PATCH 05/14] Convert screen encoder to async processor Contrary to the other tasks (controller and audio capture/encoding), the screen encoder was executed synchronously. As a consequence, scrcpy-server could not terminate until the screen encoder returned. Convert it to an async processor. This allows to terminate on controller error, and this paves the way to disable video mirroring. PR #3978 --- .../com/genymobile/scrcpy/AsyncProcessor.java | 11 +++- .../com/genymobile/scrcpy/AudioEncoder.java | 10 +++- .../genymobile/scrcpy/AudioRawRecorder.java | 5 +- .../com/genymobile/scrcpy/Controller.java | 3 +- .../com/genymobile/scrcpy/ScreenEncoder.java | 50 +++++++++++++++++-- .../java/com/genymobile/scrcpy/Server.java | 46 +++++++++++++---- 6 files changed, 105 insertions(+), 20 deletions(-) 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 ac2f0a31ca..a1abd71b20 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java @@ -115,16 +115,22 @@ private void outputThread(MediaCodec mediaCodec) throws IOException, Interrupted } @Override - public void start() { + 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(); diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java b/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java index 32efc35419..685ac3bd7c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java @@ -54,16 +54,19 @@ private void record() throws IOException, AudioCaptureForegroundException { } @Override - public void start() { + 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(); diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index ab09c33640..9a4e275a1b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -85,7 +85,7 @@ private void control() throws IOException { } @Override - public void start() { + public void start(TerminationListener listener) { thread = new Thread(() -> { try { control(); @@ -93,6 +93,7 @@ public void start() { // this is expected on close } finally { Ln.d("Controller stopped"); + listener.onTerminated(true); } }); thread.start(); 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 fade721474..4d72d1e89b 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 } @@ -122,22 +151,17 @@ private static void scrcpy(Options options) throws IOException, ConfigurationExc 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(); From e89e772c7c3df65e33362a542cd900f46cf62baf Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 7 May 2023 12:02:45 +0200 Subject: [PATCH 06/14] Remove unnecessary 'else' Some server parameters may depend on one another. For example, audio_bit_rate is meaningless if audio is false. But it is inconsistent to disable some parameters based on these dependencies checks, but not others. Handling all dependencies between parameters would add too much complexity for no benefit. So just pass individual parameters independently. PR #3978 --- app/src/server.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/server.c b/app/src/server.c index 8c4e9a95dc..2b35c085cf 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -231,7 +231,8 @@ execute_server(struct sc_server *server, } 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) { From 8c650e53cd37a53d5c3aa746c30a71c6b742a4e2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 7 May 2023 12:08:50 +0200 Subject: [PATCH 07/14] Add --no-video Similar to --no-audio, add --no-video to play audio only. Fixes #3842 PR #3978 --- app/data/bash-completion/scrcpy | 1 + app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 4 + app/src/cli.c | 21 ++++- app/src/options.c | 1 + app/src/options.h | 1 + app/src/recorder.c | 69 ++++++++++------ app/src/recorder.h | 3 +- app/src/scrcpy.c | 54 ++++++++----- app/src/server.c | 80 ++++++++++++------- app/src/server.h | 1 + doc/audio.md | 15 ++++ doc/recording.md | 6 +- doc/video.md | 10 +++ .../genymobile/scrcpy/DesktopConnection.java | 49 +++++++++--- .../java/com/genymobile/scrcpy/Options.java | 8 ++ .../java/com/genymobile/scrcpy/Server.java | 15 ++-- 17 files changed, 243 insertions(+), 96 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index a0fca23db8..cdc9270fb4 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -37,6 +37,7 @@ _scrcpy() { --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 ccb51a2c5d..5e40b2fb9c 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -43,6 +43,7 @@ arguments=( '--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/scrcpy.1 b/app/scrcpy.1 index 6ef0168076..29d14b58db 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -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 066f46fc82..1558ef0c13 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -73,6 +73,7 @@ enum { OPT_AUDIO_BUFFER, OPT_AUDIO_OUTPUT_BUFFER, OPT_NO_DISPLAY, + OPT_NO_VIDEO, }; struct sc_option { @@ -407,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", @@ -1797,6 +1803,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; @@ -2042,14 +2051,20 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], #endif #ifdef HAVE_USB - if (!opts->mirror && opts->control && !opts->otg) { + if (!(opts->mirror && opts->video) && !opts->otg) { #else - if (!opts->mirror && opts->control) { + if (!(opts->mirror && opts->video)) { #endif - LOGD("Mirroring is disabled, force --no-control"); + // 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 eec8171600..122839523c 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -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 3bb0c91ef2..4edf3f3258 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -156,6 +156,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..5cbe6873e2 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -152,7 +152,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 +176,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 +184,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 +200,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 +224,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 +239,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 +271,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 +287,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 +333,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 +656,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 +679,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 +699,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 03b643f6af..39a2f1a481 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -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, @@ -389,7 +390,7 @@ scrcpy(struct scrcpy_options *options) { sdl_set_hints(options->render_driver); // Initialize SDL video and audio in addition if mirroring is enabled - if (SDL_Init(SDL_INIT_VIDEO)) { + if (options->video && SDL_Init(SDL_INIT_VIDEO)) { LOGE("Could not initialize SDL video: %s", SDL_GetError()); goto end; } @@ -436,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 = { @@ -450,7 +453,7 @@ scrcpy(struct scrcpy_options *options) { &audio_demuxer_cbs, options); } - bool needs_video_decoder = options->mirror; + 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; @@ -471,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; @@ -482,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); @@ -671,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, @@ -684,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, @@ -713,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 2b35c085cf..9c554760db 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -226,6 +226,9 @@ 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); } @@ -464,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; @@ -471,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) { @@ -504,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; + } } } } @@ -541,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); @@ -931,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/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 9455a6fc91..e3e2cf68ea 100644 --- a/doc/recording.md +++ b/doc/recording.md @@ -13,7 +13,11 @@ 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.mp4 +``` To disable mirroring while recording: diff --git a/doc/video.md b/doc/video.md index 58aa502201..303d9048bf 100644 --- a/doc/video.md +++ b/doc/video.md @@ -170,6 +170,16 @@ 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/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/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 4d72d1e89b..db99383026 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -95,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(); @@ -121,7 +122,7 @@ private static void scrcpy(Options options) throws IOException, ConfigurationExc List asyncProcessors = new ArrayList<>(); - 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()); @@ -147,11 +148,13 @@ 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()); - asyncProcessors.add(screenEncoder); + 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) { From be86e14e051a40c62837f690282f2214f467778f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 7 May 2023 12:18:27 +0200 Subject: [PATCH 08/14] Factorize record format parsing Convert either the filename extension or the explicit record format to a sc_record_format using the same function. PR #3978 --- app/src/cli.c | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 1558ef0c13..619f372e0c 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1479,18 +1479,27 @@ 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; + } + 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 @@ -1510,18 +1519,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 From 98f4f4e68a21dd072d83b1e474d71e6f28d9eb91 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 7 May 2023 12:22:01 +0200 Subject: [PATCH 09/14] Refactor command line checks Several checks are performed when opts->record_filename is not NULL. Group them in a single block. PR #3978 --- app/src/cli.c | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 619f372e0c..4b895cd253 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1959,19 +1959,21 @@ 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); - return false; + 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->record_filename && opts->audio_codec == SC_CODEC_RAW) { - LOGW("Recording does not support RAW audio codec"); - return false; + if (opts->audio_codec == SC_CODEC_RAW) { + LOGW("Recording does not support RAW audio codec"); + return false; + } } if (opts->audio_codec == SC_CODEC_RAW) { From d6bcde565f8155b6a51ce4682ada367fe62d5a18 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 7 May 2023 12:23:51 +0200 Subject: [PATCH 10/14] Accept .m4a and .mka These are just aliases for mp4 and mkv when there is no video stream. PR #3978 --- app/src/cli.c | 12 ++++++++++++ app/src/options.h | 8 ++++++++ app/src/recorder.c | 11 ++++++++--- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 4b895cd253..40fe242ec0 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1487,6 +1487,12 @@ get_record_format(const char *name) { 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; + } return 0; } @@ -1974,6 +1980,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], 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->audio_codec == SC_CODEC_RAW) { diff --git a/app/src/options.h b/app/src/options.h index 4edf3f3258..2424638fe1 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -21,8 +21,16 @@ enum sc_record_format { SC_RECORD_FORMAT_AUTO, SC_RECORD_FORMAT_MP4, SC_RECORD_FORMAT_MKV, + SC_RECORD_FORMAT_M4A, + SC_RECORD_FORMAT_MKA, }; +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; +} + enum sc_codec { SC_CODEC_H264, SC_CODEC_H265, diff --git a/app/src/recorder.c b/app/src/recorder.c index 5cbe6873e2..10102ec426 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -60,9 +60,14 @@ 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: + return "mp4"; + case SC_RECORD_FORMAT_MKV: + case SC_RECORD_FORMAT_MKA: + return "matroska"; + default: + return NULL; } } From 7321db6f28914df81e654cc4e2fbb3deec15fe2c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 7 May 2023 12:35:27 +0200 Subject: [PATCH 11/14] Add recording to opus file Use the FFmpeg opus muxer to record an opus file. PR #3978 --- app/src/cli.c | 10 ++++++++++ app/src/options.h | 4 +++- app/src/recorder.c | 2 ++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/app/src/cli.c b/app/src/cli.c index 40fe242ec0..ee0319eeca 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1493,6 +1493,9 @@ get_record_format(const char *name) { if (!strcmp(name, "mka")) { return SC_RECORD_FORMAT_MKA; } + if (!strcmp(name, "opus")) { + return SC_RECORD_FORMAT_OPUS; + } return 0; } @@ -1986,6 +1989,13 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], 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->audio_codec == SC_CODEC_RAW) { diff --git a/app/src/options.h b/app/src/options.h index 2424638fe1..0547b4ec38 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -23,12 +23,14 @@ enum sc_record_format { SC_RECORD_FORMAT_MKV, SC_RECORD_FORMAT_M4A, SC_RECORD_FORMAT_MKA, + SC_RECORD_FORMAT_OPUS, }; 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_MKA + || fmt == SC_RECORD_FORMAT_OPUS; } enum sc_codec { diff --git a/app/src/recorder.c b/app/src/recorder.c index 10102ec426..b0c41df095 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -66,6 +66,8 @@ sc_recorder_get_format_name(enum sc_record_format format) { case SC_RECORD_FORMAT_MKV: case SC_RECORD_FORMAT_MKA: return "matroska"; + case SC_RECORD_FORMAT_OPUS: + return "opus"; default: return NULL; } From b11b363e8ec1666927ca4931e24520daa8ef7ef3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 7 May 2023 12:38:32 +0200 Subject: [PATCH 12/14] Add recording to aac file It is just an alias for mp4. PR #3978 --- app/src/cli.c | 10 ++++++++++ app/src/options.h | 4 +++- app/src/recorder.c | 1 + 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/cli.c b/app/src/cli.c index ee0319eeca..e5b18277b9 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1496,6 +1496,9 @@ get_record_format(const char *name) { if (!strcmp(name, "opus")) { return SC_RECORD_FORMAT_OPUS; } + if (!strcmp(name, "aac")) { + return SC_RECORD_FORMAT_AAC; + } return 0; } @@ -1996,6 +1999,13 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], "(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) { diff --git a/app/src/options.h b/app/src/options.h index 0547b4ec38..7c442149de 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -24,13 +24,15 @@ enum sc_record_format { 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_OPUS + || fmt == SC_RECORD_FORMAT_AAC; } enum sc_codec { diff --git a/app/src/recorder.c b/app/src/recorder.c index b0c41df095..be1cbe719b 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -62,6 +62,7 @@ sc_recorder_get_format_name(enum sc_record_format format) { switch (format) { 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: From a166eee909a66385dc5b00169f4f61b490e163dd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 7 May 2023 12:46:44 +0200 Subject: [PATCH 13/14] Upgrade FFmpeg build to 6.0-scrcpy-3 Use a build which includes the opus muxer, to support recording to .opus files. Refs PR #3978 --- app/prebuilt-deps/prepare-ffmpeg.sh | 4 ++-- cross_win32.txt | 2 +- cross_win64.txt | 2 +- release.mk | 20 ++++++++++---------- 4 files changed, 14 insertions(+), 14 deletions(-) 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/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/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)/" From d50055021284bf0da49a5ff7810b48fe0f7c492d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 7 May 2023 13:04:11 +0200 Subject: [PATCH 14/14] Update audio recording documentation Document how to record audio-only to .opus and .aac files. PR #3978 --- doc/recording.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/recording.md b/doc/recording.md index e3e2cf68ea..7d66f2ff34 100644 --- a/doc/recording.md +++ b/doc/recording.md @@ -16,7 +16,9 @@ scrcpy --no-audio --record=file.mp4 To record only the audio: ```bash -scrcpy --no-video --record=file.mp4 +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: