diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 9918918c..3867ee26 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -9,6 +9,7 @@ _scrcpy() { --audio-codec-options= --audio-dup --audio-encoder= + --audio-match-package-names= --audio-source= --audio-output-buffer= -b --video-bit-rate= @@ -192,6 +193,7 @@ _scrcpy() { |-b|--video-bit-rate \ |--audio-codec-options \ |--audio-encoder \ + |--audio-match-package-names \ |--audio-output-buffer \ |--camera-ar \ |--camera-id \ diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 450fc8f5..76380ef1 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -16,6 +16,7 @@ arguments=( '--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-match-package-names=[Only capture audio from a list of comma-separated package names]' '--audio-source=[Select the audio source]:source:(output playback mic mic-unprocessed mic-camcorder mic-voice-recognition mic-voice-communication voice-call voice-call-uplink voice-call-downlink voice-performance)' '--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]' diff --git a/app/src/cli.c b/app/src/cli.c index b2e3e30a..d09b703a 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -114,6 +114,7 @@ enum { OPT_NO_VD_SYSTEM_DECORATIONS, OPT_NO_VD_DESTROY_CONTENT, OPT_DISPLAY_IME_POLICY, + OPT_AUDIO_MATCH_PACKAGE_NAMES, }; struct sc_option { @@ -205,6 +206,13 @@ static const struct sc_option options[] = { "This feature is only available with --audio-source=playback." }, + { + .longopt_id = OPT_AUDIO_MATCH_PACKAGE_NAMES, + .longopt = "audio-match-package-names", + .argdesc = "package.name.a,package.name.b", + .text = "Only capture audio from apps with specified package names.\n" + "This feature is only available with --audio-source=playback." + }, { .longopt_id = OPT_AUDIO_ENCODER, .longopt = "audio-encoder", @@ -2786,6 +2794,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_AUDIO_DUP: opts->audio_dup = true; break; + case OPT_AUDIO_MATCH_PACKAGE_NAMES: + opts->audio_match_package_names = optarg; + break; case 'G': opts->gamepad_input_mode = SC_GAMEPAD_INPUT_MODE_UHID_OR_AOA; break; @@ -3137,6 +3148,10 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], LOGI("Audio duplication enabled: audio source switched to " "\"playback\""); opts->audio_source = SC_AUDIO_SOURCE_PLAYBACK; + } else if (opts->audio_match_package_names) { + LOGI("Audio pacakage name matching enabled: audio source " + "switched to \"playback\""); + opts->audio_source = SC_AUDIO_SOURCE_PLAYBACK; } else { opts->audio_source = SC_AUDIO_SOURCE_OUTPUT; } @@ -3158,6 +3173,20 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } } + if (opts->audio_match_package_names) { + if (!opts->audio) { + LOGE("--audio-match-package-names not supported if audio is " + "disabled"); + return false; + } + + if (opts->audio_source != SC_AUDIO_SOURCE_PLAYBACK) { + LOGE("--audio-match-package-names 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 0fe82d29..c555a1f8 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -108,6 +108,7 @@ const struct scrcpy_options scrcpy_options_default = { .window = true, .mouse_hover = true, .audio_dup = false, + .audio_match_package_names = NULL, .new_display = NULL, .start_app = NULL, .angle = NULL, diff --git a/app/src/options.h b/app/src/options.h index 03b42913..c234bba9 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -323,6 +323,7 @@ struct scrcpy_options { bool window; bool mouse_hover; bool audio_dup; + const char *audio_match_package_names; const char *new_display; // [x][/] parsed by the server const char *start_app; bool vd_destroy_content; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index b3ff9b36..a9737dc1 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -440,6 +440,7 @@ scrcpy(struct scrcpy_options *options) { .video = options->video, .audio = options->audio, .audio_dup = options->audio_dup, + .audio_match_package_names = options->audio_match_package_names, .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 153219c3..19c9877d 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -298,6 +298,11 @@ execute_server(struct sc_server *server, if (params->audio_dup) { ADD_PARAM("audio_dup=true"); } + if (params->audio_match_package_names) { + VALIDATE_STRING(params->audio_match_package_names); + ADD_PARAM("audio_match_package_names=%s", + params->audio_match_package_names); + } 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 5f4592de..59cf244a 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -54,6 +54,7 @@ struct sc_server_params { bool video; bool audio; bool audio_dup; + const char* audio_match_package_names; bool show_touches; bool stay_awake; bool force_adb_forward; diff --git a/doc/audio.md b/doc/audio.md index 142626f5..7d3ea356 100644 --- a/doc/audio.md +++ b/doc/audio.md @@ -69,7 +69,7 @@ scrcpy --audio-source=mic --no-video --no-playback --record=file.opus Many sources are available: - `output` (default): forwards the whole audio output, and disables playback on the device (mapped to [`REMOTE_SUBMIX`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#REMOTE_SUBMIX)). - - `playback`: captures the audio playback (Android apps can opt-out, so the whole output is not necessarily captured). + - `playback`: captures the audio playback (only for Android 13 and above, Android apps can opt-out, so the whole output is not necessarily captured). - `mic`: captures the microphone (mapped to [`MIC`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#MIC)). - `mic-unprocessed`: captures the microphone unprocessed (raw) sound (mapped to [`UNPROCESSED`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#UNPROCESSED)). - `mic-camcorder`: captures the microphone tuned for video recording, with the same orientation as the camera if available (mapped to [`CAMCORDER`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#CAMCORDER)). @@ -80,15 +80,25 @@ Many sources are available: - `voice-call-downlink`: captures voice call downlink only (mapped to [`VOICE_DOWNLINK`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_DOWNLINK)). - `voice-performance`: captures audio meant to be processed for live performance (karaoke), includes both the microphone and the device playback (mapped to [`VOICE_PERFORMANCE`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_PERFORMANCE)). -### Duplication +### Playback source -An alternative device audio capture method is also available (only for Android -13 and above): +`--audio-source=playback` uses an alternative device audio capture method: ``` scrcpy --audio-source=playback ``` +See [#4380](https://github.com/Genymobile/scrcpy/issues/4380). + +It has two limitations comparing to the default `output` source: + +* Only Android 13 and above are supported. +* Apps can opt-out being captured. + +But it also has two extra features: + +#### Duplication + This audio source supports keeping the audio playing on the device while mirroring, with `--audio-dup`: @@ -98,12 +108,22 @@ scrcpy --audio-source=playback --audio-dup scrcpy --audio-dup # --audio-source=playback is implied ``` -However, it requires Android 13, and Android apps can opt-out (so they are not -captured). +#### Only capture some apps +You can specify which apps you want to capture audio from with +`--audio-match-package-names=`: -See [#4380](https://github.com/Genymobile/scrcpy/issues/4380). +```bash +scrcpy --audio-match-package-names=com.package.a +# multiple packages, separated by comma +scrcpy --audio-match-package-names=com.package.a,com.package.b +``` +To list the Android apps installed on the device: + +```bash +scrcpy --list-apps +``` ## Codec diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 66bb68e8..8fdc4803 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -14,7 +14,9 @@ import com.genymobile.scrcpy.video.VideoCodec; import com.genymobile.scrcpy.video.VideoSource; import com.genymobile.scrcpy.wrappers.WindowManager; +import android.content.pm.PackageManager; import android.graphics.Rect; +import android.os.Build; import android.util.Pair; import java.util.List; @@ -32,6 +34,7 @@ public class Options { private VideoSource videoSource = VideoSource.DISPLAY; private AudioSource audioSource = AudioSource.OUTPUT; private boolean audioDup; + private int[] audioMatchUids = new int[0]; private int videoBitRate = 8000000; private int audioBitRate = 128000; private float maxFps; @@ -120,6 +123,10 @@ public class Options { return audioDup; } + public int[] getAudioMatchUids() { + return audioMatchUids; + } + public int getVideoBitRate() { return videoBitRate; } @@ -358,6 +365,21 @@ public class Options { case "audio_dup": options.audioDup = Boolean.parseBoolean(value); break; + case "audio_match_package_names": + if (Build.VERSION.SDK_INT >= AndroidVersions.API_24_ANDROID_7_0) { + PackageManager pm = FakeContext.get().getPackageManager(); + String[] packageNames = value.split(","); + int[] uids = new int[packageNames.length]; + for (int j = 0; j < packageNames.length; ++j) { + try { + uids[j] = pm.getPackageUid(packageNames[j].trim(), 0); + } catch (PackageManager.NameNotFoundException e) { + throw new IllegalArgumentException("Package name " + packageNames[j] + " not found"); + } + } + options.audioMatchUids = uids; + } + 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 09cfd6cf..a7f1d860 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -129,7 +129,7 @@ public final class Server { if (audioSource.isDirect()) { audioCapture = new AudioDirectCapture(audioSource); } else { - audioCapture = new AudioPlaybackCapture(options.getAudioDup()); + audioCapture = new AudioPlaybackCapture(options.getAudioDup(), options.getAudioMatchUids()); } Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(), options.getSendFrameMeta()); 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 009a239a..2ea846e2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java @@ -20,12 +20,14 @@ import java.nio.ByteBuffer; public final class AudioPlaybackCapture implements AudioCapture { private final boolean keepPlayingOnDevice; + private final int[] matchUids; private AudioRecord recorder; private AudioRecordReader reader; - public AudioPlaybackCapture(boolean keepPlayingOnDevice) { + public AudioPlaybackCapture(boolean keepPlayingOnDevice, int[] matchUids) { this.keepPlayingOnDevice = keepPlayingOnDevice; + this.matchUids = matchUids; } @SuppressLint("PrivateApi") @@ -43,12 +45,19 @@ public final class AudioPlaybackCapture implements AudioCapture { Method setTargetMixRoleMethod = audioMixingRuleBuilderClass.getMethod("setTargetMixRole", int.class); setTargetMixRoleMethod.invoke(audioMixingRuleBuilder, mixRolePlayersConstant); - AudioAttributes attributes = new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build(); - - // audioMixingRuleBuilder.addMixRule(AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE, attributes); - int ruleMatchAttributeUsageConstant = audioMixingRuleClass.getField("RULE_MATCH_ATTRIBUTE_USAGE").getInt(null); Method addMixRuleMethod = audioMixingRuleBuilderClass.getMethod("addMixRule", int.class, Object.class); - addMixRuleMethod.invoke(audioMixingRuleBuilder, ruleMatchAttributeUsageConstant, attributes); + if (matchUids.length == 0) { + // audioMixingRuleBuilder.addMixRule(AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE, attributes); + int ruleMatchAttributeUsageConstant = audioMixingRuleClass.getField("RULE_MATCH_ATTRIBUTE_USAGE").getInt(null); + AudioAttributes attributes = new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build(); + addMixRuleMethod.invoke(audioMixingRuleBuilder, ruleMatchAttributeUsageConstant, attributes); + } else { + // audioMixingRuleBuilder.addMixRule(AudioMixingRule.RULE_MATCH_UID, uid); + int ruleMatchUidConstant = audioMixingRuleClass.getField("RULE_MATCH_UID").getInt(null); + for (int uid : matchUids) { + addMixRuleMethod.invoke(audioMixingRuleBuilder, ruleMatchUidConstant, uid); + } + } // AudioMixingRule audioMixingRule = builder.build(); Object audioMixingRule = audioMixingRuleBuilderClass.getMethod("build").invoke(audioMixingRuleBuilder);