From aceba835ccec645a771aa6ac7dff8f566664e813 Mon Sep 17 00:00:00 2001 From: christosts Date: Mon, 2 Dec 2019 11:32:36 +0000 Subject: [PATCH] Enable MediaCodec asynchronous mode Enable using MediaCodec in async mode. Expose experimental API to enable/disable the feature. PiperOrigin-RevId: 283309798 --- .../mediacodec/MediaCodecAsyncCallback.java | 150 ++++++++++++ .../mediacodec/MediaCodecRenderer.java | 211 ++++++++++++++++- .../exoplayer2/util/IntArrayQueue.java | 112 +++++++++ .../MediaCodecAsyncCallbackTest.java | 220 ++++++++++++++++++ .../exoplayer2/util/IntArrayQueueTest.java | 140 +++++++++++ 5 files changed, 826 insertions(+), 7 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAsyncCallback.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/util/IntArrayQueue.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecAsyncCallbackTest.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/util/IntArrayQueueTest.java 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 new file mode 100644 index 0000000000..dc4bcbbf38 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAsyncCallback.java @@ -0,0 +1,150 @@ +/* + * 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.NonNull; +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 bufferInfos; + private final ArrayDeque formats; + @Nullable private MediaFormat currentFormat; + @Nullable private IllegalStateException mediaCodecException; + + /** Creates a new MediaCodecAsyncCallback. */ + public MediaCodecAsyncCallback() { + availableInputBuffers = new IntArrayQueue(); + availableOutputBuffers = new IntArrayQueue(); + bufferInfos = new ArrayDeque<>(); + formats = new ArrayDeque<>(); + } + + /** + * Returns the next available input buffer index or {@link MediaCodec#INFO_TRY_AGAIN_LATER} if no + * such buffer exists. + */ + public int dequeueInputBufferIndex() { + 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) { + 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 {@link IllegalStateException} if you call this method before before { + * @link #dequeueOutputBufferIndex} 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() { + availableInputBuffers.clear(); + availableOutputBuffers.clear(); + bufferInfos.clear(); + formats.clear(); + mediaCodecException = null; + } + + @Override + public void onInputBufferAvailable(@NonNull MediaCodec mediaCodec, int i) { + availableInputBuffers.add(i); + } + + @Override + public void onOutputBufferAvailable( + @NonNull MediaCodec mediaCodec, int i, @NonNull MediaCodec.BufferInfo bufferInfo) { + availableOutputBuffers.add(i); + bufferInfos.add(bufferInfo); + } + + @Override + public void onError(@NonNull MediaCodec mediaCodec, @NonNull MediaCodec.CodecException e) { + onMediaCodecError(e); + } + + @Override + public void onOutputFormatChanged( + @NonNull MediaCodec mediaCodec, @NonNull MediaFormat mediaFormat) { + availableOutputBuffers.add(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED); + formats.add(mediaFormat); + } + + @VisibleForTesting() + void onMediaCodecError(IllegalStateException e) { + mediaCodecException = e; + } +} 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 1361bb6ad4..84780d72c2 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,10 +23,13 @@ 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; @@ -289,6 +292,51 @@ public abstract class MediaCodecRenderer extends BaseRenderer { ADAPTATION_WORKAROUND_MODE_ALWAYS }) 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. */ @@ -336,6 +384,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private long renderTimeLimitMs; private float rendererOperatingRate; @Nullable private MediaCodec codec; + @Nullable private MediaCodecAdapter codecAdapter; @Nullable private Format codecFormat; private float codecOperatingRate; @Nullable private ArrayDeque availableCodecInfos; @@ -375,6 +424,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private boolean skipMediaCodecStopOnRelease; private boolean pendingOutputEndOfStream; + private boolean useMediaCodecInAsyncMode; + protected DecoderCounters decoderCounters; /** @@ -451,6 +502,19 @@ public abstract class MediaCodecRenderer extends BaseRenderer { skipMediaCodecStopOnRelease = enabled; } + /** + * Use the underlying {@link MediaCodec} in asynchronous mode to obtain available input and output + * buffers. + * + *

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. + */ + public void experimental_setUseMediaCodecInAsyncMode(boolean enabled) { + useMediaCodecInAsyncMode = enabled; + } + @Override public final int supportsMixedMimeTypeAdaptation() { return ADAPTIVE_NOT_SEAMLESS; @@ -664,6 +728,10 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } } finally { codec = null; + if (codecAdapter != null) { + codecAdapter.shutdown(); + codecAdapter = null; + } try { if (mediaCrypto != null) { mediaCrypto.release(); @@ -763,7 +831,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return true; } - codec.flush(); + codecAdapter.flush(); resetInputBuffer(); resetOutputBuffer(); codecHotswapDeadlineMs = C.TIME_UNSET; @@ -908,10 +976,18 @@ public abstract class MediaCodecRenderer extends BaseRenderer { if (codecOperatingRate <= assumedMinimumCodecOperatingRate) { codecOperatingRate = CODEC_OPERATING_RATE_UNSET; } + + MediaCodecAdapter codecAdapter = null; try { codecInitializingTimestamp = SystemClock.elapsedRealtime(); TraceUtil.beginSection("createCodec:" + codecName); codec = MediaCodec.createByCodecName(codecName); + if (useMediaCodecInAsyncMode && Util.SDK_INT >= 21) { + codecAdapter = new AsynchronousMediaCodecAdapter(codec); + } else { + codecAdapter = new SynchronousMediaCodecAdapter(codec, getDequeueOutputBufferTimeoutUs()); + } + TraceUtil.endSection(); TraceUtil.beginSection("configureCodec"); configureCodec(codecInfo, codec, inputFormat, crypto, codecOperatingRate); @@ -922,6 +998,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecInitializedTimestamp = SystemClock.elapsedRealtime(); getCodecBuffers(codec); } catch (Exception e) { + if (codecAdapter != null) { + codecAdapter.shutdown(); + } if (codec != null) { resetCodecBuffers(); codec.release(); @@ -930,6 +1009,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } this.codec = codec; + this.codecAdapter = codecAdapter; this.codecInfo = codecInfo; this.codecOperatingRate = codecOperatingRate; codecFormat = inputFormat; @@ -1036,7 +1116,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } if (inputIndex < 0) { - inputIndex = codec.dequeueInputBuffer(0); + inputIndex = codecAdapter.dequeueInputBufferIndex(); if (inputIndex < 0) { return false; } @@ -1489,8 +1569,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { int outputIndex; if (codecNeedsEosOutputExceptionWorkaround && codecReceivedEos) { try { - outputIndex = - codec.dequeueOutputBuffer(outputBufferInfo, getDequeueOutputBufferTimeoutUs()); + outputIndex = codecAdapter.dequeueOutputBufferIndex(outputBufferInfo); } catch (IllegalStateException e) { processEndOfStream(); if (outputStreamEnded) { @@ -1500,8 +1579,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return false; } } else { - outputIndex = - codec.dequeueOutputBuffer(outputBufferInfo, getDequeueOutputBufferTimeoutUs()); + outputIndex = codecAdapter.dequeueOutputBufferIndex(outputBufferInfo); } if (outputIndex < 0) { @@ -1599,7 +1677,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { /** Processes a new output {@link MediaFormat}. */ private void processOutputFormat() throws ExoPlaybackException { - MediaFormat mediaFormat = codec.getOutputFormat(); + MediaFormat mediaFormat = codecAdapter.getOutputFormat(); if (codecAdaptationWorkaroundMode != ADAPTATION_WORKAROUND_MODE_NEVER && mediaFormat.getInteger(MediaFormat.KEY_WIDTH) == ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT && mediaFormat.getInteger(MediaFormat.KEY_HEIGHT) @@ -1944,4 +2022,123 @@ public abstract class MediaCodecRenderer extends BaseRenderer { && "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/util/IntArrayQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/util/IntArrayQueue.java new file mode 100644 index 0000000000..d6eb1ca35a --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/IntArrayQueue.java @@ -0,0 +1,112 @@ +/* + * 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.util; + +import java.util.NoSuchElementException; + +/** + * Array-based unbounded queue for int primitives with amortized O(1) add and remove. + * + *

Use this class instead of a {@link java.util.Deque} to avoid boxing int primitives to {@link + * Integer} instances. + */ +public final class IntArrayQueue { + + /** Default capacity needs to be a power of 2. */ + private static int DEFAULT_INITIAL_CAPACITY = 16; + + private int headIndex; + private int tailIndex; + private int size; + private int[] data; + private int wrapAroundMask; + + public IntArrayQueue() { + headIndex = 0; + tailIndex = -1; + size = 0; + data = new int[DEFAULT_INITIAL_CAPACITY]; + wrapAroundMask = data.length - 1; + } + + /** Add a new item to the queue. */ + public void add(int value) { + if (size == data.length) { + doubleArraySize(); + } + + tailIndex = (tailIndex + 1) & wrapAroundMask; + data[tailIndex] = value; + size++; + } + + /** + * Remove an item from the queue. + * + * @throws {@link NoSuchElementException} if the queue is empty. + */ + public int remove() { + if (size == 0) { + throw new NoSuchElementException(); + } + + int value = data[headIndex]; + headIndex = (headIndex + 1) & wrapAroundMask; + size--; + + return value; + } + + /** Returns the number of items in the queue. */ + public int size() { + return size; + } + + /** Returns whether the queue is empty. */ + public boolean isEmpty() { + return size == 0; + } + + /** Clears the queue. */ + public void clear() { + headIndex = 0; + tailIndex = -1; + size = 0; + } + + /** Returns the length of the backing array. */ + public int capacity() { + return data.length; + } + + private void doubleArraySize() { + int newCapacity = data.length << 1; + if (newCapacity < 0) { + throw new IllegalStateException(); + } + + int[] newData = new int[newCapacity]; + int itemsToRight = data.length - headIndex; + int itemsToLeft = headIndex; + System.arraycopy(data, headIndex, newData, 0, itemsToRight); + System.arraycopy(data, 0, newData, itemsToRight, itemsToLeft); + + headIndex = 0; + tailIndex = size - 1; + data = newData; + wrapAroundMask = data.length - 1; + } +} 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 new file mode 100644 index 0000000000..1ada9f8583 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecAsyncCallbackTest.java @@ -0,0 +1,220 @@ +/* + * 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.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import android.media.MediaCodec; +import android.media.MediaFormat; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.io.IOException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit tests for {@link MediaCodecAsyncCallback}. */ +@RunWith(AndroidJUnit4.class) +public class MediaCodecAsyncCallbackTest { + + private MediaCodecAsyncCallback mediaCodecAsyncCallback; + private MediaCodec codec; + + @Before + public void setUp() throws IOException { + mediaCodecAsyncCallback = new MediaCodecAsyncCallback(); + codec = MediaCodec.createByCodecName("h264"); + } + + @Test + public void dequeInputBufferIndex_afterCreation_returnsTryAgain() { + assertThat(mediaCodecAsyncCallback.dequeueInputBufferIndex()) + .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + } + + @Test + public void dequeInputBufferIndex_returnsEnqueuedBuffers() { + // Send two input buffers to the mediaCodecAsyncCallback. + mediaCodecAsyncCallback.onInputBufferAvailable(codec, 0); + mediaCodecAsyncCallback.onInputBufferAvailable(codec, 1); + + assertThat(mediaCodecAsyncCallback.dequeueInputBufferIndex()).isEqualTo(0); + assertThat(mediaCodecAsyncCallback.dequeueInputBufferIndex()).isEqualTo(1); + assertThat(mediaCodecAsyncCallback.dequeueInputBufferIndex()) + .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + } + + @Test + public void dequeInputBufferIndex_afterFlush_returnsTryAgain() { + // Send two input buffers to the mediaCodecAsyncCallback and then flush(). + mediaCodecAsyncCallback.onInputBufferAvailable(codec, 0); + mediaCodecAsyncCallback.onInputBufferAvailable(codec, 1); + mediaCodecAsyncCallback.flush(); + + assertThat(mediaCodecAsyncCallback.dequeueInputBufferIndex()) + .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + } + + @Test + public void dequeInputBufferIndex_afterFlushAndNewInputBuffer_returnsEnqueuedBuffer() { + // Send two input buffers to the mediaCodecAsyncCallback, then flush(), then send + // another input buffer. + mediaCodecAsyncCallback.onInputBufferAvailable(codec, 0); + mediaCodecAsyncCallback.onInputBufferAvailable(codec, 1); + mediaCodecAsyncCallback.flush(); + mediaCodecAsyncCallback.onInputBufferAvailable(codec, 2); + + assertThat(mediaCodecAsyncCallback.dequeueInputBufferIndex()).isEqualTo(2); + } + + @Test + public void dequeOutputBufferIndex_afterCreation_returnsTryAgain() { + MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo(); + assertThat(mediaCodecAsyncCallback.dequeueOutputBufferIndex(outBufferInfo)) + .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + } + + @Test + public void dequeOutputBufferIndex_returnsEnqueuedBuffers() { + // Send two output buffers to the mediaCodecAsyncCallback. + MediaCodec.BufferInfo bufferInfo1 = new MediaCodec.BufferInfo(); + mediaCodecAsyncCallback.onOutputBufferAvailable(codec, 0, bufferInfo1); + + MediaCodec.BufferInfo bufferInfo2 = new MediaCodec.BufferInfo(); + bufferInfo2.set(1, 1, 1, 1); + mediaCodecAsyncCallback.onOutputBufferAvailable(codec, 1, bufferInfo2); + + MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo(); + + assertThat(mediaCodecAsyncCallback.dequeueOutputBufferIndex(outBufferInfo)).isEqualTo(0); + assertThat(areEqual(outBufferInfo, bufferInfo1)).isTrue(); + assertThat(mediaCodecAsyncCallback.dequeueOutputBufferIndex(outBufferInfo)).isEqualTo(1); + assertThat(areEqual(outBufferInfo, bufferInfo2)).isTrue(); + assertThat(mediaCodecAsyncCallback.dequeueOutputBufferIndex(outBufferInfo)) + .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + } + + @Test + public void dequeOutputBufferIndex_afterFlush_returnsTryAgain() { + // Send two output buffers to the mediaCodecAsyncCallback and then flush(). + MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); + mediaCodecAsyncCallback.onOutputBufferAvailable(codec, 0, bufferInfo); + mediaCodecAsyncCallback.onOutputBufferAvailable(codec, 1, bufferInfo); + mediaCodecAsyncCallback.flush(); + MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo(); + + assertThat(mediaCodecAsyncCallback.dequeueOutputBufferIndex(outBufferInfo)) + .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + } + + @Test + public void dequeOutputBufferIndex_afterFlushAndNewOutputBuffers_returnsEnqueueBuffer() { + // Send two output buffers to the mediaCodecAsyncCallback, then flush(), then send + // another output buffer. + MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); + mediaCodecAsyncCallback.onOutputBufferAvailable(codec, 0, bufferInfo); + mediaCodecAsyncCallback.onOutputBufferAvailable(codec, 1, bufferInfo); + mediaCodecAsyncCallback.flush(); + mediaCodecAsyncCallback.onOutputBufferAvailable(codec, 2, bufferInfo); + MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo(); + + assertThat(mediaCodecAsyncCallback.dequeueOutputBufferIndex(outBufferInfo)).isEqualTo(2); + } + + @Test + public void getOutputFormat_onNewInstance_raisesException() { + try { + mediaCodecAsyncCallback.getOutputFormat(); + fail(); + } catch (IllegalStateException expected) { + } + } + + @Test + public void getOutputFormat_afterOnOutputFormatCalled_returnsFormat() { + MediaFormat format = new MediaFormat(); + mediaCodecAsyncCallback.onOutputFormatChanged(codec, format); + MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); + + assertThat(mediaCodecAsyncCallback.dequeueOutputBufferIndex(bufferInfo)) + .isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED); + assertThat(mediaCodecAsyncCallback.getOutputFormat()).isEqualTo(format); + } + + @Test + public void getOutputFormat_afterFlush_raisesCurrentFormat() { + MediaFormat format = new MediaFormat(); + mediaCodecAsyncCallback.onOutputFormatChanged(codec, format); + MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); + mediaCodecAsyncCallback.dequeueOutputBufferIndex(bufferInfo); + mediaCodecAsyncCallback.flush(); + + assertThat(mediaCodecAsyncCallback.getOutputFormat()).isEqualTo(format); + } + + @Test + public void maybeThrowExoPlaybackException_withoutErrorFromCodec_doesNotThrow() { + mediaCodecAsyncCallback.maybeThrowMediaCodecException(); + } + + @Test + public void maybeThrowExoPlaybackException_withErrorFromCodec_Throws() { + IllegalStateException exception = new IllegalStateException(); + mediaCodecAsyncCallback.onMediaCodecError(exception); + + try { + mediaCodecAsyncCallback.maybeThrowMediaCodecException(); + fail(); + } catch (IllegalStateException expected) { + } + } + + @Test + public void maybeThrowExoPlaybackException_doesNotThrowTwice() { + IllegalStateException exception = new IllegalStateException(); + mediaCodecAsyncCallback.onMediaCodecError(exception); + + try { + mediaCodecAsyncCallback.maybeThrowMediaCodecException(); + fail(); + } catch (IllegalStateException expected) { + } + + mediaCodecAsyncCallback.maybeThrowMediaCodecException(); + } + + @Test + public void maybeThrowExoPlaybackException_afterFlush_doesNotThrow() { + IllegalStateException exception = new IllegalStateException(); + mediaCodecAsyncCallback.onMediaCodecError(exception); + mediaCodecAsyncCallback.flush(); + + 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/util/IntArrayQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/IntArrayQueueTest.java new file mode 100644 index 0000000000..7210a76644 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/IntArrayQueueTest.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.util; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.util.NoSuchElementException; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link IntArrayQueue}. */ +@RunWith(AndroidJUnit4.class) +public class IntArrayQueueTest { + + @Test + public void add_willDoubleCapacity() { + IntArrayQueue queue = new IntArrayQueue(); + int capacity = queue.capacity(); + + for (int i = 0; i <= capacity; i++) { + queue.add(i); + } + + assertThat(queue.capacity()).isEqualTo(2 * capacity); + assertThat(queue.size()).isEqualTo(capacity + 1); + } + + @Test + public void isEmpty_returnsTrueAfterConstruction() { + IntArrayQueue queue = new IntArrayQueue(); + + assertThat(queue.isEmpty()).isTrue(); + } + + @Test + public void isEmpty_returnsFalseAfterAddition() { + IntArrayQueue queue = new IntArrayQueue(); + queue.add(0); + + assertThat(queue.isEmpty()).isFalse(); + } + + @Test + public void isEmpty_returnsFalseAfterRemoval() { + IntArrayQueue queue = new IntArrayQueue(); + queue.add(0); + queue.remove(); + + assertThat(queue.isEmpty()).isTrue(); + } + + @Test + public void remove_onEmptyQueue_throwsException() { + IntArrayQueue queue = new IntArrayQueue(); + + try { + queue.remove(); + fail(); + } catch (NoSuchElementException expected) { + // expected + } + } + + @Test + public void remove_returnsCorrectItem() { + IntArrayQueue queue = new IntArrayQueue(); + int value = 20; + queue.add(value); + + assertThat(queue.remove()).isEqualTo(value); + } + + @Test + public void remove_untilIsEmpty() { + IntArrayQueue queue = new IntArrayQueue(); + for (int i = 0; i < 1024; i++) { + queue.add(i); + } + + int expectedRemoved = 0; + while (!queue.isEmpty()) { + if (expectedRemoved == 15) { + System.out.println("foo"); + } + int removed = queue.remove(); + assertThat(removed).isEqualTo(expectedRemoved++); + } + } + + @Test + public void remove_withResize_returnsCorrectItem() { + IntArrayQueue queue = new IntArrayQueue(); + int nextToAdd = 0; + + while (queue.size() < queue.capacity()) { + queue.add(nextToAdd++); + } + + queue.remove(); + queue.remove(); + + // This will force the queue to wrap-around and then resize + int howManyToResize = queue.capacity() - queue.size() + 1; + for (int i = 0; i < howManyToResize; i++) { + queue.add(nextToAdd++); + } + + assertThat(queue.remove()).isEqualTo(2); + } + + @Test + public void clear_resetsQueue() { + IntArrayQueue queue = new IntArrayQueue(); + + // Add items until array re-sizes twice (capacity grows by 4) + for (int i = 0; i < 1024; i++) { + queue.add(i); + } + + queue.clear(); + + assertThat(queue.size()).isEqualTo(0); + assertThat(queue.isEmpty()).isTrue(); + } +}