diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 010125fb..8d1f1e13 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -7,6 +7,7 @@ _scrcpy() { --audio-codec= --audio-codec-options= --audio-encoder= + --audio-source= --audio-output-buffer= -b --video-bit-rate= --crop= @@ -86,6 +87,10 @@ _scrcpy() { COMPREPLY=($(compgen -W 'opus aac raw' -- "$cur")) return ;; + --audio-source) + COMPREPLY=($(compgen -W 'output mic' -- "$cur")) + return + ;; --lock-video-orientation) COMPREPLY=($(compgen -W 'unlocked initial 0 1 2 3' -- "$cur")) return diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 4f1f16b9..6e742fd7 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -14,6 +14,7 @@ arguments=( '--audio-codec=[Select the audio codec]:codec:(opus aac raw)' '--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]' '--audio-encoder=[Use a specific MediaCodec audio encoder]' + '--audio-source=[Select the audio source]:source:(output mic)' '--audio-output-buffer=[Configure the size of the SDL audio output buffer (in milliseconds)]' {-b,--video-bit-rate=}'[Encode the video at the given bit-rate]' '--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index a30e7db0..04098b9c 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -55,6 +55,12 @@ Use a specific MediaCodec audio encoder (depending on the codec provided by \fB\ The available encoders can be listed by \-\-list\-encoders. +.TP +.BI "\-\-audio\-source " source +Select the audio source (output or mic). + +Default is output. + .TP .BI "\-\-audio\-output\-buffer ms Configure the size of the SDL audio output buffer (in milliseconds). diff --git a/app/src/cli.c b/app/src/cli.c index 01b55406..533e63ca 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -76,6 +76,7 @@ enum { OPT_NO_VIDEO, OPT_NO_AUDIO_PLAYBACK, OPT_NO_VIDEO_PLAYBACK, + OPT_AUDIO_SOURCE, }; struct sc_option { @@ -161,6 +162,13 @@ static const struct sc_option options[] = { "codec provided by --audio-codec).\n" "The available encoders can be listed by --list-encoders.", }, + { + .longopt_id = OPT_AUDIO_SOURCE, + .longopt = "audio-source", + .argdesc = "source", + .text = "Select the audio source (output or mic).\n" + "Default is output.", + }, { .longopt_id = OPT_AUDIO_OUTPUT_BUFFER, .longopt = "audio-output-buffer", @@ -1588,6 +1596,22 @@ parse_audio_codec(const char *optarg, enum sc_codec *codec) { return false; } +static bool +parse_audio_source(const char *optarg, enum sc_audio_source *source) { + if (!strcmp(optarg, "mic")) { + *source = SC_AUDIO_SOURCE_MIC; + return true; + } + + if (!strcmp(optarg, "output")) { + *source = SC_AUDIO_SOURCE_OUTPUT; + return true; + } + + LOGE("Unsupported audio source: %s (expected output or mic)", optarg); + return false; +} + static bool parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], const char *optstring, const struct option *longopts) { @@ -1915,6 +1939,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } break; + case OPT_AUDIO_SOURCE: + if (!parse_audio_source(optarg, &opts->audio_source)) { + return false; + } + break; default: // getopt prints the error message on stderr return false; diff --git a/app/src/options.c b/app/src/options.c index 49cca969..e1373753 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -14,6 +14,7 @@ const struct scrcpy_options scrcpy_options_default = { .log_level = SC_LOG_LEVEL_INFO, .video_codec = SC_CODEC_H264, .audio_codec = SC_CODEC_OPUS, + .audio_source = SC_AUDIO_SOURCE_OUTPUT, .record_format = SC_RECORD_FORMAT_AUTO, .keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT, .mouse_input_mode = SC_MOUSE_INPUT_MODE_INJECT, diff --git a/app/src/options.h b/app/src/options.h index dd5d0dad..c33fafef 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -44,6 +44,11 @@ enum sc_codec { SC_CODEC_RAW, }; +enum sc_audio_source { + SC_AUDIO_SOURCE_OUTPUT, + SC_AUDIO_SOURCE_MIC, +}; + enum sc_lock_video_orientation { SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1, // lock the current orientation when scrcpy starts @@ -115,6 +120,7 @@ struct scrcpy_options { enum sc_log_level log_level; enum sc_codec video_codec; enum sc_codec audio_codec; + enum sc_audio_source audio_source; enum sc_record_format record_format; enum sc_keyboard_input_mode keyboard_input_mode; enum sc_mouse_input_mode mouse_input_mode; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 4bbc8dca..79007bf2 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -334,6 +334,7 @@ scrcpy(struct scrcpy_options *options) { .log_level = options->log_level, .video_codec = options->video_codec, .audio_codec = options->audio_codec, + .audio_source = options->audio_source, .crop = options->crop, .port_range = options->port_range, .tunnel_host = options->tunnel_host, diff --git a/app/src/server.c b/app/src/server.c index 9c554760..4e70c4d9 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -246,6 +246,10 @@ execute_server(struct sc_server *server, ADD_PARAM("audio_codec=%s", sc_server_get_codec_name(params->audio_codec)); } + if (params->audio_source != SC_AUDIO_SOURCE_OUTPUT) { + assert(params->audio_source == SC_AUDIO_SOURCE_MIC); + ADD_PARAM("audio_source=mic"); + } 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 31648445..fad44e66 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -26,6 +26,7 @@ struct sc_server_params { enum sc_log_level log_level; enum sc_codec video_codec; enum sc_codec audio_codec; + enum sc_audio_source audio_source; const char *crop; const char *video_codec_options; const char *audio_codec_options; diff --git a/doc/audio.md b/doc/audio.md index a437005c..40dd6036 100644 --- a/doc/audio.md +++ b/doc/audio.md @@ -41,6 +41,24 @@ interesting to add [buffering](#buffering) to minimize glitches: scrcpy --no-video --audio-buffer=200 ``` +## Source + +By default, the device audio output is forwarded. + +It is possible to capture the device microphone instead: + +``` +scrcpy --audio-source=mic +``` + +For example, to use the device as a dictaphone and record a capture directly on +the computer: + +``` +scrcpy --audio-source=mic --no-video --no-audio-playback --record=file.opus +``` + + ## Codec The audio codec can be selected. The possible values are `opus` (default), `aac` diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java index b8fc076b..7b20cce4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java @@ -10,7 +10,6 @@ import android.media.AudioFormat; import android.media.AudioRecord; import android.media.AudioTimestamp; import android.media.MediaCodec; -import android.media.MediaRecorder; import android.os.Build; import android.os.SystemClock; @@ -18,7 +17,6 @@ import java.nio.ByteBuffer; public final class AudioCapture { - public static final int SOURCE = MediaRecorder.AudioSource.REMOTE_SUBMIX; public static final int SAMPLE_RATE = 48000; public static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO; public static final int CHANNELS = 2; @@ -26,12 +24,18 @@ public final class AudioCapture { public static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT; public static final int BYTES_PER_SAMPLE = 2; + private final int audioSource; + private AudioRecord recorder; private final AudioTimestamp timestamp = new AudioTimestamp(); private long previousPts = 0; private long nextPts = 0; + public AudioCapture(AudioSource audioSource) { + this.audioSource = audioSource.value(); + } + public static int millisToBytes(int millis) { return SAMPLE_RATE * CHANNELS * BYTES_PER_SAMPLE * millis / 1000; } @@ -46,13 +50,13 @@ public final class AudioCapture { @TargetApi(Build.VERSION_CODES.M) @SuppressLint({"WrongConstant", "MissingPermission"}) - private static AudioRecord createAudioRecord() { + private static AudioRecord createAudioRecord(int audioSource) { AudioRecord.Builder builder = new AudioRecord.Builder(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // On older APIs, Workarounds.fillAppInfo() must be called beforehand builder.setContext(FakeContext.get()); } - builder.setAudioSource(SOURCE); + builder.setAudioSource(audioSource); builder.setAudioFormat(createAudioFormat()); int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, ENCODING); // This buffer size does not impact latency @@ -100,12 +104,12 @@ public final class AudioCapture { private void startRecording() { try { - recorder = createAudioRecord(); + recorder = createAudioRecord(audioSource); } catch (NullPointerException e) { // Creating an AudioRecord using an AudioRecord.Builder does not work on Vivo phones: // - // - - recorder = Workarounds.createAudioRecord(SOURCE, SAMPLE_RATE, CHANNEL_CONFIG, CHANNELS, CHANNEL_MASK, ENCODING); + recorder = Workarounds.createAudioRecord(audioSource, SAMPLE_RATE, CHANNEL_CONFIG, CHANNELS, CHANNEL_MASK, ENCODING); } recorder.startRecording(); } diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioSource.java b/server/src/main/java/com/genymobile/scrcpy/AudioSource.java new file mode 100644 index 00000000..466ea297 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/AudioSource.java @@ -0,0 +1,30 @@ +package com.genymobile.scrcpy; + +import android.media.MediaRecorder; + +public enum AudioSource { + OUTPUT("output", MediaRecorder.AudioSource.REMOTE_SUBMIX), + MIC("mic", MediaRecorder.AudioSource.MIC); + + private final String name; + private final int value; + + AudioSource(String name, int value) { + this.name = name; + this.value = value; + } + + int value() { + return value; + } + + static AudioSource findByName(String name) { + for (AudioSource audioSource : AudioSource.values()) { + if (name.equals(audioSource.name)) { + return audioSource; + } + } + + return null; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 7bd94cb3..23d4e383 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -14,6 +14,7 @@ public class Options { private int maxSize; private VideoCodec videoCodec = VideoCodec.H264; private AudioCodec audioCodec = AudioCodec.OPUS; + private AudioSource audioSource = AudioSource.OUTPUT; private int videoBitRate = 8000000; private int audioBitRate = 128000; private int maxFps; @@ -72,6 +73,10 @@ public class Options { return audioCodec; } + public AudioSource getAudioSource() { + return audioSource; + } + public int getVideoBitRate() { return videoBitRate; } @@ -225,6 +230,13 @@ public class Options { } options.audioCodec = audioCodec; break; + case "audio_source": + AudioSource audioSource = AudioSource.findByName(value); + if (audioSource == null) { + throw new IllegalArgumentException("Audio source " + value + " not supported"); + } + options.audioSource = audioSource; + 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 616b771e..214ac27d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -136,7 +136,7 @@ public final class Server { if (audio) { AudioCodec audioCodec = options.getAudioCodec(); - AudioCapture audioCapture = new AudioCapture(); + AudioCapture audioCapture = new AudioCapture(options.getAudioSource()); Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(), options.getSendFrameMeta()); AsyncProcessor audioRecorder;