From 485949b56ce1ed85608f69c71f10617dab1b3cfa Mon Sep 17 00:00:00 2001 From: christosts Date: Fri, 23 Oct 2020 10:32:08 +0100 Subject: [PATCH] Refactor AsynchronousMediaCoderAdapter Refactor the AsynchronousMediaCoderAdapter and move the callback thread out of the adapter so that implementation of async callback and and async queueing are consistent design-wise. PiperOrigin-RevId: 338637837 --- .../AsynchronousMediaCodecAdapter.java | 175 ++------ .../AsynchronousMediaCodecCallback.java | 325 ++++++++++++++ .../mediacodec/MediaCodecAsyncCallback.java | 157 ------- .../mediacodec/MediaCodecRenderer.java | 2 +- .../AsynchronousMediaCodecAdapterTest.java | 88 +--- ...nchronousMediaCodecBufferEnqueuerTest.java | 27 +- .../AsynchronousMediaCodecCallbackTest.java | 418 ++++++++++++++++++ .../MediaCodecAsyncCallbackTest.java | 249 ----------- 8 files changed, 794 insertions(+), 647 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecCallback.java delete mode 100644 library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAsyncCallback.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecCallbackTest.java delete mode 100644 library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecAsyncCallbackTest.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 index cb3acc0362..3a0de6fab8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java @@ -19,32 +19,25 @@ package com.google.android.exoplayer2.mediacodec; import android.media.MediaCodec; import android.media.MediaCrypto; import android.media.MediaFormat; -import android.os.Handler; import android.os.HandlerThread; import android.view.Surface; -import androidx.annotation.GuardedBy; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.decoder.CryptoInfo; -import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** - * A {@link MediaCodecAdapter} that operates the underlying {@link MediaCodec} in asynchronous mode - * and routes {@link MediaCodec.Callback} callbacks on a dedicated thread that is managed - * internally. - * - *

