From 82953735e27fc67a5e531c4b78addd6ecae84c05 Mon Sep 17 00:00:00 2001 From: Compot Developer Date: Tue, 13 Oct 2026 09:08:45 +0000 Subject: [PATCH 1/3] fix: wait up to 40 ms for a decoder input buffer, giving frames back meanwhile, before a NAL unit is dropped (#89) --- .../openipc/decoder/MediaCodecManager.java | 64 ++++++++++++------- 1 file changed, 42 insertions(+), 22 deletions(-) diff --git a/android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java b/android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java index 3d64f5fd..a9c5f9bc 100644 --- a/android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java +++ b/android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java @@ -30,6 +30,14 @@ class MediaCodecManager { /** Largest NAL unit (single view); 4K key frames at high bitrate can exceed 1 MiB. */ static final int MAX_NAL_SIZE = 2 * 1024 * 1024; + /** One wait for a free input buffer, in microseconds. */ + private static final long INPUT_POLL_US = 5_000; + /** + * How long a NAL unit waits for a free input buffer before it is dropped (#89): longer + * than a frame at 30 fps, so a decoder that only falls behind for a moment loses nothing; + * meanwhile the network queue of the stream takes the backlog. + */ + static final long INPUT_WAIT_MS = 40; private volatile MediaCodec decoder; private final Object decoderLock = new Object(); @@ -315,7 +323,15 @@ boolean decodeFrame(Frame buffer, boolean codecH265, long watchdogRef) { if ((configSeen & needed) == needed) awaitingConfig = false; } try { - int inputBufferId = codec.dequeueInputBuffer(5_000); + // No free input buffer at once: give decoded frames back (the decoder may + // be waiting for output space) and try again, for up to INPUT_WAIT_MS. + // Dropping a NAL unit corrupts the picture until the next key frame (#89) + int inputBufferId = codec.dequeueInputBuffer(INPUT_POLL_US); + long waitUntil = nowMs() + INPUT_WAIT_MS; + while (inputBufferId < 0 && nowMs() < waitUntil) { + drainOutput(codec); + inputBufferId = codec.dequeueInputBuffer(INPUT_POLL_US); + } if (inputBufferId >= 0) { ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId); if (inputBuffer != null) { @@ -327,27 +343,7 @@ boolean decodeFrame(Frame buffer, boolean codecH265, long watchdogRef) { } else { inputDropped++; } - - MediaCodec.BufferInfo info = bufferInfo; - int outputBufferId; - while ((outputBufferId = codec.dequeueOutputBuffer(info, 0)) >= 0 - || outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { - if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { - MediaFormat format = codec.getOutputFormat(); - int mWidth = format.getInteger(MediaFormat.KEY_WIDTH); - int mHeight = format.getInteger(MediaFormat.KEY_HEIGHT); - outWidth = mWidth; - outHeight = mHeight; - if (listener != null) { - listener.onResolutionChanged(mWidth, mHeight); - } - } else { - codec.releaseOutputBuffer(outputBufferId, true); - framesOut++; - lastFrameMs = nowMs(); - retry.onSuccess(); // a decoded frame: the decoder works - } - } + drainOutput(codec); } catch (Exception e) { decoder = null; releaseSlot(); @@ -362,4 +358,28 @@ boolean decodeFrame(Frame buffer, boolean codecH265, long watchdogRef) { } return needCreate; } + + /** Renders every decoded frame that is ready; caller holds decoderLock. */ + private void drainOutput(MediaCodec codec) { + MediaCodec.BufferInfo info = bufferInfo; + int outputBufferId; + while ((outputBufferId = codec.dequeueOutputBuffer(info, 0)) >= 0 + || outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { + if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { + MediaFormat format = codec.getOutputFormat(); + int mWidth = format.getInteger(MediaFormat.KEY_WIDTH); + int mHeight = format.getInteger(MediaFormat.KEY_HEIGHT); + outWidth = mWidth; + outHeight = mHeight; + if (listener != null) { + listener.onResolutionChanged(mWidth, mHeight); + } + } else { + codec.releaseOutputBuffer(outputBufferId, true); + framesOut++; + lastFrameMs = nowMs(); + retry.onSuccess(); // a decoded frame: the decoder works + } + } + } } From a99918a6d735d3efd9642c7e91acd26a08d035d4 Mon Sep 17 00:00:00 2001 From: Compot Developer Date: Tue, 13 Oct 2026 09:17:22 +0000 Subject: [PATCH 2/3] fix: the input wait gives way to a close or pause waiting for the decoder (Qodo on #106) --- .../com/openipc/decoder/MediaCodecManager.java | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java b/android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java index a9c5f9bc..a69e1aee 100644 --- a/android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java +++ b/android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java @@ -17,6 +17,7 @@ import java.nio.ByteBuffer; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; class MediaCodecManager { private static final String TAG = "OpenIPCDecoder"; @@ -62,6 +63,8 @@ class MediaCodecManager { private volatile long lastFrameMs; /** NAL units not fed because the decoder had no free input buffer: it is not keeping up. */ private volatile int inputDropped; + /** closeDecoder() calls waiting for decoderLock: the input wait gives way to them. */ + private final AtomicInteger closeWaiting = new AtomicInteger(); private volatile int outWidth; private volatile int outHeight; /** MIME type whose instance slot this manager holds while it has a decoder (#83). */ @@ -195,6 +198,15 @@ private static void releaseAsync(MediaCodec codec) { /** Release the decoder on purpose (codec switch, camera switch): no back-off. */ void closeDecoder() { + closeWaiting.incrementAndGet(); + try { + closeDecoderLocked(); + } finally { + closeWaiting.decrementAndGet(); + } + } + + private void closeDecoderLocked() { synchronized (decoderLock) { retry.onSuccess(); MediaCodec codec = decoder; @@ -328,7 +340,9 @@ boolean decodeFrame(Frame buffer, boolean codecH265, long watchdogRef) { // Dropping a NAL unit corrupts the picture until the next key frame (#89) int inputBufferId = codec.dequeueInputBuffer(INPUT_POLL_US); long waitUntil = nowMs() + INPUT_WAIT_MS; - while (inputBufferId < 0 && nowMs() < waitUntil) { + // A close or a pause waiting for the lock ends the wait at once (UI thread) + while (inputBufferId < 0 && nowMs() < waitUntil && enabled + && closeWaiting.get() == 0) { drainOutput(codec); inputBufferId = codec.dequeueInputBuffer(INPUT_POLL_US); } From 14b3decd7d5c8f350a408eefca4c7e7066e1aa8e Mon Sep 17 00:00:00 2001 From: Compot Developer Date: Tue, 13 Oct 2026 09:35:48 +0000 Subject: [PATCH 3/3] fix: a wait cut short by a close or pause is not counted as a decoder loss (Qodo on #106) --- .../src/main/java/com/openipc/decoder/MediaCodecManager.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java b/android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java index a69e1aee..96c12238 100644 --- a/android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java +++ b/android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java @@ -354,7 +354,8 @@ boolean decodeFrame(Frame buffer, boolean codecH265, long watchdogRef) { codec.queueInputBuffer(inputBufferId, 0, buffer.length(), System.nanoTime() / 1000, flag); } - } else { + } else if (enabled && closeWaiting.get() == 0) { + // Only a full wait is a decoder loss; a close or pause cut it short inputDropped++; } drainOutput(codec);