mirror of
https://github.com/Genymobile/scrcpy.git
synced 2025-04-20 03:25:03 +00:00
Add --audio-match-package-names
option
This commit is contained in:
parent
e0f37f834b
commit
5b3dda43a6
12 changed files with 106 additions and 14 deletions
|
@ -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 \
|
||||
|
|
|
@ -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]'
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
34
doc/audio.md
34
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
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Reference in a new issue