mirror of https://github.com/Genymobile/scrcpy
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
180 lines
7.3 KiB
Java
180 lines
7.3 KiB
Java
package com.genymobile.scrcpy;
|
|
|
|
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.AudioFormat;
|
|
import android.media.AudioRecord;
|
|
import android.media.AudioTimestamp;
|
|
import android.media.MediaCodec;
|
|
import android.os.Build;
|
|
import android.os.SystemClock;
|
|
|
|
import java.nio.ByteBuffer;
|
|
|
|
public final class AudioCapture {
|
|
|
|
public static final int SAMPLE_RATE = 48000;
|
|
public static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO;
|
|
public static final int CHANNELS = 2;
|
|
public static final int CHANNEL_MASK = AudioFormat.CHANNEL_IN_LEFT | AudioFormat.CHANNEL_IN_RIGHT;
|
|
public static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT;
|
|
public static final int BYTES_PER_SAMPLE = 2;
|
|
|
|
// Never read more than 1024 samples, even if the buffer is bigger (that would increase latency).
|
|
// A lower value is useless, since the system captures audio samples by blocks of 1024 (so for example if we read by blocks of 256 samples, we
|
|
// receive 4 successive blocks without waiting, then we wait for the 4 next ones).
|
|
public static final int MAX_READ_SIZE = 1024 * CHANNELS * BYTES_PER_SAMPLE;
|
|
|
|
private static final long ONE_SAMPLE_US = (1000000 + SAMPLE_RATE - 1) / SAMPLE_RATE; // 1 sample in microseconds (used for fixing PTS)
|
|
|
|
private final int audioSource;
|
|
|
|
private AudioRecord recorder;
|
|
|
|
private final AudioTimestamp timestamp = new AudioTimestamp();
|
|
private long previousRecorderTimestamp = -1;
|
|
private long previousPts = 0;
|
|
private long nextPts = 0;
|
|
|
|
public AudioCapture(AudioSource audioSource) {
|
|
this.audioSource = audioSource.value();
|
|
}
|
|
|
|
private static AudioFormat createAudioFormat() {
|
|
AudioFormat.Builder builder = new AudioFormat.Builder();
|
|
builder.setEncoding(ENCODING);
|
|
builder.setSampleRate(SAMPLE_RATE);
|
|
builder.setChannelMask(CHANNEL_CONFIG);
|
|
return builder.build();
|
|
}
|
|
|
|
@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(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 AudioCaptureForegroundException {
|
|
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 AudioCaptureForegroundException();
|
|
} else {
|
|
Ln.d("Failed to start audio capture, retrying...");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void startRecording() {
|
|
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();
|
|
}
|
|
|
|
public void start() throws AudioCaptureForegroundException {
|
|
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 directBuffer, MediaCodec.BufferInfo outBufferInfo) {
|
|
int r = recorder.read(directBuffer, MAX_READ_SIZE);
|
|
if (r <= 0) {
|
|
return r;
|
|
}
|
|
|
|
long pts;
|
|
|
|
int ret = recorder.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC);
|
|
if (ret == AudioRecord.SUCCESS && timestamp.nanoTime != previousRecorderTimestamp) {
|
|
pts = timestamp.nanoTime / 1000;
|
|
previousRecorderTimestamp = timestamp.nanoTime;
|
|
} else {
|
|
if (nextPts == 0) {
|
|
Ln.w("Could not get initial audio timestamp");
|
|
nextPts = System.nanoTime() / 1000;
|
|
}
|
|
// compute from previous timestamp and packet size
|
|
pts = nextPts;
|
|
}
|
|
|
|
long durationUs = r * 1000000L / (CHANNELS * BYTES_PER_SAMPLE * SAMPLE_RATE);
|
|
nextPts = pts + durationUs;
|
|
|
|
if (previousPts != 0 && pts < previousPts + ONE_SAMPLE_US) {
|
|
// Audio PTS may come from two sources:
|
|
// - recorder.getTimestamp() if the call works;
|
|
// - an estimation from the previous PTS and the packet size as a fallback.
|
|
//
|
|
// Therefore, the property that PTS are monotonically increasing is no guaranteed in corner cases, so enforce it.
|
|
pts = previousPts + ONE_SAMPLE_US;
|
|
}
|
|
previousPts = pts;
|
|
|
|
outBufferInfo.set(0, r, pts, 0);
|
|
return r;
|
|
}
|
|
}
|