diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecCallback.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecCallback.java index e950d3ac73..922771b1d6 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecCallback.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecCallback.java @@ -126,10 +126,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; */ public int dequeueInputBufferIndex() { synchronized (lock) { + maybeThrowException(); if (isFlushingOrShutdown()) { return MediaCodec.INFO_TRY_AGAIN_LATER; } else { - maybeThrowException(); return availableInputBuffers.isEmpty() ? MediaCodec.INFO_TRY_AGAIN_LATER : availableInputBuffers.remove(); @@ -145,10 +145,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; */ public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { synchronized (lock) { + maybeThrowException(); if (isFlushingOrShutdown()) { return MediaCodec.INFO_TRY_AGAIN_LATER; } else { - maybeThrowException(); if (availableOutputBuffers.isEmpty()) { return MediaCodec.INFO_TRY_AGAIN_LATER; } else { diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecCallbackTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecCallbackTest.java index 6138dc4e39..fb5cb03dd5 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecCallbackTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecCallbackTest.java @@ -30,6 +30,7 @@ import android.os.Looper; import androidx.test.ext.junit.runners.AndroidJUnit4; import java.io.IOException; import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import org.junit.After; @@ -105,6 +106,36 @@ public class AsynchronousMediaCodecCallbackTest { .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); } + @Test + public void dequeInputBufferIndex_withPendingFlushAndError_throwsError() throws Exception { + AtomicBoolean beforeFlushCompletes = new AtomicBoolean(); + AtomicBoolean flushCompleted = new AtomicBoolean(); + Looper callbackThreadLooper = callbackThread.getLooper(); + Handler callbackHandler = new Handler(callbackThreadLooper); + ShadowLooper shadowCallbackLooper = shadowOf(callbackThreadLooper); + // Pause the callback thread so that flush() never completes. + shadowCallbackLooper.pause(); + + // Send two input buffers to the callback, then an error, and then flush(). + asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 0); + asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 1); + MediaCodec.CodecException expectedException = createCodecException(); + asynchronousMediaCodecCallback.onError(codec, expectedException); + callbackHandler.post(() -> beforeFlushCompletes.set(true)); + asynchronousMediaCodecCallback.flush(); + callbackHandler.post(() -> flushCompleted.set(true)); + while (!beforeFlushCompletes.get()) { + shadowCallbackLooper.runOneTask(); + } + + assertThat(flushCompleted.get()).isFalse(); + MediaCodec.CodecException actualException = + assertThrows( + MediaCodec.CodecException.class, + () -> asynchronousMediaCodecCallback.dequeueInputBufferIndex()); + assertThat(actualException).isSameInstanceAs(expectedException); + } + @Test public void dequeInputBufferIndex_afterFlush_returnsTryAgain() { Looper callbackThreadLooper = callbackThread.getLooper(); @@ -218,6 +249,39 @@ public class AsynchronousMediaCodecCallbackTest { .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); } + @Test + public void dequeOutputBufferIndex_withPendingFlushAndError_throwsError() throws Exception { + AtomicBoolean beforeFlushCompletes = new AtomicBoolean(); + AtomicBoolean flushCompleted = new AtomicBoolean(); + Looper callbackThreadLooper = callbackThread.getLooper(); + Handler callbackHandler = new Handler(callbackThreadLooper); + ShadowLooper shadowCallbackLooper = shadowOf(callbackThreadLooper); + // Pause the callback thread so that flush() never completes. + shadowCallbackLooper.pause(); + + // Send two output buffers to the callback, then an error, and then flush(). + MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); + asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 0, bufferInfo); + asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 1, bufferInfo); + MediaCodec.CodecException expectedException = createCodecException(); + asynchronousMediaCodecCallback.onError(codec, expectedException); + callbackHandler.post(() -> beforeFlushCompletes.set(true)); + asynchronousMediaCodecCallback.flush(); + callbackHandler.post(() -> flushCompleted.set(true)); + while (beforeFlushCompletes.get()) { + shadowCallbackLooper.runOneTask(); + } + + assertThat(flushCompleted.get()).isFalse(); + MediaCodec.CodecException actualException = + assertThrows( + MediaCodec.CodecException.class, + () -> + asynchronousMediaCodecCallback.dequeueOutputBufferIndex( + new MediaCodec.BufferInfo())); + assertThat(actualException).isSameInstanceAs(expectedException); + } + @Test public void dequeOutputBufferIndex_afterFlush_returnsTryAgain() { Looper callbackThreadLooper = callbackThread.getLooper(); @@ -438,13 +502,24 @@ public class AsynchronousMediaCodecCallbackTest { } @Test - public void flush_withPendingError_resetsError() throws Exception { - asynchronousMediaCodecCallback.onError(codec, createCodecException()); - // Calling flush should clear any pending error. - asynchronousMediaCodecCallback.flush(); + public void flush_withPendingError_doesntResetError() throws Exception { + AtomicBoolean flushCompleted = new AtomicBoolean(); + Looper callbackThreadLooper = callbackThread.getLooper(); + ShadowLooper shadowCallbackLooper = shadowOf(callbackThreadLooper); - assertThat(asynchronousMediaCodecCallback.dequeueInputBufferIndex()) - .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + MediaCodec.CodecException expectedException = createCodecException(); + asynchronousMediaCodecCallback.onError(codec, expectedException); + // Flush and progress the looper so that flush is completed. + asynchronousMediaCodecCallback.flush(); + new Handler(callbackThreadLooper).post(() -> flushCompleted.set(true)); + shadowCallbackLooper.idle(); + + assertThat(flushCompleted.get()).isTrue(); + MediaCodec.CodecException actualException = + assertThrows( + MediaCodec.CodecException.class, + () -> asynchronousMediaCodecCallback.dequeueInputBufferIndex()); + assertThat(actualException).isSameInstanceAs(expectedException); } @Test @@ -456,7 +531,11 @@ public class AsynchronousMediaCodecCallbackTest { } /** Reflectively create a {@link MediaCodec.CodecException}. */ - private static MediaCodec.CodecException createCodecException() throws Exception { + private static MediaCodec.CodecException createCodecException() + throws NoSuchMethodException, + InvocationTargetException, + IllegalAccessException, + InstantiationException { Constructor constructor = MediaCodec.CodecException.class.getDeclaredConstructor( Integer.TYPE, Integer.TYPE, String.class);