From 3156fbfc6e4fcc07d05f6e66f58c24253cb70406 Mon Sep 17 00:00:00 2001 From: christosts Date: Mon, 9 Dec 2019 13:57:36 +0000 Subject: [PATCH] Move MediaCodecAdapter out of MediaCodecRenderer Move MediaCodeAdapter and implementations to separate files and add unit tests for AsynchronousMediaCodecAdapter. PiperOrigin-RevId: 284537185 --- .../AsynchronousMediaCodecAdapter.java | 140 +++++++++++ .../mediacodec/MediaCodecAdapter.java | 79 ++++++ .../mediacodec/MediaCodecRenderer.java | 218 +++------------- .../SynchronousMediaCodecAdapter.java | 56 +++++ .../AsynchronousMediaCodecAdapterTest.java | 235 ++++++++++++++++++ .../MediaCodecAsyncCallbackTest.java | 14 +- .../mediacodec/MediaCodecTestUtils.java | 59 +++++ 7 files changed, 610 insertions(+), 191 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAdapter.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapterTest.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecTestUtils.java 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: + *

+ * 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); + } +}