mirror of
https://github.com/Genymobile/scrcpy.git
synced 2025-08-03 06:39:39 +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-codec-options=
|
||||||
--audio-dup
|
--audio-dup
|
||||||
--audio-encoder=
|
--audio-encoder=
|
||||||
|
--audio-match-package-names=
|
||||||
--audio-source=
|
--audio-source=
|
||||||
--audio-output-buffer=
|
--audio-output-buffer=
|
||||||
-b --video-bit-rate=
|
-b --video-bit-rate=
|
||||||
|
@ -192,6 +193,7 @@ _scrcpy() {
|
||||||
|-b|--video-bit-rate \
|
|-b|--video-bit-rate \
|
||||||
|--audio-codec-options \
|
|--audio-codec-options \
|
||||||
|--audio-encoder \
|
|--audio-encoder \
|
||||||
|
|--audio-match-package-names \
|
||||||
|--audio-output-buffer \
|
|--audio-output-buffer \
|
||||||
|--camera-ar \
|
|--camera-ar \
|
||||||
|--camera-id \
|
|--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-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]'
|
||||||
'--audio-dup=[Duplicate audio]'
|
'--audio-dup=[Duplicate audio]'
|
||||||
'--audio-encoder=[Use a specific MediaCodec audio encoder]'
|
'--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-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)]'
|
'--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]'
|
{-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_SYSTEM_DECORATIONS,
|
||||||
OPT_NO_VD_DESTROY_CONTENT,
|
OPT_NO_VD_DESTROY_CONTENT,
|
||||||
OPT_DISPLAY_IME_POLICY,
|
OPT_DISPLAY_IME_POLICY,
|
||||||
|
OPT_AUDIO_MATCH_PACKAGE_NAMES,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct sc_option {
|
struct sc_option {
|
||||||
|
@ -205,6 +206,13 @@ static const struct sc_option options[] = {
|
||||||
"This feature is only available with --audio-source=playback."
|
"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_id = OPT_AUDIO_ENCODER,
|
||||||
.longopt = "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:
|
case OPT_AUDIO_DUP:
|
||||||
opts->audio_dup = true;
|
opts->audio_dup = true;
|
||||||
break;
|
break;
|
||||||
|
case OPT_AUDIO_MATCH_PACKAGE_NAMES:
|
||||||
|
opts->audio_match_package_names = optarg;
|
||||||
|
break;
|
||||||
case 'G':
|
case 'G':
|
||||||
opts->gamepad_input_mode = SC_GAMEPAD_INPUT_MODE_UHID_OR_AOA;
|
opts->gamepad_input_mode = SC_GAMEPAD_INPUT_MODE_UHID_OR_AOA;
|
||||||
break;
|
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 "
|
LOGI("Audio duplication enabled: audio source switched to "
|
||||||
"\"playback\"");
|
"\"playback\"");
|
||||||
opts->audio_source = SC_AUDIO_SOURCE_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 {
|
} else {
|
||||||
opts->audio_source = SC_AUDIO_SOURCE_OUTPUT;
|
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) {
|
if (opts->record_format && !opts->record_filename) {
|
||||||
LOGE("Record format specified without recording");
|
LOGE("Record format specified without recording");
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -108,6 +108,7 @@ const struct scrcpy_options scrcpy_options_default = {
|
||||||
.window = true,
|
.window = true,
|
||||||
.mouse_hover = true,
|
.mouse_hover = true,
|
||||||
.audio_dup = false,
|
.audio_dup = false,
|
||||||
|
.audio_match_package_names = NULL,
|
||||||
.new_display = NULL,
|
.new_display = NULL,
|
||||||
.start_app = NULL,
|
.start_app = NULL,
|
||||||
.angle = NULL,
|
.angle = NULL,
|
||||||
|
|
|
@ -323,6 +323,7 @@ struct scrcpy_options {
|
||||||
bool window;
|
bool window;
|
||||||
bool mouse_hover;
|
bool mouse_hover;
|
||||||
bool audio_dup;
|
bool audio_dup;
|
||||||
|
const char *audio_match_package_names;
|
||||||
const char *new_display; // [<width>x<height>][/<dpi>] parsed by the server
|
const char *new_display; // [<width>x<height>][/<dpi>] parsed by the server
|
||||||
const char *start_app;
|
const char *start_app;
|
||||||
bool vd_destroy_content;
|
bool vd_destroy_content;
|
||||||
|
|
|
@ -440,6 +440,7 @@ scrcpy(struct scrcpy_options *options) {
|
||||||
.video = options->video,
|
.video = options->video,
|
||||||
.audio = options->audio,
|
.audio = options->audio,
|
||||||
.audio_dup = options->audio_dup,
|
.audio_dup = options->audio_dup,
|
||||||
|
.audio_match_package_names = options->audio_match_package_names,
|
||||||
.show_touches = options->show_touches,
|
.show_touches = options->show_touches,
|
||||||
.stay_awake = options->stay_awake,
|
.stay_awake = options->stay_awake,
|
||||||
.video_codec_options = options->video_codec_options,
|
.video_codec_options = options->video_codec_options,
|
||||||
|
|
|
@ -298,6 +298,11 @@ execute_server(struct sc_server *server,
|
||||||
if (params->audio_dup) {
|
if (params->audio_dup) {
|
||||||
ADD_PARAM("audio_dup=true");
|
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) {
|
if (params->max_size) {
|
||||||
ADD_PARAM("max_size=%" PRIu16, params->max_size);
|
ADD_PARAM("max_size=%" PRIu16, params->max_size);
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,6 +54,7 @@ struct sc_server_params {
|
||||||
bool video;
|
bool video;
|
||||||
bool audio;
|
bool audio;
|
||||||
bool audio_dup;
|
bool audio_dup;
|
||||||
|
const char* audio_match_package_names;
|
||||||
bool show_touches;
|
bool show_touches;
|
||||||
bool stay_awake;
|
bool stay_awake;
|
||||||
bool force_adb_forward;
|
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:
|
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)).
|
- `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`: 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-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)).
|
- `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-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)).
|
- `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
|
`--audio-source=playback` uses an alternative device audio capture method:
|
||||||
13 and above):
|
|
||||||
|
|
||||||
```
|
```
|
||||||
scrcpy --audio-source=playback
|
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
|
This audio source supports keeping the audio playing on the device while
|
||||||
mirroring, with `--audio-dup`:
|
mirroring, with `--audio-dup`:
|
||||||
|
|
||||||
|
@ -98,12 +108,22 @@ scrcpy --audio-source=playback --audio-dup
|
||||||
scrcpy --audio-dup # --audio-source=playback is implied
|
scrcpy --audio-dup # --audio-source=playback is implied
|
||||||
```
|
```
|
||||||
|
|
||||||
However, it requires Android 13, and Android apps can opt-out (so they are not
|
#### Only capture some apps
|
||||||
captured).
|
|
||||||
|
|
||||||
|
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
|
## Codec
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,9 @@ import com.genymobile.scrcpy.video.VideoCodec;
|
||||||
import com.genymobile.scrcpy.video.VideoSource;
|
import com.genymobile.scrcpy.video.VideoSource;
|
||||||
import com.genymobile.scrcpy.wrappers.WindowManager;
|
import com.genymobile.scrcpy.wrappers.WindowManager;
|
||||||
|
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
|
import android.os.Build;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -32,6 +34,7 @@ public class Options {
|
||||||
private VideoSource videoSource = VideoSource.DISPLAY;
|
private VideoSource videoSource = VideoSource.DISPLAY;
|
||||||
private AudioSource audioSource = AudioSource.OUTPUT;
|
private AudioSource audioSource = AudioSource.OUTPUT;
|
||||||
private boolean audioDup;
|
private boolean audioDup;
|
||||||
|
private int[] audioMatchUids = new int[0];
|
||||||
private int videoBitRate = 8000000;
|
private int videoBitRate = 8000000;
|
||||||
private int audioBitRate = 128000;
|
private int audioBitRate = 128000;
|
||||||
private float maxFps;
|
private float maxFps;
|
||||||
|
@ -120,6 +123,10 @@ public class Options {
|
||||||
return audioDup;
|
return audioDup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int[] getAudioMatchUids() {
|
||||||
|
return audioMatchUids;
|
||||||
|
}
|
||||||
|
|
||||||
public int getVideoBitRate() {
|
public int getVideoBitRate() {
|
||||||
return videoBitRate;
|
return videoBitRate;
|
||||||
}
|
}
|
||||||
|
@ -358,6 +365,21 @@ public class Options {
|
||||||
case "audio_dup":
|
case "audio_dup":
|
||||||
options.audioDup = Boolean.parseBoolean(value);
|
options.audioDup = Boolean.parseBoolean(value);
|
||||||
break;
|
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":
|
case "max_size":
|
||||||
options.maxSize = Integer.parseInt(value) & ~7; // multiple of 8
|
options.maxSize = Integer.parseInt(value) & ~7; // multiple of 8
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -129,7 +129,7 @@ public final class Server {
|
||||||
if (audioSource.isDirect()) {
|
if (audioSource.isDirect()) {
|
||||||
audioCapture = new AudioDirectCapture(audioSource);
|
audioCapture = new AudioDirectCapture(audioSource);
|
||||||
} else {
|
} else {
|
||||||
audioCapture = new AudioPlaybackCapture(options.getAudioDup());
|
audioCapture = new AudioPlaybackCapture(options.getAudioDup(), options.getAudioMatchUids());
|
||||||
}
|
}
|
||||||
|
|
||||||
Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(), options.getSendFrameMeta());
|
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 {
|
public final class AudioPlaybackCapture implements AudioCapture {
|
||||||
|
|
||||||
private final boolean keepPlayingOnDevice;
|
private final boolean keepPlayingOnDevice;
|
||||||
|
private final int[] matchUids;
|
||||||
|
|
||||||
private AudioRecord recorder;
|
private AudioRecord recorder;
|
||||||
private AudioRecordReader reader;
|
private AudioRecordReader reader;
|
||||||
|
|
||||||
public AudioPlaybackCapture(boolean keepPlayingOnDevice) {
|
public AudioPlaybackCapture(boolean keepPlayingOnDevice, int[] matchUids) {
|
||||||
this.keepPlayingOnDevice = keepPlayingOnDevice;
|
this.keepPlayingOnDevice = keepPlayingOnDevice;
|
||||||
|
this.matchUids = matchUids;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("PrivateApi")
|
@SuppressLint("PrivateApi")
|
||||||
|
@ -43,12 +45,19 @@ public final class AudioPlaybackCapture implements AudioCapture {
|
||||||
Method setTargetMixRoleMethod = audioMixingRuleBuilderClass.getMethod("setTargetMixRole", int.class);
|
Method setTargetMixRoleMethod = audioMixingRuleBuilderClass.getMethod("setTargetMixRole", int.class);
|
||||||
setTargetMixRoleMethod.invoke(audioMixingRuleBuilder, mixRolePlayersConstant);
|
setTargetMixRoleMethod.invoke(audioMixingRuleBuilder, mixRolePlayersConstant);
|
||||||
|
|
||||||
AudioAttributes attributes = new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build();
|
Method addMixRuleMethod = audioMixingRuleBuilderClass.getMethod("addMixRule", int.class, Object.class);
|
||||||
|
if (matchUids.length == 0) {
|
||||||
// audioMixingRuleBuilder.addMixRule(AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE, attributes);
|
// audioMixingRuleBuilder.addMixRule(AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE, attributes);
|
||||||
int ruleMatchAttributeUsageConstant = audioMixingRuleClass.getField("RULE_MATCH_ATTRIBUTE_USAGE").getInt(null);
|
int ruleMatchAttributeUsageConstant = audioMixingRuleClass.getField("RULE_MATCH_ATTRIBUTE_USAGE").getInt(null);
|
||||||
Method addMixRuleMethod = audioMixingRuleBuilderClass.getMethod("addMixRule", int.class, Object.class);
|
AudioAttributes attributes = new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build();
|
||||||
addMixRuleMethod.invoke(audioMixingRuleBuilder, ruleMatchAttributeUsageConstant, attributes);
|
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();
|
// AudioMixingRule audioMixingRule = builder.build();
|
||||||
Object audioMixingRule = audioMixingRuleBuilderClass.getMethod("build").invoke(audioMixingRuleBuilder);
|
Object audioMixingRule = audioMixingRuleBuilderClass.getMethod("build").invoke(audioMixingRuleBuilder);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue