aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java/invalid/lena/scrcpy/VideoSink.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/VideoSink.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/VideoSink.java')
-rw-r--r--app/src/main/java/invalid/lena/scrcpy/VideoSink.java55
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);
}