Commit 485949b5 by christosts Committed by Oliver Woodman

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
parent 4783c329
......@@ -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);
}
}
@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();
@VisibleForTesting
/* package */ void onError(MediaCodec.CodecException error) {
asynchronousMediaCodecCallback.onError(codec, error);
}
@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) {
......
/*
* 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;
}
}
}
/*
* 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);
}
}
......@@ -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);
......
......@@ -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());
}
......@@ -193,25 +156,6 @@ public class AsynchronousMediaCodecAdapterTest {
}
@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.
adapter.configure(
......@@ -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();
}
}
}
......@@ -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();
}
}
......
/*
* 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();
}
}
}
/*
* 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();
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment