diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapter.java
new file mode 100644
index 0000000000..d6e4e84525
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapter.java
@@ -0,0 +1,238 @@
+/*
+ * 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 android.os.Handler;
+import android.os.HandlerThread;
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.VisibleForTesting;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+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.
+ *
+ *
After creating an instance, you need to call {@link #start()} to start the internal Thread.
+ */
+@RequiresApi(23)
+/* package */ final class DedicatedThreadAsyncMediaCodecAdapter extends MediaCodec.Callback
+ implements MediaCodecAdapter {
+
+ @IntDef({State.CREATED, State.STARTED, State.SHUT_DOWN})
+ private @interface State {
+ int CREATED = 0;
+ int STARTED = 1;
+ int SHUT_DOWN = 2;
+ }
+
+ private final MediaCodecAsyncCallback mediaCodecAsyncCallback;
+ private final MediaCodec codec;
+ private final HandlerThread handlerThread;
+ @MonotonicNonNull private Handler handler;
+ private long pendingFlushCount;
+ private @State int state;
+ private Runnable onCodecStart;
+ @Nullable private IllegalStateException internalException;
+
+ /**
+ * Creates an instance that wraps the specified {@link MediaCodec}.
+ *
+ * @param codec The {@link MediaCodec} to wrap.
+ * @param trackType One of {@link C#TRACK_TYPE_AUDIO} or {@link C#TRACK_TYPE_VIDEO}. Used for
+ * labelling the internal Thread accordingly.
+ * @throws IllegalArgumentException If {@code trackType} is not one of {@link C#TRACK_TYPE_AUDIO}
+ * or {@link C#TRACK_TYPE_VIDEO}.
+ */
+ /* package */ DedicatedThreadAsyncMediaCodecAdapter(MediaCodec codec, int trackType) {
+ this(codec, new HandlerThread(createThreadLabel(trackType)));
+ }
+
+ @VisibleForTesting
+ /* package */ DedicatedThreadAsyncMediaCodecAdapter(
+ MediaCodec codec, HandlerThread handlerThread) {
+ mediaCodecAsyncCallback = new MediaCodecAsyncCallback();
+ this.codec = codec;
+ this.handlerThread = handlerThread;
+ state = State.CREATED;
+ onCodecStart = codec::start;
+ }
+
+ /**
+ * Starts the operation of the instance.
+ *
+ *
After a call to this method, make sure to call {@link #shutdown()} to terminate the internal
+ * Thread. You can only call this method once during the lifetime of this instance; calling this
+ * method again will throw an {@link IllegalStateException}.
+ *
+ * @throws IllegalStateException If this method has been called already.
+ */
+ public synchronized void start() {
+ Assertions.checkState(state == State.CREATED);
+
+ handlerThread.start();
+ handler = new Handler(handlerThread.getLooper());
+ codec.setCallback(this, handler);
+ state = State.STARTED;
+ }
+
+ @Override
+ public synchronized int dequeueInputBufferIndex() {
+ Assertions.checkState(state == State.STARTED);
+
+ if (isFlushing()) {
+ return MediaCodec.INFO_TRY_AGAIN_LATER;
+ } else {
+ maybeThrowException();
+ return mediaCodecAsyncCallback.dequeueInputBufferIndex();
+ }
+ }
+
+ @Override
+ public synchronized int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) {
+ Assertions.checkState(state == State.STARTED);
+
+ if (isFlushing()) {
+ return MediaCodec.INFO_TRY_AGAIN_LATER;
+ } else {
+ maybeThrowException();
+ return mediaCodecAsyncCallback.dequeueOutputBufferIndex(bufferInfo);
+ }
+ }
+
+ @Override
+ public synchronized MediaFormat getOutputFormat() {
+ Assertions.checkState(state == State.STARTED);
+
+ return mediaCodecAsyncCallback.getOutputFormat();
+ }
+
+ @Override
+ public synchronized void flush() {
+ Assertions.checkState(state == State.STARTED);
+
+ codec.flush();
+ ++pendingFlushCount;
+ Util.castNonNull(handler).post(this::onFlushCompleted);
+ }
+
+ @Override
+ public synchronized void shutdown() {
+ if (state == State.STARTED) {
+ handlerThread.quit();
+ mediaCodecAsyncCallback.flush();
+ }
+
+ state = State.SHUT_DOWN;
+ }
+
+ @Override
+ public synchronized void onInputBufferAvailable(@NonNull MediaCodec codec, int index) {
+ mediaCodecAsyncCallback.onInputBufferAvailable(codec, index);
+ }
+
+ @Override
+ public synchronized void onOutputBufferAvailable(
+ @NonNull MediaCodec codec, int index, @NonNull MediaCodec.BufferInfo info) {
+ mediaCodecAsyncCallback.onOutputBufferAvailable(codec, index, info);
+ }
+
+ @Override
+ public synchronized void onError(
+ @NonNull MediaCodec codec, @NonNull MediaCodec.CodecException e) {
+ mediaCodecAsyncCallback.onError(codec, e);
+ }
+
+ @Override
+ public synchronized void onOutputFormatChanged(
+ @NonNull MediaCodec codec, @NonNull MediaFormat format) {
+ mediaCodecAsyncCallback.onOutputFormatChanged(codec, format);
+ }
+
+ @VisibleForTesting
+ /* package */ void onMediaCodecError(IllegalStateException e) {
+ mediaCodecAsyncCallback.onMediaCodecError(e);
+ }
+
+ @VisibleForTesting
+ /* package */ void setOnCodecStart(Runnable onCodecStart) {
+ this.onCodecStart = onCodecStart;
+ }
+
+ private synchronized void onFlushCompleted() {
+ if (state != State.STARTED) {
+ // The adapter has been shutdown.
+ 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 {
+ onCodecStart.run();
+ } catch (IllegalStateException e) {
+ internalException = e;
+ } catch (Exception e) {
+ internalException = new IllegalStateException(e);
+ }
+ }
+
+ private synchronized boolean isFlushing() {
+ return pendingFlushCount > 0;
+ }
+
+ private synchronized void maybeThrowException() {
+ maybeThrowInternalException();
+ mediaCodecAsyncCallback.maybeThrowMediaCodecException();
+ }
+
+ private synchronized void maybeThrowInternalException() {
+ if (internalException != null) {
+ IllegalStateException e = internalException;
+ internalException = null;
+ throw e;
+ }
+ }
+
+ private static String createThreadLabel(int trackType) {
+ StringBuilder labelBuilder = new StringBuilder("MediaCodecAsyncAdapter:");
+ if (trackType == C.TRACK_TYPE_AUDIO) {
+ labelBuilder.append("Audio");
+ } else if (trackType == C.TRACK_TYPE_VIDEO) {
+ labelBuilder.append("Video");
+ } else {
+ labelBuilder.append("Unknown(").append(trackType).append(")");
+ }
+ return labelBuilder.toString();
+ }
+}
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 1fee14a3e5..b0dc0bc6bc 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
@@ -195,7 +195,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
@Retention(RetentionPolicy.SOURCE)
@IntDef({
MediaCodecOperationMode.SYNCHRONOUS,
- MediaCodecOperationMode.ASYNCHRONOUS_PLAYBACK_THREAD
+ MediaCodecOperationMode.ASYNCHRONOUS_PLAYBACK_THREAD,
+ MediaCodecOperationMode.ASYNCHRONOUS_DEDICATED_THREAD
})
public @interface MediaCodecOperationMode {
@@ -206,6 +207,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
* callbacks to the playback Thread.
*/
int ASYNCHRONOUS_PLAYBACK_THREAD = 1;
+ /**
+ * Operates the {@link MediaCodec} in asynchronous mode and routes {@link MediaCodec.Callback}
+ * callbacks to a dedicated Thread.
+ */
+ int ASYNCHRONOUS_DEDICATED_THREAD = 2;
}
/** Indicates no codec operating rate should be set. */
@@ -472,6 +478,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
* routed to the Playback Thread. This mode requires API level ≥ 21; if the API level
* is ≤ 20, the operation mode will be set to {@link
* MediaCodecOperationMode#SYNCHRONOUS}.
+ *
{@link MediaCodecOperationMode#ASYNCHRONOUS_DEDICATED_THREAD}: The {@link MediaCodec}
+ * will operate in asynchronous mode and {@link MediaCodec.Callback} callbacks will be
+ * routed to a dedicated Thread. This mode requires API level ≥ 23; if the API level
+ * is ≤ 22, the operation mode will be set to {@link
+ * MediaCodecOperationMode#SYNCHRONOUS}.
*
* By default, the operation mode is set to {@link MediaCodecOperationMode#SYNCHRONOUS}.
*/
@@ -943,6 +954,10 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
if (mediaCodecOperationMode == MediaCodecOperationMode.ASYNCHRONOUS_PLAYBACK_THREAD
&& Util.SDK_INT >= 21) {
codecAdapter = new AsynchronousMediaCodecAdapter(codec);
+ } else if (mediaCodecOperationMode == MediaCodecOperationMode.ASYNCHRONOUS_DEDICATED_THREAD
+ && Util.SDK_INT >= 23) {
+ codecAdapter = new DedicatedThreadAsyncMediaCodecAdapter(codec, getTrackType());
+ ((DedicatedThreadAsyncMediaCodecAdapter) codecAdapter).start();
} else {
codecAdapter = new SynchronousMediaCodecAdapter(codec, getDequeueOutputBufferTimeoutUs());
}
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapterTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapterTest.java
new file mode 100644
index 0000000000..2cfb577579
--- /dev/null
+++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapterTest.java
@@ -0,0 +1,437 @@
+/*
+ * 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 static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.areEqual;
+import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.waitUntilAllEventsAreExecuted;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.media.MediaCodec;
+import android.media.MediaFormat;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+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 DedicatedThreadAsyncMediaCodecAdapter}. */
+@RunWith(AndroidJUnit4.class)
+public class DedicatedThreadAsyncMediaCodecAdapterTest {
+ private DedicatedThreadAsyncMediaCodecAdapter adapter;
+ private MediaCodec codec;
+ private TestHandlerThread handlerThread;
+ private MediaCodec.BufferInfo bufferInfo = null;
+
+ @Before
+ public void setup() throws IOException {
+ codec = MediaCodec.createByCodecName("h264");
+ handlerThread = new TestHandlerThread("TestHandlerThread");
+ adapter = new DedicatedThreadAsyncMediaCodecAdapter(codec, handlerThread);
+ bufferInfo = new MediaCodec.BufferInfo();
+ }
+
+ @After
+ public void tearDown() {
+ adapter.shutdown();
+ assertThat(TestHandlerThread.INSTANCES_STARTED.get()).isEqualTo(0);
+ }
+
+ @Test
+ public void startAndShutdown_works() {
+ adapter.start();
+ adapter.shutdown();
+ }
+
+ @Test
+ public void start_calledTwice_throwsException() {
+ adapter.start();
+ try {
+ adapter.start();
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test
+ public void dequeueInputBufferIndex_withoutStart_throwsException() {
+ try {
+ adapter.dequeueInputBufferIndex();
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test
+ public void dequeueInputBufferIndex_afterShutdown_throwsException() {
+ adapter.start();
+ adapter.shutdown();
+ try {
+ adapter.dequeueInputBufferIndex();
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test
+ public void dequeueInputBufferIndex_withAfterFlushFailed_throwsException()
+ throws InterruptedException {
+ adapter.setOnCodecStart(
+ () -> {
+ throw new IllegalStateException("codec#start() exception");
+ });
+ adapter.start();
+ adapter.flush();
+
+ assertThat(
+ waitUntilAllEventsAreExecuted(
+ handlerThread.getLooper(), /* time= */ 5, TimeUnit.SECONDS))
+ .isTrue();
+ try {
+ adapter.dequeueInputBufferIndex();
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test
+ public void dequeueInputBufferIndex_withoutInputBuffer_returnsTryAgainLater() {
+ adapter.start();
+
+ assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
+ }
+
+ @Test
+ public void dequeueInputBufferIndex_withInputBuffer_returnsInputBuffer() {
+ adapter.start();
+ adapter.onInputBufferAvailable(codec, 0);
+
+ assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(0);
+ }
+
+ @Test
+ public void dequeueInputBufferIndex_withPendingFlush_returnsTryAgainLater() {
+ adapter.start();
+ adapter.onInputBufferAvailable(codec, 0);
+ adapter.flush();
+
+ assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
+ }
+
+ @Test
+ public void dequeueInputBufferIndex_withFlushCompletedAndInputBuffer_returnsInputBuffer()
+ throws InterruptedException {
+ // Disable calling codec.start() after flush to avoid receiving buffers from the
+ // shadow codec impl
+ adapter.setOnCodecStart(() -> {});
+ adapter.start();
+ Looper looper = handlerThread.getLooper();
+ Handler handler = new Handler(looper);
+ // Enqueue 10 callbacks from codec
+ for (int i = 0; i < 10; i++) {
+ int bufferIndex = i;
+ handler.post(() -> adapter.onInputBufferAvailable(codec, bufferIndex));
+ }
+ adapter.flush(); // Enqueues a flush event after the onInputBufferAvailable callbacks
+ // Enqueue another onInputBufferAvailable after the flush event
+ handler.post(() -> adapter.onInputBufferAvailable(codec, 10));
+
+ // Wait until all tasks have been handled
+ assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue();
+ assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(10);
+ }
+
+ @Test
+ public void dequeueInputBufferIndex_withMediaCodecError_throwsException() {
+ adapter.start();
+ adapter.onMediaCodecError(new IllegalStateException("error from codec"));
+
+ try {
+ adapter.dequeueInputBufferIndex();
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test
+ public void dequeueOutputBufferIndex_withoutStart_throwsException() {
+ try {
+ adapter.dequeueOutputBufferIndex(bufferInfo);
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test
+ public void dequeueOutputBufferIndex_afterShutdown_throwsException() {
+ adapter.start();
+ adapter.shutdown();
+ try {
+ adapter.dequeueOutputBufferIndex(bufferInfo);
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test
+ public void dequeueOutputBufferIndex_withInternalException_throwsException()
+ throws InterruptedException {
+ adapter.setOnCodecStart(
+ () -> {
+ throw new RuntimeException("codec#start() exception");
+ });
+ adapter.start();
+ adapter.flush();
+
+ assertThat(
+ waitUntilAllEventsAreExecuted(
+ handlerThread.getLooper(), /* time= */ 5, TimeUnit.SECONDS))
+ .isTrue();
+ try {
+ adapter.dequeueOutputBufferIndex(bufferInfo);
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test
+ public void dequeueOutputBufferIndex_withoutInputBuffer_returnsTryAgainLater() {
+ adapter.start();
+
+ assertThat(adapter.dequeueOutputBufferIndex(bufferInfo))
+ .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
+ }
+
+ @Test
+ public void dequeueOutputBufferIndex_withOutputBuffer_returnsOutputBuffer() {
+ adapter.start();
+ MediaCodec.BufferInfo enqueuedBufferInfo = new MediaCodec.BufferInfo();
+ adapter.onOutputBufferAvailable(codec, 0, enqueuedBufferInfo);
+
+ assertThat(adapter.dequeueOutputBufferIndex((bufferInfo))).isEqualTo(0);
+ assertThat(areEqual(bufferInfo, enqueuedBufferInfo)).isTrue();
+ }
+
+ @Test
+ public void dequeueOutputBufferIndex_withPendingFlush_returnsTryAgainLater() {
+ adapter.start();
+ adapter.dequeueOutputBufferIndex(bufferInfo);
+ adapter.flush();
+
+ assertThat(adapter.dequeueOutputBufferIndex(bufferInfo))
+ .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
+ }
+
+ @Test
+ public void dequeueOutputBufferIndex_withFlushCompletedAndOutputBuffer_returnsOutputBuffer()
+ throws InterruptedException {
+ adapter.start();
+ Looper looper = handlerThread.getLooper();
+ Handler handler = new Handler(looper);
+ // Enqueue 10 callbacks from codec
+ for (int i = 0; i < 10; i++) {
+ int bufferIndex = i;
+ MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo();
+ outBufferInfo.presentationTimeUs = i;
+ handler.post(() -> adapter.onOutputBufferAvailable(codec, bufferIndex, outBufferInfo));
+ }
+ adapter.flush(); // Enqueues a flush event after the onOutputBufferAvailable callbacks
+ // Enqueue another onOutputBufferAvailable after the flush event
+ MediaCodec.BufferInfo lastBufferInfo = new MediaCodec.BufferInfo();
+ lastBufferInfo.presentationTimeUs = 10;
+ handler.post(() -> adapter.onOutputBufferAvailable(codec, 10, lastBufferInfo));
+
+ // Wait until all tasks have been handled
+ assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue();
+ assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)).isEqualTo(10);
+ assertThat(areEqual(bufferInfo, lastBufferInfo)).isTrue();
+ }
+
+ @Test
+ public void dequeueOutputBufferIndex_withMediaCodecError_throwsException() {
+ adapter.start();
+ adapter.onMediaCodecError(new IllegalStateException("error from codec"));
+
+ try {
+ adapter.dequeueOutputBufferIndex(bufferInfo);
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test
+ public void getOutputFormat_withoutStart_throwsException() {
+ try {
+ adapter.getOutputFormat();
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test
+ public void getOutputFormat_afterShutdown_throwsException() {
+ adapter.start();
+ adapter.shutdown();
+ try {
+ adapter.getOutputFormat();
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test
+ public void getOutputFormat_withoutFormatReceived_throwsException() {
+ adapter.start();
+
+ try {
+ adapter.getOutputFormat();
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test
+ public void getOutputFormat_withMultipleFormats_returnsCorrectFormat() {
+ adapter.start();
+ MediaFormat[] formats = new MediaFormat[10];
+ for (int i = 0; i < formats.length; i++) {
+ formats[i] = new MediaFormat();
+ adapter.onOutputFormatChanged(codec, formats[i]);
+ }
+
+ for (int i = 0; i < 10; i++) {
+ assertThat(adapter.dequeueOutputBufferIndex(bufferInfo))
+ .isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
+ assertThat(adapter.getOutputFormat()).isEqualTo(formats[i]);
+ // A subsequent call to getOutputFormat() should return the previously fetched format
+ assertThat(adapter.getOutputFormat()).isEqualTo(formats[i]);
+ }
+ assertThat(adapter.dequeueOutputBufferIndex(bufferInfo))
+ .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
+ }
+
+ @Test
+ public void getOutputFormat_afterFlush_returnsPreviousFormat() throws InterruptedException {
+ MediaFormat format = new MediaFormat();
+ adapter.start();
+ adapter.onOutputFormatChanged(codec, format);
+
+ assertThat(adapter.dequeueOutputBufferIndex(bufferInfo))
+ .isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
+ assertThat(adapter.getOutputFormat()).isEqualTo(format);
+
+ adapter.flush();
+ assertThat(
+ waitUntilAllEventsAreExecuted(
+ handlerThread.getLooper(), /* time= */ 5, TimeUnit.SECONDS))
+ .isTrue();
+ assertThat(adapter.getOutputFormat()).isEqualTo(format);
+ }
+
+ @Test
+ public void flush_withoutStarted_throwsException() {
+ try {
+ adapter.flush();
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test
+ public void flush_afterShutdown_throwsException() {
+ adapter.start();
+ adapter.shutdown();
+ try {
+ adapter.flush();
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test
+ public void flush_multipleTimes_onlyLastFlushExecutes() throws InterruptedException {
+ AtomicInteger onCodecStartCount = new AtomicInteger(0);
+ adapter.setOnCodecStart(() -> onCodecStartCount.incrementAndGet());
+ adapter.start();
+ Looper looper = handlerThread.getLooper();
+ Handler handler = new Handler(looper);
+ handler.post(() -> adapter.onInputBufferAvailable(codec, 0));
+ adapter.flush(); // Enqueues a flush event
+ handler.post(() -> adapter.onInputBufferAvailable(codec, 2));
+ AtomicInteger milestoneCount = new AtomicInteger(0);
+ handler.post(() -> milestoneCount.incrementAndGet());
+ adapter.flush(); // Enqueues a second flush event
+ handler.post(() -> adapter.onInputBufferAvailable(codec, 3));
+
+ // Progress the looper until the milestoneCount is increased - first flush event
+ // should have been a no-op
+ ShadowLooper shadowLooper = shadowOf(looper);
+ while (milestoneCount.get() < 1) {
+ shadowLooper.runOneTask();
+ }
+ assertThat(onCodecStartCount.get()).isEqualTo(0);
+
+ assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue();
+ assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(3);
+ assertThat(onCodecStartCount.get()).isEqualTo(1);
+ }
+
+ @Test
+ public void flush_andImmediatelyShutdown_flushIsNoOp() throws InterruptedException {
+ AtomicInteger onCodecStartCount = new AtomicInteger(0);
+ adapter.setOnCodecStart(() -> onCodecStartCount.incrementAndGet());
+ adapter.start();
+ // Obtain looper when adapter is started
+ Looper looper = handlerThread.getLooper();
+ adapter.flush();
+ adapter.shutdown();
+
+ assertThat(waitUntilAllEventsAreExecuted(looper, 5, TimeUnit.SECONDS)).isTrue();
+ // only shutdown flushes the MediaCodecAsync handler
+ assertThat(onCodecStartCount.get()).isEqualTo(0);
+ }
+
+ private static class TestHandlerThread extends HandlerThread {
+ private static final AtomicLong INSTANCES_STARTED = new AtomicLong(0);
+
+ public TestHandlerThread(String name) {
+ super(name);
+ }
+
+ @Override
+ public synchronized void start() {
+ super.start();
+ INSTANCES_STARTED.incrementAndGet();
+ }
+
+ @Override
+ public boolean quit() {
+ boolean quit = super.quit();
+ if (quit) {
+ INSTANCES_STARTED.decrementAndGet();
+ }
+ return quit;
+ }
+ }
+}