From 41d4a132c4a4babfeeda34fcce9b6a5bb9c8a8ad Mon Sep 17 00:00:00 2001 From: christosts Date: Fri, 12 Jun 2020 18:21:56 +0100 Subject: [PATCH] Add configure() in MediaCodecAdapter The correct order of initializing the MediaCodec should be (as per documentation https://developer.android.com/reference/android/media/MediaCodec#initialization) "create -> setCallback -> configure -> start" but the MediaCodecRenderer currently does "create -> configure -> setCallback -> start" MediaCodec implementations did not complain about this so far, but the wrong sequence does not work with the MediaCodec in block mode (new mode in Android R) and also the ShadowMediaCodec won't operate in asynchronous mode otherwise. To initialize the MediaCodec in the correct order, this commit adds configure() in the MediaCodecAdapter so the MediaCodecRenderer can do: adapter.configure(); // sets the callback and then configures the codec adapter.start(); // starts the codec PiperOrigin-RevId: 316127680 --- .../audio/MediaCodecAudioRenderer.java | 5 +- .../AsynchronousMediaCodecAdapter.java | 198 ++++++---- .../mediacodec/MediaCodecAdapter.java | 24 +- .../mediacodec/MediaCodecRenderer.java | 10 +- .../SynchronousMediaCodecAdapter.java | 17 + .../video/MediaCodecVideoRenderer.java | 7 +- .../AsynchronousMediaCodecAdapterTest.java | 353 +++++++----------- .../gts/DebugRenderersFactory.java | 5 +- 8 files changed, 324 insertions(+), 295 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index c98bd9bbb9..a4816c5372 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -32,6 +32,7 @@ import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispatcher; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.mediacodec.MediaCodecAdapter; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; @@ -289,7 +290,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Override protected void configureCodec( MediaCodecInfo codecInfo, - MediaCodec codec, + MediaCodecAdapter codecAdapter, Format format, @Nullable MediaCrypto crypto, float codecOperatingRate) { @@ -301,7 +302,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media && !MimeTypes.AUDIO_RAW.equals(format.sampleMimeType); MediaFormat mediaFormat = getMediaFormat(format, codecInfo.codecMimeType, codecMaxInputSize, codecOperatingRate); - codec.configure(mediaFormat, /* surface= */ null, crypto, /* flags= */ 0); + codecAdapter.configure(mediaFormat, /* surface= */ null, crypto, /* flags= */ 0); // Store the input MIME type if we're using the passthrough codec. passthroughFormat = passthroughEnabled ? format : null; } 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 35d3989c29..f7c89b89c7 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 @@ -17,9 +17,13 @@ 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.os.Looper; +import android.view.Surface; +import androidx.annotation.GuardedBy; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; @@ -45,22 +49,32 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Documented @Retention(RetentionPolicy.SOURCE) - @IntDef({STATE_CREATED, STATE_STARTED, STATE_SHUT_DOWN}) + @IntDef({STATE_CREATED, STATE_CONFIGURED, STATE_STARTED, STATE_SHUT_DOWN}) private @interface State {} private static final int STATE_CREATED = 0; - private static final int STATE_STARTED = 1; - private static final int STATE_SHUT_DOWN = 2; + private static final int STATE_CONFIGURED = 1; + 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 Runnable codecStartRunnable; private final MediaCodecInputBufferEnqueuer bufferEnqueuer; - @Nullable private IllegalStateException internalException; + + @GuardedBy("lock") + @Nullable + private IllegalStateException internalException; /** * Creates an instance that wraps the specified {@link MediaCodec}. Instances created with this @@ -101,121 +115,164 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; boolean enableAsynchronousQueueing, int trackType, HandlerThread handlerThread) { - mediaCodecAsyncCallback = new MediaCodecAsyncCallback(); + this.lock = new Object(); + this.mediaCodecAsyncCallback = new MediaCodecAsyncCallback(); this.codec = codec; this.handlerThread = handlerThread; - state = STATE_CREATED; - codecStartRunnable = codec::start; - if (enableAsynchronousQueueing) { - bufferEnqueuer = new AsynchronousMediaCodecBufferEnqueuer(codec, trackType); - } else { - bufferEnqueuer = new SynchronousMediaCodecBufferEnqueuer(this.codec); - } + this.bufferEnqueuer = + enableAsynchronousQueueing + ? new AsynchronousMediaCodecBufferEnqueuer(codec, trackType) + : new SynchronousMediaCodecBufferEnqueuer(this.codec); + this.state = STATE_CREATED; } @Override - public synchronized void start() { + public void configure( + @Nullable MediaFormat mediaFormat, + @Nullable Surface surface, + @Nullable MediaCrypto crypto, + int flags) { handlerThread.start(); handler = new Handler(handlerThread.getLooper()); codec.setCallback(this, handler); + codec.configure(mediaFormat, surface, crypto, flags); + state = STATE_CONFIGURED; + } + + @Override + public void start() { bufferEnqueuer.start(); - codecStartRunnable.run(); + codec.start(); state = STATE_STARTED; } @Override public void queueInputBuffer( int index, int offset, int size, long presentationTimeUs, int flags) { - // This method does not need to be synchronized because it does not interact with the - // mediaCodecAsyncCallback. bufferEnqueuer.queueInputBuffer(index, offset, size, presentationTimeUs, flags); } @Override public void queueSecureInputBuffer( int index, int offset, CryptoInfo info, long presentationTimeUs, int flags) { - // This method does not need to be synchronized because it does not interact with the - // mediaCodecAsyncCallback. bufferEnqueuer.queueSecureInputBuffer(index, offset, info, presentationTimeUs, flags); } @Override - public synchronized int dequeueInputBufferIndex() { - if (isFlushing()) { - return MediaCodec.INFO_TRY_AGAIN_LATER; - } else { - maybeThrowException(); - return mediaCodecAsyncCallback.dequeueInputBufferIndex(); + public int dequeueInputBufferIndex() { + synchronized (lock) { + if (isFlushing()) { + return MediaCodec.INFO_TRY_AGAIN_LATER; + } else { + maybeThrowException(); + return mediaCodecAsyncCallback.dequeueInputBufferIndex(); + } } } @Override - public synchronized int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { - if (isFlushing()) { - return MediaCodec.INFO_TRY_AGAIN_LATER; - } else { - maybeThrowException(); - return mediaCodecAsyncCallback.dequeueOutputBufferIndex(bufferInfo); + public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { + synchronized (lock) { + if (isFlushing()) { + return MediaCodec.INFO_TRY_AGAIN_LATER; + } else { + maybeThrowException(); + return mediaCodecAsyncCallback.dequeueOutputBufferIndex(bufferInfo); + } } } @Override - public synchronized MediaFormat getOutputFormat() { - return mediaCodecAsyncCallback.getOutputFormat(); - } - - @Override - public synchronized void flush() { - bufferEnqueuer.flush(); - codec.flush(); - ++pendingFlushCount; - Util.castNonNull(handler).post(this::onFlushCompleted); - } - - @Override - public synchronized void shutdown() { - if (state == STATE_STARTED) { - bufferEnqueuer.shutdown(); - handlerThread.quit(); - mediaCodecAsyncCallback.flush(); + public MediaFormat getOutputFormat() { + synchronized (lock) { + return mediaCodecAsyncCallback.getOutputFormat(); } - state = STATE_SHUT_DOWN; } @Override - public synchronized void onInputBufferAvailable(MediaCodec codec, int index) { - mediaCodecAsyncCallback.onInputBufferAvailable(codec, index); + public void flush() { + synchronized (lock) { + bufferEnqueuer.flush(); + codec.flush(); + ++pendingFlushCount; + Util.castNonNull(handler).post(this::onFlushCompleted); + } } @Override - public synchronized void onOutputBufferAvailable( - MediaCodec codec, int index, MediaCodec.BufferInfo info) { - mediaCodecAsyncCallback.onOutputBufferAvailable(codec, index, info); + 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; + } + state = STATE_SHUT_DOWN; + } } @Override - public synchronized void onError(MediaCodec codec, MediaCodec.CodecException e) { - mediaCodecAsyncCallback.onError(codec, e); + public MediaCodec getCodec() { + return codec; + } + + // Called from the handler thread. + + @Override + public void onInputBufferAvailable(MediaCodec codec, int index) { + synchronized (lock) { + mediaCodecAsyncCallback.onInputBufferAvailable(codec, index); + } } @Override - public synchronized void onOutputFormatChanged(MediaCodec codec, MediaFormat format) { - mediaCodecAsyncCallback.onOutputFormatChanged(codec, format); + 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); + } } @VisibleForTesting /* package */ void onMediaCodecError(IllegalStateException e) { - mediaCodecAsyncCallback.onMediaCodecError(e); + synchronized (lock) { + mediaCodecAsyncCallback.onMediaCodecError(e); + } } @VisibleForTesting - /* package */ void setCodecStartRunnable(Runnable codecStartRunnable) { - this.codecStartRunnable = codecStartRunnable; + @Nullable + /* package */ Looper getLooper() { + return handlerThread.getLooper(); } - private synchronized void onFlushCompleted() { - if (state != STATE_STARTED) { - // The adapter has been shutdown. + private void onFlushCompleted() { + synchronized (lock) { + onFlushCompletedSynchronized(); + } + } + + @GuardedBy("lock") + private void onFlushCompletedSynchronized() { + if (state == STATE_SHUT_DOWN) { return; } @@ -231,7 +288,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; mediaCodecAsyncCallback.flush(); try { - codecStartRunnable.run(); + codec.start(); } catch (IllegalStateException e) { internalException = e; } catch (Exception e) { @@ -239,16 +296,19 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } } - private synchronized boolean isFlushing() { + @GuardedBy("lock") + private boolean isFlushing() { return pendingFlushCount > 0; } - private synchronized void maybeThrowException() { + @GuardedBy("lock") + private void maybeThrowException() { maybeThrowInternalException(); mediaCodecAsyncCallback.maybeThrowMediaCodecException(); } - private synchronized void maybeThrowInternalException() { + @GuardedBy("lock") + private void maybeThrowInternalException() { if (internalException != null) { IllegalStateException e = internalException; internalException = null; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAdapter.java index 1be850c899..a413790847 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAdapter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAdapter.java @@ -17,7 +17,10 @@ package com.google.android.exoplayer2.mediacodec; import android.media.MediaCodec; +import android.media.MediaCrypto; import android.media.MediaFormat; +import android.view.Surface; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.decoder.CryptoInfo; /** @@ -30,12 +33,24 @@ import com.google.android.exoplayer2.decoder.CryptoInfo; * * @see com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.MediaCodecOperationMode */ -/* package */ interface MediaCodecAdapter { +public interface MediaCodecAdapter { /** - * Starts this instance. + * Configures this adapter and the underlying {@link MediaCodec}. Needs to be called before {@link + * #start()}. * - * @see MediaCodec#start(). + * @see MediaCodec#configure(MediaFormat, Surface, MediaCrypto, int) + */ + void configure( + @Nullable MediaFormat mediaFormat, + @Nullable Surface surface, + @Nullable MediaCrypto crypto, + int flags); + + /** + * Starts this instance. Needs to be called after {@link #configure}. + * + * @see MediaCodec#start() */ void start(); @@ -109,4 +124,7 @@ import com.google.android.exoplayer2.decoder.CryptoInfo; * is a risk the adapter might interact with a stopped or released {@link MediaCodec}. */ void shutdown(); + + /** Returns the {@link MediaCodec} instance of this adapter. */ + MediaCodec getCodec(); } 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 21cc04ec23..6f35f21583 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 @@ -532,7 +532,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * Configures a newly created {@link MediaCodec}. * * @param codecInfo Information about the {@link MediaCodec} being configured. - * @param codec The {@link MediaCodec} to configure. + * @param codecAdapter The {@link MediaCodecAdapter} to configure. * @param format The {@link Format} for which the codec is being configured. * @param crypto For drm protected playbacks, a {@link MediaCrypto} to use for decryption. * @param codecOperatingRate The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if @@ -540,7 +540,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { */ protected abstract void configureCodec( MediaCodecInfo codecInfo, - MediaCodec codec, + MediaCodecAdapter codecAdapter, Format format, @Nullable MediaCrypto crypto, float codecOperatingRate); @@ -1036,8 +1036,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { /** * Configures passthrough where no codec is used. Called instead of {@link - * #configureCodec(MediaCodecInfo, MediaCodec, Format, MediaCrypto, float)} when no codec is used - * in passthrough. + * #configureCodec(MediaCodecInfo, MediaCodecAdapter, Format, MediaCrypto, float)} when no codec + * is used in passthrough. */ private void initPassthrough(Format format) { disablePassthrough(); // In case of transition between 2 passthrough formats. @@ -1088,7 +1088,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { TraceUtil.endSection(); TraceUtil.beginSection("configureCodec"); - configureCodec(codecInfo, codec, inputFormat, crypto, codecOperatingRate); + configureCodec(codecInfo, codecAdapter, inputFormat, crypto, codecOperatingRate); TraceUtil.endSection(); TraceUtil.beginSection("startCodec"); codecAdapter.start(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java index f50b49e602..f5138e90f0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java @@ -17,7 +17,10 @@ package com.google.android.exoplayer2.mediacodec; import android.media.MediaCodec; +import android.media.MediaCrypto; import android.media.MediaFormat; +import android.view.Surface; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.decoder.CryptoInfo; /** @@ -31,6 +34,15 @@ import com.google.android.exoplayer2.decoder.CryptoInfo; this.codec = mediaCodec; } + @Override + public void configure( + @Nullable MediaFormat mediaFormat, + @Nullable Surface surface, + @Nullable MediaCrypto crypto, + int flags) { + codec.configure(mediaFormat, surface, crypto, flags); + } + @Override public void start() { codec.start(); @@ -71,4 +83,9 @@ import com.google.android.exoplayer2.decoder.CryptoInfo; @Override public void shutdown() {} + + @Override + public MediaCodec getCodec() { + return codec; + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 60e376cb76..aefd52ab11 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -42,6 +42,7 @@ import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.mediacodec.MediaCodecAdapter; import com.google.android.exoplayer2.mediacodec.MediaCodecDecoderException; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer; @@ -552,7 +553,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @Override protected void configureCodec( MediaCodecInfo codecInfo, - MediaCodec codec, + MediaCodecAdapter codecAdapter, Format format, @Nullable MediaCrypto crypto, float codecOperatingRate) { @@ -575,9 +576,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } surface = dummySurface; } - codec.configure(mediaFormat, surface, crypto, 0); + codecAdapter.configure(mediaFormat, surface, crypto, 0); if (Util.SDK_INT >= 23 && tunneling) { - tunnelingOnFrameRenderedListener = new OnFrameRenderedListenerV23(codec); + tunnelingOnFrameRenderedListener = new OnFrameRenderedListenerV23(codecAdapter.getCodec()); } } 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 ee6f8690e2..9a3596d518 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 @@ -16,32 +16,25 @@ 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.robolectric.Shadows.shadowOf; -import static org.robolectric.annotation.LooperMode.Mode.LEGACY; +import static org.robolectric.annotation.LooperMode.Mode.PAUSED; import android.media.MediaCodec; import android.media.MediaFormat; -import android.os.Handler; import android.os.HandlerThread; -import android.os.Looper; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import java.io.IOException; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.Shadows; import org.robolectric.annotation.LooperMode; -import org.robolectric.shadows.ShadowLooper; /** Unit tests for {@link AsynchronousMediaCodecAdapter}. */ -@LooperMode(LEGACY) +@LooperMode(PAUSED) @RunWith(AndroidJUnit4.class) public class AsynchronousMediaCodecAdapterTest { private AsynchronousMediaCodecAdapter adapter; @@ -59,7 +52,6 @@ public class AsynchronousMediaCodecAdapterTest { /* enableAsynchronousQueueing= */ false, /* trackType= */ C.TRACK_TYPE_VIDEO, handlerThread); - adapter.setCodecStartRunnable(() -> {}); bufferInfo = new MediaCodec.BufferInfo(); } @@ -67,51 +59,46 @@ public class AsynchronousMediaCodecAdapterTest { public void tearDown() { adapter.shutdown(); - assertThat(TestHandlerThread.INSTANCES_STARTED.get()).isEqualTo(0); - } - - @Test - public void startAndShutdown_works() { - adapter.start(); - adapter.shutdown(); - } - - @Test - public void dequeueInputBufferIndex_withAfterFlushFailed_throwsException() { - AtomicInteger codecStartCalls = new AtomicInteger(0); - adapter.setCodecStartRunnable( - () -> { - if (codecStartCalls.incrementAndGet() == 2) { - throw new IllegalStateException("codec#start() exception"); - } - }); - adapter.start(); - adapter.flush(); - - // Wait until all tasks have been handled. - Shadows.shadowOf(handlerThread.getLooper()).idle(); - assertThrows(IllegalStateException.class, () -> adapter.dequeueInputBufferIndex()); + assertThat(handlerThread.hasQuit()).isTrue(); } @Test public void dequeueInputBufferIndex_withoutInputBuffer_returnsTryAgainLater() { + adapter.configure( + createMediaFormat("foo"), /* surface= */ null, /* crypto= */ null, /* flags= */ 0); adapter.start(); + // After start(), the ShadowMediaCodec offers one input buffer, which is available only if we + // progress the adapter's looper. We don't progress the looper so that the buffer is not + // available. + assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); } @Test public void dequeueInputBufferIndex_withInputBuffer_returnsInputBuffer() { + adapter.configure( + createMediaFormat("foo"), /* surface= */ null, /* crypto= */ null, /* flags= */ 0); + adapter.start(); - adapter.onInputBufferAvailable(codec, 0); + // After start(), the ShadowMediaCodec offers input buffer 0, which is available only if we + // progress the adapter's looper. + shadowOf(adapter.getLooper()).idle(); assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(0); } @Test public void dequeueInputBufferIndex_withPendingFlush_returnsTryAgainLater() { + adapter.configure( + createMediaFormat("foo"), /* surface= */ null, /* crypto= */ null, /* flags= */ 0); adapter.start(); - adapter.onInputBufferAvailable(codec, 0); + // After start(), the ShadowMediaCodec offers input buffer 0, which is available only if we + // progress the adapter's looper. + shadowOf(adapter.getLooper()).idle(); + + // Flush enqueues a task in the looper, but we won't progress the looper to leave flush() + // in a pending state. adapter.flush(); assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); @@ -119,153 +106,137 @@ public class AsynchronousMediaCodecAdapterTest { @Test public void dequeueInputBufferIndex_withFlushCompletedAndInputBuffer_returnsInputBuffer() { + adapter.configure( + createMediaFormat("foo"), /* surface= */ null, /* crypto= */ null, /* flags= */ 0); adapter.start(); - Looper looper = handlerThread.getLooper(); - Handler handler = new Handler(looper); - // Enqueue 10 callbacks from codec - for (int i = 0; i < 10; i++) { - int bufferIndex = i; - handler.post(() -> adapter.onInputBufferAvailable(codec, bufferIndex)); - } - adapter.flush(); // Enqueues a flush event after the onInputBufferAvailable callbacks - // Enqueue another onInputBufferAvailable after the flush event - handler.post(() -> adapter.onInputBufferAvailable(codec, 10)); + // After start(), the ShadowMediaCodec offers input buffer 0, which is available only if we + // progress the adapter's looper. + shadowOf(adapter.getLooper()).idle(); - // Wait until all tasks have been handled. - Shadows.shadowOf(handlerThread.getLooper()).idle(); - assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(10); + adapter.flush(); + // Progress the looper to complete flush(): the adapter should call codec.start(), triggering + // the ShadowMediaCodec to offer input buffer 0. + shadowOf(adapter.getLooper()).idle(); + + assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(0); } @Test public void dequeueInputBufferIndex_withMediaCodecError_throwsException() { + adapter.configure( + createMediaFormat("foo"), /* surface= */ null, /* crypto= */ null, /* flags= */ 0); adapter.start(); + + // Set an error directly on the adapter (not through the looper). adapter.onMediaCodecError(new IllegalStateException("error from codec")); assertThrows(IllegalStateException.class, () -> adapter.dequeueInputBufferIndex()); } @Test - public void dequeueOutputBufferIndex_withInternalException_throwsException() { - AtomicInteger codecStartCalls = new AtomicInteger(0); - adapter.setCodecStartRunnable( - () -> { - if (codecStartCalls.incrementAndGet() == 2) { - throw new RuntimeException("codec#start() exception"); - } - }); + public void dequeueInputBufferIndex_afterShutdown_returnsTryAgainLater() { + adapter.configure( + createMediaFormat("foo"), /* surface= */ null, /* crypto= */ null, /* flags= */ 0); adapter.start(); - adapter.flush(); + // After start(), the ShadowMediaCodec offers input buffer 0, which is available only if we + // progress the adapter's looper. + shadowOf(adapter.getLooper()).idle(); - // Wait until all tasks have been handled. - Shadows.shadowOf(handlerThread.getLooper()).idle(); - assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo)); + adapter.shutdown(); + + assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); } @Test - public void dequeueOutputBufferIndex_withoutInputBuffer_returnsTryAgainLater() { - adapter.start(); + public void dequeueOutputBufferIndex_withoutOutputBuffer_returnsTryAgainLater() { + adapter.configure( + createMediaFormat("foo"), /* surface= */ null, /* crypto= */ null, /* flags= */ 0); + adapter.start(); + // After start(), the ShadowMediaCodec offers an output format change. + shadowOf(adapter.getLooper()).idle(); + + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) + .isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED); + // Assert that output buffer is available. assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); } @Test public void dequeueOutputBufferIndex_withOutputBuffer_returnsOutputBuffer() { + adapter.configure( + createMediaFormat("foo"), /* surface= */ null, /* crypto= */ null, /* flags= */ 0); adapter.start(); - MediaCodec.BufferInfo enqueuedBufferInfo = new MediaCodec.BufferInfo(); - adapter.onOutputBufferAvailable(codec, 0, enqueuedBufferInfo); + // After start(), the ShadowMediaCodec offers input buffer 0, which is available only if we + // progress the adapter's looper. + shadowOf(adapter.getLooper()).idle(); - assertThat(adapter.dequeueOutputBufferIndex((bufferInfo))).isEqualTo(0); - assertBufferInfosEqual(enqueuedBufferInfo, bufferInfo); + int index = adapter.dequeueInputBufferIndex(); + adapter.queueInputBuffer(index, 0, 0, 0, 0); + // Progress the looper so that the ShadowMediaCodec processes the input buffer. + shadowOf(adapter.getLooper()).idle(); + + // The ShadowMediaCodec will first offer an output format and then the output buffer. + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) + .isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED); + // Assert it's the ShadowMediaCodec's output format + assertThat(adapter.getOutputFormat().getByteBuffer("csd-0")).isNotNull(); + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)).isEqualTo(index); } @Test public void dequeueOutputBufferIndex_withPendingFlush_returnsTryAgainLater() { + adapter.configure( + createMediaFormat("foo"), /* surface= */ null, /* crypto= */ null, /* flags= */ 0); adapter.start(); - adapter.dequeueOutputBufferIndex(bufferInfo); + // After start(), the ShadowMediaCodec offers input buffer 0, which is available only if we + // progress the adapter's looper. + shadowOf(adapter.getLooper()).idle(); + + // Flush enqueues a task in the looper, but we won't progress the looper to leave flush() + // in a pending state. adapter.flush(); assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); } - @Test - public void dequeueOutputBufferIndex_withFlushCompletedAndOutputBuffer_returnsOutputBuffer() { - adapter.start(); - Looper looper = handlerThread.getLooper(); - Handler handler = new Handler(looper); - // Enqueue 10 callbacks from codec - for (int i = 0; i < 10; i++) { - int bufferIndex = i; - MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo(); - outBufferInfo.presentationTimeUs = i; - handler.post(() -> adapter.onOutputBufferAvailable(codec, bufferIndex, outBufferInfo)); - } - adapter.flush(); // Enqueues a flush event after the onOutputBufferAvailable callbacks - // Enqueue another onOutputBufferAvailable after the flush event - MediaCodec.BufferInfo lastBufferInfo = new MediaCodec.BufferInfo(); - lastBufferInfo.presentationTimeUs = 10; - handler.post(() -> adapter.onOutputBufferAvailable(codec, 10, lastBufferInfo)); - - // Wait until all tasks have been handled. - Shadows.shadowOf(handlerThread.getLooper()).idle(); - assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)).isEqualTo(10); - assertBufferInfosEqual(lastBufferInfo, bufferInfo); - } - @Test public void dequeueOutputBufferIndex_withMediaCodecError_throwsException() { + adapter.configure( + createMediaFormat("foo"), /* surface= */ null, /* crypto= */ null, /* flags= */ 0); adapter.start(); + + // Set an error directly on the adapter. adapter.onMediaCodecError(new IllegalStateException("error from codec")); assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo)); } @Test - public void dequeueOutputBufferIndex_withPendingOutputFormat_returnsPendingOutputFormat() { + public void dequeueOutputBufferIndex_afterShutdown_returnsTryAgainLater() { + adapter.configure( + createMediaFormat("foo"), /* surface= */ null, /* crypto= */ null, /* flags= */ 0); adapter.start(); - MediaCodec.BufferInfo outputBufferInfo = new MediaCodec.BufferInfo(); - MediaFormat pendingMediaFormat = new MediaFormat(); + // After start(), the ShadowMediaCodec offers input buffer 0, which is available only if we + // progress the adapter's looper. + shadowOf(adapter.getLooper()).idle(); - adapter.onOutputFormatChanged(codec, new MediaFormat()); - adapter.onOutputBufferAvailable(codec, /* index= */ 0, new MediaCodec.BufferInfo()); - adapter.onOutputFormatChanged(codec, pendingMediaFormat); - adapter.onOutputBufferAvailable(codec, /* index= */ 1, new MediaCodec.BufferInfo()); - // Flush should clear the output queue except from the last pending output format received. - adapter.flush(); - shadowOf(handlerThread.getLooper()).idle(); - adapter.onOutputBufferAvailable(codec, /* index= */ 2, new MediaCodec.BufferInfo()); + int index = adapter.dequeueInputBufferIndex(); + adapter.queueInputBuffer(index, 0, 0, 0, 0); + // Progress the looper so that the ShadowMediaCodec processes the input buffer. + shadowOf(adapter.getLooper()).idle(); + adapter.shutdown(); - assertThat(adapter.dequeueOutputBufferIndex(outputBufferInfo)) - .isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED); - assertThat(adapter.getOutputFormat()).isEqualTo(pendingMediaFormat); - assertThat(adapter.dequeueOutputBufferIndex(outputBufferInfo)).isEqualTo(2); - } - - @Test - public void dequeueOutputBufferIndex_withPendingAndNewOutputFormat_returnsNewOutputFormat() { - adapter.start(); - MediaCodec.BufferInfo outputBufferInfo = new MediaCodec.BufferInfo(); - MediaFormat pendingMediaFormat = new MediaFormat(); - MediaFormat latestOutputFormat = new MediaFormat(); - - adapter.onOutputFormatChanged(codec, new MediaFormat()); - adapter.onOutputBufferAvailable(codec, /* index= */ 0, new MediaCodec.BufferInfo()); - adapter.onOutputFormatChanged(codec, pendingMediaFormat); - adapter.onOutputBufferAvailable(codec, /* index= */ 1, new MediaCodec.BufferInfo()); - // Flush should clear the output queue except from the last pending output format received. - adapter.flush(); - shadowOf(handlerThread.getLooper()).idle(); - // New output format should overwrite the pending format. - adapter.onOutputFormatChanged(codec, latestOutputFormat); - - assertThat(adapter.dequeueOutputBufferIndex(outputBufferInfo)) - .isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED); - assertThat(adapter.getOutputFormat()).isEqualTo(latestOutputFormat); + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) + .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); } @Test public void getOutputFormat_withoutFormatReceived_throwsException() { + adapter.configure( + createMediaFormat("foo"), /* surface= */ null, /* crypto= */ null, /* flags= */ 0); adapter.start(); assertThrows(IllegalStateException.class, () -> adapter.getOutputFormat()); @@ -273,107 +244,67 @@ public class AsynchronousMediaCodecAdapterTest { @Test public void getOutputFormat_withMultipleFormats_returnsCorrectFormat() { + adapter.configure( + createMediaFormat("foo"), /* surface= */ null, /* crypto= */ null, /* flags= */ 0); adapter.start(); - MediaFormat[] formats = new MediaFormat[10]; - for (int i = 0; i < formats.length; i++) { - formats[i] = new MediaFormat(); - adapter.onOutputFormatChanged(codec, formats[i]); - } + // After start(), the ShadowMediaCodec offers an output format, which is available only if we + // progress the adapter's looper. + shadowOf(adapter.getLooper()).idle(); - for (int i = 0; i < 10; i++) { - assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) - .isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED); - assertThat(adapter.getOutputFormat()).isEqualTo(formats[i]); - // A subsequent call to getOutputFormat() should return the previously fetched format - assertThat(adapter.getOutputFormat()).isEqualTo(formats[i]); - } + // Add another format directly on the adapter. + adapter.onOutputFormatChanged(codec, createMediaFormat("format2")); + + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) + .isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED); + // The first format is the ShadowMediaCodec's output format. + assertThat(adapter.getOutputFormat().getByteBuffer("csd-0")).isNotNull(); + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) + .isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED); + // The 2nd format is the format we enqueued 'manually' above. + assertThat(adapter.getOutputFormat().getString("name")).isEqualTo("format2"); assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); } @Test public void getOutputFormat_afterFlush_returnsPreviousFormat() { + adapter.configure( + createMediaFormat("foo"), /* surface= */ null, /* crypto= */ null, /* flags= */ 0); + adapter.start(); + // After start(), the ShadowMediaCodec offers an output format, which is available only if we + // progress the adapter's looper. + shadowOf(adapter.getLooper()).idle(); + + adapter.dequeueOutputBufferIndex(bufferInfo); + MediaFormat outputFormat = adapter.getOutputFormat(); + // Flush the adapter and progress the looper so that flush is completed. + adapter.flush(); + shadowOf(adapter.getLooper()).idle(); + + assertThat(adapter.getOutputFormat()).isEqualTo(outputFormat); + } + + private static MediaFormat createMediaFormat(String name) { MediaFormat format = new MediaFormat(); - adapter.start(); - adapter.onOutputFormatChanged(codec, format); - - assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) - .isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED); - assertThat(adapter.getOutputFormat()).isEqualTo(format); - - adapter.flush(); - - // Wait until all tasks have been handled. - Shadows.shadowOf(handlerThread.getLooper()).idle(); - assertThat(adapter.getOutputFormat()).isEqualTo(format); - } - - @Test - public void flush_multipleTimes_onlyLastFlushExecutes() { - AtomicInteger codecStartCalls = new AtomicInteger(0); - adapter.setCodecStartRunnable(() -> codecStartCalls.incrementAndGet()); - adapter.start(); - Looper looper = handlerThread.getLooper(); - Handler handler = new Handler(looper); - handler.post(() -> adapter.onInputBufferAvailable(codec, 0)); - adapter.flush(); // Enqueues a flush event - handler.post(() -> adapter.onInputBufferAvailable(codec, 2)); - AtomicInteger milestoneCount = new AtomicInteger(0); - handler.post(() -> milestoneCount.incrementAndGet()); - adapter.flush(); // Enqueues a second flush event - handler.post(() -> adapter.onInputBufferAvailable(codec, 3)); - - // Progress the looper until the milestoneCount is increased. - // adapter.start() will call codec.start(). First flush event should not call codec.start(). - ShadowLooper shadowLooper = shadowOf(looper); - while (milestoneCount.get() < 1) { - shadowLooper.runOneTask(); - } - assertThat(codecStartCalls.get()).isEqualTo(1); - - // Wait until all tasks have been handled. - shadowLooper.idle(); - assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(3); - assertThat(codecStartCalls.get()).isEqualTo(2); - } - - @Test - public void flush_andImmediatelyShutdown_flushIsNoOp() { - AtomicInteger onCodecStartCount = new AtomicInteger(0); - adapter.setCodecStartRunnable(() -> onCodecStartCount.incrementAndGet()); - adapter.start(); - // Grab reference to Looper before shutting down the adapter otherwise handlerThread.getLooper() - // might return null. - Looper looper = handlerThread.getLooper(); - adapter.flush(); - adapter.shutdown(); - - // Wait until all tasks have been handled. - Shadows.shadowOf(looper).idle(); - // Only adapter.start() calls onCodecStart. - assertThat(onCodecStartCount.get()).isEqualTo(1); + format.setString("name", name); + return format; } private static class TestHandlerThread extends HandlerThread { - private static final AtomicLong INSTANCES_STARTED = new AtomicLong(0); + private boolean quit; - public TestHandlerThread(String name) { - super(name); + TestHandlerThread(String label) { + super(label); } - @Override - public synchronized void start() { - super.start(); - INSTANCES_STARTED.incrementAndGet(); + public boolean hasQuit() { + return quit; } @Override public boolean quit() { - boolean quit = super.quit(); - if (quit) { - INSTANCES_STARTED.decrementAndGet(); - } - return quit; + quit = true; + return super.quit(); } } } diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DebugRenderersFactory.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DebugRenderersFactory.java index 04b15f5240..28a0c05440 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DebugRenderersFactory.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DebugRenderersFactory.java @@ -27,6 +27,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.mediacodec.MediaCodecAdapter; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; @@ -107,7 +108,7 @@ import java.util.ArrayList; @Override protected void configureCodec( MediaCodecInfo codecInfo, - MediaCodec codec, + MediaCodecAdapter codecAdapter, Format format, MediaCrypto crypto, float operatingRate) { @@ -117,7 +118,7 @@ import java.util.ArrayList; // dropped frames allowed, this is not desired behavior. Hence we skip (rather than drop) // frames up to the current playback position [Internal: b/66494991]. skipToPositionBeforeRenderingFirstFrame = getState() == Renderer.STATE_STARTED; - super.configureCodec(codecInfo, codec, format, crypto, operatingRate); + super.configureCodec(codecInfo, codecAdapter, format, crypto, operatingRate); } @Override