From c2a6d30d7bcaeced94a953ed0e0b574b78c9145f Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Sun, 2 Aug 2026 19:54:07 +0200 Subject: [PATCH] fix(android/decode): a codec input slot the feeder can't fill goes back, and so does the AU MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `AMediaCodec_getInputBuffer` returning null for an index the input-available callback had just handed us dropped both the slot and the access unit on the floor. Every sibling path in this loop recycles the slot — the orphan-part discard and the oversize drop both say so in as many words — because nothing was written and nothing was queued, so it is still ours. Forgetting it leaks one of the codec's input buffers per occurrence: we never use it again and the codec never frees what it never received, so the pipeline runs out of input slots, `pending_aus` overflows into its drop-oldest arm, and the resulting keyframe storm reads as a decode fault rather than a bookkeeping one. The AU went with it, silently — no keyframe request, no freeze gate, unlike every other loss path here — leaving a hole in the reference chain whose concealment was free to reach the screen. Both go back now. `break` rather than `continue`, because a codec that cannot hand out an input buffer it has just advertised is in no state to be fed the rest of the parked queue on this pass, and retrying the same index against every parked AU would burn the whole backlog for nothing; the loop comes round again on the housekeeping wake within 5 ms if it was transient. Gates: cargo ndk check green on arm64 and armv7, fmt clean, Android clippy at the same 4 pre-existing warnings as the base commit. Co-Authored-By: Claude Opus 5 (1M context) --- clients/android/native/src/decode/async_loop.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/clients/android/native/src/decode/async_loop.rs b/clients/android/native/src/decode/async_loop.rs index 825f2089..dc9cb30d 100644 --- a/clients/android/native/src/decode/async_loop.rs +++ b/clients/android/native/src/decode/async_loop.rs @@ -822,8 +822,21 @@ fn feed_ready( } } let Some(dst) = codec.input_buffer(idx) else { - log::warn!("decode: input_buffer({idx}) returned None — dropping AU"); - continue; + // Nothing was written and nothing was queued, so BOTH stay ours. Dropping the slot + // here leaked one of the codec's input buffers per occurrence — we forget it and the + // codec never frees what it never received, so the pipeline quietly runs out of input + // slots, `pending_aus` overflows, and the resulting drop storm reads as a decode + // fault. Dropping the AU on top of that punched a hole in the reference chain with no + // keyframe request behind it, unlike every sibling path here. + // + // `break`, not `continue`: a codec that cannot hand out an input buffer it just + // advertised is in no state to be fed the rest of the parked queue this pass, and + // retrying the same index against every parked AU would burn the whole backlog. The + // loop re-runs within the housekeeping wake (≤ 5 ms) if it was transient. + log::warn!("decode: input_buffer({idx}) returned None — retrying next pass"); + free_inputs.push_front(idx); + pending_aus.push_front(frame); + break; }; let au = &frame.data; if au.len() > dst.len() {