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 deleted file mode 100644 index 040ef340ed..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java +++ /dev/null @@ -1,159 +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 android.os.Handler; -import android.os.Looper; -import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; -import androidx.annotation.VisibleForTesting; -import com.google.android.exoplayer2.decoder.CryptoInfo; -import com.google.android.exoplayer2.util.Assertions; - -/** - * A {@link MediaCodecAdapter} that operates the {@link MediaCodec} in asynchronous mode. - * - *

The AsynchronousMediaCodecAdapter routes callbacks to the current thread's {@link Looper} - * obtained via {@link Looper#myLooper()} - */ -@RequiresApi(21) -/* package */ final class AsynchronousMediaCodecAdapter implements MediaCodecAdapter { - private final MediaCodecAsyncCallback mediaCodecAsyncCallback; - private final Handler handler; - private final MediaCodec codec; - @Nullable private IllegalStateException internalException; - private boolean flushing; - private Runnable codecStartRunnable; - - /** - * Create a new {@code AsynchronousMediaCodecAdapter}. - * - * @param codec The {@link MediaCodec} to wrap. - */ - public AsynchronousMediaCodecAdapter(MediaCodec codec) { - this(codec, Assertions.checkNotNull(Looper.myLooper())); - } - - @VisibleForTesting - /* package */ AsynchronousMediaCodecAdapter(MediaCodec codec, Looper looper) { - mediaCodecAsyncCallback = new MediaCodecAsyncCallback(); - handler = new Handler(looper); - this.codec = codec; - this.codec.setCallback(mediaCodecAsyncCallback); - codecStartRunnable = codec::start; - } - - @Override - public void start() { - codecStartRunnable.run(); - } - - @Override - public void queueInputBuffer( - int index, int offset, int size, long presentationTimeUs, int flags) { - codec.queueInputBuffer(index, offset, size, presentationTimeUs, flags); - } - - @Override - public void queueSecureInputBuffer( - int index, int offset, CryptoInfo info, long presentationTimeUs, int flags) { - codec.queueSecureInputBuffer( - index, offset, info.getFrameworkCryptoInfo(), presentationTimeUs, flags); - } - - @Override - public int dequeueInputBufferIndex() { - if (flushing) { - return MediaCodec.INFO_TRY_AGAIN_LATER; - } else { - maybeThrowException(); - return mediaCodecAsyncCallback.dequeueInputBufferIndex(); - } - } - - @Override - public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { - if (flushing) { - return MediaCodec.INFO_TRY_AGAIN_LATER; - } else { - maybeThrowException(); - return mediaCodecAsyncCallback.dequeueOutputBufferIndex(bufferInfo); - } - } - - @Override - public MediaFormat getOutputFormat() { - return mediaCodecAsyncCallback.getOutputFormat(); - } - - @Override - public void flush() { - clearPendingFlushState(); - flushing = true; - codec.flush(); - handler.post(this::onCompleteFlush); - } - - @Override - public void shutdown() { - clearPendingFlushState(); - } - - @VisibleForTesting - /* package */ MediaCodec.Callback getMediaCodecCallback() { - return mediaCodecAsyncCallback; - } - - private void onCompleteFlush() { - flushing = false; - mediaCodecAsyncCallback.flush(); - try { - codecStartRunnable.run(); - } catch (IllegalStateException e) { - // Catch IllegalStateException directly so that we don't have to wrap it. - internalException = e; - } catch (Exception e) { - internalException = new IllegalStateException(e); - } - } - - @VisibleForTesting - /* package */ void setCodecStartRunnable(Runnable codecStartRunnable) { - this.codecStartRunnable = codecStartRunnable; - } - - private void maybeThrowException() throws IllegalStateException { - maybeThrowInternalException(); - mediaCodecAsyncCallback.maybeThrowMediaCodecException(); - } - - private void maybeThrowInternalException() { - if (internalException != null) { - IllegalStateException e = internalException; - internalException = null; - throw e; - } - } - - /** Clear state related to pending flush events. */ - private void clearPendingFlushState() { - handler.removeCallbacksAndMessages(null); - internalException = null; - } -} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index b85fbe3a71..27f0621cfc 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 @@ -73,11 +73,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * *

*/ @Documented @@ -85,42 +82,27 @@ public abstract class MediaCodecRenderer extends BaseRenderer { @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) @IntDef({ OPERATION_MODE_SYNCHRONOUS, - OPERATION_MODE_ASYNCHRONOUS_PLAYBACK_THREAD, OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD, - OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK, OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_ASYNCHRONOUS_QUEUEING, - OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK_ASYNCHRONOUS_QUEUEING }) public @interface MediaCodecOperationMode {} + // TODO: Refactor these constants once internal evaluation completed. + // Do not assign values 1, 3 and 5 to a new operation mode until the evaluation is completed, + // otherwise existing clients may operate one of the dropped modes. + // [Internal ref: b/132684114] /** Operates the {@link MediaCodec} in synchronous mode. */ public static final int OPERATION_MODE_SYNCHRONOUS = 0; - /** - * Operates the {@link MediaCodec} in asynchronous mode and routes {@link MediaCodec.Callback} - * callbacks to the playback thread. - */ - public static final int OPERATION_MODE_ASYNCHRONOUS_PLAYBACK_THREAD = 1; /** * Operates the {@link MediaCodec} in asynchronous mode and routes {@link MediaCodec.Callback} * callbacks to a dedicated thread. */ public static final int OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD = 2; - /** - * Operates the {@link MediaCodec} in asynchronous mode and routes {@link MediaCodec.Callback} - * callbacks to a dedicated thread. Uses granular locking for input and output buffers. - */ - public static final int OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK = 3; /** * Same as {@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD}, and offloads queueing to another * thread. */ public static final int OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_ASYNCHRONOUS_QUEUEING = 4; - /** - * Same as {@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK}, and offloads queueing - * to another thread. - */ - public static final int - OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK_ASYNCHRONOUS_QUEUEING = 5; /** Thrown when a failure occurs instantiating a decoder. */ public static class DecoderInitializationException extends Exception { @@ -488,25 +470,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * * By default, the operation mode is set to {@link * MediaCodecRenderer#OPERATION_MODE_SYNCHRONOUS}. @@ -1103,27 +1073,15 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecInitializingTimestamp = SystemClock.elapsedRealtime(); TraceUtil.beginSection("createCodec:" + codecName); codec = MediaCodec.createByCodecName(codecName); - if (mediaCodecOperationMode == OPERATION_MODE_ASYNCHRONOUS_PLAYBACK_THREAD - && Util.SDK_INT >= 21) { - codecAdapter = new AsynchronousMediaCodecAdapter(codec); - } else if (mediaCodecOperationMode == OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD + if (mediaCodecOperationMode == OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD && Util.SDK_INT >= 23) { codecAdapter = new DedicatedThreadAsyncMediaCodecAdapter(codec, getTrackType()); - } else if (mediaCodecOperationMode == OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK - && Util.SDK_INT >= 23) { - codecAdapter = new MultiLockAsyncMediaCodecAdapter(codec, getTrackType()); } else if (mediaCodecOperationMode == OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_ASYNCHRONOUS_QUEUEING && Util.SDK_INT >= 23) { codecAdapter = new DedicatedThreadAsyncMediaCodecAdapter( codec, /* enableAsynchronousQueueing= */ true, getTrackType()); - } else if (mediaCodecOperationMode - == OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK_ASYNCHRONOUS_QUEUEING - && Util.SDK_INT >= 23) { - codecAdapter = - new MultiLockAsyncMediaCodecAdapter( - codec, /* enableAsynchronousQueueing= */ true, getTrackType()); } else { codecAdapter = new SynchronousMediaCodecAdapter(codec); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapter.java deleted file mode 100644 index d51f985ed7..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapter.java +++ /dev/null @@ -1,385 +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 android.os.Handler; -import android.os.HandlerThread; -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.IntArrayQueue; -import com.google.android.exoplayer2.util.Util; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.ArrayDeque; -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. - * - *

The main difference of this class compared to the {@link - * DedicatedThreadAsyncMediaCodecAdapter} is that its internal implementation applies finer-grained - * locking. The {@link DedicatedThreadAsyncMediaCodecAdapter} uses a single lock to synchronize - * access, whereas this class uses a different lock to access the available input and available - * output buffer indexes returned from the {@link MediaCodec}. This class assumes that the {@link - * MediaCodecAdapter} methods will be accessed by the playback thread and the {@link - * MediaCodec.Callback} methods will be accessed by the internal thread. This class is - * NOT generally thread-safe in the sense that its public methods cannot be called - * by any thread. - */ -@RequiresApi(23) -/* package */ final class MultiLockAsyncMediaCodecAdapter extends MediaCodec.Callback - implements MediaCodecAdapter { - - @Documented - @Retention(RetentionPolicy.SOURCE) - @IntDef({STATE_CREATED, 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 final MediaCodec codec; - private final Object inputBufferLock; - private final Object outputBufferLock; - private final Object objectStateLock; - - @GuardedBy("inputBufferLock") - private final IntArrayQueue availableInputBuffers; - - @GuardedBy("outputBufferLock") - private final IntArrayQueue availableOutputBuffers; - - @GuardedBy("outputBufferLock") - private final ArrayDeque bufferInfos; - - @GuardedBy("outputBufferLock") - private final ArrayDeque formats; - - @GuardedBy("objectStateLock") - private @MonotonicNonNull MediaFormat currentFormat; - - @GuardedBy("objectStateLock") - private long pendingFlush; - - @GuardedBy("objectStateLock") - @Nullable - private IllegalStateException codecException; - - private final HandlerThread handlerThread; - private @MonotonicNonNull Handler handler; - private Runnable codecStartRunnable; - private final MediaCodecInputBufferEnqueuer bufferEnqueuer; - - @GuardedBy("objectStateLock") - @State - private int state; - - /** - * Creates a new instance that wraps the specified {@link MediaCodec}. An instance created with - * this constructor will queue input buffers synchronously. - * - * @param codec The {@link MediaCodec} to wrap. - * @param trackType One of {@link C#TRACK_TYPE_AUDIO} or {@link C#TRACK_TYPE_VIDEO}. Used for - * labelling the internal thread accordingly. - */ - /* package */ MultiLockAsyncMediaCodecAdapter(MediaCodec codec, int trackType) { - this( - codec, - /* enableAsynchronousQueueing= */ false, - trackType, - new HandlerThread(createThreadLabel(trackType))); - } - - /** - * Creates a new instance that wraps the specified {@link MediaCodec}. - * - * @param codec The {@link MediaCodec} to wrap. - * @param enableAsynchronousQueueing Whether input buffers will be queued asynchronously. - * @param trackType One of {@link C#TRACK_TYPE_AUDIO} or {@link C#TRACK_TYPE_VIDEO}. Used for - * labelling the internal thread accordingly. - */ - /* package */ MultiLockAsyncMediaCodecAdapter( - MediaCodec codec, boolean enableAsynchronousQueueing, int trackType) { - this( - codec, - enableAsynchronousQueueing, - trackType, - new HandlerThread(createThreadLabel(trackType))); - } - - @VisibleForTesting - /* package */ MultiLockAsyncMediaCodecAdapter( - MediaCodec codec, - boolean enableAsynchronousQueueing, - int trackType, - HandlerThread handlerThread) { - this.codec = codec; - inputBufferLock = new Object(); - outputBufferLock = new Object(); - objectStateLock = new Object(); - availableInputBuffers = new IntArrayQueue(); - availableOutputBuffers = new IntArrayQueue(); - bufferInfos = new ArrayDeque<>(); - formats = new ArrayDeque<>(); - codecException = null; - this.handlerThread = handlerThread; - codecStartRunnable = codec::start; - if (enableAsynchronousQueueing) { - bufferEnqueuer = new AsynchronousMediaCodecBufferEnqueuer(codec, trackType); - } else { - bufferEnqueuer = new SynchronousMediaCodecBufferEnqueuer(codec); - } - state = STATE_CREATED; - } - - @Override - public void start() { - synchronized (objectStateLock) { - handlerThread.start(); - handler = new Handler(handlerThread.getLooper()); - codec.setCallback(this, handler); - bufferEnqueuer.start(); - codecStartRunnable.run(); - state = STATE_STARTED; - } - } - - @Override - public int dequeueInputBufferIndex() { - synchronized (objectStateLock) { - if (isFlushing()) { - return MediaCodec.INFO_TRY_AGAIN_LATER; - } else { - maybeThrowException(); - return dequeueAvailableInputBufferIndex(); - } - } - } - - @Override - public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { - synchronized (objectStateLock) { - if (isFlushing()) { - return MediaCodec.INFO_TRY_AGAIN_LATER; - } else { - maybeThrowException(); - return dequeueAvailableOutputBufferIndex(bufferInfo); - } - } - } - - @Override - public MediaFormat getOutputFormat() { - synchronized (objectStateLock) { - if (currentFormat == null) { - throw new IllegalStateException(); - } - - return currentFormat; - } - } - - @Override - public void queueInputBuffer( - int index, int offset, int size, long presentationTimeUs, int flags) { - // This method does not need to be synchronized because it is not interacting with - // MediaCodec.Callback and dequeueing buffers operations. - 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 is not interacting with - // MediaCodec.Callback and dequeueing buffers operations. - bufferEnqueuer.queueSecureInputBuffer(index, offset, info, presentationTimeUs, flags); - } - - @Override - public void flush() { - synchronized (objectStateLock) { - bufferEnqueuer.flush(); - codec.flush(); - pendingFlush++; - Util.castNonNull(handler).post(this::onFlushComplete); - } - } - - @Override - public void shutdown() { - synchronized (objectStateLock) { - if (state == STATE_STARTED) { - bufferEnqueuer.shutdown(); - handlerThread.quit(); - } - state = STATE_SHUT_DOWN; - } - } - - @VisibleForTesting - /* package */ void setCodecStartRunnable(Runnable codecStartRunnable) { - this.codecStartRunnable = codecStartRunnable; - } - - private int dequeueAvailableInputBufferIndex() { - synchronized (inputBufferLock) { - return availableInputBuffers.isEmpty() - ? MediaCodec.INFO_TRY_AGAIN_LATER - : availableInputBuffers.remove(); - } - } - - @GuardedBy("objectStateLock") - private int dequeueAvailableOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { - int bufferIndex; - synchronized (outputBufferLock) { - if (availableOutputBuffers.isEmpty()) { - bufferIndex = MediaCodec.INFO_TRY_AGAIN_LATER; - } else { - bufferIndex = availableOutputBuffers.remove(); - if (bufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { - currentFormat = formats.remove(); - } else if (bufferIndex >= 0) { - MediaCodec.BufferInfo outBufferInfo = bufferInfos.remove(); - bufferInfo.set( - outBufferInfo.offset, - outBufferInfo.size, - outBufferInfo.presentationTimeUs, - outBufferInfo.flags); - } - } - } - return bufferIndex; - } - - @GuardedBy("objectStateLock") - private boolean isFlushing() { - return pendingFlush > 0; - } - - @GuardedBy("objectStateLock") - private void maybeThrowException() { - @Nullable IllegalStateException exception = codecException; - if (exception != null) { - codecException = null; - throw exception; - } - } - - // Called by the internal thread. - - @Override - public void onInputBufferAvailable(MediaCodec codec, int index) { - synchronized (inputBufferLock) { - availableInputBuffers.add(index); - } - } - - @Override - public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) { - synchronized (outputBufferLock) { - availableOutputBuffers.add(index); - bufferInfos.add(info); - } - } - - @Override - public void onError(MediaCodec codec, MediaCodec.CodecException e) { - onMediaCodecError(e); - } - - @Override - public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) { - synchronized (outputBufferLock) { - availableOutputBuffers.add(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED); - formats.add(format); - } - } - - @VisibleForTesting - /* package */ void onMediaCodecError(IllegalStateException e) { - synchronized (objectStateLock) { - codecException = e; - } - } - - private void onFlushComplete() { - synchronized (objectStateLock) { - if (state == STATE_SHUT_DOWN) { - return; - } - - --pendingFlush; - if (pendingFlush > 0) { - // Another flush() has been called. - return; - } else if (pendingFlush < 0) { - // This should never happen. - codecException = new IllegalStateException(); - return; - } - - clearAvailableInput(); - clearAvailableOutput(); - codecException = null; - try { - codecStartRunnable.run(); - } catch (IllegalStateException e) { - codecException = e; - } catch (Exception e) { - codecException = new IllegalStateException(e); - } - } - } - - private void clearAvailableInput() { - synchronized (inputBufferLock) { - availableInputBuffers.clear(); - } - } - - private void clearAvailableOutput() { - synchronized (outputBufferLock) { - availableOutputBuffers.clear(); - bufferInfos.clear(); - formats.clear(); - } - } - - private static String createThreadLabel(int trackType) { - StringBuilder labelBuilder = new StringBuilder("ExoPlayer:MediaCodecAsyncAdapter:"); - if (trackType == C.TRACK_TYPE_AUDIO) { - labelBuilder.append("Audio"); - } else if (trackType == C.TRACK_TYPE_VIDEO) { - labelBuilder.append("Video"); - } else { - labelBuilder.append("Unknown(").append(trackType).append(")"); - } - return labelBuilder.toString(); - } -} 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 deleted file mode 100644 index c36bf74c9c..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapterTest.java +++ /dev/null @@ -1,303 +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.assertThrows; -import static org.robolectric.Shadows.shadowOf; -import static org.robolectric.annotation.LooperMode.Mode.LEGACY; - -import android.media.MediaCodec; -import android.media.MediaFormat; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Looper; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import java.io.IOException; -import java.util.concurrent.atomic.AtomicInteger; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.annotation.LooperMode; - -/** Unit tests for {@link AsynchronousMediaCodecAdapter}. */ -@LooperMode(LEGACY) -@RunWith(AndroidJUnit4.class) -public class AsynchronousMediaCodecAdapterTest { - private AsynchronousMediaCodecAdapter adapter; - private MediaCodec codec; - private HandlerThread handlerThread; - private Looper looper; - private MediaCodec.BufferInfo bufferInfo; - - @Before - public void setUp() throws IOException { - handlerThread = new HandlerThread("TestHandler"); - handlerThread.start(); - looper = handlerThread.getLooper(); - codec = MediaCodec.createByCodecName("h264"); - adapter = new AsynchronousMediaCodecAdapter(codec, looper); - adapter.setCodecStartRunnable(() -> {}); - bufferInfo = new MediaCodec.BufferInfo(); - } - - @After - public void tearDown() { - adapter.shutdown(); - handlerThread.quit(); - } - - @Test - public void dequeueInputBufferIndex_withoutInputBuffer_returnsTryAgainLater() { - adapter.start(); - - assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); - } - - @Test - public void dequeueInputBufferIndex_withInputBuffer_returnsInputBuffer() { - adapter.start(); - adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 0); - - assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(0); - } - - @Test - public void dequeueInputBufferIndex_whileFlushing_returnsTryAgainLater() { - adapter.start(); - - adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 0); - // A callback that is pending. - new Handler(looper) - .post(() -> adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 1)); - adapter.flush(); - - assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); - } - - @Test - public void dequeueInputBufferIndex_afterFlushCompletes_returnsNextInputBuffer() { - adapter.start(); - Handler handler = new Handler(looper); - handler.post( - () -> adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 0)); - adapter.flush(); // enqueues a flush event on the looper - handler.post( - () -> adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 1)); - - // Wait until all tasks have been handled. - shadowOf(looper).idle(); - assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(1); - } - - @Test - public void dequeueInputBufferIndex_afterFlushCompletesWithError_throwsException() { - AtomicInteger calls = new AtomicInteger(0); - adapter.setCodecStartRunnable( - () -> { - if (calls.incrementAndGet() == 2) { - throw new IllegalStateException(); - } - }); - adapter.start(); - adapter.flush(); - - // Wait until all tasks have been handled. - shadowOf(looper).idle(); - assertThrows( - IllegalStateException.class, - () -> { - adapter.dequeueInputBufferIndex(); - }); - } - - @Test - public void dequeueOutputBufferIndex_withoutOutputBuffer_returnsTryAgainLater() { - adapter.start(); - - assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) - .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); - } - - @Test - public void dequeueOutputBufferIndex_withOutputBuffer_returnsOutputBuffer() { - adapter.start(); - MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo(); - outBufferInfo.presentationTimeUs = 10; - adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 0, outBufferInfo); - - assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)).isEqualTo(0); - assertBufferInfosEqual(bufferInfo, outBufferInfo); - } - - @Test - public void dequeueOutputBufferIndex_whileFlushing_returnsTryAgainLater() { - adapter.start(); - adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 0, bufferInfo); - adapter.flush(); - adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 1, bufferInfo); - - assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) - .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); - } - - @Test - public void dequeueOutputBufferIndex_afterFlushCompletes_returnsNextOutputBuffer() { - adapter.start(); - Handler handler = new Handler(looper); - MediaCodec.BufferInfo info0 = new MediaCodec.BufferInfo(); - handler.post( - () -> adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 0, info0)); - adapter.flush(); // enqueues a flush event on the looper - MediaCodec.BufferInfo info1 = new MediaCodec.BufferInfo(); - info1.presentationTimeUs = 1; - handler.post( - () -> adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 1, info1)); - - // Wait until all tasks have been handled. - shadowOf(looper).idle(); - assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)).isEqualTo(1); - assertBufferInfosEqual(info1, bufferInfo); - } - - @Test - public void dequeueOutputBufferIndex_afterFlushCompletesWithError_throwsException() { - AtomicInteger calls = new AtomicInteger(0); - adapter.setCodecStartRunnable( - () -> { - if (calls.incrementAndGet() == 2) { - throw new RuntimeException("codec#start() exception"); - } - }); - adapter.start(); - adapter.flush(); - - // Wait until all tasks have been handled. - shadowOf(looper).idle(); - assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo)); - } - - @Test - public void dequeueOutputBufferIndex_withPendingOutputFormat_returnsPendingOutputFormat() { - MediaFormat pendingOutputFormat = new MediaFormat(); - MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo(); - MediaCodec.Callback mediaCodecCallback = adapter.getMediaCodecCallback(); - Handler handler = new Handler(looper); - adapter.start(); - - // Enqueue callbacks - handler.post(() -> mediaCodecCallback.onOutputFormatChanged(codec, new MediaFormat())); - handler.post( - () -> - mediaCodecCallback.onOutputBufferAvailable( - codec, /* index= */ 0, new MediaCodec.BufferInfo())); - handler.post(() -> mediaCodecCallback.onOutputFormatChanged(codec, pendingOutputFormat)); - handler.post( - () -> - mediaCodecCallback.onOutputBufferAvailable( - codec, /* index= */ 1, new MediaCodec.BufferInfo())); - adapter.flush(); - // After flush is complete, MediaCodec sends on output buffer. - handler.post( - () -> - mediaCodecCallback.onOutputBufferAvailable( - codec, /* index= */ 2, new MediaCodec.BufferInfo())); - shadowOf(looper).idle(); - - assertThat(adapter.dequeueOutputBufferIndex(outBufferInfo)) - .isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED); - assertThat(adapter.getOutputFormat()).isEqualTo(pendingOutputFormat); - assertThat(adapter.dequeueOutputBufferIndex(outBufferInfo)).isEqualTo(2); - } - - @Test - public void dequeueOutputBufferIndex_withPendingAndNewOutputFormat_returnsNewOutputFormat() { - MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo(); - MediaCodec.Callback mediaCodecCallback = adapter.getMediaCodecCallback(); - Handler handler = new Handler(looper); - adapter.start(); - - // Enqueue callbacks - handler.post(() -> mediaCodecCallback.onOutputFormatChanged(codec, new MediaFormat())); - handler.post( - () -> - mediaCodecCallback.onOutputBufferAvailable( - codec, /* index= */ 0, new MediaCodec.BufferInfo())); - adapter.flush(); - // After flush is complete, MediaCodec sends an output format change, it should overwrite - // the pending format. - MediaFormat newMediaFormat = new MediaFormat(); - handler.post(() -> mediaCodecCallback.onOutputFormatChanged(codec, newMediaFormat)); - shadowOf(looper).idle(); - - assertThat(adapter.dequeueOutputBufferIndex(outBufferInfo)) - .isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED); - assertThat(adapter.getOutputFormat()).isEqualTo(newMediaFormat); - } - - @Test - public void getOutputFormat_withMultipleFormats_returnsFormatsInCorrectOrder() { - adapter.start(); - MediaFormat[] formats = new MediaFormat[10]; - MediaCodec.Callback mediaCodecCallback = adapter.getMediaCodecCallback(); - for (int i = 0; i < formats.length; i++) { - formats[i] = new MediaFormat(); - mediaCodecCallback.onOutputFormatChanged(codec, formats[i]); - } - - for (MediaFormat format : formats) { - assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) - .isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED); - assertThat(adapter.getOutputFormat()).isEqualTo(format); - // Call it again to ensure same format is returned - assertThat(adapter.getOutputFormat()).isEqualTo(format); - } - // Obtain next output buffer - assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) - .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); - // Format should remain as is - assertThat(adapter.getOutputFormat()).isEqualTo(formats[formats.length - 1]); - } - - @Test - public void getOutputFormat_afterFlush_returnsPreviousFormat() { - adapter.start(); - MediaFormat format = new MediaFormat(); - adapter.getMediaCodecCallback().onOutputFormatChanged(codec, format); - adapter.dequeueOutputBufferIndex(bufferInfo); - adapter.flush(); - - // Wait until all tasks have been handled. - shadowOf(looper).idle(); - assertThat(adapter.getOutputFormat()).isEqualTo(format); - } - - @Test - public void shutdown_withPendingFlush_cancelsFlush() { - AtomicInteger onCodecStartCalled = new AtomicInteger(0); - adapter.setCodecStartRunnable(() -> onCodecStartCalled.incrementAndGet()); - adapter.start(); - adapter.flush(); - adapter.shutdown(); - - // Wait until all tasks have been handled. - shadowOf(looper).idle(); - assertThat(onCodecStartCalled.get()).isEqualTo(1); - } -} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapterTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapterTest.java deleted file mode 100644 index cfe9cf2900..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapterTest.java +++ /dev/null @@ -1,331 +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.assertThrows; -import static org.robolectric.Shadows.shadowOf; -import static org.robolectric.annotation.LooperMode.Mode.LEGACY; - -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 MultiLockAsyncMediaCodecAdapter}. */ -@LooperMode(LEGACY) -@RunWith(AndroidJUnit4.class) -public class MultiLockAsyncMediaCodecAdapterTest { - private MultiLockAsyncMediaCodecAdapter adapter; - private MediaCodec codec; - private MediaCodec.BufferInfo bufferInfo; - private TestHandlerThread handlerThread; - - @Before - public void setUp() throws IOException { - codec = MediaCodec.createByCodecName("h264"); - handlerThread = new TestHandlerThread("TestHandlerThread"); - adapter = - new MultiLockAsyncMediaCodecAdapter( - codec, /* enableAsynchronousQueueing= */ false, C.TRACK_TYPE_VIDEO, handlerThread); - adapter.setCodecStartRunnable(() -> {}); - bufferInfo = new MediaCodec.BufferInfo(); - } - - @After - 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() - throws InterruptedException { - AtomicInteger codecStartCalls = new AtomicInteger(0); - adapter.setCodecStartRunnable( - () -> { - if (codecStartCalls.incrementAndGet() == 2) { - throw new IllegalStateException("codec#start() exception"); - } - }); - adapter.start(); - adapter.flush(); - - Shadows.shadowOf(handlerThread.getLooper()).idle(); - assertThrows(IllegalStateException.class, () -> adapter.dequeueInputBufferIndex()); - } - - @Test - public void dequeueInputBufferIndex_withoutInputBuffer_returnsTryAgainLater() { - adapter.start(); - - assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); - } - - @Test - public void dequeueInputBufferIndex_withInputBuffer_returnsInputBuffer() { - adapter.start(); - adapter.onInputBufferAvailable(codec, 0); - - assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(0); - } - - @Test - public void dequeueInputBufferIndex_withPendingFlush_returnsTryAgainLater() { - adapter.start(); - adapter.onInputBufferAvailable(codec, 0); - adapter.flush(); - - assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); - } - - @Test - public void dequeueInputBufferIndex_withFlushCompletedAndInputBuffer_returnsInputBuffer() - throws InterruptedException { - 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)); - - // Wait until all tasks have been handled - Shadows.shadowOf(handlerThread.getLooper()).idle(); - assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(10); - } - - @Test - public void dequeueInputBufferIndex_withMediaCodecError_throwsException() { - adapter.start(); - adapter.onMediaCodecError(new IllegalStateException("error from codec")); - - assertThrows(IllegalStateException.class, () -> adapter.dequeueInputBufferIndex()); - } - - - @Test - public void dequeueOutputBufferIndex_withInternalException_throwsException() - throws InterruptedException { - AtomicInteger codecStartCalls = new AtomicInteger(0); - adapter.setCodecStartRunnable( - () -> { - if (codecStartCalls.incrementAndGet() == 2) { - throw new RuntimeException("codec#start() exception"); - } - }); - adapter.start(); - adapter.flush(); - - Shadows.shadowOf(handlerThread.getLooper()).idle(); - assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo)); - } - - @Test - public void dequeueOutputBufferIndex_withoutInputBuffer_returnsTryAgainLater() { - adapter.start(); - - assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) - .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); - } - - @Test - public void dequeueOutputBufferIndex_withOutputBuffer_returnsOutputBuffer() { - adapter.start(); - MediaCodec.BufferInfo enqueuedBufferInfo = new MediaCodec.BufferInfo(); - adapter.onOutputBufferAvailable(codec, 0, enqueuedBufferInfo); - - assertThat(adapter.dequeueOutputBufferIndex((bufferInfo))).isEqualTo(0); - assertBufferInfosEqual(enqueuedBufferInfo, bufferInfo); - } - - @Test - public void dequeueOutputBufferIndex_withPendingFlush_returnsTryAgainLater() { - adapter.start(); - adapter.dequeueOutputBufferIndex(bufferInfo); - adapter.flush(); - - assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) - .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); - } - - @Test - public void dequeueOutputBufferIndex_withFlushCompletedAndOutputBuffer_returnsOutputBuffer() - throws InterruptedException { - 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.start(); - adapter.onMediaCodecError(new IllegalStateException("error from codec")); - - assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo)); - } - - @Test - public void getOutputFormat_withoutFormatReceived_throwsException() { - adapter.start(); - - assertThrows(IllegalStateException.class, () -> adapter.getOutputFormat()); - } - - @Test - public void getOutputFormat_withMultipleFormats_returnsCorrectFormat() { - adapter.start(); - MediaFormat[] formats = new MediaFormat[10]; - for (int i = 0; i < formats.length; i++) { - formats[i] = new MediaFormat(); - adapter.onOutputFormatChanged(codec, formats[i]); - } - - 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]); - } - assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) - .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); - } - - @Test - public void getOutputFormat_afterFlush_returnsPreviousFormat() { - 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(); - 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() called codec.start() but first flush event should have been a no-op - ShadowLooper shadowLooper = shadowOf(looper); - while (milestoneCount.get() < 1) { - shadowLooper.runOneTask(); - } - assertThat(codecStartCalls.get()).isEqualTo(1); - - shadowLooper.idle(); - assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(3); - assertThat(codecStartCalls.get()).isEqualTo(2); - } - - @Test - public void flush_andImmediatelyShutdown_flushIsNoOp() { - AtomicInteger codecStartCalls = new AtomicInteger(0); - adapter.setCodecStartRunnable(() -> codecStartCalls.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(); - - Shadows.shadowOf(looper).idle(); - // Only adapter.start() called codec#start() - assertThat(codecStartCalls.get()).isEqualTo(1); - } - - private static class TestHandlerThread extends HandlerThread { - - private static final AtomicLong INSTANCES_STARTED = new AtomicLong(0); - - public TestHandlerThread(String name) { - super(name); - } - - @Override - public synchronized void start() { - super.start(); - INSTANCES_STARTED.incrementAndGet(); - } - - @Override - public boolean quit() { - boolean quit = super.quit(); - INSTANCES_STARTED.decrementAndGet(); - return quit; - } - } -}