Remove dropped MediaCodecAdadpters

Delete the AsynchronousMediaCodecAdapter, the
MultiLockAsyncMediaCodecAdapter and their tests.

PiperOrigin-RevId: 315694296
This commit is contained in:
christosts 2020-06-10 16:32:25 +01:00 committed by Oliver Woodman
parent 95b61eb835
commit 3ce57ae2e8
5 changed files with 5 additions and 1225 deletions

View File

@ -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.
*
* <p>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;
}
}

View File

@ -73,11 +73,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
*
* <ul>
* <li>{@link #OPERATION_MODE_SYNCHRONOUS}
* <li>{@link #OPERATION_MODE_ASYNCHRONOUS_PLAYBACK_THREAD}
* <li>{@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD}
* <li>{@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK}
* <li>{@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_ASYNCHRONOUS_QUEUEING}
* <li>{@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK_ASYNCHRONOUS_QUEUEING}
* </ul>
*/
@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 {
* <ul>
* <li>{@link #OPERATION_MODE_SYNCHRONOUS}: The {@link MediaCodec} will operate in
* synchronous mode.
* <li>{@link #OPERATION_MODE_ASYNCHRONOUS_PLAYBACK_THREAD}: The {@link MediaCodec} will
* operate in asynchronous mode and {@link MediaCodec.Callback} callbacks will be routed
* to the playback thread. This mode requires API level &ge; 21; if the API level is
* &le; 20, the operation mode will be set to {@link
* MediaCodecRenderer#OPERATION_MODE_SYNCHRONOUS}.
* <li>{@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD}: The {@link MediaCodec} will
* operate in asynchronous mode and {@link MediaCodec.Callback} callbacks will be routed
* to a dedicated thread. This mode requires API level &ge; 23; if the API level is &le;
* 22, the operation mode will be set to {@link #OPERATION_MODE_SYNCHRONOUS}.
* <li>{@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK}: Same as {@link
* #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD} and, in addition, input buffers will
* submitted to the {@link MediaCodec} in a separate thread.
* <li>{@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_ASYNCHRONOUS_QUEUEING}: Same as
* {@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD} and, in addition, input buffers
* will be submitted to the {@link MediaCodec} in a separate thread.
* <li>{@link
* #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK_ASYNCHRONOUS_QUEUEING}: Same
* as {@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK} and, in addition,
* input buffers will be submitted to the {@link MediaCodec} in a separate thread.
* </ul>
* 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);
}

View File

@ -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.
*
* <p>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
* <strong>NOT</strong> 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<MediaCodec.BufferInfo> bufferInfos;
@GuardedBy("outputBufferLock")
private final ArrayDeque<MediaFormat> 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();
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}
}