diff options
Diffstat (limited to 'app/src/main/java/invalid/lena/scrcpy/AudioSink.java')
| -rw-r--r-- | app/src/main/java/invalid/lena/scrcpy/AudioSink.java | 176 |
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) {} + } } } |