diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index d5f129d0..e0928cbd 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -6,6 +6,7 @@ _scrcpy() { --audio-buffer= --audio-codec= --audio-codec-options= + --audio-dup --audio-encoder= --audio-source= --audio-output-buffer= diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index c49c24eb..0f06ba4b 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -13,6 +13,7 @@ arguments=( '--audio-buffer=[Configure the audio buffering delay (in milliseconds)]' '--audio-codec=[Select the audio codec]:codec:(opus aac flac raw)' '--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]' + '--audio-dup=[Duplicate audio]' '--audio-encoder=[Use a specific MediaCodec audio encoder]' '--audio-source=[Select the audio source]:source:(output mic playback)' '--audio-output-buffer=[Configure the size of the SDL audio output buffer (in milliseconds)]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 19b4ab6b..de2b8ac6 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -49,6 +49,12 @@ The list of possible codec options is available in the Android documentation: +.TP +.B \-\-audio\-dup +Duplicate audio (capture and keep playing on the device). + +This feature is only available with --audio-source=playback. + .TP .BI "\-\-audio\-encoder " name Use a specific MediaCodec audio encoder (depending on the codec provided by \fB\-\-audio\-codec\fR). diff --git a/app/src/cli.c b/app/src/cli.c index 1699e46d..1792384e 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -100,6 +100,7 @@ enum { OPT_NO_WINDOW, OPT_MOUSE_BIND, OPT_NO_MOUSE_HOVER, + OPT_AUDIO_DUP, }; struct sc_option { @@ -177,6 +178,13 @@ static const struct sc_option options[] = { "Android documentation: " "", }, + { + .longopt_id = OPT_AUDIO_DUP, + .longopt = "audio-dup", + .text = "Duplicate audio (capture and keep playing on the device).\n" + "This feature is only available with --audio-source=playback." + + }, { .longopt_id = OPT_AUDIO_ENCODER, .longopt = "audio-encoder", @@ -2615,6 +2623,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_NO_WINDOW: opts->window = false; break; + case OPT_AUDIO_DUP: + opts->audio_dup = true; + break; default: // getopt prints the error message on stderr return false; @@ -2891,6 +2902,18 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } } + if (opts->audio_dup) { + if (!opts->audio) { + LOGE("--audio-dup not supported if audio is disabled"); + return false; + } + + if (opts->audio_source != SC_AUDIO_SOURCE_PLAYBACK) { + LOGE("--audio-dup is specific to --audio-source=playback"); + return false; + } + } + if (opts->record_format && !opts->record_filename) { LOGE("Record format specified without recording"); return false; diff --git a/app/src/options.c b/app/src/options.c index 5eec6427..6fca6ad5 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -101,6 +101,7 @@ const struct scrcpy_options scrcpy_options_default = { .list = 0, .window = true, .mouse_hover = true, + .audio_dup = false, }; enum sc_orientation diff --git a/app/src/options.h b/app/src/options.h index 403685e3..140d12b1 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -297,6 +297,7 @@ struct scrcpy_options { uint8_t list; bool window; bool mouse_hover; + bool audio_dup; }; extern const struct scrcpy_options scrcpy_options_default; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 376d5839..43864661 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -394,6 +394,7 @@ scrcpy(struct scrcpy_options *options) { .display_id = options->display_id, .video = options->video, .audio = options->audio, + .audio_dup = options->audio_dup, .show_touches = options->show_touches, .stay_awake = options->stay_awake, .video_codec_options = options->video_codec_options, diff --git a/app/src/server.c b/app/src/server.c index e32aa556..0db29183 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -292,6 +292,9 @@ execute_server(struct sc_server *server, ADD_PARAM("audio_source=%s", sc_server_get_audio_source_name(params->audio_source)); } + if (params->audio_dup) { + ADD_PARAM("audio_dup=true"); + } if (params->max_size) { ADD_PARAM("max_size=%" PRIu16, params->max_size); } diff --git a/app/src/server.h b/app/src/server.h index 062af0a9..cffa510e 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -50,6 +50,7 @@ struct sc_server_params { uint32_t display_id; bool video; bool audio; + bool audio_dup; bool show_touches; bool stay_awake; bool force_adb_forward; diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 143fbb9a..2f86d8ce 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -26,6 +26,7 @@ public class Options { private AudioCodec audioCodec = AudioCodec.OPUS; private VideoSource videoSource = VideoSource.DISPLAY; private AudioSource audioSource = AudioSource.OUTPUT; + private boolean audioDup; private int videoBitRate = 8000000; private int audioBitRate = 128000; private int maxFps; @@ -100,6 +101,10 @@ public class Options { return audioSource; } + public boolean getAudioDup() { + return audioDup; + } + public int getVideoBitRate() { return videoBitRate; } @@ -303,6 +308,9 @@ public class Options { } options.audioSource = audioSource; break; + case "audio_dup": + options.audioDup = Boolean.parseBoolean(value); + break; case "max_size": options.maxSize = Integer.parseInt(value) & ~7; // multiple of 8 break; diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 75d9ea15..11429e6c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -167,7 +167,13 @@ public final class Server { if (audio) { AudioCodec audioCodec = options.getAudioCodec(); AudioSource audioSource = options.getAudioSource(); - AudioCapture audioCapture = audioSource.isDirect() ? new AudioDirectCapture(audioSource) : new AudioPlaybackCapture(); + AudioCapture audioCapture; + if (audioSource.isDirect()) { + audioCapture = new AudioDirectCapture(audioSource); + } else { + audioCapture = new AudioPlaybackCapture(options.getAudioDup()); + } + Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(), options.getSendFrameMeta()); AsyncProcessor audioRecorder; if (audioCodec == AudioCodec.RAW) { diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java index 2a0d23fb..e38493f2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java @@ -18,9 +18,15 @@ import java.nio.ByteBuffer; public final class AudioPlaybackCapture implements AudioCapture { + private final boolean keepPlayingOnDevice; + private AudioRecord recorder; private AudioRecordReader reader; + public AudioPlaybackCapture(boolean keepPlayingOnDevice) { + this.keepPlayingOnDevice = keepPlayingOnDevice; + } + @SuppressLint("PrivateApi") private AudioRecord createAudioRecord() throws AudioCaptureException { // See @@ -60,7 +66,8 @@ public final class AudioPlaybackCapture implements AudioCapture { Method setFormat = audioMixBuilder.getClass().getMethod("setFormat", AudioFormat.class); setFormat.invoke(audioMixBuilder, AudioConfig.createAudioFormat()); - int routeFlags = audioMixClass.getField("ROUTE_FLAG_LOOP_BACK").getInt(null); + String routeFlagName = keepPlayingOnDevice ? "ROUTE_FLAG_LOOP_BACK_RENDER" : "ROUTE_FLAG_LOOP_BACK"; + int routeFlags = audioMixClass.getField(routeFlagName).getInt(null); // audioMixBuilder.setRouteFlags(routeFlag); Method setRouteFlags = audioMixBuilder.getClass().getMethod("setRouteFlags", int.class);