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