This adapter supports queueing input buffers asynchronously. + * A {@link MediaCodecAdapter} that operates the underlying {@link MediaCodec} in asynchronous mode, + * routes {@link MediaCodec.Callback} callbacks on a dedicated thread that is managed internally, + * and queues input buffers asynchronously. */ @RequiresApi(23) -/* package */ final class AsynchronousMediaCodecAdapter extends MediaCodec.Callback - implements MediaCodecAdapter { +/* package */ final class AsynchronousMediaCodecAdapter implements MediaCodecAdapter { @Documented @Retention(RetentionPolicy.SOURCE) @@ -56,24 +49,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private static final int STATE_STARTED = 2; private static final int STATE_SHUT_DOWN = 3; - private final Object lock; - - @GuardedBy("lock") - private final MediaCodecAsyncCallback mediaCodecAsyncCallback; - private final MediaCodec codec; - private final HandlerThread handlerThread; - private @MonotonicNonNull Handler handler; - - @GuardedBy("lock") - private long pendingFlushCount; - - private @State int state; + private final AsynchronousMediaCodecCallback asynchronousMediaCodecCallback; private final AsynchronousMediaCodecBufferEnqueuer bufferEnqueuer; - - @GuardedBy("lock") - @Nullable - private IllegalStateException internalException; + @State private int state; /** * Creates an instance that wraps the specified {@link MediaCodec}. @@ -85,21 +64,15 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /* package */ AsynchronousMediaCodecAdapter(MediaCodec codec, int trackType) { this( codec, - trackType, new HandlerThread(createCallbackThreadLabel(trackType)), new HandlerThread(createQueueingThreadLabel(trackType))); } @VisibleForTesting /* package */ AsynchronousMediaCodecAdapter( - MediaCodec codec, - int trackType, - HandlerThread callbackThread, - HandlerThread enqueueingThread) { - this.lock = new Object(); - this.mediaCodecAsyncCallback = new MediaCodecAsyncCallback(); + MediaCodec codec, HandlerThread callbackThread, HandlerThread enqueueingThread) { this.codec = codec; - this.handlerThread = callbackThread; + this.asynchronousMediaCodecCallback = new AsynchronousMediaCodecCallback(callbackThread); this.bufferEnqueuer = new AsynchronousMediaCodecBufferEnqueuer(codec, enqueueingThread); this.state = STATE_CREATED; } @@ -110,9 +83,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Nullable Surface surface, @Nullable MediaCrypto crypto, int flags) { - handlerThread.start(); - handler = new Handler(handlerThread.getLooper()); - codec.setCallback(this, handler); + asynchronousMediaCodecCallback.initialize(codec); codec.configure(mediaFormat, surface, crypto, flags); state = STATE_CONFIGURED; } @@ -138,60 +109,40 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public int dequeueInputBufferIndex() { - synchronized (lock) { - if (isFlushing()) { - return MediaCodec.INFO_TRY_AGAIN_LATER; - } else { - maybeThrowException(); - return mediaCodecAsyncCallback.dequeueInputBufferIndex(); - } - } + return asynchronousMediaCodecCallback.dequeueInputBufferIndex(); } @Override public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { - synchronized (lock) { - if (isFlushing()) { - return MediaCodec.INFO_TRY_AGAIN_LATER; - } else { - maybeThrowException(); - return mediaCodecAsyncCallback.dequeueOutputBufferIndex(bufferInfo); - } - } + return asynchronousMediaCodecCallback.dequeueOutputBufferIndex(bufferInfo); } @Override public MediaFormat getOutputFormat() { - synchronized (lock) { - return mediaCodecAsyncCallback.getOutputFormat(); - } + return asynchronousMediaCodecCallback.getOutputFormat(); } @Override public void flush() { - synchronized (lock) { - bufferEnqueuer.flush(); - codec.flush(); - ++pendingFlushCount; - Util.castNonNull(handler).post(this::onFlushCompleted); - } + // The order of calls is important: + // First, flush the bufferEnqueuer to stop queueing input buffers. + // Second, flush the codec to stop producing available input/output buffers. + // Third, flush the callback after flushing the codec so that in-flight callbacks are discarded. + bufferEnqueuer.flush(); + codec.flush(); + // When flushAsync() is completed, start the codec again. + asynchronousMediaCodecCallback.flushAsync(/* onFlushCompleted= */ codec::start); } @Override public void shutdown() { - synchronized (lock) { if (state == STATE_STARTED) { bufferEnqueuer.shutdown(); } if (state == STATE_CONFIGURED || state == STATE_STARTED) { - handlerThread.quit(); - mediaCodecAsyncCallback.flush(); - // Leave the adapter in a flushing state so that - // it will not dequeue anything. - ++pendingFlushCount; + asynchronousMediaCodecCallback.shutdown(); } state = STATE_SHUT_DOWN; - } } @Override @@ -199,86 +150,14 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; return codec; } - // Called from the handler thread. - - @Override - public void onInputBufferAvailable(MediaCodec codec, int index) { - synchronized (lock) { - mediaCodecAsyncCallback.onInputBufferAvailable(codec, index); - } + @VisibleForTesting + /* package */ void onError(MediaCodec.CodecException error) { + asynchronousMediaCodecCallback.onError(codec, error); } - @Override - public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) { - synchronized (lock) { - mediaCodecAsyncCallback.onOutputBufferAvailable(codec, index, info); - } - } - - @Override - public void onError(MediaCodec codec, MediaCodec.CodecException e) { - synchronized (lock) { - mediaCodecAsyncCallback.onError(codec, e); - } - } - - @Override - public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) { - synchronized (lock) { - mediaCodecAsyncCallback.onOutputFormatChanged(codec, format); - } - } - - private void onFlushCompleted() { - synchronized (lock) { - onFlushCompletedSynchronized(); - } - } - - @GuardedBy("lock") - private void onFlushCompletedSynchronized() { - if (state == STATE_SHUT_DOWN) { - return; - } - - --pendingFlushCount; - if (pendingFlushCount > 0) { - // Another flush() has been called. - return; - } else if (pendingFlushCount < 0) { - // This should never happen. - internalException = new IllegalStateException(); - return; - } - - mediaCodecAsyncCallback.flush(); - try { - codec.start(); - } catch (IllegalStateException e) { - internalException = e; - } catch (Exception e) { - internalException = new IllegalStateException(e); - } - } - - @GuardedBy("lock") - private boolean isFlushing() { - return pendingFlushCount > 0; - } - - @GuardedBy("lock") - private void maybeThrowException() { - maybeThrowInternalException(); - mediaCodecAsyncCallback.maybeThrowMediaCodecException(); - } - - @GuardedBy("lock") - private void maybeThrowInternalException() { - if (internalException != null) { - IllegalStateException e = internalException; - internalException = null; - throw e; - } + @VisibleForTesting + /* package */ void onOutputFormatChanged(MediaFormat format) { + asynchronousMediaCodecCallback.onOutputFormatChanged(codec, format); } private static String createCallbackThreadLabel(int trackType) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecCallback.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecCallback.java new file mode 100644 index 0000000000..f05d752061 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecCallback.java @@ -0,0 +1,325 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.exoplayer2.mediacodec; + +import static com.google.android.exoplayer2.util.Assertions.checkState; + +import android.media.MediaCodec; +import android.media.MediaFormat; +import android.os.Handler; +import android.os.HandlerThread; +import androidx.annotation.GuardedBy; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import com.google.android.exoplayer2.util.IntArrayQueue; +import com.google.android.exoplayer2.util.Util; +import java.util.ArrayDeque; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** A {@link MediaCodec.Callback} that routes callbacks on a separate thread. */ +@RequiresApi(23) +/* package */ final class AsynchronousMediaCodecCallback extends MediaCodec.Callback { + private final Object lock; + + private final HandlerThread callbackThread; + private @MonotonicNonNull Handler handler; + + @GuardedBy("lock") + private final IntArrayQueue availableInputBuffers; + + @GuardedBy("lock") + private final IntArrayQueue availableOutputBuffers; + + @GuardedBy("lock") + private final ArrayDeque bufferInfos; + + @GuardedBy("lock") + private final ArrayDeque formats; + + @GuardedBy("lock") + @Nullable + private MediaFormat currentFormat; + + @GuardedBy("lock") + @Nullable + private MediaFormat pendingOutputFormat; + + @GuardedBy("lock") + @Nullable + private MediaCodec.CodecException mediaCodecException; + + @GuardedBy("lock") + private long pendingFlushCount; + + @GuardedBy("lock") + private boolean shutDown; + + @GuardedBy("lock") + @Nullable + private IllegalStateException internalException; + + /** + * Creates a new instance. + * + * @param callbackThread The thread that will be used for routing the {@link MediaCodec} + * callbacks. The thread must not be started. + */ + /* package */ AsynchronousMediaCodecCallback(HandlerThread callbackThread) { + this.lock = new Object(); + this.callbackThread = callbackThread; + this.availableInputBuffers = new IntArrayQueue(); + this.availableOutputBuffers = new IntArrayQueue(); + this.bufferInfos = new ArrayDeque<>(); + this.formats = new ArrayDeque<>(); + } + + /** + * Sets the callback on {@code codec} and starts the background callback thread. + * + *

Make sure to call {@link #shutdown()} to stop the background thread and release its + * resources. + * + * @see MediaCodec#setCallback(MediaCodec.Callback, Handler) + */ + public void initialize(MediaCodec codec) { + checkState(handler == null); + + callbackThread.start(); + Handler handler = new Handler(callbackThread.getLooper()); + codec.setCallback(this, handler); + // Initialize this.handler at the very end ensuring the callback in not considered configured + // if MediaCodec raises an exception. + this.handler = handler; + } + + /** + * Shuts down this instance. + * + *

This method will stop the callback thread. After calling it, callbacks will no longer be + * handled and dequeue methods will return {@link MediaCodec#INFO_TRY_AGAIN_LATER}. + */ + public void shutdown() { + synchronized (lock) { + shutDown = true; + callbackThread.quit(); + flushInternal(); + } + } + + /** + * Returns the next available input buffer index or {@link MediaCodec#INFO_TRY_AGAIN_LATER} if no + * such buffer exists. + */ + public int dequeueInputBufferIndex() { + synchronized (lock) { + if (isFlushingOrShutdown()) { + return MediaCodec.INFO_TRY_AGAIN_LATER; + } else { + maybeThrowException(); + return availableInputBuffers.isEmpty() + ? MediaCodec.INFO_TRY_AGAIN_LATER + : availableInputBuffers.remove(); + } + } + } + + /** + * Returns the next available output buffer index. If the next available output is a MediaFormat + * change, it will return {@link MediaCodec#INFO_OUTPUT_FORMAT_CHANGED} and you should call {@link + * #getOutputFormat()} to get the format. If there is no available output, this method will return + * {@link MediaCodec#INFO_TRY_AGAIN_LATER}. + */ + public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { + synchronized (lock) { + if (isFlushingOrShutdown()) { + return MediaCodec.INFO_TRY_AGAIN_LATER; + } else { + maybeThrowException(); + if (availableOutputBuffers.isEmpty()) { + return MediaCodec.INFO_TRY_AGAIN_LATER; + } else { + int bufferIndex = availableOutputBuffers.remove(); + if (bufferIndex >= 0) { + MediaCodec.BufferInfo nextBufferInfo = bufferInfos.remove(); + bufferInfo.set( + nextBufferInfo.offset, + nextBufferInfo.size, + nextBufferInfo.presentationTimeUs, + nextBufferInfo.flags); + } else if (bufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { + currentFormat = formats.remove(); + } + return bufferIndex; + } + } + } + } + + /** + * Returns the {@link MediaFormat} signalled by the underlying {@link MediaCodec}. + * + *

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