Add --audio-match-package-names option

This commit is contained in:
Simon Chan 2025-04-01 16:47:41 +08:00
parent e0f37f834b
commit 5b3dda43a6
No known key found for this signature in database
GPG key ID: A8B69F750B9BCEDD
12 changed files with 106 additions and 14 deletions

View file

@ -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 \

View file

@ -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]'

View file

@ -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;

View file

@ -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,

View file

@ -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; // [<width>x<height>][/<dpi>] parsed by the server
const char *start_app;
bool vd_destroy_content;

View file

@ -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,

View file

@ -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);
}

View file

@ -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;

View file

@ -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

View file

@ -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;

View file

@ -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());

View file

@ -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);