aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java/invalid/lena/scrcpy/VideoSink.java
diff options
context:
space:
mode:
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);
}