Extract AudioCapture interface
Move the implementation to AudioDirectCapture and extract an AudioCapture interface. This will allow to provide another AudioCapture implementation. PR #5102 <https://github.com/Genymobile/scrcpy/pull/5102>
This commit is contained in:
parent
053bf83f58
commit
0f076083e8
@ -2,6 +2,7 @@ package com.genymobile.scrcpy;
|
|||||||
|
|
||||||
import com.genymobile.scrcpy.audio.AudioCapture;
|
import com.genymobile.scrcpy.audio.AudioCapture;
|
||||||
import com.genymobile.scrcpy.audio.AudioCodec;
|
import com.genymobile.scrcpy.audio.AudioCodec;
|
||||||
|
import com.genymobile.scrcpy.audio.AudioDirectCapture;
|
||||||
import com.genymobile.scrcpy.audio.AudioEncoder;
|
import com.genymobile.scrcpy.audio.AudioEncoder;
|
||||||
import com.genymobile.scrcpy.audio.AudioRawRecorder;
|
import com.genymobile.scrcpy.audio.AudioRawRecorder;
|
||||||
import com.genymobile.scrcpy.control.ControlChannel;
|
import com.genymobile.scrcpy.control.ControlChannel;
|
||||||
@ -163,7 +164,7 @@ public final class Server {
|
|||||||
|
|
||||||
if (audio) {
|
if (audio) {
|
||||||
AudioCodec audioCodec = options.getAudioCodec();
|
AudioCodec audioCodec = options.getAudioCodec();
|
||||||
AudioCapture audioCapture = new AudioCapture(options.getAudioSource());
|
AudioCapture audioCapture = new AudioDirectCapture(options.getAudioSource());
|
||||||
Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(), options.getSendFrameMeta());
|
Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(), options.getSendFrameMeta());
|
||||||
AsyncProcessor audioRecorder;
|
AsyncProcessor audioRecorder;
|
||||||
if (audioCodec == AudioCodec.RAW) {
|
if (audioCodec == AudioCodec.RAW) {
|
||||||
|
@ -1,134 +1,20 @@
|
|||||||
package com.genymobile.scrcpy.audio;
|
package com.genymobile.scrcpy.audio;
|
||||||
|
|
||||||
import com.genymobile.scrcpy.FakeContext;
|
|
||||||
import com.genymobile.scrcpy.util.Ln;
|
|
||||||
import com.genymobile.scrcpy.Workarounds;
|
|
||||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.content.ComponentName;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.media.AudioRecord;
|
|
||||||
import android.media.MediaCodec;
|
import android.media.MediaCodec;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.SystemClock;
|
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
public final class AudioCapture {
|
public interface AudioCapture {
|
||||||
|
void checkCompatibility() throws AudioCaptureException;
|
||||||
|
void start() throws AudioCaptureException;
|
||||||
|
void stop();
|
||||||
|
|
||||||
private static final int SAMPLE_RATE = AudioConfig.SAMPLE_RATE;
|
/**
|
||||||
private static final int CHANNEL_CONFIG = AudioConfig.CHANNEL_CONFIG;
|
* Read a chunk of {@link AudioConfig#MAX_READ_SIZE} samples.
|
||||||
private static final int CHANNELS = AudioConfig.CHANNELS;
|
*
|
||||||
private static final int CHANNEL_MASK = AudioConfig.CHANNEL_MASK;
|
* @param outDirectBuffer The target buffer
|
||||||
private static final int ENCODING = AudioConfig.ENCODING;
|
* @param outBufferInfo The info to provide to MediaCodec
|
||||||
|
* @return the number of bytes actually read.
|
||||||
private final int audioSource;
|
*/
|
||||||
|
int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo);
|
||||||
private AudioRecord recorder;
|
|
||||||
private AudioRecordReader reader;
|
|
||||||
|
|
||||||
public AudioCapture(AudioSource audioSource) {
|
|
||||||
this.audioSource = audioSource.value();
|
|
||||||
}
|
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.M)
|
|
||||||
@SuppressLint({"WrongConstant", "MissingPermission"})
|
|
||||||
private static AudioRecord createAudioRecord(int audioSource) {
|
|
||||||
AudioRecord.Builder builder = new AudioRecord.Builder();
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
// On older APIs, Workarounds.fillAppInfo() must be called beforehand
|
|
||||||
builder.setContext(FakeContext.get());
|
|
||||||
}
|
|
||||||
builder.setAudioSource(audioSource);
|
|
||||||
builder.setAudioFormat(AudioConfig.createAudioFormat());
|
|
||||||
int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, ENCODING);
|
|
||||||
// This buffer size does not impact latency
|
|
||||||
builder.setBufferSizeInBytes(8 * minBufferSize);
|
|
||||||
return builder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void startWorkaroundAndroid11() {
|
|
||||||
// Android 11 requires Apps to be at foreground to record audio.
|
|
||||||
// Normally, each App has its own user ID, so Android checks whether the requesting App has the user ID that's at the foreground.
|
|
||||||
// But scrcpy server is NOT an App, it's a Java application started from Android shell, so it has the same user ID (2000) with Android
|
|
||||||
// shell ("com.android.shell").
|
|
||||||
// If there is an Activity from Android shell running at foreground, then the permission system will believe scrcpy is also in the
|
|
||||||
// foreground.
|
|
||||||
Intent intent = new Intent(Intent.ACTION_MAIN);
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
||||||
intent.addCategory(Intent.CATEGORY_LAUNCHER);
|
|
||||||
intent.setComponent(new ComponentName(FakeContext.PACKAGE_NAME, "com.android.shell.HeapDumpActivity"));
|
|
||||||
ServiceManager.getActivityManager().startActivity(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void stopWorkaroundAndroid11() {
|
|
||||||
ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void tryStartRecording(int attempts, int delayMs) throws AudioCaptureException {
|
|
||||||
while (attempts-- > 0) {
|
|
||||||
// Wait for activity to start
|
|
||||||
SystemClock.sleep(delayMs);
|
|
||||||
try {
|
|
||||||
startRecording();
|
|
||||||
return; // it worked
|
|
||||||
} catch (UnsupportedOperationException e) {
|
|
||||||
if (attempts == 0) {
|
|
||||||
Ln.e("Failed to start audio capture");
|
|
||||||
Ln.e("On Android 11, audio capture must be started in the foreground, make sure that the device is unlocked when starting "
|
|
||||||
+ "scrcpy.");
|
|
||||||
throw new AudioCaptureException();
|
|
||||||
} else {
|
|
||||||
Ln.d("Failed to start audio capture, retrying...");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startRecording() throws AudioCaptureException {
|
|
||||||
try {
|
|
||||||
recorder = createAudioRecord(audioSource);
|
|
||||||
} catch (NullPointerException e) {
|
|
||||||
// Creating an AudioRecord using an AudioRecord.Builder does not work on Vivo phones:
|
|
||||||
// - <https://github.com/Genymobile/scrcpy/issues/3805>
|
|
||||||
// - <https://github.com/Genymobile/scrcpy/pull/3862>
|
|
||||||
recorder = Workarounds.createAudioRecord(audioSource, SAMPLE_RATE, CHANNEL_CONFIG, CHANNELS, CHANNEL_MASK, ENCODING);
|
|
||||||
}
|
|
||||||
recorder.startRecording();
|
|
||||||
reader = new AudioRecordReader(recorder);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void checkCompatibility() throws AudioCaptureException {
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
|
||||||
Ln.w("Audio disabled: it is not supported before Android 11");
|
|
||||||
throw new AudioCaptureException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void start() throws AudioCaptureException {
|
|
||||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
|
|
||||||
startWorkaroundAndroid11();
|
|
||||||
try {
|
|
||||||
tryStartRecording(5, 100);
|
|
||||||
} finally {
|
|
||||||
stopWorkaroundAndroid11();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
startRecording();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stop() {
|
|
||||||
if (recorder != null) {
|
|
||||||
// Will call .stop() if necessary, without throwing an IllegalStateException
|
|
||||||
recorder.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.N)
|
|
||||||
public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) {
|
|
||||||
return reader.read(outDirectBuffer, outBufferInfo);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,138 @@
|
|||||||
|
package com.genymobile.scrcpy.audio;
|
||||||
|
|
||||||
|
import com.genymobile.scrcpy.FakeContext;
|
||||||
|
import com.genymobile.scrcpy.Workarounds;
|
||||||
|
import com.genymobile.scrcpy.util.Ln;
|
||||||
|
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.media.AudioRecord;
|
||||||
|
import android.media.MediaCodec;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.SystemClock;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
public class AudioDirectCapture implements AudioCapture {
|
||||||
|
|
||||||
|
private static final int SAMPLE_RATE = AudioConfig.SAMPLE_RATE;
|
||||||
|
private static final int CHANNEL_CONFIG = AudioConfig.CHANNEL_CONFIG;
|
||||||
|
private static final int CHANNELS = AudioConfig.CHANNELS;
|
||||||
|
private static final int CHANNEL_MASK = AudioConfig.CHANNEL_MASK;
|
||||||
|
private static final int ENCODING = AudioConfig.ENCODING;
|
||||||
|
|
||||||
|
private final int audioSource;
|
||||||
|
|
||||||
|
private AudioRecord recorder;
|
||||||
|
private AudioRecordReader reader;
|
||||||
|
|
||||||
|
public AudioDirectCapture(AudioSource audioSource) {
|
||||||
|
this.audioSource = audioSource.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.M)
|
||||||
|
@SuppressLint({"WrongConstant", "MissingPermission"})
|
||||||
|
private static AudioRecord createAudioRecord(int audioSource) {
|
||||||
|
AudioRecord.Builder builder = new AudioRecord.Builder();
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
// On older APIs, Workarounds.fillAppInfo() must be called beforehand
|
||||||
|
builder.setContext(FakeContext.get());
|
||||||
|
}
|
||||||
|
builder.setAudioSource(audioSource);
|
||||||
|
builder.setAudioFormat(AudioConfig.createAudioFormat());
|
||||||
|
int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, ENCODING);
|
||||||
|
// This buffer size does not impact latency
|
||||||
|
builder.setBufferSizeInBytes(8 * minBufferSize);
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void startWorkaroundAndroid11() {
|
||||||
|
// Android 11 requires Apps to be at foreground to record audio.
|
||||||
|
// Normally, each App has its own user ID, so Android checks whether the requesting App has the user ID that's at the foreground.
|
||||||
|
// But scrcpy server is NOT an App, it's a Java application started from Android shell, so it has the same user ID (2000) with Android
|
||||||
|
// shell ("com.android.shell").
|
||||||
|
// If there is an Activity from Android shell running at foreground, then the permission system will believe scrcpy is also in the
|
||||||
|
// foreground.
|
||||||
|
Intent intent = new Intent(Intent.ACTION_MAIN);
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
intent.addCategory(Intent.CATEGORY_LAUNCHER);
|
||||||
|
intent.setComponent(new ComponentName(FakeContext.PACKAGE_NAME, "com.android.shell.HeapDumpActivity"));
|
||||||
|
ServiceManager.getActivityManager().startActivity(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void stopWorkaroundAndroid11() {
|
||||||
|
ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void tryStartRecording(int attempts, int delayMs) throws AudioCaptureException {
|
||||||
|
while (attempts-- > 0) {
|
||||||
|
// Wait for activity to start
|
||||||
|
SystemClock.sleep(delayMs);
|
||||||
|
try {
|
||||||
|
startRecording();
|
||||||
|
return; // it worked
|
||||||
|
} catch (UnsupportedOperationException e) {
|
||||||
|
if (attempts == 0) {
|
||||||
|
Ln.e("Failed to start audio capture");
|
||||||
|
Ln.e("On Android 11, audio capture must be started in the foreground, make sure that the device is unlocked when starting "
|
||||||
|
+ "scrcpy.");
|
||||||
|
throw new AudioCaptureException();
|
||||||
|
} else {
|
||||||
|
Ln.d("Failed to start audio capture, retrying...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startRecording() throws AudioCaptureException {
|
||||||
|
try {
|
||||||
|
recorder = createAudioRecord(audioSource);
|
||||||
|
} catch (NullPointerException e) {
|
||||||
|
// Creating an AudioRecord using an AudioRecord.Builder does not work on Vivo phones:
|
||||||
|
// - <https://github.com/Genymobile/scrcpy/issues/3805>
|
||||||
|
// - <https://github.com/Genymobile/scrcpy/pull/3862>
|
||||||
|
recorder = Workarounds.createAudioRecord(audioSource, SAMPLE_RATE, CHANNEL_CONFIG, CHANNELS, CHANNEL_MASK, ENCODING);
|
||||||
|
}
|
||||||
|
recorder.startRecording();
|
||||||
|
reader = new AudioRecordReader(recorder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void checkCompatibility() throws AudioCaptureException {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||||
|
Ln.w("Audio disabled: it is not supported before Android 11");
|
||||||
|
throw new AudioCaptureException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start() throws AudioCaptureException {
|
||||||
|
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
|
||||||
|
startWorkaroundAndroid11();
|
||||||
|
try {
|
||||||
|
tryStartRecording(5, 100);
|
||||||
|
} finally {
|
||||||
|
stopWorkaroundAndroid11();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
startRecording();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user