mirror of
https://github.com/Genymobile/scrcpy.git
synced 2025-12-17 21:44:20 +01:00
Add audio playback capture method
Add a new method to capture audio playback. It requires Android 13 (where the Shell app has MODIFY_AUDIO_ROUTING permission). The main benefit is that it supports keeping audio playing on the device (implemented in a further commit). Fixes #4380 <https://github.com/Genymobile/scrcpy/issues/4380> PR #5102 <https://github.com/Genymobile/scrcpy/pull/5102> Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com>
This commit is contained in:
@@ -4,7 +4,9 @@ import com.genymobile.scrcpy.audio.AudioCapture;
|
||||
import com.genymobile.scrcpy.audio.AudioCodec;
|
||||
import com.genymobile.scrcpy.audio.AudioDirectCapture;
|
||||
import com.genymobile.scrcpy.audio.AudioEncoder;
|
||||
import com.genymobile.scrcpy.audio.AudioPlaybackCapture;
|
||||
import com.genymobile.scrcpy.audio.AudioRawRecorder;
|
||||
import com.genymobile.scrcpy.audio.AudioSource;
|
||||
import com.genymobile.scrcpy.control.ControlChannel;
|
||||
import com.genymobile.scrcpy.control.Controller;
|
||||
import com.genymobile.scrcpy.control.DeviceMessage;
|
||||
@@ -164,7 +166,8 @@ public final class Server {
|
||||
|
||||
if (audio) {
|
||||
AudioCodec audioCodec = options.getAudioCodec();
|
||||
AudioCapture audioCapture = new AudioDirectCapture(options.getAudioSource());
|
||||
AudioSource audioSource = options.getAudioSource();
|
||||
AudioCapture audioCapture = audioSource.isDirect() ? new AudioDirectCapture(audioSource) : new AudioPlaybackCapture();
|
||||
Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(), options.getSendFrameMeta());
|
||||
AsyncProcessor audioRecorder;
|
||||
if (audioCodec == AudioCodec.RAW) {
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
package com.genymobile.scrcpy.audio;
|
||||
|
||||
import com.genymobile.scrcpy.FakeContext;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.media.AudioAttributes;
|
||||
import android.media.AudioFormat;
|
||||
import android.media.AudioManager;
|
||||
import android.media.AudioRecord;
|
||||
import android.media.MediaCodec;
|
||||
import android.os.Build;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public final class AudioPlaybackCapture implements AudioCapture {
|
||||
|
||||
private AudioRecord recorder;
|
||||
private AudioRecordReader reader;
|
||||
|
||||
@SuppressLint("PrivateApi")
|
||||
private AudioRecord createAudioRecord() throws AudioCaptureException {
|
||||
// See <https://github.com/Genymobile/scrcpy/issues/4380>
|
||||
try {
|
||||
Class<?> audioMixingRuleClass = Class.forName("android.media.audiopolicy.AudioMixingRule");
|
||||
Class<?> audioMixingRuleBuilderClass = Class.forName("android.media.audiopolicy.AudioMixingRule$Builder");
|
||||
|
||||
// AudioMixingRule.Builder audioMixingRuleBuilder = new AudioMixingRule.Builder();
|
||||
Object audioMixingRuleBuilder = audioMixingRuleBuilderClass.getConstructor().newInstance();
|
||||
|
||||
// audioMixingRuleBuilder.setTargetMixRole(AudioMixingRule.MIX_ROLE_PLAYERS);
|
||||
int mixRolePlayersConstant = audioMixingRuleClass.getField("MIX_ROLE_PLAYERS").getInt(null);
|
||||
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);
|
||||
|
||||
// AudioMixingRule audioMixingRule = builder.build();
|
||||
Object audioMixingRule = audioMixingRuleBuilderClass.getMethod("build").invoke(audioMixingRuleBuilder);
|
||||
|
||||
// audioMixingRuleBuilder.voiceCommunicationCaptureAllowed(true);
|
||||
Method voiceCommunicationCaptureAllowedMethod = audioMixingRuleBuilderClass.getMethod("voiceCommunicationCaptureAllowed", boolean.class);
|
||||
voiceCommunicationCaptureAllowedMethod.invoke(audioMixingRuleBuilder, true);
|
||||
|
||||
Class<?> audioMixClass = Class.forName("android.media.audiopolicy.AudioMix");
|
||||
Class<?> audioMixBuilderClass = Class.forName("android.media.audiopolicy.AudioMix$Builder");
|
||||
|
||||
// AudioMix.Builder audioMixBuilder = new AudioMix.Builder(audioMixingRule);
|
||||
Object audioMixBuilder = audioMixBuilderClass.getConstructor(audioMixingRuleClass).newInstance(audioMixingRule);
|
||||
|
||||
// audioMixBuilder.setFormat(createAudioFormat());
|
||||
Method setFormat = audioMixBuilder.getClass().getMethod("setFormat", AudioFormat.class);
|
||||
setFormat.invoke(audioMixBuilder, AudioConfig.createAudioFormat());
|
||||
|
||||
int routeFlags = audioMixClass.getField("ROUTE_FLAG_LOOP_BACK").getInt(null);
|
||||
|
||||
// audioMixBuilder.setRouteFlags(routeFlag);
|
||||
Method setRouteFlags = audioMixBuilder.getClass().getMethod("setRouteFlags", int.class);
|
||||
setRouteFlags.invoke(audioMixBuilder, routeFlags);
|
||||
|
||||
// AudioMix audioMix = audioMixBuilder.build();
|
||||
Object audioMix = audioMixBuilderClass.getMethod("build").invoke(audioMixBuilder);
|
||||
|
||||
Class<?> audioPolicyClass = Class.forName("android.media.audiopolicy.AudioPolicy");
|
||||
Class<?> audioPolicyBuilderClass = Class.forName("android.media.audiopolicy.AudioPolicy$Builder");
|
||||
|
||||
// AudioPolicy.Builder audioPolicyBuilder = new AudioPolicy.Builder();
|
||||
Object audioPolicyBuilder = audioPolicyBuilderClass.getConstructor(Context.class).newInstance(FakeContext.get());
|
||||
|
||||
// audioPolicyBuilder.addMix(audioMix);
|
||||
Method addMixMethod = audioPolicyBuilderClass.getMethod("addMix", audioMixClass);
|
||||
addMixMethod.invoke(audioPolicyBuilder, audioMix);
|
||||
|
||||
// AudioPolicy audioPolicy = audioPolicyBuilder.build();
|
||||
Object audioPolicy = audioPolicyBuilderClass.getMethod("build").invoke(audioPolicyBuilder);
|
||||
|
||||
// AudioManager.registerAudioPolicyStatic(audioPolicy);
|
||||
Method registerAudioPolicyStaticMethod = AudioManager.class.getDeclaredMethod("registerAudioPolicyStatic", audioPolicyClass);
|
||||
registerAudioPolicyStaticMethod.setAccessible(true);
|
||||
int result = (int) registerAudioPolicyStaticMethod.invoke(null, audioPolicy);
|
||||
if (result != 0) {
|
||||
throw new RuntimeException("registerAudioPolicy() returned " + result);
|
||||
}
|
||||
|
||||
// audioPolicy.createAudioRecordSink(audioPolicy);
|
||||
Method createAudioRecordSinkClass = audioPolicyClass.getMethod("createAudioRecordSink", audioMixClass);
|
||||
return (AudioRecord) createAudioRecordSinkClass.invoke(audioPolicy, audioMix);
|
||||
} catch (Exception e) {
|
||||
Ln.e("Could not capture audio playback", e);
|
||||
throw new AudioCaptureException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkCompatibility() throws AudioCaptureException {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
Ln.w("Audio disabled: audio playback capture source not supported before Android 13");
|
||||
throw new AudioCaptureException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() throws AudioCaptureException {
|
||||
recorder = createAudioRecord();
|
||||
recorder.startRecording();
|
||||
reader = new AudioRecordReader(recorder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
if (recorder != null) {
|
||||
// Will call .stop() if necessary, without throwing an IllegalStateException
|
||||
recorder.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@TargetApi(Build.VERSION_CODES.N)
|
||||
public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) {
|
||||
return reader.read(outDirectBuffer, outBufferInfo);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,8 @@ package com.genymobile.scrcpy.audio;
|
||||
|
||||
public enum AudioSource {
|
||||
OUTPUT("output"),
|
||||
MIC("mic");
|
||||
MIC("mic"),
|
||||
PLAYBACK("playback");
|
||||
|
||||
private final String name;
|
||||
|
||||
@@ -10,6 +11,10 @@ public enum AudioSource {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public boolean isDirect() {
|
||||
return this != PLAYBACK;
|
||||
}
|
||||
|
||||
public static AudioSource findByName(String name) {
|
||||
for (AudioSource audioSource : AudioSource.values()) {
|
||||
if (name.equals(audioSource.name)) {
|
||||
|
||||
Reference in New Issue
Block a user