aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java/invalid/lena/scrcpy/AudioSink.java
diff options
context:
space:
mode:
authorLena <lena@omega>2026-07-01 00:00:00 +0000
committerLena <lena@omega>2026-07-01 00:00:00 +0000
commitff7acf898b48359275a5b09b82ed926a945233f8 (patch)
tree5e45b04be22cb1531f075ec35eb6f91518e09165 /app/src/main/java/invalid/lena/scrcpy/AudioSink.java
parentbddc306de0cebde44f99b3a9c4ddba130946bef3 (diff)
downloadscrcpy-android-ff7acf898b48359275a5b09b82ed926a945233f8.tar.gz
app: fix session lifecycle, teardown, and opus playback
Opus playback never worked: feed() called dequeueInputBuffer() on a codec in async-callback mode, which always throws, so every packet was silently dropped. Feed input through the async callback with a bounded pending queue instead. Make bring-up transactional: roll back partially opened streams and the server shell on failure, join reader threads on stop, and close the owning ADB streams before releasing sinks. Replace the openAbstract watchdog thread with a real timeout in the vendored AdbConnection.open(), which also removes the half-open stream from the lookup table on failure. Harden the activity: release owned Surfaces, gate callbacks on a destroyed flag and a session generation, serialize reconnect, and handle target replacement via singleTask + onNewIntent. Propagate device-list write failures instead of swallowing them, bound the clipboard payload and the video pending queue against hostile peers, and discard incomplete recordings instead of keeping corrupt files. Use the mediaPlayback foreground-service type; Android 15 stops dataSync services after six hours. Drop the unused ACCESS_NETWORK_STATE permission.
Diffstat (limited to 'app/src/main/java/invalid/lena/scrcpy/AudioSink.java')
-rw-r--r--app/src/main/java/invalid/lena/scrcpy/AudioSink.java74
1 files changed, 53 insertions, 21 deletions
diff --git a/app/src/main/java/invalid/lena/scrcpy/AudioSink.java b/app/src/main/java/invalid/lena/scrcpy/AudioSink.java
index 2b3e32d..7f09620 100644
--- a/app/src/main/java/invalid/lena/scrcpy/AudioSink.java
+++ b/app/src/main/java/invalid/lena/scrcpy/AudioSink.java
@@ -12,6 +12,8 @@ import android.os.HandlerThread;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
+import java.util.ArrayDeque;
+import java.util.Deque;
// Audio output: AudioTrack writing 48 kHz stereo 16-bit PCM. The
// upstream feed is either raw PCM (passthrough) or Opus packets
@@ -21,22 +23,32 @@ public final class AudioSink implements AudioFrames {
private static final int SAMPLE_RATE = 48_000;
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;
// Defaults documented at <https://developer.android.com/reference/android/media/MediaCodec#CSD>.
private static final long DEFAULT_PRE_ROLL_NS = 80_000_000L;
private AudioTrack track;
- private MediaCodec opusCodec;
+ private volatile MediaCodec opusCodec;
private HandlerThread opusThread;
private Handler opusHandler;
private boolean opusConfigured;
private int fourcc;
+ private final Object lock = new Object();
+ private final Runnable onFatalError;
+ private final Deque<Integer> freeOpusInputs = new ArrayDeque<>();
+ private final Deque<byte[]> pendingOpus = new ArrayDeque<>(MAX_PENDING_OPUS);
+
private volatile boolean released;
public volatile long frames; // public read for the status overlay
private long droppedBytes;
private long lastDropLogMs;
+ public AudioSink(Runnable onFatalError) {
+ this.onFatalError = onFatalError;
+ }
+
@Override
public void start(int fourcc) {
this.fourcc = fourcc;
@@ -71,11 +83,14 @@ 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) {
- // Frames are queued synchronously from feed(); we
- // don't pull on this callback. This handler exists
- // so MediaCodec's async machinery is wired up.
+ 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);
+ }
}
@Override public void onOutputBufferAvailable(MediaCodec mc, int idx,
MediaCodec.BufferInfo info) {
@@ -146,21 +161,33 @@ public final class AudioSink implements AudioFrames {
return;
}
if (!opusConfigured) return;
- // Encoded packet → MediaCodec input.
- int idx;
- try { idx = opusCodec.dequeueInputBuffer(0); }
- catch (IllegalStateException e) { return; }
- if (idx < 0) {
- // No input buffer right now; drop the packet. Opus is forgiving
- // about gaps for short stalls.
- return;
+ byte[] packet = new byte[len];
+ System.arraycopy(data, off, packet, 0, len);
+ synchronized (lock) {
+ if (released || opusCodec == null) return;
+ Integer idx = freeOpusInputs.pollFirst();
+ if (idx != null) {
+ submitOpus(opusCodec, idx, packet);
+ return;
+ }
+ if (pendingOpus.size() == MAX_PENDING_OPUS) pendingOpus.pollFirst();
+ pendingOpus.offerLast(packet);
}
+ }
+
+ // 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) {
try {
- ByteBuffer in = opusCodec.getInputBuffer(idx);
- if (in == null) return;
+ ByteBuffer in = codec.getInputBuffer(idx);
+ if (in == null || packet.length > in.capacity()) {
+ Log.e("audio sink: opus packet exceeds codec input (%d bytes)", packet.length);
+ if (onFatalError != null) onFatalError.run();
+ return;
+ }
in.clear();
- in.put(data, off, len);
- opusCodec.queueInputBuffer(idx, 0, len, 0, 0);
+ in.put(packet);
+ codec.queueInputBuffer(idx, 0, packet.length, 0, 0);
frames++;
} catch (IllegalStateException e) {
Log.w("audio sink: opus queueInputBuffer: %s", e);
@@ -194,9 +221,14 @@ public final class AudioSink implements AudioFrames {
@Override
public void release() {
- released = true;
- MediaCodec c = opusCodec;
- opusCodec = null;
+ MediaCodec c;
+ synchronized (lock) {
+ released = true;
+ c = opusCodec;
+ opusCodec = null;
+ freeOpusInputs.clear();
+ pendingOpus.clear();
+ }
HandlerThread ht = opusThread;
opusThread = null;
opusHandler = null;