Refactor AsynchronousMediaCoderAdapter

Refactor the AsynchronousMediaCoderAdapter and move the callback thread
out of the adapter so that implementation of async callback and and
async queueing are consistent design-wise.

PiperOrigin-RevId: 338637837
This commit is contained in:
christosts 2020-10-23 10:32:08 +01:00 committed by Oliver Woodman
parent 4783c329cc
commit 485949b56c
8 changed files with 794 additions and 647 deletions

View file

@ -19,32 +19,25 @@ package com.google.android.exoplayer2.mediacodec;
import android.media.MediaCodec;
import android.media.MediaCrypto;
import android.media.MediaFormat;
import android.os.Handler;
import android.os.HandlerThread;
import android.view.Surface;
import androidx.annotation.GuardedBy;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.CryptoInfo;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* A {@link MediaCodecAdapter} that operates the underlying {@link MediaCodec} in asynchronous mode
* and routes {@link MediaCodec.Callback} callbacks on a dedicated thread that is managed
* internally.
*
* <p>This adapter supports queueing input buffers asynchronously.
* A {@link MediaCodecAdapter} that operates the underlying {@link MediaCodec} in asynchronous mode,
* routes {@link MediaCodec.Callback} callbacks on a dedicated thread that is managed internally,
* and queues input buffers asynchronously.
*/
@RequiresApi(23)
/* package */ final class AsynchronousMediaCodecAdapter extends MediaCodec.Callback
implements MediaCodecAdapter {
/* package */ final class AsynchronousMediaCodecAdapter implements MediaCodecAdapter {
@Documented
@Retention(RetentionPolicy.SOURCE)
@ -56,24 +49,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private static final int STATE_STARTED = 2;
private static final int STATE_SHUT_DOWN = 3;
private final Object lock;
@GuardedBy("lock")
private final MediaCodecAsyncCallback mediaCodecAsyncCallback;
private final MediaCodec codec;
private final HandlerThread handlerThread;
private @MonotonicNonNull Handler handler;
@GuardedBy("lock")
private long pendingFlushCount;
private @State int state;
private final AsynchronousMediaCodecCallback asynchronousMediaCodecCallback;
private final AsynchronousMediaCodecBufferEnqueuer bufferEnqueuer;
@GuardedBy("lock")
@Nullable
private IllegalStateException internalException;
@State private int state;
/**
* Creates an instance that wraps the specified {@link MediaCodec}.
@ -85,21 +64,15 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/* package */ AsynchronousMediaCodecAdapter(MediaCodec codec, int trackType) {
this(
codec,
trackType,
new HandlerThread(createCallbackThreadLabel(trackType)),
new HandlerThread(createQueueingThreadLabel(trackType)));
}
@VisibleForTesting
/* package */ AsynchronousMediaCodecAdapter(
MediaCodec codec,
int trackType,
HandlerThread callbackThread,
HandlerThread enqueueingThread) {
this.lock = new Object();
this.mediaCodecAsyncCallback = new MediaCodecAsyncCallback();
MediaCodec codec, HandlerThread callbackThread, HandlerThread enqueueingThread) {
this.codec = codec;
this.handlerThread = callbackThread;
this.asynchronousMediaCodecCallback = new AsynchronousMediaCodecCallback(callbackThread);
this.bufferEnqueuer = new AsynchronousMediaCodecBufferEnqueuer(codec, enqueueingThread);
this.state = STATE_CREATED;
}
@ -110,9 +83,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Nullable Surface surface,
@Nullable MediaCrypto crypto,
int flags) {
handlerThread.start();
handler = new Handler(handlerThread.getLooper());
codec.setCallback(this, handler);
asynchronousMediaCodecCallback.initialize(codec);
codec.configure(mediaFormat, surface, crypto, flags);
state = STATE_CONFIGURED;
}
@ -138,60 +109,40 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Override
public int dequeueInputBufferIndex() {
synchronized (lock) {
if (isFlushing()) {
return MediaCodec.INFO_TRY_AGAIN_LATER;
} else {
maybeThrowException();
return mediaCodecAsyncCallback.dequeueInputBufferIndex();
}
}
return asynchronousMediaCodecCallback.dequeueInputBufferIndex();
}
@Override
public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) {
synchronized (lock) {
if (isFlushing()) {
return MediaCodec.INFO_TRY_AGAIN_LATER;
} else {
maybeThrowException();
return mediaCodecAsyncCallback.dequeueOutputBufferIndex(bufferInfo);
}
}
return asynchronousMediaCodecCallback.dequeueOutputBufferIndex(bufferInfo);
}
@Override
public MediaFormat getOutputFormat() {
synchronized (lock) {
return mediaCodecAsyncCallback.getOutputFormat();
}
return asynchronousMediaCodecCallback.getOutputFormat();
}
@Override
public void flush() {
synchronized (lock) {
bufferEnqueuer.flush();
codec.flush();
++pendingFlushCount;
Util.castNonNull(handler).post(this::onFlushCompleted);
}
// The order of calls is important:
// First, flush the bufferEnqueuer to stop queueing input buffers.
// Second, flush the codec to stop producing available input/output buffers.
// Third, flush the callback after flushing the codec so that in-flight callbacks are discarded.
bufferEnqueuer.flush();
codec.flush();
// When flushAsync() is completed, start the codec again.
asynchronousMediaCodecCallback.flushAsync(/* onFlushCompleted= */ codec::start);
}
@Override
public void shutdown() {
synchronized (lock) {
if (state == STATE_STARTED) {
bufferEnqueuer.shutdown();
}
if (state == STATE_CONFIGURED || state == STATE_STARTED) {
handlerThread.quit();
mediaCodecAsyncCallback.flush();
// Leave the adapter in a flushing state so that
// it will not dequeue anything.
++pendingFlushCount;
asynchronousMediaCodecCallback.shutdown();
}
state = STATE_SHUT_DOWN;
}
}
@Override
@ -199,86 +150,14 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return codec;
}
// Called from the handler thread.
@Override
public void onInputBufferAvailable(MediaCodec codec, int index) {
synchronized (lock) {
mediaCodecAsyncCallback.onInputBufferAvailable(codec, index);
}
@VisibleForTesting
/* package */ void onError(MediaCodec.CodecException error) {
asynchronousMediaCodecCallback.onError(codec, error);
}
@Override
public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {
synchronized (lock) {
mediaCodecAsyncCallback.onOutputBufferAvailable(codec, index, info);
}
}
@Override
public void onError(MediaCodec codec, MediaCodec.CodecException e) {
synchronized (lock) {
mediaCodecAsyncCallback.onError(codec, e);
}
}
@Override
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
synchronized (lock) {
mediaCodecAsyncCallback.onOutputFormatChanged(codec, format);
}
}
private void onFlushCompleted() {
synchronized (lock) {
onFlushCompletedSynchronized();
}
}
@GuardedBy("lock")
private void onFlushCompletedSynchronized() {
if (state == STATE_SHUT_DOWN) {
return;
}
--pendingFlushCount;
if (pendingFlushCount > 0) {
// Another flush() has been called.
return;
} else if (pendingFlushCount < 0) {
// This should never happen.
internalException = new IllegalStateException();
return;
}
mediaCodecAsyncCallback.flush();
try {
codec.start();
} catch (IllegalStateException e) {
internalException = e;
} catch (Exception e) {
internalException = new IllegalStateException(e);
}
}
@GuardedBy("lock")
private boolean isFlushing() {
return pendingFlushCount > 0;
}
@GuardedBy("lock")
private void maybeThrowException() {
maybeThrowInternalException();
mediaCodecAsyncCallback.maybeThrowMediaCodecException();
}
@GuardedBy("lock")
private void maybeThrowInternalException() {
if (internalException != null) {
IllegalStateException e = internalException;
internalException = null;
throw e;
}
@VisibleForTesting
/* package */ void onOutputFormatChanged(MediaFormat format) {
asynchronousMediaCodecCallback.onOutputFormatChanged(codec, format);
}
private static String createCallbackThreadLabel(int trackType) {

View file

@ -0,0 +1,325 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.mediacodec;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import android.media.MediaCodec;
import android.media.MediaFormat;
import android.os.Handler;
import android.os.HandlerThread;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.util.IntArrayQueue;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayDeque;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** A {@link MediaCodec.Callback} that routes callbacks on a separate thread. */
@RequiresApi(23)
/* package */ final class AsynchronousMediaCodecCallback extends MediaCodec.Callback {
private final Object lock;
private final HandlerThread callbackThread;
private @MonotonicNonNull Handler handler;
@GuardedBy("lock")
private final IntArrayQueue availableInputBuffers;
@GuardedBy("lock")
private final IntArrayQueue availableOutputBuffers;
@GuardedBy("lock")
private final ArrayDeque<MediaCodec.BufferInfo> bufferInfos;
@GuardedBy("lock")
private final ArrayDeque<MediaFormat> formats;
@GuardedBy("lock")
@Nullable
private MediaFormat currentFormat;
@GuardedBy("lock")
@Nullable
private MediaFormat pendingOutputFormat;
@GuardedBy("lock")
@Nullable
private MediaCodec.CodecException mediaCodecException;
@GuardedBy("lock")
private long pendingFlushCount;
@GuardedBy("lock")
private boolean shutDown;
@GuardedBy("lock")
@Nullable
private IllegalStateException internalException;
/**
* Creates a new instance.
*
* @param callbackThread The thread that will be used for routing the {@link MediaCodec}
* callbacks. The thread must not be started.
*/
/* package */ AsynchronousMediaCodecCallback(HandlerThread callbackThread) {
this.lock = new Object();
this.callbackThread = callbackThread;
this.availableInputBuffers = new IntArrayQueue();
this.availableOutputBuffers = new IntArrayQueue();
this.bufferInfos = new ArrayDeque<>();
this.formats = new ArrayDeque<>();
}
/**
* Sets the callback on {@code codec} and starts the background callback thread.
*
* <p>Make sure to call {@link #shutdown()} to stop the background thread and release its
* resources.
*
* @see MediaCodec#setCallback(MediaCodec.Callback, Handler)
*/
public void initialize(MediaCodec codec) {
checkState(handler == null);
callbackThread.start();
Handler handler = new Handler(callbackThread.getLooper());
codec.setCallback(this, handler);
// Initialize this.handler at the very end ensuring the callback in not considered configured
// if MediaCodec raises an exception.
this.handler = handler;
}
/**
* Shuts down this instance.
*
* <p>This method will stop the callback thread. After calling it, callbacks will no longer be
* handled and dequeue methods will return {@link MediaCodec#INFO_TRY_AGAIN_LATER}.
*/
public void shutdown() {
synchronized (lock) {
shutDown = true;
callbackThread.quit();
flushInternal();
}
}
/**
* Returns the next available input buffer index or {@link MediaCodec#INFO_TRY_AGAIN_LATER} if no
* such buffer exists.
*/
public int dequeueInputBufferIndex() {
synchronized (lock) {
if (isFlushingOrShutdown()) {
return MediaCodec.INFO_TRY_AGAIN_LATER;
} else {
maybeThrowException();
return availableInputBuffers.isEmpty()
? MediaCodec.INFO_TRY_AGAIN_LATER
: availableInputBuffers.remove();
}
}
}
/**
* Returns the next available output buffer index. If the next available output is a MediaFormat
* change, it will return {@link MediaCodec#INFO_OUTPUT_FORMAT_CHANGED} and you should call {@link
* #getOutputFormat()} to get the format. If there is no available output, this method will return
* {@link MediaCodec#INFO_TRY_AGAIN_LATER}.
*/
public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) {
synchronized (lock) {
if (isFlushingOrShutdown()) {
return MediaCodec.INFO_TRY_AGAIN_LATER;
} else {
maybeThrowException();
if (availableOutputBuffers.isEmpty()) {
return MediaCodec.INFO_TRY_AGAIN_LATER;
} else {
int bufferIndex = availableOutputBuffers.remove();
if (bufferIndex >= 0) {
MediaCodec.BufferInfo nextBufferInfo = bufferInfos.remove();
bufferInfo.set(
nextBufferInfo.offset,
nextBufferInfo.size,
nextBufferInfo.presentationTimeUs,
nextBufferInfo.flags);
} else if (bufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
currentFormat = formats.remove();
}
return bufferIndex;
}
}
}
}
/**
* Returns the {@link MediaFormat} signalled by the underlying {@link MediaCodec}.
*
* <p>Call this <b>after</b> {@link #dequeueOutputBufferIndex} returned {@link
* MediaCodec#INFO_OUTPUT_FORMAT_CHANGED}.
*
* @throws IllegalStateException If called before {@link #dequeueOutputBufferIndex} has returned
* {@link MediaCodec#INFO_OUTPUT_FORMAT_CHANGED}.
*/
public MediaFormat getOutputFormat() {
synchronized (lock) {
if (currentFormat == null) {
throw new IllegalStateException();
}
return currentFormat;
}
}
/**
* Initiates a flush asynchronously, which will be completed on the callback thread. When the
* flush is complete, it will trigger {@code onFlushCompleted} from the callback thread.
*
* @param onFlushCompleted A {@link Runnable} that will be called when flush is completed. {@code
* onFlushCompleted} will be called from the scallback thread, therefore it should execute
* synchronized and thread-safe code.
*/
public void flushAsync(Runnable onFlushCompleted) {
synchronized (lock) {
++pendingFlushCount;
Util.castNonNull(handler).post(() -> this.onFlushCompleted(onFlushCompleted));
}
}
// Called from the callback thread.
@Override
public void onInputBufferAvailable(@NonNull MediaCodec codec, int index) {
synchronized (lock) {
availableInputBuffers.add(index);
}
}
@Override
public void onOutputBufferAvailable(
@NonNull MediaCodec codec, int index, @NonNull MediaCodec.BufferInfo info) {
synchronized (lock) {
if (pendingOutputFormat != null) {
addOutputFormat(pendingOutputFormat);
pendingOutputFormat = null;
}
availableOutputBuffers.add(index);
bufferInfos.add(info);
}
}
@Override
public void onError(@NonNull MediaCodec codec, @NonNull MediaCodec.CodecException e) {
synchronized (lock) {
mediaCodecException = e;
}
}
@Override
public void onOutputFormatChanged(@NonNull MediaCodec codec, @NonNull MediaFormat format) {
synchronized (lock) {
addOutputFormat(format);
pendingOutputFormat = null;
}
}
private void onFlushCompleted(Runnable onFlushCompleted) {
synchronized (lock) {
onFlushCompletedSynchronized(onFlushCompleted);
}
}
@GuardedBy("lock")
private void onFlushCompletedSynchronized(Runnable onFlushCompleted) {
if (shutDown) {
return;
}
--pendingFlushCount;
if (pendingFlushCount > 0) {
// Another flush() has been called.
return;
} else if (pendingFlushCount < 0) {
// This should never happen.
setInternalException(new IllegalStateException());
return;
}
flushInternal();
try {
onFlushCompleted.run();
} catch (IllegalStateException e) {
setInternalException(e);
} catch (Exception e) {
setInternalException(new IllegalStateException(e));
}
}
/** Flushes all available input and output buffers and any error that was previously set. */
@GuardedBy("lock")
private void flushInternal() {
pendingOutputFormat = formats.isEmpty() ? null : formats.getLast();
availableInputBuffers.clear();
availableOutputBuffers.clear();
bufferInfos.clear();
formats.clear();
mediaCodecException = null;
}
@GuardedBy("lock")
private boolean isFlushingOrShutdown() {
return pendingFlushCount > 0 || shutDown;
}
@GuardedBy("lock")
private void addOutputFormat(MediaFormat mediaFormat) {
availableOutputBuffers.add(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
formats.add(mediaFormat);
}
@GuardedBy("lock")
private void maybeThrowException() {
maybeThrowInternalException();
maybeThrowMediaCodecException();
}
@GuardedBy("lock")
private void maybeThrowInternalException() {
if (internalException != null) {
IllegalStateException e = internalException;
internalException = null;
throw e;
}
}
@GuardedBy("lock")
private void maybeThrowMediaCodecException() {
if (mediaCodecException != null) {
MediaCodec.CodecException codecException = mediaCodecException;
mediaCodecException = null;
throw codecException;
}
}
private void setInternalException(IllegalStateException e) {
synchronized (lock) {
internalException = e;
}
}
}

View file

@ -1,157 +0,0 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.mediacodec;
import android.media.MediaCodec;
import android.media.MediaFormat;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.util.IntArrayQueue;
import java.util.ArrayDeque;
/** Handles the asynchronous callbacks from {@link android.media.MediaCodec.Callback}. */
@RequiresApi(21)
/* package */ final class MediaCodecAsyncCallback extends MediaCodec.Callback {
private final IntArrayQueue availableInputBuffers;
private final IntArrayQueue availableOutputBuffers;
private final ArrayDeque<MediaCodec.BufferInfo> bufferInfos;
private final ArrayDeque<MediaFormat> formats;
@Nullable private MediaFormat currentFormat;
@Nullable private MediaFormat pendingOutputFormat;
@Nullable private IllegalStateException mediaCodecException;
/** Creates a new MediaCodecAsyncCallback. */
public MediaCodecAsyncCallback() {
availableInputBuffers = new IntArrayQueue();
availableOutputBuffers = new IntArrayQueue();
bufferInfos = new ArrayDeque<>();
formats = new ArrayDeque<>();
}
/**
* Returns the next available input buffer index or {@link MediaCodec#INFO_TRY_AGAIN_LATER} if no
* such buffer exists.
*/
public int dequeueInputBufferIndex() {
return availableInputBuffers.isEmpty()
? MediaCodec.INFO_TRY_AGAIN_LATER
: availableInputBuffers.remove();
}
/**
* Returns the next available output buffer index. If the next available output is a MediaFormat
* change, it will return {@link MediaCodec#INFO_OUTPUT_FORMAT_CHANGED} and you should call {@link
* #getOutputFormat()} to get the format. If there is no available output, this method will return
* {@link MediaCodec#INFO_TRY_AGAIN_LATER}.
*/
public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) {
if (availableOutputBuffers.isEmpty()) {
return MediaCodec.INFO_TRY_AGAIN_LATER;
} else {
int bufferIndex = availableOutputBuffers.remove();
if (bufferIndex >= 0) {
MediaCodec.BufferInfo nextBufferInfo = bufferInfos.remove();
bufferInfo.set(
nextBufferInfo.offset,
nextBufferInfo.size,
nextBufferInfo.presentationTimeUs,
nextBufferInfo.flags);
} else if (bufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
currentFormat = formats.remove();
}
return bufferIndex;
}
}
/**
* Returns the {@link MediaFormat} signalled by the underlying {@link MediaCodec}.
*
* <p>Call this <b>after</b> {@link #dequeueOutputBufferIndex} returned {@link
* MediaCodec#INFO_OUTPUT_FORMAT_CHANGED}.
*
* @throws IllegalStateException If called before {@link #dequeueOutputBufferIndex} has returned
* {@link MediaCodec#INFO_OUTPUT_FORMAT_CHANGED}.
*/
public MediaFormat getOutputFormat() throws IllegalStateException {
if (currentFormat == null) {
throw new IllegalStateException();
}
return currentFormat;
}
/**
* Checks and throws an {@link IllegalStateException} if an error was previously set on this
* instance via {@link #onError}.
*/
public void maybeThrowMediaCodecException() throws IllegalStateException {
IllegalStateException exception = mediaCodecException;
mediaCodecException = null;
if (exception != null) {
throw exception;
}
}
/**
* Flushes the MediaCodecAsyncCallback. This method removes all available input and output buffers
* and any error that was previously set.
*/
public void flush() {
pendingOutputFormat = formats.isEmpty() ? null : formats.getLast();
availableInputBuffers.clear();
availableOutputBuffers.clear();
bufferInfos.clear();
formats.clear();
mediaCodecException = null;
}
@Override
public void onInputBufferAvailable(MediaCodec mediaCodec, int index) {
availableInputBuffers.add(index);
}
@Override
public void onOutputBufferAvailable(
MediaCodec mediaCodec, int index, MediaCodec.BufferInfo bufferInfo) {
if (pendingOutputFormat != null) {
addOutputFormat(pendingOutputFormat);
pendingOutputFormat = null;
}
availableOutputBuffers.add(index);
bufferInfos.add(bufferInfo);
}
@Override
public void onError(MediaCodec mediaCodec, MediaCodec.CodecException e) {
onMediaCodecError(e);
}
@Override
public void onOutputFormatChanged(MediaCodec mediaCodec, MediaFormat mediaFormat) {
addOutputFormat(mediaFormat);
pendingOutputFormat = null;
}
@VisibleForTesting()
void onMediaCodecError(IllegalStateException e) {
mediaCodecException = e;
}
private void addOutputFormat(MediaFormat mediaFormat) {
availableOutputBuffers.add(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
formats.add(mediaFormat);
}
}

View file

@ -1067,7 +1067,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
codecOperatingRate = CODEC_OPERATING_RATE_UNSET;
}
MediaCodecAdapter codecAdapter = null;
@Nullable MediaCodecAdapter codecAdapter = null;
try {
codecInitializingTimestamp = SystemClock.elapsedRealtime();
TraceUtil.beginSection("createCodec:" + codecName);

View file

@ -24,7 +24,6 @@ import android.media.MediaCodec;
import android.media.MediaFormat;
import android.os.HandlerThread;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import java.io.IOException;
import java.lang.reflect.Constructor;
import org.junit.After;
@ -38,18 +37,16 @@ import org.robolectric.shadows.ShadowLooper;
public class AsynchronousMediaCodecAdapterTest {
private AsynchronousMediaCodecAdapter adapter;
private MediaCodec codec;
private TestHandlerThread callbackThread;
private HandlerThread callbackThread;
private HandlerThread queueingThread;
private MediaCodec.BufferInfo bufferInfo;
@Before
public void setUp() throws IOException {
codec = MediaCodec.createByCodecName("h264");
callbackThread = new TestHandlerThread("TestCallbackThread");
callbackThread = new HandlerThread("TestCallbackThread");
queueingThread = new HandlerThread("TestQueueingThread");
adapter =
new AsynchronousMediaCodecAdapter(
codec, /* trackType= */ C.TRACK_TYPE_VIDEO, callbackThread, queueingThread);
adapter = new AsynchronousMediaCodecAdapter(codec, callbackThread, queueingThread);
bufferInfo = new MediaCodec.BufferInfo();
}
@ -57,8 +54,6 @@ public class AsynchronousMediaCodecAdapterTest {
public void tearDown() {
adapter.shutdown();
codec.release();
assertThat(callbackThread.hasQuit()).isTrue();
}
@Test
@ -85,39 +80,7 @@ public class AsynchronousMediaCodecAdapterTest {
assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(0);
}
@Test
public void dequeueInputBufferIndex_withPendingFlush_returnsTryAgainLater() {
adapter.configure(
createMediaFormat("foo"), /* surface= */ null, /* crypto= */ null, /* flags= */ 0);
adapter.start();
// After adapter.start(), the ShadowMediaCodec offers input buffer 0. We run all currently
// enqueued messages and pause the looper so that flush is not completed.
ShadowLooper shadowLooper = shadowOf(callbackThread.getLooper());
shadowLooper.idle();
shadowLooper.pause();
adapter.flush();
assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
}
@Test
public void dequeueInputBufferIndex_withFlushCompletedAndInputBuffer_returnsInputBuffer() {
adapter.configure(
createMediaFormat("foo"), /* surface= */ null, /* crypto= */ null, /* flags= */ 0);
adapter.start();
// After adapter.start(), the ShadowMediaCodec offers input buffer 0. We advance the looper to
// make sure all messages have been propagated to the adapter.
ShadowLooper shadowLooper = shadowOf(callbackThread.getLooper());
shadowLooper.idle();
adapter.flush();
// Progress the looper to complete flush(): the adapter should call codec.start(), triggering
// the ShadowMediaCodec to offer input buffer 0.
shadowLooper.idle();
assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(0);
}
@Test
public void dequeueInputBufferIndex_withMediaCodecError_throwsException() throws Exception {
@ -128,7 +91,7 @@ public class AsynchronousMediaCodecAdapterTest {
adapter.start();
// Set an error directly on the adapter (not through the looper).
adapter.onError(codec, createCodecException());
adapter.onError(createCodecException());
assertThrows(IllegalStateException.class, () -> adapter.dequeueInputBufferIndex());
}
@ -192,25 +155,6 @@ public class AsynchronousMediaCodecAdapterTest {
assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)).isEqualTo(index);
}
@Test
public void dequeueOutputBufferIndex_withPendingFlush_returnsTryAgainLater() {
adapter.configure(
createMediaFormat("foo"), /* surface= */ null, /* crypto= */ null, /* flags= */ 0);
adapter.start();
// After start(), the ShadowMediaCodec offers input buffer 0, which is available only if we
// progress the adapter's looper.
ShadowLooper shadowLooper = shadowOf(callbackThread.getLooper());
shadowLooper.idle();
// Flush enqueues a task in the looper, but we will pause the looper to leave flush()
// in an incomplete state.
shadowLooper.pause();
adapter.flush();
assertThat(adapter.dequeueOutputBufferIndex(bufferInfo))
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
}
@Test
public void dequeueOutputBufferIndex_withMediaCodecError_throwsException() throws Exception {
// Pause the looper so that we interact with the adapter from this thread only.
@ -220,7 +164,7 @@ public class AsynchronousMediaCodecAdapterTest {
adapter.start();
// Set an error directly on the adapter.
adapter.onError(codec, createCodecException());
adapter.onError(createCodecException());
assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo));
}
@ -266,8 +210,8 @@ public class AsynchronousMediaCodecAdapterTest {
// progress the adapter's looper.
shadowOf(callbackThread.getLooper()).idle();
// Add another format directly on the adapter.
adapter.onOutputFormatChanged(codec, createMediaFormat("format2"));
// Add another format on the adapter.
adapter.onOutputFormatChanged(createMediaFormat("format2"));
assertThat(adapter.dequeueOutputBufferIndex(bufferInfo))
.isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
@ -314,22 +258,4 @@ public class AsynchronousMediaCodecAdapterTest {
return constructor.newInstance(
/* errorCode= */ 0, /* actionCode= */ 0, /* detailMessage= */ "error from codec");
}
private static class TestHandlerThread extends HandlerThread {
private boolean quit;
TestHandlerThread(String label) {
super(label);
}
public boolean hasQuit() {
return quit;
}
@Override
public boolean quit() {
quit = true;
return super.quit();
}
}
}

View file

@ -30,7 +30,6 @@ import com.google.android.exoplayer2.decoder.CryptoInfo;
import com.google.android.exoplayer2.util.ConditionVariable;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicLong;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
@ -65,7 +64,7 @@ public class AsynchronousMediaCodecBufferEnqueuerTest {
enqueuer.shutdown();
codec.stop();
codec.release();
assertThat(TestHandlerThread.INSTANCES_STARTED.get()).isEqualTo(0);
assertThat(!handlerThread.hasStarted() || handlerThread.hasQuit()).isTrue();
}
@Test
@ -221,25 +220,31 @@ public class AsynchronousMediaCodecBufferEnqueuerTest {
}
private static class TestHandlerThread extends HandlerThread {
private static final AtomicLong INSTANCES_STARTED = new AtomicLong(0);
private boolean started;
private boolean quit;
TestHandlerThread(String name) {
super(name);
TestHandlerThread(String label) {
super(label);
}
public boolean hasStarted() {
return started;
}
public boolean hasQuit() {
return quit;
}
@Override
public synchronized void start() {
super.start();
INSTANCES_STARTED.incrementAndGet();
started = true;
}
@Override
public boolean quit() {
boolean quit = super.quit();
if (quit) {
INSTANCES_STARTED.decrementAndGet();
}
return quit;
quit = true;
return super.quit();
}
}

View file

@ -0,0 +1,418 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.mediacodec;
import static com.google.android.exoplayer2.testutil.TestUtil.assertBufferInfosEqual;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
import static org.robolectric.Shadows.shadowOf;
import android.media.MediaCodec;
import android.media.MediaFormat;
import android.os.HandlerThread;
import android.os.Looper;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.shadows.ShadowLooper;
/** Unit tests for {@link AsynchronousMediaCodecCallback}. */
@RunWith(AndroidJUnit4.class)
public class AsynchronousMediaCodecCallbackTest {
private AsynchronousMediaCodecCallback asynchronousMediaCodecCallback;
private TestHandlerThread callbackThread;
private MediaCodec codec;
@Before
public void setUp() throws IOException {
callbackThread = new TestHandlerThread("TestCallbackThread");
codec = MediaCodec.createByCodecName("h264");
asynchronousMediaCodecCallback = new AsynchronousMediaCodecCallback(callbackThread);
asynchronousMediaCodecCallback.initialize(codec);
}
@After
public void tearDown() {
codec.release();
asynchronousMediaCodecCallback.shutdown();
assertThat(callbackThread.hasQuit()).isTrue();
}
@Test
public void dequeInputBufferIndex_afterCreation_returnsTryAgain() {
assertThat(asynchronousMediaCodecCallback.dequeueInputBufferIndex())
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
}
@Test
public void dequeInputBufferIndex_returnsEnqueuedBuffers() {
// Send two input buffers to the callback.
asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 0);
asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 1);
assertThat(asynchronousMediaCodecCallback.dequeueInputBufferIndex()).isEqualTo(0);
assertThat(asynchronousMediaCodecCallback.dequeueInputBufferIndex()).isEqualTo(1);
assertThat(asynchronousMediaCodecCallback.dequeueInputBufferIndex())
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
}
@Test
public void dequeInputBufferIndex_withPendingFlush_returnsTryAgain() {
Looper callbackThreadLooper = callbackThread.getLooper();
AtomicBoolean flushCompleted = new AtomicBoolean();
// Pause the callback thread so that flush() never completes.
shadowOf(callbackThreadLooper).pause();
// Send two input buffers to the callback and then flush().
asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 0);
asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 1);
asynchronousMediaCodecCallback.flushAsync(
/* onFlushCompleted= */ () -> flushCompleted.set(true));
assertThat(flushCompleted.get()).isFalse();
assertThat(asynchronousMediaCodecCallback.dequeueInputBufferIndex())
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
}
@Test
public void dequeInputBufferIndex_afterFlush_returnsTryAgain() {
Looper callbackThreadLooper = callbackThread.getLooper();
AtomicBoolean flushCompleted = new AtomicBoolean();
// Send two input buffers to the callback and then flush().
asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 0);
asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 1);
asynchronousMediaCodecCallback.flushAsync(
/* onFlushCompleted= */ () -> flushCompleted.set(true));
// Progress the callback thread so that flush() completes.
shadowOf(callbackThreadLooper).idle();
assertThat(flushCompleted.get()).isTrue();
assertThat(asynchronousMediaCodecCallback.dequeueInputBufferIndex())
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
}
@Test
public void dequeInputBufferIndex_afterFlushAndNewInputBuffer_returnsEnqueuedBuffer() {
Looper callbackThreadLooper = callbackThread.getLooper();
AtomicBoolean flushCompleted = new AtomicBoolean();
// Send two input buffers to the callback, then flush(), then send
// another input buffer.
asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 0);
asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 1);
asynchronousMediaCodecCallback.flushAsync(
/* onFlushCompleted= */ () -> flushCompleted.set(true));
// Progress the callback thread so that flush() completes.
shadowOf(callbackThreadLooper).idle();
asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 2);
assertThat(flushCompleted.get()).isTrue();
assertThat(asynchronousMediaCodecCallback.dequeueInputBufferIndex()).isEqualTo(2);
}
@Test
public void dequeueInputBufferIndex_afterShutdown_returnsTryAgainLater() {
asynchronousMediaCodecCallback.onInputBufferAvailable(codec, /* index= */ 1);
asynchronousMediaCodecCallback.shutdown();
assertThat(asynchronousMediaCodecCallback.dequeueInputBufferIndex())
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
}
@Test
public void dequeueInputBufferIndex_afterOnErrorCallback_throwsError() throws Exception {
asynchronousMediaCodecCallback.onError(codec, createCodecException());
assertThrows(
MediaCodec.CodecException.class,
() -> asynchronousMediaCodecCallback.dequeueInputBufferIndex());
}
@Test
public void dequeueInputBufferIndex_afterFlushCompletedWithError_throwsError() throws Exception {
MediaCodec.CodecException codecException = createCodecException();
asynchronousMediaCodecCallback.flushAsync(
() -> {
throw codecException;
});
shadowOf(callbackThread.getLooper()).idle();
assertThrows(
MediaCodec.CodecException.class,
() -> asynchronousMediaCodecCallback.dequeueInputBufferIndex());
}
@Test
public void dequeOutputBufferIndex_afterCreation_returnsTryAgain() {
MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo();
assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(outBufferInfo))
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
}
@Test
public void dequeOutputBufferIndex_returnsEnqueuedBuffers() {
// Send two output buffers to the callback.
MediaCodec.BufferInfo bufferInfo1 = new MediaCodec.BufferInfo();
asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 0, bufferInfo1);
MediaCodec.BufferInfo bufferInfo2 = new MediaCodec.BufferInfo();
bufferInfo2.set(1, 1, 1, 1);
asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 1, bufferInfo2);
MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo();
assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(outBufferInfo)).isEqualTo(0);
assertBufferInfosEqual(bufferInfo1, outBufferInfo);
assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(outBufferInfo)).isEqualTo(1);
assertBufferInfosEqual(bufferInfo2, outBufferInfo);
assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(outBufferInfo))
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
}
@Test
public void dequeOutputBufferIndex_withPendingFlush_returnsTryAgain() {
Looper callbackThreadLooper = callbackThread.getLooper();
AtomicBoolean flushCompleted = new AtomicBoolean();
// Pause the callback thread so that flush() never completes.
shadowOf(callbackThreadLooper).pause();
// Send two output buffers to the callback and then flush().
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 0, bufferInfo);
asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 1, bufferInfo);
asynchronousMediaCodecCallback.flushAsync(
/* onFlushCompleted= */ () -> flushCompleted.set(true));
assertThat(flushCompleted.get()).isFalse();
assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(new MediaCodec.BufferInfo()))
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
}
@Test
public void dequeOutputBufferIndex_afterFlush_returnsTryAgain() {
Looper callbackThreadLooper = callbackThread.getLooper();
AtomicBoolean flushCompleted = new AtomicBoolean();
// Send two output buffers to the callback and then flush().
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 0, bufferInfo);
asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 1, bufferInfo);
asynchronousMediaCodecCallback.flushAsync(
/* onFlushCompleted= */ () -> flushCompleted.set(true));
// Progress the callback looper so that flush() completes.
shadowOf(callbackThreadLooper).idle();
assertThat(flushCompleted.get()).isTrue();
assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(new MediaCodec.BufferInfo()))
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
}
@Test
public void dequeOutputBufferIndex_afterFlushAndNewOutputBuffers_returnsEnqueueBuffer() {
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
Looper callbackThreadLooper = callbackThread.getLooper();
AtomicBoolean flushCompleted = new AtomicBoolean();
// Send two output buffers to the callback, then flush(), then send
// another output buffer.
asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 0, bufferInfo);
asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 1, bufferInfo);
asynchronousMediaCodecCallback.flushAsync(
/* onFlushCompleted= */ () -> flushCompleted.set(true));
// Progress the callback looper so that flush() completes.
shadowOf(callbackThreadLooper).idle();
asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 2, bufferInfo);
MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo();
assertThat(flushCompleted.get()).isTrue();
assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(outBufferInfo)).isEqualTo(2);
}
@Test
public void dequeOutputBufferIndex_withPendingOutputFormat_returnsPendingOutputFormat() {
MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo();
Looper callbackThreadLooper = callbackThread.getLooper();
AtomicBoolean flushCompleted = new AtomicBoolean();
asynchronousMediaCodecCallback.onOutputFormatChanged(codec, new MediaFormat());
asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, /* index= */ 0, outBufferInfo);
MediaFormat pendingMediaFormat = new MediaFormat();
asynchronousMediaCodecCallback.onOutputFormatChanged(codec, pendingMediaFormat);
// flush() should not discard the last format.
asynchronousMediaCodecCallback.flushAsync(
/* onFlushCompleted= */ () -> flushCompleted.set(true));
// Progress the callback looper so that flush() completes.
shadowOf(callbackThreadLooper).idle();
// Right after flush(), we send an output buffer: the pending output format should be
// dequeued first.
asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, /* index= */ 1, outBufferInfo);
assertThat(flushCompleted.get()).isTrue();
assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(outBufferInfo))
.isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
assertThat(asynchronousMediaCodecCallback.getOutputFormat()).isEqualTo(pendingMediaFormat);
assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(outBufferInfo)).isEqualTo(1);
}
@Test
public void dequeOutputBufferIndex_withPendingOutputFormatAndNewFormat_returnsNewFormat() {
MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo();
Looper callbackThreadLooper = callbackThread.getLooper();
AtomicBoolean flushCompleted = new AtomicBoolean();
asynchronousMediaCodecCallback.onOutputFormatChanged(codec, new MediaFormat());
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, /* index= */ 0, bufferInfo);
MediaFormat pendingMediaFormat = new MediaFormat();
asynchronousMediaCodecCallback.onOutputFormatChanged(codec, pendingMediaFormat);
// flush() should not discard the last format.
asynchronousMediaCodecCallback.flushAsync(
/* onFlushCompleted= */ () -> flushCompleted.set(true));
// Progress the callback looper so that flush() completes.
shadowOf(callbackThreadLooper).idle();
// The first callback after flush() is a new MediaFormat, it should overwrite the pending
// format.
MediaFormat newFormat = new MediaFormat();
asynchronousMediaCodecCallback.onOutputFormatChanged(codec, newFormat);
asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, /* index= */ 1, bufferInfo);
assertThat(flushCompleted.get()).isTrue();
assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(outBufferInfo))
.isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
assertThat(asynchronousMediaCodecCallback.getOutputFormat()).isEqualTo(newFormat);
assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(outBufferInfo)).isEqualTo(1);
}
@Test
public void dequeueOutputBufferIndex_afterShutdown_returnsTryAgainLater() {
asynchronousMediaCodecCallback.onOutputBufferAvailable(
codec, /* index= */ 1, new MediaCodec.BufferInfo());
asynchronousMediaCodecCallback.shutdown();
assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(new MediaCodec.BufferInfo()))
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
}
@Test
public void dequeueOutputBufferIndex_afterOnErrorCallback_throwsError() throws Exception {
asynchronousMediaCodecCallback.onError(codec, createCodecException());
assertThrows(
MediaCodec.CodecException.class,
() -> asynchronousMediaCodecCallback.dequeueOutputBufferIndex(new MediaCodec.BufferInfo()));
}
@Test
public void dequeueOutputBufferIndex_afterFlushCompletedWithError_throwsError() throws Exception {
MediaCodec.CodecException codecException = createCodecException();
asynchronousMediaCodecCallback.flushAsync(
() -> {
throw codecException;
});
shadowOf(callbackThread.getLooper()).idle();
assertThrows(
MediaCodec.CodecException.class,
() -> asynchronousMediaCodecCallback.dequeueOutputBufferIndex(new MediaCodec.BufferInfo()));
}
@Test
public void getOutputFormat_onNewInstance_raisesException() {
try {
asynchronousMediaCodecCallback.getOutputFormat();
fail();
} catch (IllegalStateException expected) {
}
}
@Test
public void getOutputFormat_afterOnOutputFormatCalled_returnsFormat() {
MediaFormat format = new MediaFormat();
asynchronousMediaCodecCallback.onOutputFormatChanged(codec, format);
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(bufferInfo))
.isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
assertThat(asynchronousMediaCodecCallback.getOutputFormat()).isEqualTo(format);
}
@Test
public void getOutputFormat_afterFlush_returnsCurrentFormat() {
MediaFormat format = new MediaFormat();
Looper callbackThreadLooper = callbackThread.getLooper();
AtomicBoolean flushCompleted = new AtomicBoolean();
asynchronousMediaCodecCallback.onOutputFormatChanged(codec, format);
asynchronousMediaCodecCallback.dequeueOutputBufferIndex(new MediaCodec.BufferInfo());
asynchronousMediaCodecCallback.flushAsync(
/* onFlushCompleted= */ () -> flushCompleted.set(true));
// Progress the callback looper so that flush() completes.
shadowOf(callbackThreadLooper).idle();
assertThat(flushCompleted.get()).isTrue();
assertThat(asynchronousMediaCodecCallback.getOutputFormat()).isEqualTo(format);
}
@Test
public void flush_withPendingFlush_onlyLastFlushCompletes() {
ShadowLooper callbackLooperShadow = shadowOf(callbackThread.getLooper());
callbackLooperShadow.pause();
AtomicInteger flushCompleted = new AtomicInteger();
asynchronousMediaCodecCallback.flushAsync(/* onFlushCompleted= */ () -> flushCompleted.set(1));
asynchronousMediaCodecCallback.flushAsync(/* onFlushCompleted= */ () -> flushCompleted.set(2));
callbackLooperShadow.idle();
assertThat(flushCompleted.get()).isEqualTo(2);
}
/** Reflectively create a {@link MediaCodec.CodecException}. */
private static MediaCodec.CodecException createCodecException() throws Exception {
Constructor<MediaCodec.CodecException> constructor =
MediaCodec.CodecException.class.getDeclaredConstructor(
Integer.TYPE, Integer.TYPE, String.class);
return constructor.newInstance(
/* errorCode= */ 0, /* actionCode= */ 0, /* detailMessage= */ "error from codec");
}
private static class TestHandlerThread extends HandlerThread {
private boolean quit;
TestHandlerThread(String label) {
super(label);
}
public boolean hasQuit() {
return quit;
}
@Override
public boolean quit() {
quit = true;
return super.quit();
}
}
}

