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
new file mode 100644
index 0000000000..c0596c0550
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java
@@ -0,0 +1,140 @@
+/*
+ * 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.Looper;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.VisibleForTesting;
+import com.google.android.exoplayer2.util.Assertions;
+
+/**
+ * A {@link MediaCodecAdapter} that operates the {@link MediaCodec} in asynchronous mode.
+ *
+ *
The AsynchronousMediaCodecAdapter routes callbacks to the current Thread's {@link Looper}
+ * obtained via {@link Looper#myLooper()}
+ */
+@RequiresApi(21)
+/* package */ final class AsynchronousMediaCodecAdapter implements MediaCodecAdapter {
+ private MediaCodecAsyncCallback mediaCodecAsyncCallback;
+ private final Handler handler;
+ private final MediaCodec codec;
+ @Nullable private IllegalStateException internalException;
+ private boolean flushing;
+ private Runnable onCodecStart;
+
+ /**
+ * Create a new {@code AsynchronousMediaCodecAdapter}.
+ *
+ * @param codec the {@link MediaCodec} to wrap.
+ */
+ public AsynchronousMediaCodecAdapter(MediaCodec codec) {
+ this(codec, Assertions.checkNotNull(Looper.myLooper()));
+ }
+
+ @VisibleForTesting
+ /* package */ AsynchronousMediaCodecAdapter(MediaCodec codec, Looper looper) {
+ this.mediaCodecAsyncCallback = new MediaCodecAsyncCallback();
+ handler = new Handler(looper);
+ this.codec = codec;
+ this.codec.setCallback(mediaCodecAsyncCallback);
+ onCodecStart = () -> codec.start();
+ }
+
+ @Override
+ public int dequeueInputBufferIndex() {
+ if (flushing) {
+ return MediaCodec.INFO_TRY_AGAIN_LATER;
+ } else {
+ maybeThrowException();
+ return mediaCodecAsyncCallback.dequeueInputBufferIndex();
+ }
+ }
+
+ @Override
+ public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) {
+ if (flushing) {
+ return MediaCodec.INFO_TRY_AGAIN_LATER;
+ } else {
+ maybeThrowException();
+ return mediaCodecAsyncCallback.dequeueOutputBufferIndex(bufferInfo);
+ }
+ }
+
+ @Override
+ public MediaFormat getOutputFormat() {
+ return mediaCodecAsyncCallback.getOutputFormat();
+ }
+
+ @Override
+ public void flush() {
+ clearPendingFlushState();
+ flushing = true;
+ codec.flush();
+ handler.post(this::onCompleteFlush);
+ }
+
+ @Override
+ public void shutdown() {
+ clearPendingFlushState();
+ }
+
+ @VisibleForTesting
+ /* package */ MediaCodec.Callback getMediaCodecCallback() {
+ return mediaCodecAsyncCallback;
+ }
+
+ private void onCompleteFlush() {
+ flushing = false;
+ mediaCodecAsyncCallback.flush();
+ try {
+ onCodecStart.run();
+ } catch (IllegalStateException e) {
+ // Catch IllegalStateException directly so that we don't have to wrap it.
+ internalException = e;
+ } catch (Exception e) {
+ internalException = new IllegalStateException(e);
+ }
+ }
+
+ @VisibleForTesting
+ /* package */ void setOnCodecStart(Runnable onCodecStart) {
+ this.onCodecStart = onCodecStart;
+ }
+
+ private void maybeThrowException() throws IllegalStateException {
+ maybeThrowInternalException();
+ mediaCodecAsyncCallback.maybeThrowMediaCodecException();
+ }
+
+ private void maybeThrowInternalException() {
+ if (internalException != null) {
+ IllegalStateException e = internalException;
+ internalException = null;
+ throw e;
+ }
+ }
+
+ /** Clear state related to pending flush events. */
+ private void clearPendingFlushState() {
+ handler.removeCallbacksAndMessages(null);
+ internalException = null;
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAdapter.java
new file mode 100644
index 0000000000..9d86f37736
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAdapter.java
@@ -0,0 +1,79 @@
+/*
+ * 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;
+
+/**
+ * Abstracts {@link MediaCodec} operations.
+ *
+ *
{@code MediaCodecAdapter} offers a common interface to interact with a {@link MediaCodec}
+ * regardless of the {@link
+ * com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.MediaCodecOperationMode} the {@link
+ * MediaCodec} is operating in.
+ *
+ * @see com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.MediaCodecOperationMode
+ */
+/* package */ interface MediaCodecAdapter {
+
+ /**
+ * Returns the next available input buffer index from the underlying {@link MediaCodec} or {@link
+ * MediaCodec#INFO_TRY_AGAIN_LATER} if no such buffer exists.
+ *
+ * @throws {@link IllegalStateException} if the underlying {@link MediaCodec} raised an error.
+ */
+ int dequeueInputBufferIndex();
+
+ /**
+ * Returns the next available output buffer index from the underlying {@link MediaCodec}. 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}.
+ *
+ * @throws {@link IllegalStateException} if the underlying {@link MediaCodec} raised an error.
+ */
+ int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo);
+
+ /**
+ * Gets the {@link MediaFormat} that was output from the {@link MediaCodec}.
+ *
+ *
Call this method if a previous call to {@link #dequeueOutputBufferIndex} returned {@link
+ * MediaCodec#INFO_OUTPUT_FORMAT_CHANGED}.
+ */
+ MediaFormat getOutputFormat();
+
+ /**
+ * Flushes the {@code MediaCodecAdapter}.
+ *
+ *
Note: {@link #flush()} should also call any {@link MediaCodec} methods needed to flush the
+ * {@link MediaCodec}, i.e., {@link MediaCodec#flush()} and optionally {@link
+ * MediaCodec#start()}, if the {@link MediaCodec} operates in asynchronous mode.
+ */
+ void flush();
+
+ /**
+ * Shutdown the {@code MediaCodecAdapter}.
+ *
+ *
Note: This method does not release the underlying {@link MediaCodec}. Make sure to call
+ * {@link #shutdown()} before stopping or releasing the underlying {@link MediaCodec} to ensure
+ * the adapter is fully shutdown before the {@link MediaCodec} stops executing. Otherwise, there
+ * is a risk the adapter might interact with a stopped or released {@link MediaCodec}.
+ */
+ void shutdown();
+}
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 10276abd5f..e5b62a97cc 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
@@ -23,13 +23,10 @@ import android.media.MediaCrypto;
import android.media.MediaCryptoException;
import android.media.MediaFormat;
import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
import android.os.SystemClock;
import androidx.annotation.CheckResult;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.BaseRenderer;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
@@ -193,6 +190,24 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
}
+ /** The modes to operate the {@link MediaCodec}. */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ MediaCodecOperationMode.SYNCHRONOUS,
+ MediaCodecOperationMode.ASYNCHRONOUS_PLAYBACK_THREAD
+ })
+ public @interface MediaCodecOperationMode {
+
+ /** Operates the {@link MediaCodec} in synchronous mode. */
+ int SYNCHRONOUS = 0;
+ /**
+ * Operates the {@link MediaCodec} in asynchronous mode and routes {@link MediaCodec.Callback}
+ * callbacks to the playback Thread.
+ */
+ int ASYNCHRONOUS_PLAYBACK_THREAD = 1;
+ }
+
/** Indicates no codec operating rate should be set. */
protected static final float CODEC_OPERATING_RATE_UNSET = -1;
@@ -293,50 +308,6 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
})
private @interface AdaptationWorkaroundMode {}
- /**
- * Abstracts {@link MediaCodec} operations that differ whether a {@link MediaCodec} is used in
- * synchronous or asynchronous mode.
- */
- private interface MediaCodecAdapter {
-
- /**
- * Returns the next available input buffer index from the underlying {@link MediaCodec} or
- * {@link MediaCodec#INFO_TRY_AGAIN_LATER} if no such buffer exists.
- *
- * @throws {@link IllegalStateException} if the underling {@link MediaCodec} raised an error.
- */
- int dequeueInputBufferIndex();
-
- /**
- * Returns the next available output buffer index from the underlying {@link MediaCodec}. 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}.
- *
- * @throws {@link IllegalStateException} if the underling {@link MediaCodec} raised an error.
- */
- int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo);
-
- /**
- * Gets the {@link MediaFormat} that was output from the {@link MediaCodec}.
- *
- *
Call this method if a previous call to {@link #dequeueOutputBufferIndex} returned {@link
- * MediaCodec#INFO_OUTPUT_FORMAT_CHANGED}.
- */
- MediaFormat getOutputFormat();
-
- /** Flushes the {@code MediaCodecAdapter}. */
- void flush();
-
- /**
- * Shutdown the {@code MediaCodecAdapter}.
- *
- *
Note: it does not release the underlying codec.
- */
- void shutdown();
- }
-
/**
* The adaptation workaround is never used.
*/
@@ -424,7 +395,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
private boolean skipMediaCodecStopOnRelease;
private boolean pendingOutputEndOfStream;
- private boolean useMediaCodecInAsyncMode;
+ private @MediaCodecOperationMode int mediaCodecOperationMode;
protected DecoderCounters decoderCounters;
@@ -470,6 +441,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
codecOperatingRate = CODEC_OPERATING_RATE_UNSET;
rendererOperatingRate = 1f;
renderTimeLimitMs = C.TIME_UNSET;
+ mediaCodecOperationMode = MediaCodecOperationMode.SYNCHRONOUS;
}
/**
@@ -503,16 +475,25 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
/**
- * Use the underlying {@link MediaCodec} in asynchronous mode to obtain available input and output
- * buffers.
+ * Set the mode of operation of the underlying {@link MediaCodec}.
*
*
This method is experimental, and will be renamed or removed in a future release. It should
* only be called before the renderer is used.
*
- * @param enabled enable of disable the feature.
+ * @param mode the mode of the MediaCodec. The supported modes are:
+ *
+ * - {@link MediaCodecOperationMode#SYNCHRONOUS}: The {@link MediaCodec} will operate in
+ * synchronous mode.
+ *
- {@link MediaCodecOperationMode#ASYNCHRONOUS_PLAYBACK_THREAD}: The {@link MediaCodec}
+ * will operate in asynchronous mode and {@link MediaCodec.Callback} callbacks will be
+ * 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}.
+ *
+ * By default, the operation mode is set to {@link MediaCodecOperationMode#SYNCHRONOUS}.
*/
- public void experimental_setUseMediaCodecInAsyncMode(boolean enabled) {
- useMediaCodecInAsyncMode = enabled;
+ public void experimental_setMediaCodecOperationMode(@MediaCodecOperationMode int mode) {
+ mediaCodecOperationMode = mode;
}
@Override
@@ -709,6 +690,10 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
availableCodecInfos = null;
codecInfo = null;
codecFormat = null;
+ if (codecAdapter != null) {
+ codecAdapter.shutdown();
+ codecAdapter = null;
+ }
resetInputBuffer();
resetOutputBuffer();
resetCodecBuffers();
@@ -730,10 +715,6 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
} finally {
codec = null;
- if (codecAdapter != null) {
- codecAdapter.shutdown();
- codecAdapter = null;
- }
try {
if (mediaCrypto != null) {
mediaCrypto.release();
@@ -982,7 +963,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
codecInitializingTimestamp = SystemClock.elapsedRealtime();
TraceUtil.beginSection("createCodec:" + codecName);
codec = MediaCodec.createByCodecName(codecName);
- if (useMediaCodecInAsyncMode && Util.SDK_INT >= 21) {
+ if (mediaCodecOperationMode == MediaCodecOperationMode.ASYNCHRONOUS_PLAYBACK_THREAD
+ && Util.SDK_INT >= 21) {
codecAdapter = new AsynchronousMediaCodecAdapter(codec);
} else {
codecAdapter = new SynchronousMediaCodecAdapter(codec, getDequeueOutputBufferTimeoutUs());
@@ -2021,124 +2003,4 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
return Util.SDK_INT <= 18 && format.channelCount == 1
&& "OMX.MTK.AUDIO.DECODER.MP3".equals(name);
}
-
- @RequiresApi(21)
- private static class AsynchronousMediaCodecAdapter implements MediaCodecAdapter {
-
- private MediaCodecAsyncCallback mediaCodecAsyncCallback;
- private final Handler handler;
- private final MediaCodec codec;
- @Nullable private IllegalStateException internalException;
- private boolean flushing;
-
- public AsynchronousMediaCodecAdapter(MediaCodec codec) {
- mediaCodecAsyncCallback = new MediaCodecAsyncCallback();
- handler = new Handler(Looper.myLooper());
- this.codec = codec;
- this.codec.setCallback(mediaCodecAsyncCallback);
- }
-
- @Override
- public int dequeueInputBufferIndex() {
- if (flushing) {
- return MediaCodec.INFO_TRY_AGAIN_LATER;
- } else {
- maybeThrowException();
- return mediaCodecAsyncCallback.dequeueInputBufferIndex();
- }
- }
-
- @Override
- public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) {
- if (flushing) {
- return MediaCodec.INFO_TRY_AGAIN_LATER;
- } else {
- maybeThrowException();
- return mediaCodecAsyncCallback.dequeueOutputBufferIndex(bufferInfo);
- }
- }
-
- @Override
- public MediaFormat getOutputFormat() {
- return mediaCodecAsyncCallback.getOutputFormat();
- }
-
- @Override
- public void flush() {
- clearPendingFlushState();
- flushing = true;
- codec.flush();
- handler.post(this::onCompleteFlush);
- }
-
- @Override
- public void shutdown() {
- clearPendingFlushState();
- }
-
- private void onCompleteFlush() {
- flushing = false;
- mediaCodecAsyncCallback.flush();
- try {
- codec.start();
- } catch (IllegalStateException e) {
- // Catch IllegalStateException directly so that we don't have to wrap it
- internalException = e;
- } catch (Exception e) {
- internalException = new IllegalStateException(e);
- }
- }
-
- private void maybeThrowException() throws IllegalStateException {
- maybeThrowInternalException();
- mediaCodecAsyncCallback.maybeThrowMediaCodecException();
- }
-
- private void maybeThrowInternalException() {
- if (internalException != null) {
- IllegalStateException e = internalException;
- internalException = null;
- throw e;
- }
- }
-
- /** Clear state related to pending flush events. */
- private void clearPendingFlushState() {
- handler.removeCallbacksAndMessages(null);
- internalException = null;
- }
- }
-
- private static class SynchronousMediaCodecAdapter implements MediaCodecAdapter {
- private final MediaCodec codec;
- private final long dequeueOutputBufferTimeoutMs;
-
- public SynchronousMediaCodecAdapter(MediaCodec mediaCodec, long dequeueOutputBufferTimeoutMs) {
- this.codec = mediaCodec;
- this.dequeueOutputBufferTimeoutMs = dequeueOutputBufferTimeoutMs;
- }
-
- @Override
- public int dequeueInputBufferIndex() {
- return codec.dequeueInputBuffer(0);
- }
-
- @Override
- public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) {
- return codec.dequeueOutputBuffer(bufferInfo, dequeueOutputBufferTimeoutMs);
- }
-
- @Override
- public MediaFormat getOutputFormat() {
- return codec.getOutputFormat();
- }
-
- @Override
- public void flush() {
- codec.flush();
- }
-
- @Override
- public void shutdown() {}
- }
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java
new file mode 100644
index 0000000000..8caf72ecf4
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java
@@ -0,0 +1,56 @@
+/*
+ * 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;
+
+/**
+ * A {@link MediaCodecAdapter} that operates the underlying {@link MediaCodec} in synchronous mode.
+ */
+/* package */ final class SynchronousMediaCodecAdapter implements MediaCodecAdapter {
+ private final MediaCodec codec;
+ private final long dequeueOutputBufferTimeoutMs;
+
+ public SynchronousMediaCodecAdapter(MediaCodec mediaCodec, long dequeueOutputBufferTimeoutMs) {
+ this.codec = mediaCodec;
+ this.dequeueOutputBufferTimeoutMs = dequeueOutputBufferTimeoutMs;
+ }
+
+ @Override
+ public int dequeueInputBufferIndex() {
+ return codec.dequeueInputBuffer(0);
+ }
+
+ @Override
+ public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) {
+ return codec.dequeueOutputBuffer(bufferInfo, dequeueOutputBufferTimeoutMs);
+ }
+
+ @Override
+ public MediaFormat getOutputFormat() {
+ return codec.getOutputFormat();
+ }
+
+ @Override
+ public void flush() {
+ codec.flush();
+ }
+
+ @Override
+ public void shutdown() {}
+}
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
new file mode 100644
index 0000000000..d2bb0fcc5b
--- /dev/null
+++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapterTest.java
@@ -0,0 +1,235 @@
+/*
+ * 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 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.AtomicBoolean;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit tests for {@link AsynchronousMediaCodecAdapter}. */
+@RunWith(AndroidJUnit4.class)
+public class AsynchronousMediaCodecAdapterTest {
+ private AsynchronousMediaCodecAdapter adapter;
+ private MediaCodec codec;
+ private HandlerThread handlerThread;
+ private Looper looper;
+ private MediaCodec.BufferInfo bufferInfo;
+
+ @Before
+ public void setup() throws IOException {
+ handlerThread = new HandlerThread("TestHandlerThread");
+ handlerThread.start();
+ looper = handlerThread.getLooper();
+ codec = MediaCodec.createByCodecName("h264");
+ adapter = new AsynchronousMediaCodecAdapter(codec, looper);
+ bufferInfo = new MediaCodec.BufferInfo();
+ }
+
+ @After
+ public void tearDown() {
+ handlerThread.quit();
+ }
+
+ @Test
+ public void dequeueInputBufferIndex_withoutInputBuffer_returnsTryAgainLater() {
+ assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
+ }
+
+ @Test
+ public void dequeueInputBufferIndex_withInputBuffer_returnsInputBuffer() {
+ adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 0);
+
+ assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(0);
+ }
+
+ @Test
+ public void dequeueInputBufferIndex_whileFlushing_returnsTryAgainLater() {
+ adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 0);
+ adapter.flush();
+ adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 1);
+
+ assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
+ }
+
+ @Test
+ public void dequeueInputBufferIndex_afterFlushCompletes_returnsNextInputBuffer()
+ throws InterruptedException {
+ // Disable calling codec.start() after flush() completes to avoid receiving buffers from the
+ // shadow codec impl
+ adapter.setOnCodecStart(() -> {});
+ Handler handler = new Handler(looper);
+ handler.post(
+ () -> adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 0));
+ adapter.flush(); // enqueues a flush event on the looper
+ handler.post(
+ () -> adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 1));
+
+ assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue();
+ assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(1);
+ }
+
+ @Test
+ public void dequeueInputBufferIndex_afterFlushCompletesWithError_throwsException()
+ throws InterruptedException {
+ adapter.setOnCodecStart(
+ () -> {
+ throw new IllegalStateException("codec#start() exception");
+ });
+ adapter.flush();
+
+ assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue();
+ try {
+ adapter.dequeueInputBufferIndex();
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test
+ public void dequeueOutputBufferIndex_withoutOutputBuffer_returnsTryAgainLater() {
+ assertThat(adapter.dequeueOutputBufferIndex(bufferInfo))
+ .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
+ }
+
+ @Test
+ public void dequeueOutputBufferIndex_withOutputBuffer_returnsOutputBuffer() {
+ MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo();
+ outBufferInfo.presentationTimeUs = 10;
+ adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 0, outBufferInfo);
+
+ assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)).isEqualTo(0);
+ assertThat(areEqual(bufferInfo, outBufferInfo)).isTrue();
+ }
+
+ @Test
+ public void dequeueOutputBufferIndex_whileFlushing_returnsTryAgainLater() {
+ adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 0, bufferInfo);
+ adapter.flush();
+ adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 1, bufferInfo);
+
+ assertThat(adapter.dequeueOutputBufferIndex(bufferInfo))
+ .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
+ }
+
+ @Test
+ public void dequeueOutputBufferIndex_afterFlushCompletes_returnsNextOutputBuffer()
+ throws InterruptedException {
+ // Disable calling codec.start() after flush() completes to avoid receiving buffers from the
+ // shadow codec impl
+ adapter.setOnCodecStart(() -> {});
+ Handler handler = new Handler(looper);
+ MediaCodec.BufferInfo info0 = new MediaCodec.BufferInfo();
+ handler.post(
+ () -> adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 0, info0));
+ adapter.flush(); // enqueues a flush event on the looper
+ MediaCodec.BufferInfo info1 = new MediaCodec.BufferInfo();
+ info1.presentationTimeUs = 1;
+ handler.post(
+ () -> adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 1, info1));
+
+ assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue();
+ assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)).isEqualTo(1);
+ assertThat(areEqual(bufferInfo, info1)).isTrue();
+ }
+
+ @Test
+ public void dequeueOutputBufferIndex_afterFlushCompletesWithError_throwsException()
+ throws InterruptedException {
+ adapter.setOnCodecStart(
+ () -> {
+ throw new RuntimeException("codec#start() exception");
+ });
+ adapter.flush();
+
+ assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue();
+ try {
+ adapter.dequeueOutputBufferIndex(bufferInfo);
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test
+ public void getOutputFormat_withoutFormat_throwsException() {
+ try {
+ adapter.getOutputFormat();
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test
+ public void getOutputFormat_withMultipleFormats_returnsFormatsInCorrectOrder() {
+ MediaFormat[] formats = new MediaFormat[10];
+ MediaCodec.Callback mediaCodecCallback = adapter.getMediaCodecCallback();
+ for (int i = 0; i < formats.length; i++) {
+ formats[i] = new MediaFormat();
+ mediaCodecCallback.onOutputFormatChanged(codec, formats[i]);
+ }
+
+ for (MediaFormat format : formats) {
+ assertThat(adapter.dequeueOutputBufferIndex(bufferInfo))
+ .isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
+ assertThat(adapter.getOutputFormat()).isEqualTo(format);
+ // Call it again to ensure same format is returned
+ assertThat(adapter.getOutputFormat()).isEqualTo(format);
+ }
+ // Obtain next output buffer
+ assertThat(adapter.dequeueOutputBufferIndex(bufferInfo))
+ .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
+ // Format should remain as is
+ assertThat(adapter.getOutputFormat()).isEqualTo(formats[formats.length - 1]);
+ }
+
+ @Test
+ public void getOutputFormat_afterFlush_returnsPreviousFormat() throws InterruptedException {
+ MediaFormat format = new MediaFormat();
+ adapter.getMediaCodecCallback().onOutputFormatChanged(codec, format);
+ adapter.dequeueOutputBufferIndex(bufferInfo);
+ adapter.flush();
+
+ assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue();
+ assertThat(adapter.getOutputFormat()).isEqualTo(format);
+ }
+
+ @Test
+ public void shutdown_withPendingFlush_cancelsFlush() throws InterruptedException {
+ AtomicBoolean onCodecStartCalled = new AtomicBoolean(false);
+ Runnable onCodecStart = () -> onCodecStartCalled.set(true);
+ adapter.setOnCodecStart(onCodecStart);
+ adapter.flush();
+ adapter.shutdown();
+
+ assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue();
+ assertThat(onCodecStartCalled.get()).isFalse();
+ }
+}
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecAsyncCallbackTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecAsyncCallbackTest.java
index 1ada9f8583..5b6af91110 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecAsyncCallbackTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecAsyncCallbackTest.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.mediacodec;
+import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.areEqual;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
@@ -204,17 +205,4 @@ public class MediaCodecAsyncCallbackTest {
mediaCodecAsyncCallback.maybeThrowMediaCodecException();
}
-
- /**
- * Compares if two {@link android.media.MediaCodec.BufferInfo} are equal by inspecting {@link
- * android.media.MediaCodec.BufferInfo#flags}, {@link android.media.MediaCodec.BufferInfo#size},
- * {@link android.media.MediaCodec.BufferInfo#presentationTimeUs} and {@link
- * android.media.MediaCodec.BufferInfo#offset}.
- */
- private static boolean areEqual(MediaCodec.BufferInfo lhs, MediaCodec.BufferInfo rhs) {
- return lhs.flags == rhs.flags
- && lhs.offset == rhs.offset
- && lhs.presentationTimeUs == rhs.presentationTimeUs
- && lhs.size == rhs.size;
- }
}
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecTestUtils.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecTestUtils.java
new file mode 100644
index 0000000000..ea816be4aa
--- /dev/null
+++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecTestUtils.java
@@ -0,0 +1,59 @@
+/*
+ * 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 org.robolectric.Shadows.shadowOf;
+
+import android.media.MediaCodec;
+import android.os.Handler;
+import android.os.Looper;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/** Testing utilities for MediaCodec related test classes */
+public class MediaCodecTestUtils {
+ /**
+ * Compares if two {@link android.media.MediaCodec.BufferInfo} are equal by inspecting {@link
+ * android.media.MediaCodec.BufferInfo#flags}, {@link android.media.MediaCodec.BufferInfo#size},
+ * {@link android.media.MediaCodec.BufferInfo#presentationTimeUs} and {@link
+ * android.media.MediaCodec.BufferInfo#offset}.
+ */
+ public static boolean areEqual(MediaCodec.BufferInfo lhs, MediaCodec.BufferInfo rhs) {
+ return lhs.flags == rhs.flags
+ && lhs.offset == rhs.offset
+ && lhs.presentationTimeUs == rhs.presentationTimeUs
+ && lhs.size == rhs.size;
+ }
+
+ /**
+ * Blocks until all events of a shadow looper are executed or the specified time elapses.
+ *
+ * @param looper the shadow looper
+ * @param time the time to wait
+ * @param unit the time units
+ * @return true if all events are executed, false if the time elapsed.
+ * @throws InterruptedException if the Thread was interrupted while waiting.
+ */
+ public static boolean waitUntilAllEventsAreExecuted(Looper looper, long time, TimeUnit unit)
+ throws InterruptedException {
+ Handler handler = new Handler(looper);
+ CountDownLatch latch = new CountDownLatch(1);
+ handler.post(() -> latch.countDown());
+ shadowOf(looper).idle();
+ return latch.await(time, unit);
+ }
+}