Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/data/bash-completion/scrcpy
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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 \
Expand Down
1 change: 1 addition & 0 deletions app/data/zsh-completion/_scrcpy
Original file line number Diff line number Diff line change
Expand Up @@ -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]'
Expand Down
29 changes: 29 additions & 0 deletions app/src/cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
Expand Down
1 change: 1 addition & 0 deletions app/src/options.c
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions app/src/options.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions app/src/scrcpy.c
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions app/src/server.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
1 change: 1 addition & 0 deletions app/src/server.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
34 changes: 27 additions & 7 deletions doc/audio.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)).
Expand All @@ -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`:

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

Expand Down
22 changes: 22 additions & 0 deletions server/src/main/java/com/genymobile/scrcpy/Options.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
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;
Expand All @@ -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;
Expand Down Expand Up @@ -120,6 +123,10 @@ public boolean getAudioDup() {
return audioDup;
}

public int[] getAudioMatchUids() {
return audioMatchUids;
}

public int getVideoBitRate() {
return videoBitRate;
}
Expand Down Expand Up @@ -358,6 +365,21 @@ public static Options parse(String... args) {
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;
Expand Down
2 changes: 1 addition & 1 deletion server/src/main/java/com/genymobile/scrcpy/Server.java
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ private static void scrcpy(Options options) throws IOException, ConfigurationExc
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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@
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")
Expand All @@ -43,12 +45,19 @@ private AudioRecord createAudioRecord() throws AudioCaptureException {
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);
Expand Down