diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java new file mode 100644 index 00000000..ab843837 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java @@ -0,0 +1,74 @@ +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.MediaRecorder; +import android.os.Build; +import android.os.SystemClock; + +public final class AudioEncoder { + + private static final int SAMPLE_RATE = 48000; + private static final int CHANNELS = 2; + + private Thread thread; + + private static AudioFormat createAudioFormat() { + AudioFormat.Builder builder = new AudioFormat.Builder(); + builder.setEncoding(AudioFormat.ENCODING_PCM_16BIT); + builder.setSampleRate(SAMPLE_RATE); + builder.setChannelMask(CHANNELS == 2 ? AudioFormat.CHANNEL_IN_STEREO : AudioFormat.CHANNEL_IN_MONO); + return builder.build(); + } + + @TargetApi(Build.VERSION_CODES.M) + @SuppressLint({"WrongConstant", "MissingPermission"}) + private static AudioRecord createAudioRecord() { + 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(MediaRecorder.AudioSource.REMOTE_SUBMIX); + builder.setAudioFormat(createAudioFormat()); + builder.setBufferSizeInBytes(1024 * 1024); + return builder.build(); + } + + public void start() { + AudioRecord recorder = createAudioRecord(); + + thread = new Thread(() -> { + recorder.startRecording(); + try { + int BUFFER_MS = 15; // do not buffer more than BUFFER_MS milliseconds + byte[] buf = new byte[SAMPLE_RATE * CHANNELS * BUFFER_MS / 1000]; + while (!Thread.currentThread().isInterrupted()) { + int r = recorder.read(buf, 0, buf.length); + if (r > 0) { + Ln.i("Audio captured: " + r + " bytes"); + } + if (r < 0) { + Ln.e("Audio capture error: " + r); + } + } + } finally { + recorder.stop(); + } + }); + thread.start(); + } + + public void stop() { + if (thread != null) { + thread.interrupt(); + thread = null; + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 06281223..8ada46f4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -4,6 +4,7 @@ import android.graphics.Rect; import android.media.MediaCodecInfo; import android.os.BatteryManager; import android.os.Build; +import android.provider.MediaStore; import java.io.IOException; import java.util.List; @@ -69,12 +70,21 @@ public final class Server { int uid = options.getUid(); boolean tunnelForward = options.isTunnelForward(); boolean control = options.getControl(); + boolean audio = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R; // TODO option boolean sendDummyByte = options.getSendDummyByte(); Workarounds.prepareMainLooper(); - if (Build.BRAND.equalsIgnoreCase("meizu")) { - // - // + + // + // + boolean mustFillAppInfo = Build.BRAND.equalsIgnoreCase("meizu"); + + // Before Android 11, audio is not supported. + // Since Android 12, we can properly set a context on the AudioRecord. + // Only on Android 11 we must fill app info for the AudioRecord to work. + mustFillAppInfo |= audio && Build.VERSION.SDK_INT == Build.VERSION_CODES.R; + + if (mustFillAppInfo) { Workarounds.fillAppInfo(); } @@ -95,6 +105,12 @@ public final class Server { device.setClipboardListener(text -> controllerRef.getSender().pushClipboardText(text)); } + AudioEncoder audioEncoder = null; + if (audio) { + audioEncoder = new AudioEncoder(); + audioEncoder.start(); + } + try { // synchronous screenEncoder.streamScreen(device, connection.getVideoFd()); @@ -106,6 +122,9 @@ public final class Server { if (controller != null) { controller.stop(); } + if (audioEncoder != null) { + audioEncoder.stop(); + } } } }