View file

@ -1,249 +0,0 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.mediacodec;
import static com.google.android.exoplayer2.testutil.TestUtil.assertBufferInfosEqual;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import android.media.MediaCodec;
import android.media.MediaFormat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.io.IOException;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit tests for {@link MediaCodecAsyncCallback}. */
@RunWith(AndroidJUnit4.class)
public class MediaCodecAsyncCallbackTest {
private MediaCodecAsyncCallback mediaCodecAsyncCallback;
private MediaCodec codec;
@Before
public void setUp() throws IOException {
mediaCodecAsyncCallback = new MediaCodecAsyncCallback();
codec = MediaCodec.createByCodecName("h264");
}
@Test
public void dequeInputBufferIndex_afterCreation_returnsTryAgain() {
assertThat(mediaCodecAsyncCallback.dequeueInputBufferIndex())
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
}
@Test
public void dequeInputBufferIndex_returnsEnqueuedBuffers() {
// Send two input buffers to the mediaCodecAsyncCallback.
mediaCodecAsyncCallback.onInputBufferAvailable(codec, 0);
mediaCodecAsyncCallback.onInputBufferAvailable(codec, 1);
assertThat(mediaCodecAsyncCallback.dequeueInputBufferIndex()).isEqualTo(0);
assertThat(mediaCodecAsyncCallback.dequeueInputBufferIndex()).isEqualTo(1);
assertThat(mediaCodecAsyncCallback.dequeueInputBufferIndex())
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
}
@Test
public void dequeInputBufferIndex_afterFlush_returnsTryAgain() {
// Send two input buffers to the mediaCodecAsyncCallback and then flush().
mediaCodecAsyncCallback.onInputBufferAvailable(codec, 0);
mediaCodecAsyncCallback.onInputBufferAvailable(codec, 1);
mediaCodecAsyncCallback.flush();
assertThat(mediaCodecAsyncCallback.dequeueInputBufferIndex())
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
}
@Test
public void dequeInputBufferIndex_afterFlushAndNewInputBuffer_returnsEnqueuedBuffer() {
// Send two input buffers to the mediaCodecAsyncCallback, then flush(), then send
// another input buffer.
mediaCodecAsyncCallback.onInputBufferAvailable(codec, 0);
mediaCodecAsyncCallback.onInputBufferAvailable(codec, 1);
mediaCodecAsyncCallback.flush();
mediaCodecAsyncCallback.onInputBufferAvailable(codec, 2);
assertThat(mediaCodecAsyncCallback.dequeueInputBufferIndex()).isEqualTo(2);
}
@Test
public void dequeOutputBufferIndex_afterCreation_returnsTryAgain() {
MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo();
assertThat(mediaCodecAsyncCallback.dequeueOutputBufferIndex(outBufferInfo))
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
}
@Test
public void dequeOutputBufferIndex_returnsEnqueuedBuffers() {
// Send two output buffers to the mediaCodecAsyncCallback.
MediaCodec.BufferInfo bufferInfo1 = new MediaCodec.BufferInfo();
mediaCodecAsyncCallback.onOutputBufferAvailable(codec, 0, bufferInfo1);
MediaCodec.BufferInfo bufferInfo2 = new MediaCodec.BufferInfo();
bufferInfo2.set(1, 1, 1, 1);
mediaCodecAsyncCallback.onOutputBufferAvailable(codec, 1, bufferInfo2);
MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo();
assertThat(mediaCodecAsyncCallback.dequeueOutputBufferIndex(outBufferInfo)).isEqualTo(0);
assertBufferInfosEqual(bufferInfo1, outBufferInfo);
assertThat(mediaCodecAsyncCallback.dequeueOutputBufferIndex(outBufferInfo)).isEqualTo(1);
assertBufferInfosEqual(bufferInfo2, outBufferInfo);
assertThat(mediaCodecAsyncCallback.dequeueOutputBufferIndex(outBufferInfo))
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
}
@Test
public void dequeOutputBufferIndex_afterFlush_returnsTryAgain() {
// Send two output buffers to the mediaCodecAsyncCallback and then flush().
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
mediaCodecAsyncCallback.onOutputBufferAvailable(codec, 0, bufferInfo);
mediaCodecAsyncCallback.onOutputBufferAvailable(codec, 1, bufferInfo);
mediaCodecAsyncCallback.flush();
MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo();
assertThat(mediaCodecAsyncCallback.dequeueOutputBufferIndex(outBufferInfo))
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
}
@Test
public void dequeOutputBufferIndex_afterFlushAndNewOutputBuffers_returnsEnqueueBuffer() {
// Send two output buffers to the mediaCodecAsyncCallback, then flush(), then send
// another output buffer.
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
mediaCodecAsyncCallback.onOutputBufferAvailable(codec, 0, bufferInfo);
mediaCodecAsyncCallback.onOutputBufferAvailable(codec, 1, bufferInfo);
mediaCodecAsyncCallback.flush();
mediaCodecAsyncCallback.onOutputBufferAvailable(codec, 2, bufferInfo);
MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo();
assertThat(mediaCodecAsyncCallback.dequeueOutputBufferIndex(outBufferInfo)).isEqualTo(2);
}
@Test
public void dequeOutputBufferIndex_withPendingOutputFormat_returnsPendingOutputFormat() {
MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo();
mediaCodecAsyncCallback.onOutputFormatChanged(codec, new MediaFormat());
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
mediaCodecAsyncCallback.onOutputBufferAvailable(codec, /* index= */ 0, bufferInfo);
MediaFormat pendingMediaFormat = new MediaFormat();
mediaCodecAsyncCallback.onOutputFormatChanged(codec, pendingMediaFormat);
// Flush should not discard the last format.
mediaCodecAsyncCallback.flush();
// First callback after flush is an output buffer, pending output format should be pushed first.
mediaCodecAsyncCallback.onOutputBufferAvailable(codec, /* index= */ 1, bufferInfo);
assertThat(mediaCodecAsyncCallback.dequeueOutputBufferIndex(outBufferInfo))
.isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
assertThat(mediaCodecAsyncCallback.getOutputFormat()).isEqualTo(pendingMediaFormat);
assertThat(mediaCodecAsyncCallback.dequeueOutputBufferIndex(outBufferInfo)).isEqualTo(1);
}
@Test
public void dequeOutputBufferIndex_withPendingOutputFormatAndNewFormat_returnsNewFormat() {
mediaCodecAsyncCallback.onOutputFormatChanged(codec, new MediaFormat());
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
mediaCodecAsyncCallback.onOutputBufferAvailable(codec, /* index= */ 0, bufferInfo);
MediaFormat pendingMediaFormat = new MediaFormat();
mediaCodecAsyncCallback.onOutputFormatChanged(codec, pendingMediaFormat);
// Flush should not discard the last format
mediaCodecAsyncCallback.flush();
// The first callback after flush is a new MediaFormat, it should overwrite the pending format.
MediaFormat newFormat = new MediaFormat();
mediaCodecAsyncCallback.onOutputFormatChanged(codec, newFormat);
mediaCodecAsyncCallback.onOutputBufferAvailable(codec, /* index= */ 1, bufferInfo);
MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo();
assertThat(mediaCodecAsyncCallback.dequeueOutputBufferIndex(outBufferInfo))
.isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
assertThat(mediaCodecAsyncCallback.getOutputFormat()).isEqualTo(newFormat);
assertThat(mediaCodecAsyncCallback.dequeueOutputBufferIndex(outBufferInfo)).isEqualTo(1);
}
@Test
public void getOutputFormat_onNewInstance_raisesException() {
try {
mediaCodecAsyncCallback.getOutputFormat();
fail();
} catch (IllegalStateException expected) {
}
}
@Test
public void getOutputFormat_afterOnOutputFormatCalled_returnsFormat() {
MediaFormat format = new MediaFormat();
mediaCodecAsyncCallback.onOutputFormatChanged(codec, format);
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
assertThat(mediaCodecAsyncCallback.dequeueOutputBufferIndex(bufferInfo))
.isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
assertThat(mediaCodecAsyncCallback.getOutputFormat()).isEqualTo(format);
}
@Test
public void getOutputFormat_afterFlush_raisesCurrentFormat() {
MediaFormat format = new MediaFormat();
mediaCodecAsyncCallback.onOutputFormatChanged(codec, format);
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
mediaCodecAsyncCallback.dequeueOutputBufferIndex(bufferInfo);
mediaCodecAsyncCallback.flush();
assertThat(mediaCodecAsyncCallback.getOutputFormat()).isEqualTo(format);
}
@Test
public void maybeThrowExoPlaybackException_withoutErrorFromCodec_doesNotThrow() {
mediaCodecAsyncCallback.maybeThrowMediaCodecException();
}
@Test
public void maybeThrowExoPlaybackException_withErrorFromCodec_Throws() {
IllegalStateException exception = new IllegalStateException();
mediaCodecAsyncCallback.onMediaCodecError(exception);
try {
mediaCodecAsyncCallback.maybeThrowMediaCodecException();
fail();
} catch (IllegalStateException expected) {
}
}
@Test
public void maybeThrowExoPlaybackException_doesNotThrowTwice() {
IllegalStateException exception = new IllegalStateException();
mediaCodecAsyncCallback.onMediaCodecError(exception);
try {
mediaCodecAsyncCallback.maybeThrowMediaCodecException();
fail();
} catch (IllegalStateException expected) {
}
mediaCodecAsyncCallback.maybeThrowMediaCodecException();
}
@Test
public void maybeThrowExoPlaybackException_afterFlush_doesNotThrow() {
IllegalStateException exception = new IllegalStateException();
mediaCodecAsyncCallback.onMediaCodecError(exception);
mediaCodecAsyncCallback.flush();
mediaCodecAsyncCallback.maybeThrowMediaCodecException();
}
}