diff options
| author | Lena <lena@omega> | 2026-07-01 00:00:00 +0000 |
|---|---|---|
| committer | Lena <lena@omega> | 2026-07-01 00:00:00 +0000 |
| commit | ff7acf898b48359275a5b09b82ed926a945233f8 (patch) | |
| tree | 5e45b04be22cb1531f075ec35eb6f91518e09165 /app/src/main/java/invalid/lena/scrcpy/VideoSink.java | |
| parent | bddc306de0cebde44f99b3a9c4ddba130946bef3 (diff) | |
| download | scrcpy-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/VideoSink.java')
| -rw-r--r-- | app/src/main/java/invalid/lena/scrcpy/VideoSink.java | 55 |
1 files changed, 34 insertions, 21 deletions
diff --git a/app/src/main/java/invalid/lena/scrcpy/VideoSink.java b/app/src/main/java/invalid/lena/scrcpy/VideoSink.java index 2d69339..ff8f449 100644 --- a/app/src/main/java/invalid/lena/scrcpy/VideoSink.java +++ b/app/src/main/java/invalid/lena/scrcpy/VideoSink.java @@ -20,8 +20,8 @@ import java.util.Iterator; // plus a corresponding pool of free input buffer indices. // // Back-pressure policy: when no input buffer is free and the pending -// queue is full, the oldest pending non-config frame is dropped. Config -// frames (CSD) always survive - the decoder cannot start without them. +// queue is full, replace the oldest frame of the same kind, or the oldest +// frame overall. The queue remains bounded even if a peer floods CSD. // Keyframes are not distinguishable here (the flag stays in VideoStream) // so they drop like any delta frame; the picture heals at the next one. // @@ -36,8 +36,9 @@ public final class VideoSink implements VideoFrames { private static final int MAX_PENDING = 8; private volatile Surface surface; + private final Runnable onFatalError; public volatile long frames; // public read for the status overlay - private MediaCodec codec; + private volatile MediaCodec codec; private HandlerThread handlerThread; private Handler handler; @@ -56,8 +57,9 @@ public final class VideoSink implements VideoFrames { Frame(byte[] d, long pts, boolean cfg) { data = d; ptsUs = pts; isConfig = cfg; } } - public VideoSink(Surface surface) { + public VideoSink(Surface surface, Runnable onFatalError) { this.surface = surface; + this.onFatalError = onFatalError; } // Swap the output Surface without rebuilding MediaCodec. Passing null @@ -97,7 +99,7 @@ public final class VideoSink implements VideoFrames { codec = MediaCodec.createDecoderByType(mime); codec.setCallback(new MediaCodec.Callback() { @Override public void onInputBufferAvailable(MediaCodec mc, int idx) { - onFreeInput(idx); + onFreeInput(mc, idx); } @Override public void onOutputBufferAvailable(MediaCodec mc, int idx, MediaCodec.BufferInfo info) { try { @@ -118,6 +120,7 @@ public final class VideoSink implements VideoFrames { } @Override public void onError(MediaCodec mc, MediaCodec.CodecException e) { Log.e(e, "video sink: codec error"); + if (mc == codec && onFatalError != null) onFatalError.run(); } @Override public void onOutputFormatChanged(MediaCodec mc, MediaFormat fmt) { Log.i("video sink: output format %s", fmt); @@ -137,20 +140,26 @@ public final class VideoSink implements VideoFrames { if (released) return; // Try to drain immediately if there's a free input. while (!pending.isEmpty() && !freeInputs.isEmpty()) { - submit(pending.pollFirst(), freeInputs.pollFirst()); + submit(codec, pending.pollFirst(), freeInputs.pollFirst()); } if (!freeInputs.isEmpty()) { - submit(new Frame(data, ptsUs, isConfig), freeInputs.pollFirst()); + submit(codec, new Frame(data, ptsUs, isConfig), freeInputs.pollFirst()); return; } - // Queue, with bounded drop policy on non-config frames. - if (pending.size() >= MAX_PENDING && !isConfig) { - // Drop the oldest non-config frame to avoid stalling - // forever. Config frames must survive: the decoder - // cannot start without its CSD. + // Keep the queue strictly bounded. A newer config frame replaces + // an older one; retaining every config packet lets a hostile peer + // turn the queue into an unbounded allocation sink. + if (pending.size() >= MAX_PENDING) { + boolean removed = false; for (Iterator<Frame> it = pending.iterator(); it.hasNext(); ) { - if (!it.next().isConfig) { it.remove(); break; } + Frame f = it.next(); + if (f.isConfig == isConfig) { + it.remove(); + removed = true; + break; + } } + if (!removed) pending.pollFirst(); } pending.offerLast(new Frame(data, ptsUs, isConfig)); } @@ -196,10 +205,10 @@ public final class VideoSink implements VideoFrames { } // Internal - runs on the MediaCodec callback thread. - private void onFreeInput(int idx) { + private void onFreeInput(MediaCodec mc, int idx) { synchronized (lock) { - if (released) return; - if (!pending.isEmpty()) submit(pending.pollFirst(), idx); + if (released || mc != codec) return; + if (!pending.isEmpty()) submit(mc, pending.pollFirst(), idx); else freeInputs.offerLast(idx); } } @@ -207,15 +216,19 @@ public final class VideoSink implements VideoFrames { // Must be called with `lock` held. codec can be null mid-reconfigure // (teardownCodec runs unlocked); the frame is dropped like any other // back-pressure casualty. - private void submit(Frame f, int idx) { - if (codec == null) return; + private void submit(MediaCodec mc, Frame f, int idx) { + if (mc == null || mc != codec) return; try { - ByteBuffer buf = codec.getInputBuffer(idx); - if (buf == null) return; + ByteBuffer buf = mc.getInputBuffer(idx); + if (buf == null || f.data.length > buf.capacity()) { + Log.e("video sink: frame exceeds codec input (%d bytes)", f.data.length); + if (onFatalError != null) onFatalError.run(); + return; + } buf.clear(); buf.put(f.data); int flags = f.isConfig ? MediaCodec.BUFFER_FLAG_CODEC_CONFIG : 0; - codec.queueInputBuffer(idx, 0, f.data.length, f.ptsUs, flags); + mc.queueInputBuffer(idx, 0, f.data.length, f.ptsUs, flags); } catch (IllegalStateException e) { Log.w("video sink: queueInputBuffer: %s", e); } |