aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java/invalid/lena/scrcpy/AudioSink.java
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java/invalid/lena/scrcpy/AudioSink.java')
-rw-r--r--app/src/main/java/invalid/lena/scrcpy/AudioSink.java176
1 files changed, 124 insertions, 52 deletions
diff --git a/app/src/main/java/invalid/lena/scrcpy/AudioSink.java b/app/src/main/java/invalid/lena/scrcpy/AudioSink.java
index 5507c5c..f14f85c 100644
--- a/app/src/main/java/invalid/lena/scrcpy/AudioSink.java
+++ b/app/src/main/java/invalid/lena/scrcpy/AudioSink.java
@@ -14,6 +14,8 @@ import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayDeque;
import java.util.Deque;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
// Audio output: AudioTrack writing 48 kHz stereo 16-bit PCM. The
@@ -25,6 +27,7 @@ public final class AudioSink implements AudioFrames {
private static final int CHANNEL_OUT = AudioFormat.CHANNEL_OUT_STEREO;
private static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT;
private static final int MAX_PENDING_OPUS = 16;
+ private static final int MAX_PENDING_PCM = 16;
private static final int MAX_PACKET_BYTES = 256 * 1024;
// Defaults documented at <https://developer.android.com/reference/android/media/MediaCodec#CSD>.
@@ -34,24 +37,29 @@ public final class AudioSink implements AudioFrames {
private volatile MediaCodec opusCodec;
private HandlerThread opusThread;
private Handler opusHandler;
+ private Thread opusOutputThread;
private volatile boolean opusConfigured;
private int fourcc;
private final Object lock = new Object();
- private final Runnable onFatalError;
- private final AtomicBoolean fatalReported = new AtomicBoolean();
+ private final AtomicBoolean failed = new AtomicBoolean();
private final AtomicBoolean playbackReported = new AtomicBoolean();
private final Deque<Integer> freeOpusInputs = new ArrayDeque<>();
- private final Deque<byte[]> pendingOpus = new ArrayDeque<>(MAX_PENDING_OPUS);
+ private final Deque<Packet> pendingOpus = new ArrayDeque<>(MAX_PENDING_OPUS);
+ private final ArrayBlockingQueue<byte[]> pendingPcm =
+ new ArrayBlockingQueue<>(MAX_PENDING_PCM);
- private volatile boolean released;
- private long droppedBytes;
- private long lastDropLogMs;
+ private static final class Packet {
+ final byte[] data;
+ final long ptsUs;
- public AudioSink(Runnable onFatalError) {
- this.onFatalError = onFatalError;
+ Packet(byte[] data, long ptsUs) {
+ this.data = data;
+ this.ptsUs = ptsUs;
+ }
}
+ private volatile boolean released;
@Override
public void start(int fourcc) {
this.fourcc = fourcc;
@@ -96,25 +104,31 @@ public final class AudioSink implements AudioFrames {
} catch (IOException e) {
throw new IllegalStateException("audio sink: no opus decoder", e);
}
- opusCodec.setCallback(new MediaCodec.Callback() {
- @Override public void onInputBufferAvailable(MediaCodec mc, int idx) {
+ opusOutputThread = new Thread(this::playOpus, "audio-out");
+ opusOutputThread.start();
+ opusCodec.setCallback(new MediaCodec.Callback() {
+ @Override public void onInputBufferAvailable(MediaCodec mc, int idx) {
synchronized (lock) {
- if (released || mc != opusCodec) return;
- byte[] packet = pendingOpus.pollFirst();
- if (packet == null) freeOpusInputs.offerLast(idx);
- else submitOpus(mc, idx, packet);
+ if (released || failed.get() || mc != opusCodec) return;
+ Packet packet = pendingOpus.pollFirst();
+ if (packet == null) {
+ freeOpusInputs.offerLast(idx);
+ } else {
+ lock.notifyAll();
+ submitOpus(mc, idx, packet);
+ }
}
}
@Override public void onOutputBufferAvailable(MediaCodec mc, int idx,
MediaCodec.BufferInfo info) {
+ byte[] pcm = null;
try {
ByteBuffer out = mc.getOutputBuffer(idx);
- if (out != null && info.size > 0 && !released) {
- byte[] pcm = new byte[info.size];
+ if (out != null && info.size > 0 && !released && !failed.get()) {
+ pcm = new byte[info.size];
out.position(info.offset);
out.limit(info.offset + info.size);
out.get(pcm);
- writePcm(pcm, 0, pcm.length);
}
} catch (IllegalStateException e) {
reportFatal(e, "audio sink: opus output");
@@ -122,6 +136,7 @@ public final class AudioSink implements AudioFrames {
try { mc.releaseOutputBuffer(idx, false); }
catch (IllegalStateException ignored) {}
}
+ if (pcm != null) queuePcm(pcm);
}
@Override public void onError(MediaCodec mc, MediaCodec.CodecException e) {
reportFatal(e, "audio sink: opus codec error");
@@ -134,7 +149,7 @@ public final class AudioSink implements AudioFrames {
}
@Override
- public void feed(byte[] data, int off, int len, boolean isConfig) {
+ public void feed(byte[] data, int off, int len, long ptsUs, boolean isConfig) {
if (data == null || off < 0 || len < 0 || off > data.length - len) {
throw new IndexOutOfBoundsException("invalid audio frame range");
}
@@ -142,16 +157,16 @@ public final class AudioSink implements AudioFrames {
reportFatal(null, "audio sink: packet too large (" + len + " bytes)");
return;
}
- if (released || len == 0) return;
+ if (released || failed.get() || len == 0) return;
if (fourcc == Wire.CODEC_OPUS) {
- feedOpus(data, off, len, isConfig);
+ feedOpus(data, off, len, ptsUs, isConfig);
} else {
// Raw PCM: passthrough.
writePcm(data, off, len);
}
}
- private void feedOpus(byte[] data, int off, int len, boolean isConfig) {
+ private void feedOpus(byte[] data, int off, int len, long ptsUs, boolean isConfig) {
if (isConfig) {
if (opusConfigured) return; // already configured
// OpusHead is 19 bytes; pre_skip lives at bytes [10..11]. Reject
@@ -183,31 +198,45 @@ public final class AudioSink implements AudioFrames {
if (!opusConfigured) return;
byte[] packet = new byte[len];
System.arraycopy(data, off, packet, 0, len);
+ Packet p = new Packet(packet, ptsUs);
synchronized (lock) {
if (released || opusCodec == null) return;
+ // Apply socket backpressure during decoder bursts. Dropping a
+ // compressed packet corrupts playback; growing the queue only
+ // postpones the same failure.
+ while (!released && !failed.get() && freeOpusInputs.isEmpty()
+ && pendingOpus.size() == MAX_PENDING_OPUS) {
+ try {
+ lock.wait();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return;
+ }
+ }
+ if (released || failed.get() || opusCodec == null) return;
Integer idx = freeOpusInputs.pollFirst();
if (idx != null) {
- submitOpus(opusCodec, idx, packet);
+ submitOpus(opusCodec, idx, p);
return;
}
- if (pendingOpus.size() == MAX_PENDING_OPUS) pendingOpus.pollFirst();
- pendingOpus.offerLast(packet);
+ pendingOpus.offerLast(p);
}
}
// Must be called with lock held. Async MediaCodec input indices belong
// to the codec instance that delivered them.
- private void submitOpus(MediaCodec codec, int idx, byte[] packet) {
+ private void submitOpus(MediaCodec codec, int idx, Packet packet) {
try {
ByteBuffer in = codec.getInputBuffer(idx);
- if (in == null || packet.length > in.capacity()) {
+ if (in == null || packet.data.length > in.capacity()) {
reportFatal(null,
- "audio sink: opus packet exceeds codec input (" + packet.length + " bytes)");
+ "audio sink: opus packet exceeds codec input ("
+ + packet.data.length + " bytes)");
return;
}
in.clear();
- in.put(packet);
- codec.queueInputBuffer(idx, 0, packet.length, 0, 0);
+ in.put(packet.data);
+ codec.queueInputBuffer(idx, 0, packet.data.length, packet.ptsUs, 0);
} catch (IllegalStateException e) {
reportFatal(e, "audio sink: opus queueInputBuffer");
}
@@ -220,32 +249,60 @@ public final class AudioSink implements AudioFrames {
return b;
}
- private void writePcm(byte[] data, int off, int len) {
- AudioTrack t = track;
- if (released || t == null || len <= 0) return;
- int written = t.write(data, off, len, AudioTrack.WRITE_NON_BLOCKING);
- if (written < 0) {
- reportFatal(null, "audio sink: AudioTrack write rc=" + written);
- return;
+ private void queuePcm(byte[] pcm) {
+ try {
+ while (!released && !failed.get()
+ && !pendingPcm.offer(pcm, 100, TimeUnit.MILLISECONDS)) {
+ // Wait for the blocking AudioTrack writer to make room.
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
}
- if (written > 0 && playbackReported.compareAndSet(false, true)) {
- Log.i("audio sink: playback started");
+ }
+
+ private void playOpus() {
+ try {
+ while (!released && !failed.get()) {
+ byte[] pcm = pendingPcm.take();
+ writePcm(pcm, 0, pcm.length);
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } catch (RuntimeException e) {
+ reportFatal(e, "audio sink: AudioTrack write");
}
- if (written < len) {
- droppedBytes += (len - written);
- long now = System.currentTimeMillis();
- if (now - lastDropLogMs > 1000L) {
- lastDropLogMs = now;
- Log.w("audio sink: ring full, dropped %d bytes so far", droppedBytes);
+ }
+
+ private void writePcm(byte[] data, int off, int len) {
+ AudioTrack t = track;
+ if (released || failed.get() || t == null || len <= 0) return;
+ int end = off + len;
+ while (!released && !failed.get() && off < end) {
+ int written = t.write(data, off, end - off, AudioTrack.WRITE_BLOCKING);
+ if (written <= 0) {
+ if (!released) {
+ reportFatal(null, "audio sink: AudioTrack write rc=" + written);
+ }
+ return;
+ }
+ off += written;
+ if (playbackReported.compareAndSet(false, true)) {
+ Log.i("audio sink: playback started");
}
}
}
private void reportFatal(Exception error, String message) {
- if (!fatalReported.compareAndSet(false, true)) return;
+ if (!failed.compareAndSet(false, true)) return;
if (error == null) Log.e("%s", message);
else Log.e(error, "%s", message);
- if (onFatalError != null) onFatalError.run();
+ synchronized (lock) {
+ pendingOpus.clear();
+ lock.notifyAll();
+ }
+ pendingPcm.clear();
+ Thread t = opusOutputThread;
+ if (t != null) t.interrupt();
}
@Override
@@ -257,20 +314,35 @@ public final class AudioSink implements AudioFrames {
opusCodec = null;
freeOpusInputs.clear();
pendingOpus.clear();
+ lock.notifyAll();
}
+ AudioTrack t = track;
+ track = null;
+ // Stop playback first so the blocking writer can return before
+ // AudioTrack is released.
+ if (t != null) {
+ try { t.pause(); t.flush(); t.stop(); } catch (Exception ignored) {}
+ }
+
HandlerThread ht = opusThread;
opusThread = null;
opusHandler = null;
+ Thread out = opusOutputThread;
+ opusOutputThread = null;
+ if (out != null) out.interrupt();
if (c != null) {
try { c.stop(); } catch (Exception ignored) {}
try { c.release(); } catch (Exception ignored) {}
}
if (ht != null) ht.quitSafely();
-
- AudioTrack t = track;
- track = null;
- if (t == null) return;
- try { t.pause(); t.flush(); t.stop(); } catch (Exception ignored) {}
- try { t.release(); } catch (Exception ignored) {}
+ if (out != null && out != Thread.currentThread()) {
+ try { out.join(1_000); }
+ catch (InterruptedException e) { Thread.currentThread().interrupt(); }
+ if (out.isAlive()) Log.e("audio sink: output thread did not stop");
+ }
+ pendingPcm.clear();
+ if (t != null) {
+ try { t.release(); } catch (Exception ignored) {}
+ }
}
}