diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 263c784d..8a88e276 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -2,6 +2,7 @@ package com.genymobile.scrcpy; 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.AudioRawRecorder; import com.genymobile.scrcpy.control.ControlChannel; @@ -163,7 +164,7 @@ public final class Server { if (audio) { 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()); AsyncProcessor audioRecorder; if (audioCodec == AudioCodec.RAW) { diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java index cfc3455e..62903f83 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java @@ -1,134 +1,20 @@ 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.os.Build; -import android.os.SystemClock; 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; - 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 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: - // - - // - - 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); - } + /** + * Read a chunk of {@link AudioConfig#MAX_READ_SIZE} samples. + * + * @param outDirectBuffer The target buffer + * @param outBufferInfo The info to provide to MediaCodec + * @return the number of bytes actually read. + */ + int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo); } diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java new file mode 100644 index 00000000..c0331467 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java @@ -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: + // - + // - + 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); + } +}