diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java index cb3acc0362..3a0de6fab8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java @@ -19,32 +19,25 @@ package com.google.android.exoplayer2.mediacodec; import android.media.MediaCodec; import android.media.MediaCrypto; import android.media.MediaFormat; -import android.os.Handler; import android.os.HandlerThread; import android.view.Surface; -import androidx.annotation.GuardedBy; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.decoder.CryptoInfo; -import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** - * A {@link MediaCodecAdapter} that operates the underlying {@link MediaCodec} in asynchronous mode - * and routes {@link MediaCodec.Callback} callbacks on a dedicated thread that is managed - * internally. - * - *
This adapter supports queueing input buffers asynchronously.
+ * A {@link MediaCodecAdapter} that operates the underlying {@link MediaCodec} in asynchronous mode,
+ * routes {@link MediaCodec.Callback} callbacks on a dedicated thread that is managed internally,
+ * and queues input buffers asynchronously.
*/
@RequiresApi(23)
-/* package */ final class AsynchronousMediaCodecAdapter extends MediaCodec.Callback
- implements MediaCodecAdapter {
+/* package */ final class AsynchronousMediaCodecAdapter implements MediaCodecAdapter {
@Documented
@Retention(RetentionPolicy.SOURCE)
@@ -56,24 +49,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private static final int STATE_STARTED = 2;
private static final int STATE_SHUT_DOWN = 3;
- private final Object lock;
-
- @GuardedBy("lock")
- private final MediaCodecAsyncCallback mediaCodecAsyncCallback;
-
private final MediaCodec codec;
- private final HandlerThread handlerThread;
- private @MonotonicNonNull Handler handler;
-
- @GuardedBy("lock")
- private long pendingFlushCount;
-
- private @State int state;
+ private final AsynchronousMediaCodecCallback asynchronousMediaCodecCallback;
private final AsynchronousMediaCodecBufferEnqueuer bufferEnqueuer;
-
- @GuardedBy("lock")
- @Nullable
- private IllegalStateException internalException;
+ @State private int state;
/**
* Creates an instance that wraps the specified {@link MediaCodec}.
@@ -85,21 +64,15 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/* package */ AsynchronousMediaCodecAdapter(MediaCodec codec, int trackType) {
this(
codec,
- trackType,
new HandlerThread(createCallbackThreadLabel(trackType)),
new HandlerThread(createQueueingThreadLabel(trackType)));
}
@VisibleForTesting
/* package */ AsynchronousMediaCodecAdapter(
- MediaCodec codec,
- int trackType,
- HandlerThread callbackThread,
- HandlerThread enqueueingThread) {
- this.lock = new Object();
- this.mediaCodecAsyncCallback = new MediaCodecAsyncCallback();
+ MediaCodec codec, HandlerThread callbackThread, HandlerThread enqueueingThread) {
this.codec = codec;
- this.handlerThread = callbackThread;
+ this.asynchronousMediaCodecCallback = new AsynchronousMediaCodecCallback(callbackThread);
this.bufferEnqueuer = new AsynchronousMediaCodecBufferEnqueuer(codec, enqueueingThread);
this.state = STATE_CREATED;
}
@@ -110,9 +83,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Nullable Surface surface,
@Nullable MediaCrypto crypto,
int flags) {
- handlerThread.start();
- handler = new Handler(handlerThread.getLooper());
- codec.setCallback(this, handler);
+ asynchronousMediaCodecCallback.initialize(codec);
codec.configure(mediaFormat, surface, crypto, flags);
state = STATE_CONFIGURED;
}
@@ -138,60 +109,40 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Override
public int dequeueInputBufferIndex() {
- synchronized (lock) {
- if (isFlushing()) {
- return MediaCodec.INFO_TRY_AGAIN_LATER;
- } else {
- maybeThrowException();
- return mediaCodecAsyncCallback.dequeueInputBufferIndex();
- }
- }
+ return asynchronousMediaCodecCallback.dequeueInputBufferIndex();
}
@Override
public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) {
- synchronized (lock) {
- if (isFlushing()) {
- return MediaCodec.INFO_TRY_AGAIN_LATER;
- } else {
- maybeThrowException();
- return mediaCodecAsyncCallback.dequeueOutputBufferIndex(bufferInfo);
- }
- }
+ return asynchronousMediaCodecCallback.dequeueOutputBufferIndex(bufferInfo);
}
@Override
public MediaFormat getOutputFormat() {
- synchronized (lock) {
- return mediaCodecAsyncCallback.getOutputFormat();
- }
+ return asynchronousMediaCodecCallback.getOutputFormat();
}
@Override
public void flush() {
- synchronized (lock) {
- bufferEnqueuer.flush();
- codec.flush();
- ++pendingFlushCount;
- Util.castNonNull(handler).post(this::onFlushCompleted);
- }
+ // The order of calls is important:
+ // First, flush the bufferEnqueuer to stop queueing input buffers.
+ // Second, flush the codec to stop producing available input/output buffers.
+ // Third, flush the callback after flushing the codec so that in-flight callbacks are discarded.
+ bufferEnqueuer.flush();
+ codec.flush();
+ // When flushAsync() is completed, start the codec again.
+ asynchronousMediaCodecCallback.flushAsync(/* onFlushCompleted= */ codec::start);
}
@Override
public void shutdown() {
- synchronized (lock) {
if (state == STATE_STARTED) {
bufferEnqueuer.shutdown();
}
if (state == STATE_CONFIGURED || state == STATE_STARTED) {
- handlerThread.quit();
- mediaCodecAsyncCallback.flush();
- // Leave the adapter in a flushing state so that
- // it will not dequeue anything.
- ++pendingFlushCount;
+ asynchronousMediaCodecCallback.shutdown();
}
state = STATE_SHUT_DOWN;
- }
}
@Override
@@ -199,86 +150,14 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return codec;
}
- // Called from the handler thread.
-
- @Override
- public void onInputBufferAvailable(MediaCodec codec, int index) {
- synchronized (lock) {
- mediaCodecAsyncCallback.onInputBufferAvailable(codec, index);
- }
+ @VisibleForTesting
+ /* package */ void onError(MediaCodec.CodecException error) {
+ asynchronousMediaCodecCallback.onError(codec, error);
}
- @Override
- public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {
- synchronized (lock) {
- mediaCodecAsyncCallback.onOutputBufferAvailable(codec, index, info);
- }
- }
-
- @Override
- public void onError(MediaCodec codec, MediaCodec.CodecException e) {
- synchronized (lock) {
- mediaCodecAsyncCallback.onError(codec, e);
- }
- }
-
- @Override
- public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
- synchronized (lock) {
- mediaCodecAsyncCallback.onOutputFormatChanged(codec, format);
- }
- }
-
- private void onFlushCompleted() {
- synchronized (lock) {
- onFlushCompletedSynchronized();
- }
- }
-
- @GuardedBy("lock")
- private void onFlushCompletedSynchronized() {
- if (state == STATE_SHUT_DOWN) {
- return;
- }
-
- --pendingFlushCount;
- if (pendingFlushCount > 0) {
- // Another flush() has been called.
- return;
- } else if (pendingFlushCount < 0) {
- // This should never happen.
- internalException = new IllegalStateException();
- return;
- }
-
- mediaCodecAsyncCallback.flush();
- try {
- codec.start();
- } catch (IllegalStateException e) {
- internalException = e;
- } catch (Exception e) {
- internalException = new IllegalStateException(e);
- }
- }
-
- @GuardedBy("lock")
- private boolean isFlushing() {
- return pendingFlushCount > 0;
- }
-
- @GuardedBy("lock")
- private void maybeThrowException() {
- maybeThrowInternalException();
- mediaCodecAsyncCallback.maybeThrowMediaCodecException();
- }
-
- @GuardedBy("lock")
- private void maybeThrowInternalException() {
- if (internalException != null) {
- IllegalStateException e = internalException;
- internalException = null;
- throw e;
- }
+ @VisibleForTesting
+ /* package */ void onOutputFormatChanged(MediaFormat format) {
+ asynchronousMediaCodecCallback.onOutputFormatChanged(codec, format);
}
private static String createCallbackThreadLabel(int trackType) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecCallback.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecCallback.java
new file mode 100644
index 0000000000..f05d752061
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecCallback.java
@@ -0,0 +1,325 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.exoplayer2.mediacodec;
+
+import static com.google.android.exoplayer2.util.Assertions.checkState;
+
+import android.media.MediaCodec;
+import android.media.MediaFormat;
+import android.os.Handler;
+import android.os.HandlerThread;
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import com.google.android.exoplayer2.util.IntArrayQueue;
+import com.google.android.exoplayer2.util.Util;
+import java.util.ArrayDeque;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+
+/** A {@link MediaCodec.Callback} that routes callbacks on a separate thread. */
+@RequiresApi(23)
+/* package */ final class AsynchronousMediaCodecCallback extends MediaCodec.Callback {
+ private final Object lock;
+
+ private final HandlerThread callbackThread;
+ private @MonotonicNonNull Handler handler;
+
+ @GuardedBy("lock")
+ private final IntArrayQueue availableInputBuffers;
+
+ @GuardedBy("lock")
+ private final IntArrayQueue availableOutputBuffers;
+
+ @GuardedBy("lock")
+ private final ArrayDeque Make sure to call {@link #shutdown()} to stop the background thread and release its
+ * resources.
+ *
+ * @see MediaCodec#setCallback(MediaCodec.Callback, Handler)
+ */
+ public void initialize(MediaCodec codec) {
+ checkState(handler == null);
+
+ callbackThread.start();
+ Handler handler = new Handler(callbackThread.getLooper());
+ codec.setCallback(this, handler);
+ // Initialize this.handler at the very end ensuring the callback in not considered configured
+ // if MediaCodec raises an exception.
+ this.handler = handler;
+ }
+
+ /**
+ * Shuts down this instance.
+ *
+ * This method will stop the callback thread. After calling it, callbacks will no longer be
+ * handled and dequeue methods will return {@link MediaCodec#INFO_TRY_AGAIN_LATER}.
+ */
+ public void shutdown() {
+ synchronized (lock) {
+ shutDown = true;
+ callbackThread.quit();
+ flushInternal();
+ }
+ }
+
+ /**
+ * Returns the next available input buffer index or {@link MediaCodec#INFO_TRY_AGAIN_LATER} if no
+ * such buffer exists.
+ */
+ public int dequeueInputBufferIndex() {
+ synchronized (lock) {
+ if (isFlushingOrShutdown()) {
+ return MediaCodec.INFO_TRY_AGAIN_LATER;
+ } else {
+ maybeThrowException();
+ return availableInputBuffers.isEmpty()
+ ? MediaCodec.INFO_TRY_AGAIN_LATER
+ : availableInputBuffers.remove();
+ }
+ }
+ }
+
+ /**
+ * Returns the next available output buffer index. If the next available output is a MediaFormat
+ * change, it will return {@link MediaCodec#INFO_OUTPUT_FORMAT_CHANGED} and you should call {@link
+ * #getOutputFormat()} to get the format. If there is no available output, this method will return
+ * {@link MediaCodec#INFO_TRY_AGAIN_LATER}.
+ */
+ public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) {
+ synchronized (lock) {
+ if (isFlushingOrShutdown()) {
+ return MediaCodec.INFO_TRY_AGAIN_LATER;
+ } else {
+ maybeThrowException();
+ if (availableOutputBuffers.isEmpty()) {
+ return MediaCodec.INFO_TRY_AGAIN_LATER;
+ } else {
+ int bufferIndex = availableOutputBuffers.remove();
+ if (bufferIndex >= 0) {
+ MediaCodec.BufferInfo nextBufferInfo = bufferInfos.remove();
+ bufferInfo.set(
+ nextBufferInfo.offset,
+ nextBufferInfo.size,
+ nextBufferInfo.presentationTimeUs,
+ nextBufferInfo.flags);
+ } else if (bufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
+ currentFormat = formats.remove();
+ }
+ return bufferIndex;
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the {@link MediaFormat} signalled by the underlying {@link MediaCodec}.
+ *
+ * Call this after {@link #dequeueOutputBufferIndex} returned {@link
+ * MediaCodec#INFO_OUTPUT_FORMAT_CHANGED}.
+ *
+ * @throws IllegalStateException If called before {@link #dequeueOutputBufferIndex} has returned
+ * {@link MediaCodec#INFO_OUTPUT_FORMAT_CHANGED}.
+ */
+ public MediaFormat getOutputFormat() {
+ synchronized (lock) {
+ if (currentFormat == null) {
+ throw new IllegalStateException();
+ }
+ return currentFormat;
+ }
+ }
+
+ /**
+ * Initiates a flush asynchronously, which will be completed on the callback thread. When the
+ * flush is complete, it will trigger {@code onFlushCompleted} from the callback thread.
+ *
+ * @param onFlushCompleted A {@link Runnable} that will be called when flush is completed. {@code
+ * onFlushCompleted} will be called from the scallback thread, therefore it should execute
+ * synchronized and thread-safe code.
+ */
+ public void flushAsync(Runnable onFlushCompleted) {
+ synchronized (lock) {
+ ++pendingFlushCount;
+ Util.castNonNull(handler).post(() -> this.onFlushCompleted(onFlushCompleted));
+ }
+ }
+
+ // Called from the callback thread.
+
+ @Override
+ public void onInputBufferAvailable(@NonNull MediaCodec codec, int index) {
+ synchronized (lock) {
+ availableInputBuffers.add(index);
+ }
+ }
+
+ @Override
+ public void onOutputBufferAvailable(
+ @NonNull MediaCodec codec, int index, @NonNull MediaCodec.BufferInfo info) {
+ synchronized (lock) {
+ if (pendingOutputFormat != null) {
+ addOutputFormat(pendingOutputFormat);
+ pendingOutputFormat = null;
+ }
+ availableOutputBuffers.add(index);
+ bufferInfos.add(info);
+ }
+ }
+
+ @Override
+ public void onError(@NonNull MediaCodec codec, @NonNull MediaCodec.CodecException e) {
+ synchronized (lock) {
+ mediaCodecException = e;
+ }
+ }
+
+ @Override
+ public void onOutputFormatChanged(@NonNull MediaCodec codec, @NonNull MediaFormat format) {
+ synchronized (lock) {
+ addOutputFormat(format);
+ pendingOutputFormat = null;
+ }
+ }
+
+ private void onFlushCompleted(Runnable onFlushCompleted) {
+ synchronized (lock) {
+ onFlushCompletedSynchronized(onFlushCompleted);
+ }
+ }
+
+ @GuardedBy("lock")
+ private void onFlushCompletedSynchronized(Runnable onFlushCompleted) {
+ if (shutDown) {
+ return;
+ }
+
+ --pendingFlushCount;
+ if (pendingFlushCount > 0) {
+ // Another flush() has been called.
+ return;
+ } else if (pendingFlushCount < 0) {
+ // This should never happen.
+ setInternalException(new IllegalStateException());
+ return;
+ }
+ flushInternal();
+ try {
+ onFlushCompleted.run();
+ } catch (IllegalStateException e) {
+ setInternalException(e);
+ } catch (Exception e) {
+ setInternalException(new IllegalStateException(e));
+ }
+ }
+
+ /** Flushes all available input and output buffers and any error that was previously set. */
+ @GuardedBy("lock")
+ private void flushInternal() {
+ pendingOutputFormat = formats.isEmpty() ? null : formats.getLast();
+ availableInputBuffers.clear();
+ availableOutputBuffers.clear();
+ bufferInfos.clear();
+ formats.clear();
+ mediaCodecException = null;
+ }
+
+ @GuardedBy("lock")
+ private boolean isFlushingOrShutdown() {
+ return pendingFlushCount > 0 || shutDown;
+ }
+
+ @GuardedBy("lock")
+ private void addOutputFormat(MediaFormat mediaFormat) {
+ availableOutputBuffers.add(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
+ formats.add(mediaFormat);
+ }
+
+ @GuardedBy("lock")
+ private void maybeThrowException() {
+ maybeThrowInternalException();
+ maybeThrowMediaCodecException();
+ }
+
+ @GuardedBy("lock")
+ private void maybeThrowInternalException() {
+ if (internalException != null) {
+ IllegalStateException e = internalException;
+ internalException = null;
+ throw e;
+ }
+ }
+
+ @GuardedBy("lock")
+ private void maybeThrowMediaCodecException() {
+ if (mediaCodecException != null) {
+ MediaCodec.CodecException codecException = mediaCodecException;
+ mediaCodecException = null;
+ throw codecException;
+ }
+ }
+
+ private void setInternalException(IllegalStateException e) {
+ synchronized (lock) {
+ internalException = e;
+ }
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAsyncCallback.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAsyncCallback.java
deleted file mode 100644
index 65f0c266a9..0000000000
--- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAsyncCallback.java
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.google.android.exoplayer2.mediacodec;
-
-import android.media.MediaCodec;
-import android.media.MediaFormat;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.annotation.VisibleForTesting;
-import com.google.android.exoplayer2.util.IntArrayQueue;
-import java.util.ArrayDeque;
-
-/** Handles the asynchronous callbacks from {@link android.media.MediaCodec.Callback}. */
-@RequiresApi(21)
-/* package */ final class MediaCodecAsyncCallback extends MediaCodec.Callback {
- private final IntArrayQueue availableInputBuffers;
- private final IntArrayQueue availableOutputBuffers;
- private final ArrayDeque Call this after {@link #dequeueOutputBufferIndex} returned {@link
- * MediaCodec#INFO_OUTPUT_FORMAT_CHANGED}.
- *
- * @throws IllegalStateException If called before {@link #dequeueOutputBufferIndex} has returned
- * {@link MediaCodec#INFO_OUTPUT_FORMAT_CHANGED}.
- */
- public MediaFormat getOutputFormat() throws IllegalStateException {
- if (currentFormat == null) {
- throw new IllegalStateException();
- }
- return currentFormat;
- }
-
- /**
- * Checks and throws an {@link IllegalStateException} if an error was previously set on this
- * instance via {@link #onError}.
- */
- public void maybeThrowMediaCodecException() throws IllegalStateException {
- IllegalStateException exception = mediaCodecException;
- mediaCodecException = null;
- if (exception != null) {
- throw exception;
- }
- }
-
- /**
- * Flushes the MediaCodecAsyncCallback. This method removes all available input and output buffers
- * and any error that was previously set.
- */
- public void flush() {
- pendingOutputFormat = formats.isEmpty() ? null : formats.getLast();
- availableInputBuffers.clear();
- availableOutputBuffers.clear();
- bufferInfos.clear();
- formats.clear();
- mediaCodecException = null;
- }
-
- @Override
- public void onInputBufferAvailable(MediaCodec mediaCodec, int index) {
- availableInputBuffers.add(index);
- }
-
- @Override
- public void onOutputBufferAvailable(
- MediaCodec mediaCodec, int index, MediaCodec.BufferInfo bufferInfo) {
- if (pendingOutputFormat != null) {
- addOutputFormat(pendingOutputFormat);
- pendingOutputFormat = null;
- }
- availableOutputBuffers.add(index);
- bufferInfos.add(bufferInfo);
- }
-
- @Override
- public void onError(MediaCodec mediaCodec, MediaCodec.CodecException e) {
- onMediaCodecError(e);
- }
-
- @Override
- public void onOutputFormatChanged(MediaCodec mediaCodec, MediaFormat mediaFormat) {
- addOutputFormat(mediaFormat);
- pendingOutputFormat = null;
- }
-
- @VisibleForTesting()
- void onMediaCodecError(IllegalStateException e) {
- mediaCodecException = e;
- }
-
- private void addOutputFormat(MediaFormat mediaFormat) {
- availableOutputBuffers.add(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
- formats.add(mediaFormat);
- }
-}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
index ecaa4e6400..d5092a8e51 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
@@ -1067,7 +1067,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
codecOperatingRate = CODEC_OPERATING_RATE_UNSET;
}
- MediaCodecAdapter codecAdapter = null;
+ @Nullable MediaCodecAdapter codecAdapter = null;
try {
codecInitializingTimestamp = SystemClock.elapsedRealtime();
TraceUtil.beginSection("createCodec:" + codecName);
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapterTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapterTest.java
index 0128b77add..6c3294c2aa 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapterTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapterTest.java
@@ -24,7 +24,6 @@ import android.media.MediaCodec;
import android.media.MediaFormat;
import android.os.HandlerThread;
import androidx.test.ext.junit.runners.AndroidJUnit4;
-import com.google.android.exoplayer2.C;
import java.io.IOException;
import java.lang.reflect.Constructor;
import org.junit.After;
@@ -38,18 +37,16 @@ import org.robolectric.shadows.ShadowLooper;
public class AsynchronousMediaCodecAdapterTest {
private AsynchronousMediaCodecAdapter adapter;
private MediaCodec codec;
- private TestHandlerThread callbackThread;
+ private HandlerThread callbackThread;
private HandlerThread queueingThread;
private MediaCodec.BufferInfo bufferInfo;
@Before
public void setUp() throws IOException {
codec = MediaCodec.createByCodecName("h264");
- callbackThread = new TestHandlerThread("TestCallbackThread");
+ callbackThread = new HandlerThread("TestCallbackThread");
queueingThread = new HandlerThread("TestQueueingThread");
- adapter =
- new AsynchronousMediaCodecAdapter(
- codec, /* trackType= */ C.TRACK_TYPE_VIDEO, callbackThread, queueingThread);
+ adapter = new AsynchronousMediaCodecAdapter(codec, callbackThread, queueingThread);
bufferInfo = new MediaCodec.BufferInfo();
}
@@ -57,8 +54,6 @@ public class AsynchronousMediaCodecAdapterTest {
public void tearDown() {
adapter.shutdown();
codec.release();
-
- assertThat(callbackThread.hasQuit()).isTrue();
}
@Test
@@ -85,39 +80,7 @@ public class AsynchronousMediaCodecAdapterTest {
assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(0);
}
- @Test
- public void dequeueInputBufferIndex_withPendingFlush_returnsTryAgainLater() {
- adapter.configure(
- createMediaFormat("foo"), /* surface= */ null, /* crypto= */ null, /* flags= */ 0);
- adapter.start();
- // After adapter.start(), the ShadowMediaCodec offers input buffer 0. We run all currently
- // enqueued messages and pause the looper so that flush is not completed.
- ShadowLooper shadowLooper = shadowOf(callbackThread.getLooper());
- shadowLooper.idle();
- shadowLooper.pause();
- adapter.flush();
-
- assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
- }
-
- @Test
- public void dequeueInputBufferIndex_withFlushCompletedAndInputBuffer_returnsInputBuffer() {
- adapter.configure(
- createMediaFormat("foo"), /* surface= */ null, /* crypto= */ null, /* flags= */ 0);
- adapter.start();
- // After adapter.start(), the ShadowMediaCodec offers input buffer 0. We advance the looper to
- // make sure all messages have been propagated to the adapter.
- ShadowLooper shadowLooper = shadowOf(callbackThread.getLooper());
- shadowLooper.idle();
-
- adapter.flush();
- // Progress the looper to complete flush(): the adapter should call codec.start(), triggering
- // the ShadowMediaCodec to offer input buffer 0.
- shadowLooper.idle();
-
- assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(0);
- }
@Test
public void dequeueInputBufferIndex_withMediaCodecError_throwsException() throws Exception {
@@ -128,7 +91,7 @@ public class AsynchronousMediaCodecAdapterTest {
adapter.start();
// Set an error directly on the adapter (not through the looper).
- adapter.onError(codec, createCodecException());
+ adapter.onError(createCodecException());
assertThrows(IllegalStateException.class, () -> adapter.dequeueInputBufferIndex());
}
@@ -192,25 +155,6 @@ public class AsynchronousMediaCodecAdapterTest {
assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)).isEqualTo(index);
}
- @Test
- public void dequeueOutputBufferIndex_withPendingFlush_returnsTryAgainLater() {
- adapter.configure(
- createMediaFormat("foo"), /* surface= */ null, /* crypto= */ null, /* flags= */ 0);
- adapter.start();
- // After start(), the ShadowMediaCodec offers input buffer 0, which is available only if we
- // progress the adapter's looper.
- ShadowLooper shadowLooper = shadowOf(callbackThread.getLooper());
- shadowLooper.idle();
-
- // Flush enqueues a task in the looper, but we will pause the looper to leave flush()
- // in an incomplete state.
- shadowLooper.pause();
- adapter.flush();
-
- assertThat(adapter.dequeueOutputBufferIndex(bufferInfo))
- .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
- }
-
@Test
public void dequeueOutputBufferIndex_withMediaCodecError_throwsException() throws Exception {
// Pause the looper so that we interact with the adapter from this thread only.
@@ -220,7 +164,7 @@ public class AsynchronousMediaCodecAdapterTest {
adapter.start();
// Set an error directly on the adapter.
- adapter.onError(codec, createCodecException());
+ adapter.onError(createCodecException());
assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo));
}
@@ -266,8 +210,8 @@ public class AsynchronousMediaCodecAdapterTest {
// progress the adapter's looper.
shadowOf(callbackThread.getLooper()).idle();
- // Add another format directly on the adapter.
- adapter.onOutputFormatChanged(codec, createMediaFormat("format2"));
+ // Add another format on the adapter.
+ adapter.onOutputFormatChanged(createMediaFormat("format2"));
assertThat(adapter.dequeueOutputBufferIndex(bufferInfo))
.isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
@@ -314,22 +258,4 @@ public class AsynchronousMediaCodecAdapterTest {
return constructor.newInstance(
/* errorCode= */ 0, /* actionCode= */ 0, /* detailMessage= */ "error from codec");
}
-
- private static class TestHandlerThread extends HandlerThread {
- private boolean quit;
-
- TestHandlerThread(String label) {
- super(label);
- }
-
- public boolean hasQuit() {
- return quit;
- }
-
- @Override
- public boolean quit() {
- quit = true;
- return super.quit();
- }
- }
}
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecBufferEnqueuerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecBufferEnqueuerTest.java
index e27c428a94..9e2c715b31 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecBufferEnqueuerTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecBufferEnqueuerTest.java
@@ -30,7 +30,6 @@ import com.google.android.exoplayer2.decoder.CryptoInfo;
import com.google.android.exoplayer2.util.ConditionVariable;
import java.io.IOException;
import java.nio.ByteBuffer;
-import java.util.concurrent.atomic.AtomicLong;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
@@ -65,7 +64,7 @@ public class AsynchronousMediaCodecBufferEnqueuerTest {
enqueuer.shutdown();
codec.stop();
codec.release();
- assertThat(TestHandlerThread.INSTANCES_STARTED.get()).isEqualTo(0);
+ assertThat(!handlerThread.hasStarted() || handlerThread.hasQuit()).isTrue();
}
@Test
@@ -221,25 +220,31 @@ public class AsynchronousMediaCodecBufferEnqueuerTest {
}
private static class TestHandlerThread extends HandlerThread {
- private static final AtomicLong INSTANCES_STARTED = new AtomicLong(0);
+ private boolean started;
+ private boolean quit;
- TestHandlerThread(String name) {
- super(name);
+ TestHandlerThread(String label) {
+ super(label);
+ }
+
+ public boolean hasStarted() {
+ return started;
+ }
+
+ public boolean hasQuit() {
+ return quit;
}
@Override
public synchronized void start() {
super.start();
- INSTANCES_STARTED.incrementAndGet();
+ started = true;
}
@Override
public boolean quit() {
- boolean quit = super.quit();
- if (quit) {
- INSTANCES_STARTED.decrementAndGet();
- }
- return quit;
+ quit = true;
+ return super.quit();
}
}
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecCallbackTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecCallbackTest.java
new file mode 100644
index 0000000000..6ca468d739
--- /dev/null
+++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecCallbackTest.java
@@ -0,0 +1,418 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.exoplayer2.mediacodec;
+
+import static com.google.android.exoplayer2.testutil.TestUtil.assertBufferInfosEqual;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.fail;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.media.MediaCodec;
+import android.media.MediaFormat;
+import android.os.HandlerThread;
+import android.os.Looper;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.shadows.ShadowLooper;
+
+/** Unit tests for {@link AsynchronousMediaCodecCallback}. */
+@RunWith(AndroidJUnit4.class)
+public class AsynchronousMediaCodecCallbackTest {
+
+ private AsynchronousMediaCodecCallback asynchronousMediaCodecCallback;
+ private TestHandlerThread callbackThread;
+ private MediaCodec codec;
+
+ @Before
+ public void setUp() throws IOException {
+ callbackThread = new TestHandlerThread("TestCallbackThread");
+ codec = MediaCodec.createByCodecName("h264");
+ asynchronousMediaCodecCallback = new AsynchronousMediaCodecCallback(callbackThread);
+ asynchronousMediaCodecCallback.initialize(codec);
+ }
+
+ @After
+ public void tearDown() {
+ codec.release();
+ asynchronousMediaCodecCallback.shutdown();
+
+ assertThat(callbackThread.hasQuit()).isTrue();
+ }
+
+ @Test
+ public void dequeInputBufferIndex_afterCreation_returnsTryAgain() {
+ assertThat(asynchronousMediaCodecCallback.dequeueInputBufferIndex())
+ .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
+ }
+
+ @Test
+ public void dequeInputBufferIndex_returnsEnqueuedBuffers() {
+ // Send two input buffers to the callback.
+ asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 0);
+ asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 1);
+
+ assertThat(asynchronousMediaCodecCallback.dequeueInputBufferIndex()).isEqualTo(0);
+ assertThat(asynchronousMediaCodecCallback.dequeueInputBufferIndex()).isEqualTo(1);
+ assertThat(asynchronousMediaCodecCallback.dequeueInputBufferIndex())
+ .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
+ }
+
+ @Test
+ public void dequeInputBufferIndex_withPendingFlush_returnsTryAgain() {
+ Looper callbackThreadLooper = callbackThread.getLooper();
+ AtomicBoolean flushCompleted = new AtomicBoolean();
+ // Pause the callback thread so that flush() never completes.
+ shadowOf(callbackThreadLooper).pause();
+
+ // Send two input buffers to the callback and then flush().
+ asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 0);
+ asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 1);
+ asynchronousMediaCodecCallback.flushAsync(
+ /* onFlushCompleted= */ () -> flushCompleted.set(true));
+ assertThat(flushCompleted.get()).isFalse();
+ assertThat(asynchronousMediaCodecCallback.dequeueInputBufferIndex())
+ .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
+ }
+
+ @Test
+ public void dequeInputBufferIndex_afterFlush_returnsTryAgain() {
+ Looper callbackThreadLooper = callbackThread.getLooper();
+ AtomicBoolean flushCompleted = new AtomicBoolean();
+
+ // Send two input buffers to the callback and then flush().
+ asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 0);
+ asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 1);
+ asynchronousMediaCodecCallback.flushAsync(
+ /* onFlushCompleted= */ () -> flushCompleted.set(true));
+ // Progress the callback thread so that flush() completes.
+ shadowOf(callbackThreadLooper).idle();
+
+ assertThat(flushCompleted.get()).isTrue();
+ assertThat(asynchronousMediaCodecCallback.dequeueInputBufferIndex())
+ .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
+ }
+
+ @Test
+ public void dequeInputBufferIndex_afterFlushAndNewInputBuffer_returnsEnqueuedBuffer() {
+ Looper callbackThreadLooper = callbackThread.getLooper();
+ AtomicBoolean flushCompleted = new AtomicBoolean();
+
+ // Send two input buffers to the callback, then flush(), then send
+ // another input buffer.
+ asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 0);
+ asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 1);
+ asynchronousMediaCodecCallback.flushAsync(
+ /* onFlushCompleted= */ () -> flushCompleted.set(true));
+ // Progress the callback thread so that flush() completes.
+ shadowOf(callbackThreadLooper).idle();
+ asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 2);
+
+ assertThat(flushCompleted.get()).isTrue();
+ assertThat(asynchronousMediaCodecCallback.dequeueInputBufferIndex()).isEqualTo(2);
+ }
+
+ @Test
+ public void dequeueInputBufferIndex_afterShutdown_returnsTryAgainLater() {
+ asynchronousMediaCodecCallback.onInputBufferAvailable(codec, /* index= */ 1);
+
+ asynchronousMediaCodecCallback.shutdown();
+
+ assertThat(asynchronousMediaCodecCallback.dequeueInputBufferIndex())
+ .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
+ }
+
+ @Test
+ public void dequeueInputBufferIndex_afterOnErrorCallback_throwsError() throws Exception {
+ asynchronousMediaCodecCallback.onError(codec, createCodecException());
+
+ assertThrows(
+ MediaCodec.CodecException.class,
+ () -> asynchronousMediaCodecCallback.dequeueInputBufferIndex());
+ }
+
+ @Test
+ public void dequeueInputBufferIndex_afterFlushCompletedWithError_throwsError() throws Exception {
+ MediaCodec.CodecException codecException = createCodecException();
+ asynchronousMediaCodecCallback.flushAsync(
+ () -> {
+ throw codecException;
+ });
+ shadowOf(callbackThread.getLooper()).idle();
+
+ assertThrows(
+ MediaCodec.CodecException.class,
+ () -> asynchronousMediaCodecCallback.dequeueInputBufferIndex());
+ }
+
+ @Test
+ public void dequeOutputBufferIndex_afterCreation_returnsTryAgain() {
+ MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo();
+ assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(outBufferInfo))
+ .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
+ }
+
+ @Test
+ public void dequeOutputBufferIndex_returnsEnqueuedBuffers() {
+ // Send two output buffers to the callback.
+ MediaCodec.BufferInfo bufferInfo1 = new MediaCodec.BufferInfo();
+ asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 0, bufferInfo1);
+ MediaCodec.BufferInfo bufferInfo2 = new MediaCodec.BufferInfo();
+ bufferInfo2.set(1, 1, 1, 1);
+ asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 1, bufferInfo2);
+
+ MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo();
+ assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(outBufferInfo)).isEqualTo(0);
+ assertBufferInfosEqual(bufferInfo1, outBufferInfo);
+ assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(outBufferInfo)).isEqualTo(1);
+ assertBufferInfosEqual(bufferInfo2, outBufferInfo);
+ assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(outBufferInfo))
+ .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
+ }
+
+ @Test
+ public void dequeOutputBufferIndex_withPendingFlush_returnsTryAgain() {
+ Looper callbackThreadLooper = callbackThread.getLooper();
+ AtomicBoolean flushCompleted = new AtomicBoolean();
+ // Pause the callback thread so that flush() never completes.
+ shadowOf(callbackThreadLooper).pause();
+
+ // Send two output buffers to the callback and then flush().
+ MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
+ asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 0, bufferInfo);
+ asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 1, bufferInfo);
+ asynchronousMediaCodecCallback.flushAsync(
+ /* onFlushCompleted= */ () -> flushCompleted.set(true));
+
+ assertThat(flushCompleted.get()).isFalse();
+ assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(new MediaCodec.BufferInfo()))
+ .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
+ }
+
+ @Test
+ public void dequeOutputBufferIndex_afterFlush_returnsTryAgain() {
+ Looper callbackThreadLooper = callbackThread.getLooper();
+ AtomicBoolean flushCompleted = new AtomicBoolean();
+
+ // Send two output buffers to the callback and then flush().
+ MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
+ asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 0, bufferInfo);
+ asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 1, bufferInfo);
+ asynchronousMediaCodecCallback.flushAsync(
+ /* onFlushCompleted= */ () -> flushCompleted.set(true));
+ // Progress the callback looper so that flush() completes.
+ shadowOf(callbackThreadLooper).idle();
+
+ assertThat(flushCompleted.get()).isTrue();
+ assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(new MediaCodec.BufferInfo()))
+ .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
+ }
+
+ @Test
+ public void dequeOutputBufferIndex_afterFlushAndNewOutputBuffers_returnsEnqueueBuffer() {
+ MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
+ Looper callbackThreadLooper = callbackThread.getLooper();
+ AtomicBoolean flushCompleted = new AtomicBoolean();
+
+ // Send two output buffers to the callback, then flush(), then send
+ // another output buffer.
+ asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 0, bufferInfo);
+ asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 1, bufferInfo);
+ asynchronousMediaCodecCallback.flushAsync(
+ /* onFlushCompleted= */ () -> flushCompleted.set(true));
+ // Progress the callback looper so that flush() completes.
+ shadowOf(callbackThreadLooper).idle();
+ asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 2, bufferInfo);
+ MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo();
+
+ assertThat(flushCompleted.get()).isTrue();
+ assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(outBufferInfo)).isEqualTo(2);
+ }
+
+ @Test
+ public void dequeOutputBufferIndex_withPendingOutputFormat_returnsPendingOutputFormat() {
+ MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo();
+ Looper callbackThreadLooper = callbackThread.getLooper();
+ AtomicBoolean flushCompleted = new AtomicBoolean();
+
+ asynchronousMediaCodecCallback.onOutputFormatChanged(codec, new MediaFormat());
+ asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, /* index= */ 0, outBufferInfo);
+ MediaFormat pendingMediaFormat = new MediaFormat();
+ asynchronousMediaCodecCallback.onOutputFormatChanged(codec, pendingMediaFormat);
+ // flush() should not discard the last format.
+ asynchronousMediaCodecCallback.flushAsync(
+ /* onFlushCompleted= */ () -> flushCompleted.set(true));
+ // Progress the callback looper so that flush() completes.
+ shadowOf(callbackThreadLooper).idle();
+ // Right after flush(), we send an output buffer: the pending output format should be
+ // dequeued first.
+ asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, /* index= */ 1, outBufferInfo);
+
+ assertThat(flushCompleted.get()).isTrue();
+ assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(outBufferInfo))
+ .isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
+ assertThat(asynchronousMediaCodecCallback.getOutputFormat()).isEqualTo(pendingMediaFormat);
+ assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(outBufferInfo)).isEqualTo(1);
+ }
+
+ @Test
+ public void dequeOutputBufferIndex_withPendingOutputFormatAndNewFormat_returnsNewFormat() {
+ MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo();
+ Looper callbackThreadLooper = callbackThread.getLooper();
+ AtomicBoolean flushCompleted = new AtomicBoolean();
+
+ asynchronousMediaCodecCallback.onOutputFormatChanged(codec, new MediaFormat());
+ MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
+ asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, /* index= */ 0, bufferInfo);
+ MediaFormat pendingMediaFormat = new MediaFormat();
+ asynchronousMediaCodecCallback.onOutputFormatChanged(codec, pendingMediaFormat);
+ // flush() should not discard the last format.
+ asynchronousMediaCodecCallback.flushAsync(
+ /* onFlushCompleted= */ () -> flushCompleted.set(true));
+ // Progress the callback looper so that flush() completes.
+ shadowOf(callbackThreadLooper).idle();
+ // The first callback after flush() is a new MediaFormat, it should overwrite the pending
+ // format.
+ MediaFormat newFormat = new MediaFormat();
+ asynchronousMediaCodecCallback.onOutputFormatChanged(codec, newFormat);
+ asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, /* index= */ 1, bufferInfo);
+
+ assertThat(flushCompleted.get()).isTrue();
+ assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(outBufferInfo))
+ .isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
+ assertThat(asynchronousMediaCodecCallback.getOutputFormat()).isEqualTo(newFormat);
+ assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(outBufferInfo)).isEqualTo(1);
+ }
+
+ @Test
+ public void dequeueOutputBufferIndex_afterShutdown_returnsTryAgainLater() {
+ asynchronousMediaCodecCallback.onOutputBufferAvailable(
+ codec, /* index= */ 1, new MediaCodec.BufferInfo());
+
+ asynchronousMediaCodecCallback.shutdown();
+
+ assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(new MediaCodec.BufferInfo()))
+ .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
+ }
+
+ @Test
+ public void dequeueOutputBufferIndex_afterOnErrorCallback_throwsError() throws Exception {
+ asynchronousMediaCodecCallback.onError(codec, createCodecException());
+
+ assertThrows(
+ MediaCodec.CodecException.class,
+ () -> asynchronousMediaCodecCallback.dequeueOutputBufferIndex(new MediaCodec.BufferInfo()));
+ }
+
+ @Test
+ public void dequeueOutputBufferIndex_afterFlushCompletedWithError_throwsError() throws Exception {
+ MediaCodec.CodecException codecException = createCodecException();
+ asynchronousMediaCodecCallback.flushAsync(
+ () -> {
+ throw codecException;
+ });
+ shadowOf(callbackThread.getLooper()).idle();
+
+ assertThrows(
+ MediaCodec.CodecException.class,
+ () -> asynchronousMediaCodecCallback.dequeueOutputBufferIndex(new MediaCodec.BufferInfo()));
+ }
+
+ @Test
+ public void getOutputFormat_onNewInstance_raisesException() {
+ try {
+ asynchronousMediaCodecCallback.getOutputFormat();
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test
+ public void getOutputFormat_afterOnOutputFormatCalled_returnsFormat() {
+ MediaFormat format = new MediaFormat();
+ asynchronousMediaCodecCallback.onOutputFormatChanged(codec, format);
+ MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
+
+ assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(bufferInfo))
+ .isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
+ assertThat(asynchronousMediaCodecCallback.getOutputFormat()).isEqualTo(format);
+ }
+
+ @Test
+ public void getOutputFormat_afterFlush_returnsCurrentFormat() {
+ MediaFormat format = new MediaFormat();
+ Looper callbackThreadLooper = callbackThread.getLooper();
+ AtomicBoolean flushCompleted = new AtomicBoolean();
+
+ asynchronousMediaCodecCallback.onOutputFormatChanged(codec, format);
+ asynchronousMediaCodecCallback.dequeueOutputBufferIndex(new MediaCodec.BufferInfo());
+ asynchronousMediaCodecCallback.flushAsync(
+ /* onFlushCompleted= */ () -> flushCompleted.set(true));
+ // Progress the callback looper so that flush() completes.
+ shadowOf(callbackThreadLooper).idle();
+
+ assertThat(flushCompleted.get()).isTrue();
+ assertThat(asynchronousMediaCodecCallback.getOutputFormat()).isEqualTo(format);
+ }
+
+ @Test
+ public void flush_withPendingFlush_onlyLastFlushCompletes() {
+ ShadowLooper callbackLooperShadow = shadowOf(callbackThread.getLooper());
+ callbackLooperShadow.pause();
+ AtomicInteger flushCompleted = new AtomicInteger();
+
+ asynchronousMediaCodecCallback.flushAsync(/* onFlushCompleted= */ () -> flushCompleted.set(1));
+ asynchronousMediaCodecCallback.flushAsync(/* onFlushCompleted= */ () -> flushCompleted.set(2));
+ callbackLooperShadow.idle();
+
+ assertThat(flushCompleted.get()).isEqualTo(2);
+ }
+
+ /** Reflectively create a {@link MediaCodec.CodecException}. */
+ private static MediaCodec.CodecException createCodecException() throws Exception {
+ Constructor