Remove dropped MediaCodecAdadpters
Delete the AsynchronousMediaCodecAdapter, the MultiLockAsyncMediaCodecAdapter and their tests. PiperOrigin-RevId: 315694296
This commit is contained in:
parent
95b61eb835
commit
3ce57ae2e8
@ -1,159 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.exoplayer2.mediacodec;
|
||||
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaFormat;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import com.google.android.exoplayer2.decoder.CryptoInfo;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
|
||||
/**
|
||||
* A {@link MediaCodecAdapter} that operates the {@link MediaCodec} in asynchronous mode.
|
||||
*
|
||||
* <p>The AsynchronousMediaCodecAdapter routes callbacks to the current thread's {@link Looper}
|
||||
* obtained via {@link Looper#myLooper()}
|
||||
*/
|
||||
@RequiresApi(21)
|
||||
/* package */ final class AsynchronousMediaCodecAdapter implements MediaCodecAdapter {
|
||||
private final MediaCodecAsyncCallback mediaCodecAsyncCallback;
|
||||
private final Handler handler;
|
||||
private final MediaCodec codec;
|
||||
@Nullable private IllegalStateException internalException;
|
||||
private boolean flushing;
|
||||
private Runnable codecStartRunnable;
|
||||
|
||||
/**
|
||||
* Create a new {@code AsynchronousMediaCodecAdapter}.
|
||||
*
|
||||
* @param codec The {@link MediaCodec} to wrap.
|
||||
*/
|
||||
public AsynchronousMediaCodecAdapter(MediaCodec codec) {
|
||||
this(codec, Assertions.checkNotNull(Looper.myLooper()));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
/* package */ AsynchronousMediaCodecAdapter(MediaCodec codec, Looper looper) {
|
||||
mediaCodecAsyncCallback = new MediaCodecAsyncCallback();
|
||||
handler = new Handler(looper);
|
||||
this.codec = codec;
|
||||
this.codec.setCallback(mediaCodecAsyncCallback);
|
||||
codecStartRunnable = codec::start;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
codecStartRunnable.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueInputBuffer(
|
||||
int index, int offset, int size, long presentationTimeUs, int flags) {
|
||||
codec.queueInputBuffer(index, offset, size, presentationTimeUs, flags);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueSecureInputBuffer(
|
||||
int index, int offset, CryptoInfo info, long presentationTimeUs, int flags) {
|
||||
codec.queueSecureInputBuffer(
|
||||
index, offset, info.getFrameworkCryptoInfo(), presentationTimeUs, flags);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int dequeueInputBufferIndex() {
|
||||
if (flushing) {
|
||||
return MediaCodec.INFO_TRY_AGAIN_LATER;
|
||||
} else {
|
||||
maybeThrowException();
|
||||
return mediaCodecAsyncCallback.dequeueInputBufferIndex();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) {
|
||||
if (flushing) {
|
||||
return MediaCodec.INFO_TRY_AGAIN_LATER;
|
||||
} else {
|
||||
maybeThrowException();
|
||||
return mediaCodecAsyncCallback.dequeueOutputBufferIndex(bufferInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaFormat getOutputFormat() {
|
||||
return mediaCodecAsyncCallback.getOutputFormat();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() {
|
||||
clearPendingFlushState();
|
||||
flushing = true;
|
||||
codec.flush();
|
||||
handler.post(this::onCompleteFlush);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
clearPendingFlushState();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
/* package */ MediaCodec.Callback getMediaCodecCallback() {
|
||||
return mediaCodecAsyncCallback;
|
||||
}
|
||||
|
||||
private void onCompleteFlush() {
|
||||
flushing = false;
|
||||
mediaCodecAsyncCallback.flush();
|
||||
try {
|
||||
codecStartRunnable.run();
|
||||
} catch (IllegalStateException e) {
|
||||
// Catch IllegalStateException directly so that we don't have to wrap it.
|
||||
internalException = e;
|
||||
} catch (Exception e) {
|
||||
internalException = new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
/* package */ void setCodecStartRunnable(Runnable codecStartRunnable) {
|
||||
this.codecStartRunnable = codecStartRunnable;
|
||||
}
|
||||
|
||||
private void maybeThrowException() throws IllegalStateException {
|
||||
maybeThrowInternalException();
|
||||
mediaCodecAsyncCallback.maybeThrowMediaCodecException();
|
||||
}
|
||||
|
||||
private void maybeThrowInternalException() {
|
||||
if (internalException != null) {
|
||||
IllegalStateException e = internalException;
|
||||
internalException = null;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/** Clear state related to pending flush events. */
|
||||
private void clearPendingFlushState() {
|
||||
handler.removeCallbacksAndMessages(null);
|
||||
internalException = null;
|
||||
}
|
||||
}
|
@ -73,11 +73,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link #OPERATION_MODE_SYNCHRONOUS}
|
||||
* <li>{@link #OPERATION_MODE_ASYNCHRONOUS_PLAYBACK_THREAD}
|
||||
* <li>{@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD}
|
||||
* <li>{@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK}
|
||||
* <li>{@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_ASYNCHRONOUS_QUEUEING}
|
||||
* <li>{@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK_ASYNCHRONOUS_QUEUEING}
|
||||
* </ul>
|
||||
*/
|
||||
@Documented
|
||||
@ -85,42 +82,27 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
|
||||
@IntDef({
|
||||
OPERATION_MODE_SYNCHRONOUS,
|
||||
OPERATION_MODE_ASYNCHRONOUS_PLAYBACK_THREAD,
|
||||
OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD,
|
||||
OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK,
|
||||
OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_ASYNCHRONOUS_QUEUEING,
|
||||
OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK_ASYNCHRONOUS_QUEUEING
|
||||
})
|
||||
public @interface MediaCodecOperationMode {}
|
||||
|
||||
// TODO: Refactor these constants once internal evaluation completed.
|
||||
// Do not assign values 1, 3 and 5 to a new operation mode until the evaluation is completed,
|
||||
// otherwise existing clients may operate one of the dropped modes.
|
||||
// [Internal ref: b/132684114]
|
||||
/** Operates the {@link MediaCodec} in synchronous mode. */
|
||||
public static final int OPERATION_MODE_SYNCHRONOUS = 0;
|
||||
/**
|
||||
* Operates the {@link MediaCodec} in asynchronous mode and routes {@link MediaCodec.Callback}
|
||||
* callbacks to the playback thread.
|
||||
*/
|
||||
public static final int OPERATION_MODE_ASYNCHRONOUS_PLAYBACK_THREAD = 1;
|
||||
/**
|
||||
* Operates the {@link MediaCodec} in asynchronous mode and routes {@link MediaCodec.Callback}
|
||||
* callbacks to a dedicated thread.
|
||||
*/
|
||||
public static final int OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD = 2;
|
||||
/**
|
||||
* Operates the {@link MediaCodec} in asynchronous mode and routes {@link MediaCodec.Callback}
|
||||
* callbacks to a dedicated thread. Uses granular locking for input and output buffers.
|
||||
*/
|
||||
public static final int OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK = 3;
|
||||
/**
|
||||
* Same as {@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD}, and offloads queueing to another
|
||||
* thread.
|
||||
*/
|
||||
public static final int OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_ASYNCHRONOUS_QUEUEING = 4;
|
||||
/**
|
||||
* Same as {@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK}, and offloads queueing
|
||||
* to another thread.
|
||||
*/
|
||||
public static final int
|
||||
OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK_ASYNCHRONOUS_QUEUEING = 5;
|
||||
|
||||
/** Thrown when a failure occurs instantiating a decoder. */
|
||||
public static class DecoderInitializationException extends Exception {
|
||||
@ -488,25 +470,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
* <ul>
|
||||
* <li>{@link #OPERATION_MODE_SYNCHRONOUS}: The {@link MediaCodec} will operate in
|
||||
* synchronous mode.
|
||||
* <li>{@link #OPERATION_MODE_ASYNCHRONOUS_PLAYBACK_THREAD}: The {@link MediaCodec} will
|
||||
* operate in asynchronous mode and {@link MediaCodec.Callback} callbacks will be routed
|
||||
* to the playback thread. This mode requires API level ≥ 21; if the API level is
|
||||
* ≤ 20, the operation mode will be set to {@link
|
||||
* MediaCodecRenderer#OPERATION_MODE_SYNCHRONOUS}.
|
||||
* <li>{@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD}: The {@link MediaCodec} will
|
||||
* operate in asynchronous mode and {@link MediaCodec.Callback} callbacks will be routed
|
||||
* to a dedicated thread. This mode requires API level ≥ 23; if the API level is ≤
|
||||
* 22, the operation mode will be set to {@link #OPERATION_MODE_SYNCHRONOUS}.
|
||||
* <li>{@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK}: Same as {@link
|
||||
* #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD} and, in addition, input buffers will
|
||||
* submitted to the {@link MediaCodec} in a separate thread.
|
||||
* <li>{@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_ASYNCHRONOUS_QUEUEING}: Same as
|
||||
* {@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD} and, in addition, input buffers
|
||||
* will be submitted to the {@link MediaCodec} in a separate thread.
|
||||
* <li>{@link
|
||||
* #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK_ASYNCHRONOUS_QUEUEING}: Same
|
||||
* as {@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK} and, in addition,
|
||||
* input buffers will be submitted to the {@link MediaCodec} in a separate thread.
|
||||
* </ul>
|
||||
* By default, the operation mode is set to {@link
|
||||
* MediaCodecRenderer#OPERATION_MODE_SYNCHRONOUS}.
|
||||
@ -1103,27 +1073,15 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
codecInitializingTimestamp = SystemClock.elapsedRealtime();
|
||||
TraceUtil.beginSection("createCodec:" + codecName);
|
||||
codec = MediaCodec.createByCodecName(codecName);
|
||||
if (mediaCodecOperationMode == OPERATION_MODE_ASYNCHRONOUS_PLAYBACK_THREAD
|
||||
&& Util.SDK_INT >= 21) {
|
||||
codecAdapter = new AsynchronousMediaCodecAdapter(codec);
|
||||
} else if (mediaCodecOperationMode == OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD
|
||||
if (mediaCodecOperationMode == OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD
|
||||
&& Util.SDK_INT >= 23) {
|
||||
codecAdapter = new DedicatedThreadAsyncMediaCodecAdapter(codec, getTrackType());
|
||||
} else if (mediaCodecOperationMode == OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK
|
||||
&& Util.SDK_INT >= 23) {
|
||||
codecAdapter = new MultiLockAsyncMediaCodecAdapter(codec, getTrackType());
|
||||
} else if (mediaCodecOperationMode
|
||||
== OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_ASYNCHRONOUS_QUEUEING
|
||||
&& Util.SDK_INT >= 23) {
|
||||
codecAdapter =
|
||||
new DedicatedThreadAsyncMediaCodecAdapter(
|
||||
codec, /* enableAsynchronousQueueing= */ true, getTrackType());
|
||||
} else if (mediaCodecOperationMode
|
||||
== OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK_ASYNCHRONOUS_QUEUEING
|
||||
&& Util.SDK_INT >= 23) {
|
||||
codecAdapter =
|
||||
new MultiLockAsyncMediaCodecAdapter(
|
||||
codec, /* enableAsynchronousQueueing= */ true, getTrackType());
|
||||
} else {
|
||||
codecAdapter = new SynchronousMediaCodecAdapter(codec);
|
||||
}
|
||||
|
@ -1,385 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.exoplayer2.mediacodec;
|
||||
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaFormat;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import androidx.annotation.GuardedBy;
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.decoder.CryptoInfo;
|
||||
import com.google.android.exoplayer2.util.IntArrayQueue;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.ArrayDeque;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
/**
|
||||
* A {@link MediaCodecAdapter} that operates the underlying {@link MediaCodec} in asynchronous mode
|
||||
* and routes {@link MediaCodec.Callback} callbacks on a dedicated thread that is managed
|
||||
* internally.
|
||||
*
|
||||
* <p>The main difference of this class compared to the {@link
|
||||
* DedicatedThreadAsyncMediaCodecAdapter} is that its internal implementation applies finer-grained
|
||||
* locking. The {@link DedicatedThreadAsyncMediaCodecAdapter} uses a single lock to synchronize
|
||||
* access, whereas this class uses a different lock to access the available input and available
|
||||
* output buffer indexes returned from the {@link MediaCodec}. This class assumes that the {@link
|
||||
* MediaCodecAdapter} methods will be accessed by the playback thread and the {@link
|
||||
* MediaCodec.Callback} methods will be accessed by the internal thread. This class is
|
||||
* <strong>NOT</strong> generally thread-safe in the sense that its public methods cannot be called
|
||||
* by any thread.
|
||||
*/
|
||||
@RequiresApi(23)
|
||||
/* package */ final class MultiLockAsyncMediaCodecAdapter extends MediaCodec.Callback
|
||||
implements MediaCodecAdapter {
|
||||
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({STATE_CREATED, STATE_STARTED, STATE_SHUT_DOWN})
|
||||
private @interface State {}
|
||||
|
||||
private static final int STATE_CREATED = 0;
|
||||
private static final int STATE_STARTED = 1;
|
||||
private static final int STATE_SHUT_DOWN = 2;
|
||||
|
||||
private final MediaCodec codec;
|
||||
private final Object inputBufferLock;
|
||||
private final Object outputBufferLock;
|
||||
private final Object objectStateLock;
|
||||
|
||||
@GuardedBy("inputBufferLock")
|
||||
private final IntArrayQueue availableInputBuffers;
|
||||
|
||||
@GuardedBy("outputBufferLock")
|
||||
private final IntArrayQueue availableOutputBuffers;
|
||||
|
||||
@GuardedBy("outputBufferLock")
|
||||
private final ArrayDeque<MediaCodec.BufferInfo> bufferInfos;
|
||||
|
||||
@GuardedBy("outputBufferLock")
|
||||
private final ArrayDeque<MediaFormat> formats;
|
||||
|
||||
@GuardedBy("objectStateLock")
|
||||
private @MonotonicNonNull MediaFormat currentFormat;
|
||||
|
||||
@GuardedBy("objectStateLock")
|
||||
private long pendingFlush;
|
||||
|
||||
@GuardedBy("objectStateLock")
|
||||
@Nullable
|
||||
private IllegalStateException codecException;
|
||||
|
||||
private final HandlerThread handlerThread;
|
||||
private @MonotonicNonNull Handler handler;
|
||||
private Runnable codecStartRunnable;
|
||||
private final MediaCodecInputBufferEnqueuer bufferEnqueuer;
|
||||
|
||||
@GuardedBy("objectStateLock")
|
||||
@State
|
||||
private int state;
|
||||
|
||||
/**
|
||||
* Creates a new instance that wraps the specified {@link MediaCodec}. An instance created with
|
||||
* this constructor will queue input buffers synchronously.
|
||||
*
|
||||
* @param codec The {@link MediaCodec} to wrap.
|
||||
* @param trackType One of {@link C#TRACK_TYPE_AUDIO} or {@link C#TRACK_TYPE_VIDEO}. Used for
|
||||
* labelling the internal thread accordingly.
|
||||
*/
|
||||
/* package */ MultiLockAsyncMediaCodecAdapter(MediaCodec codec, int trackType) {
|
||||
this(
|
||||
codec,
|
||||
/* enableAsynchronousQueueing= */ false,
|
||||
trackType,
|
||||
new HandlerThread(createThreadLabel(trackType)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance that wraps the specified {@link MediaCodec}.
|
||||
*
|
||||
* @param codec The {@link MediaCodec} to wrap.
|
||||
* @param enableAsynchronousQueueing Whether input buffers will be queued asynchronously.
|
||||
* @param trackType One of {@link C#TRACK_TYPE_AUDIO} or {@link C#TRACK_TYPE_VIDEO}. Used for
|
||||
* labelling the internal thread accordingly.
|
||||
*/
|
||||
/* package */ MultiLockAsyncMediaCodecAdapter(
|
||||
MediaCodec codec, boolean enableAsynchronousQueueing, int trackType) {
|
||||
this(
|
||||
codec,
|
||||
enableAsynchronousQueueing,
|
||||
trackType,
|
||||
new HandlerThread(createThreadLabel(trackType)));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
/* package */ MultiLockAsyncMediaCodecAdapter(
|
||||
MediaCodec codec,
|
||||
boolean enableAsynchronousQueueing,
|
||||
int trackType,
|
||||
HandlerThread handlerThread) {
|
||||
this.codec = codec;
|
||||
inputBufferLock = new Object();
|
||||
outputBufferLock = new Object();
|
||||
objectStateLock = new Object();
|
||||
availableInputBuffers = new IntArrayQueue();
|
||||
availableOutputBuffers = new IntArrayQueue();
|
||||
bufferInfos = new ArrayDeque<>();
|
||||
formats = new ArrayDeque<>();
|
||||
codecException = null;
|
||||
this.handlerThread = handlerThread;
|
||||
codecStartRunnable = codec::start;
|
||||
if (enableAsynchronousQueueing) {
|
||||
bufferEnqueuer = new AsynchronousMediaCodecBufferEnqueuer(codec, trackType);
|
||||
} else {
|
||||
bufferEnqueuer = new SynchronousMediaCodecBufferEnqueuer(codec);
|
||||
}
|
||||
state = STATE_CREATED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
synchronized (objectStateLock) {
|
||||
handlerThread.start();
|
||||
handler = new Handler(handlerThread.getLooper());
|
||||
codec.setCallback(this, handler);
|
||||
bufferEnqueuer.start();
|
||||
codecStartRunnable.run();
|
||||
state = STATE_STARTED;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int dequeueInputBufferIndex() {
|
||||
synchronized (objectStateLock) {
|
||||
if (isFlushing()) {
|
||||
return MediaCodec.INFO_TRY_AGAIN_LATER;
|
||||
} else {
|
||||
maybeThrowException();
|
||||
return dequeueAvailableInputBufferIndex();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) {
|
||||
synchronized (objectStateLock) {
|
||||
if (isFlushing()) {
|
||||
return MediaCodec.INFO_TRY_AGAIN_LATER;
|
||||
} else {
|
||||
maybeThrowException();
|
||||
return dequeueAvailableOutputBufferIndex(bufferInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaFormat getOutputFormat() {
|
||||
synchronized (objectStateLock) {
|
||||
if (currentFormat == null) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
return currentFormat;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueInputBuffer(
|
||||
int index, int offset, int size, long presentationTimeUs, int flags) {
|
||||
// This method does not need to be synchronized because it is not interacting with
|
||||
// MediaCodec.Callback and dequeueing buffers operations.
|
||||
bufferEnqueuer.queueInputBuffer(index, offset, size, presentationTimeUs, flags);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueSecureInputBuffer(
|
||||
int index, int offset, CryptoInfo info, long presentationTimeUs, int flags) {
|
||||
// This method does not need to be synchronized because it is not interacting with
|
||||
// MediaCodec.Callback and dequeueing buffers operations.
|
||||
bufferEnqueuer.queueSecureInputBuffer(index, offset, info, presentationTimeUs, flags);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() {
|
||||
synchronized (objectStateLock) {
|
||||
bufferEnqueuer.flush();
|
||||
codec.flush();
|
||||
pendingFlush++;
|
||||
Util.castNonNull(handler).post(this::onFlushComplete);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
synchronized (objectStateLock) {
|
||||
if (state == STATE_STARTED) {
|
||||
bufferEnqueuer.shutdown();
|
||||
handlerThread.quit();
|
||||
}
|
||||
state = STATE_SHUT_DOWN;
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
/* package */ void setCodecStartRunnable(Runnable codecStartRunnable) {
|
||||
this.codecStartRunnable = codecStartRunnable;
|
||||
}
|
||||
|
||||
private int dequeueAvailableInputBufferIndex() {
|
||||
synchronized (inputBufferLock) {
|
||||
return availableInputBuffers.isEmpty()
|
||||
? MediaCodec.INFO_TRY_AGAIN_LATER
|
||||
: availableInputBuffers.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@GuardedBy("objectStateLock")
|
||||
private int dequeueAvailableOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) {
|
||||
int bufferIndex;
|
||||
synchronized (outputBufferLock) {
|
||||
if (availableOutputBuffers.isEmpty()) {
|
||||
bufferIndex = MediaCodec.INFO_TRY_AGAIN_LATER;
|
||||
} else {
|
||||
bufferIndex = availableOutputBuffers.remove();
|
||||
if (bufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
|
||||
currentFormat = formats.remove();
|
||||
} else if (bufferIndex >= 0) {
|
||||
MediaCodec.BufferInfo outBufferInfo = bufferInfos.remove();
|
||||
bufferInfo.set(
|
||||
outBufferInfo.offset,
|
||||
outBufferInfo.size,
|
||||
outBufferInfo.presentationTimeUs,
|
||||
outBufferInfo.flags);
|
||||
}
|
||||
}
|
||||
}
|
||||
return bufferIndex;
|
||||
}
|
||||
|
||||
@GuardedBy("objectStateLock")
|
||||
private boolean isFlushing() {
|
||||
return pendingFlush > 0;
|
||||
}
|
||||
|
||||
@GuardedBy("objectStateLock")
|
||||
private void maybeThrowException() {
|
||||
@Nullable IllegalStateException exception = codecException;
|
||||
if (exception != null) {
|
||||
codecException = null;
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
// Called by the internal thread.
|
||||
|
||||
@Override
|
||||
public void onInputBufferAvailable(MediaCodec codec, int index) {
|
||||
synchronized (inputBufferLock) {
|
||||
availableInputBuffers.add(index);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {
|
||||
synchronized (outputBufferLock) {
|
||||
availableOutputBuffers.add(index);
|
||||
bufferInfos.add(info);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(MediaCodec codec, MediaCodec.CodecException e) {
|
||||
onMediaCodecError(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
|
||||
synchronized (outputBufferLock) {
|
||||
availableOutputBuffers.add(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
|
||||
formats.add(format);
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
/* package */ void onMediaCodecError(IllegalStateException e) {
|
||||
synchronized (objectStateLock) {
|
||||
codecException = e;
|
||||
}
|
||||
}
|
||||
|
||||
private void onFlushComplete() {
|
||||
synchronized (objectStateLock) {
|
||||
if (state == STATE_SHUT_DOWN) {
|
||||
return;
|
||||
}
|
||||
|
||||
--pendingFlush;
|
||||
if (pendingFlush > 0) {
|
||||
// Another flush() has been called.
|
||||
return;
|
||||
} else if (pendingFlush < 0) {
|
||||
// This should never happen.
|
||||
codecException = new IllegalStateException();
|
||||
return;
|
||||
}
|
||||
|
||||
clearAvailableInput();
|
||||
clearAvailableOutput();
|
||||
codecException = null;
|
||||
try {
|
||||
codecStartRunnable.run();
|
||||
} catch (IllegalStateException e) {
|
||||
codecException = e;
|
||||
} catch (Exception e) {
|
||||
codecException = new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void clearAvailableInput() {
|
||||
synchronized (inputBufferLock) {
|
||||
availableInputBuffers.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void clearAvailableOutput() {
|
||||
synchronized (outputBufferLock) {
|
||||
availableOutputBuffers.clear();
|
||||
bufferInfos.clear();
|
||||
formats.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private static String createThreadLabel(int trackType) {
|
||||
StringBuilder labelBuilder = new StringBuilder("ExoPlayer:MediaCodecAsyncAdapter:");
|
||||
if (trackType == C.TRACK_TYPE_AUDIO) {
|
||||
labelBuilder.append("Audio");
|
||||
} else if (trackType == C.TRACK_TYPE_VIDEO) {
|
||||
labelBuilder.append("Video");
|
||||
} else {
|
||||
labelBuilder.append("Unknown(").append(trackType).append(")");
|
||||
}
|
||||
return labelBuilder.toString();
|
||||
}
|
||||
}
|
@ -1,303 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.exoplayer2.mediacodec;
|
||||
|
||||
import static com.google.android.exoplayer2.testutil.TestUtil.assertBufferInfosEqual;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.robolectric.Shadows.shadowOf;
|
||||
import static org.robolectric.annotation.LooperMode.Mode.LEGACY;
|
||||
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaFormat;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.Looper;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.annotation.LooperMode;
|
||||
|
||||
/** Unit tests for {@link AsynchronousMediaCodecAdapter}. */
|
||||
@LooperMode(LEGACY)
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class AsynchronousMediaCodecAdapterTest {
|
||||
private AsynchronousMediaCodecAdapter adapter;
|
||||
private MediaCodec codec;
|
||||
private HandlerThread handlerThread;
|
||||
private Looper looper;
|
||||
private MediaCodec.BufferInfo bufferInfo;
|
||||
|
||||
@Before
|
||||
public void setUp() throws IOException {
|
||||
handlerThread = new HandlerThread("TestHandler");
|
||||
handlerThread.start();
|
||||
looper = handlerThread.getLooper();
|
||||
codec = MediaCodec.createByCodecName("h264");
|
||||
adapter = new AsynchronousMediaCodecAdapter(codec, looper);
|
||||
adapter.setCodecStartRunnable(() -> {});
|
||||
bufferInfo = new MediaCodec.BufferInfo();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
adapter.shutdown();
|
||||
handlerThread.quit();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueInputBufferIndex_withoutInputBuffer_returnsTryAgainLater() {
|
||||
adapter.start();
|
||||
|
||||
assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueInputBufferIndex_withInputBuffer_returnsInputBuffer() {
|
||||
adapter.start();
|
||||
adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 0);
|
||||
|
||||
assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueInputBufferIndex_whileFlushing_returnsTryAgainLater() {
|
||||
adapter.start();
|
||||
|
||||
adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 0);
|
||||
// A callback that is pending.
|
||||
new Handler(looper)
|
||||
.post(() -> adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 1));
|
||||
adapter.flush();
|
||||
|
||||
assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueInputBufferIndex_afterFlushCompletes_returnsNextInputBuffer() {
|
||||
adapter.start();
|
||||
Handler handler = new Handler(looper);
|
||||
handler.post(
|
||||
() -> adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 0));
|
||||
adapter.flush(); // enqueues a flush event on the looper
|
||||
handler.post(
|
||||
() -> adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 1));
|
||||
|
||||
// Wait until all tasks have been handled.
|
||||
shadowOf(looper).idle();
|
||||
assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueInputBufferIndex_afterFlushCompletesWithError_throwsException() {
|
||||
AtomicInteger calls = new AtomicInteger(0);
|
||||
adapter.setCodecStartRunnable(
|
||||
() -> {
|
||||
if (calls.incrementAndGet() == 2) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
});
|
||||
adapter.start();
|
||||
adapter.flush();
|
||||
|
||||
// Wait until all tasks have been handled.
|
||||
shadowOf(looper).idle();
|
||||
assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> {
|
||||
adapter.dequeueInputBufferIndex();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueOutputBufferIndex_withoutOutputBuffer_returnsTryAgainLater() {
|
||||
adapter.start();
|
||||
|
||||
assertThat(adapter.dequeueOutputBufferIndex(bufferInfo))
|
||||
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueOutputBufferIndex_withOutputBuffer_returnsOutputBuffer() {
|
||||
adapter.start();
|
||||
MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo();
|
||||
outBufferInfo.presentationTimeUs = 10;
|
||||
adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 0, outBufferInfo);
|
||||
|
||||
assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)).isEqualTo(0);
|
||||
assertBufferInfosEqual(bufferInfo, outBufferInfo);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueOutputBufferIndex_whileFlushing_returnsTryAgainLater() {
|
||||
adapter.start();
|
||||
adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 0, bufferInfo);
|
||||
adapter.flush();
|
||||
adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 1, bufferInfo);
|
||||
|
||||
assertThat(adapter.dequeueOutputBufferIndex(bufferInfo))
|
||||
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueOutputBufferIndex_afterFlushCompletes_returnsNextOutputBuffer() {
|
||||
adapter.start();
|
||||
Handler handler = new Handler(looper);
|
||||
MediaCodec.BufferInfo info0 = new MediaCodec.BufferInfo();
|
||||
handler.post(
|
||||
() -> adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 0, info0));
|
||||
adapter.flush(); // enqueues a flush event on the looper
|
||||
MediaCodec.BufferInfo info1 = new MediaCodec.BufferInfo();
|
||||
info1.presentationTimeUs = 1;
|
||||
handler.post(
|
||||
() -> adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 1, info1));
|
||||
|
||||
// Wait until all tasks have been handled.
|
||||
shadowOf(looper).idle();
|
||||
assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)).isEqualTo(1);
|
||||
assertBufferInfosEqual(info1, bufferInfo);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueOutputBufferIndex_afterFlushCompletesWithError_throwsException() {
|
||||
AtomicInteger calls = new AtomicInteger(0);
|
||||
adapter.setCodecStartRunnable(
|
||||
() -> {
|
||||
if (calls.incrementAndGet() == 2) {
|
||||
throw new RuntimeException("codec#start() exception");
|
||||
}
|
||||
});
|
||||
adapter.start();
|
||||
adapter.flush();
|
||||
|
||||
// Wait until all tasks have been handled.
|
||||
shadowOf(looper).idle();
|
||||
assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueOutputBufferIndex_withPendingOutputFormat_returnsPendingOutputFormat() {
|
||||
MediaFormat pendingOutputFormat = new MediaFormat();
|
||||
MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo();
|
||||
MediaCodec.Callback mediaCodecCallback = adapter.getMediaCodecCallback();
|
||||
Handler handler = new Handler(looper);
|
||||
adapter.start();
|
||||
|
||||
// Enqueue callbacks
|
||||
handler.post(() -> mediaCodecCallback.onOutputFormatChanged(codec, new MediaFormat()));
|
||||
handler.post(
|
||||
() ->
|
||||
mediaCodecCallback.onOutputBufferAvailable(
|
||||
codec, /* index= */ 0, new MediaCodec.BufferInfo()));
|
||||
handler.post(() -> mediaCodecCallback.onOutputFormatChanged(codec, pendingOutputFormat));
|
||||
handler.post(
|
||||
() ->
|
||||
mediaCodecCallback.onOutputBufferAvailable(
|
||||
codec, /* index= */ 1, new MediaCodec.BufferInfo()));
|
||||
adapter.flush();
|
||||
// After flush is complete, MediaCodec sends on output buffer.
|
||||
handler.post(
|
||||
() ->
|
||||
mediaCodecCallback.onOutputBufferAvailable(
|
||||
codec, /* index= */ 2, new MediaCodec.BufferInfo()));
|
||||
shadowOf(looper).idle();
|
||||
|
||||
assertThat(adapter.dequeueOutputBufferIndex(outBufferInfo))
|
||||
.isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
|
||||
assertThat(adapter.getOutputFormat()).isEqualTo(pendingOutputFormat);
|
||||
assertThat(adapter.dequeueOutputBufferIndex(outBufferInfo)).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueOutputBufferIndex_withPendingAndNewOutputFormat_returnsNewOutputFormat() {
|
||||
MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo();
|
||||
MediaCodec.Callback mediaCodecCallback = adapter.getMediaCodecCallback();
|
||||
Handler handler = new Handler(looper);
|
||||
adapter.start();
|
||||
|
||||
// Enqueue callbacks
|
||||
handler.post(() -> mediaCodecCallback.onOutputFormatChanged(codec, new MediaFormat()));
|
||||
handler.post(
|
||||
() ->
|
||||
mediaCodecCallback.onOutputBufferAvailable(
|
||||
codec, /* index= */ 0, new MediaCodec.BufferInfo()));
|
||||
adapter.flush();
|
||||
// After flush is complete, MediaCodec sends an output format change, it should overwrite
|
||||
// the pending format.
|
||||
MediaFormat newMediaFormat = new MediaFormat();
|
||||
handler.post(() -> mediaCodecCallback.onOutputFormatChanged(codec, newMediaFormat));
|
||||
shadowOf(looper).idle();
|
||||
|
||||
assertThat(adapter.dequeueOutputBufferIndex(outBufferInfo))
|
||||
.isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
|
||||
assertThat(adapter.getOutputFormat()).isEqualTo(newMediaFormat);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getOutputFormat_withMultipleFormats_returnsFormatsInCorrectOrder() {
|
||||
adapter.start();
|
||||
MediaFormat[] formats = new MediaFormat[10];
|
||||
MediaCodec.Callback mediaCodecCallback = adapter.getMediaCodecCallback();
|
||||
for (int i = 0; i < formats.length; i++) {
|
||||
formats[i] = new MediaFormat();
|
||||
mediaCodecCallback.onOutputFormatChanged(codec, formats[i]);
|
||||
}
|
||||
|
||||
for (MediaFormat format : formats) {
|
||||
assertThat(adapter.dequeueOutputBufferIndex(bufferInfo))
|
||||
.isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
|
||||
assertThat(adapter.getOutputFormat()).isEqualTo(format);
|
||||
// Call it again to ensure same format is returned
|
||||
assertThat(adapter.getOutputFormat()).isEqualTo(format);
|
||||
}
|
||||
// Obtain next output buffer
|
||||
assertThat(adapter.dequeueOutputBufferIndex(bufferInfo))
|
||||
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
|
||||
// Format should remain as is
|
||||
assertThat(adapter.getOutputFormat()).isEqualTo(formats[formats.length - 1]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getOutputFormat_afterFlush_returnsPreviousFormat() {
|
||||
adapter.start();
|
||||
MediaFormat format = new MediaFormat();
|
||||
adapter.getMediaCodecCallback().onOutputFormatChanged(codec, format);
|
||||
adapter.dequeueOutputBufferIndex(bufferInfo);
|
||||
adapter.flush();
|
||||
|
||||
// Wait until all tasks have been handled.
|
||||
shadowOf(looper).idle();
|
||||
assertThat(adapter.getOutputFormat()).isEqualTo(format);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shutdown_withPendingFlush_cancelsFlush() {
|
||||
AtomicInteger onCodecStartCalled = new AtomicInteger(0);
|
||||
adapter.setCodecStartRunnable(() -> onCodecStartCalled.incrementAndGet());
|
||||
adapter.start();
|
||||
adapter.flush();
|
||||
adapter.shutdown();
|
||||
|
||||
// Wait until all tasks have been handled.
|
||||
shadowOf(looper).idle();
|
||||
assertThat(onCodecStartCalled.get()).isEqualTo(1);
|
||||
}
|
||||
}
|
@ -1,331 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.exoplayer2.mediacodec;
|
||||
|
||||
import static com.google.android.exoplayer2.testutil.TestUtil.assertBufferInfosEqual;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.robolectric.Shadows.shadowOf;
|
||||
import static org.robolectric.annotation.LooperMode.Mode.LEGACY;
|
||||
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaFormat;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.Looper;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.Shadows;
|
||||
import org.robolectric.annotation.LooperMode;
|
||||
import org.robolectric.shadows.ShadowLooper;
|
||||
|
||||
/** Unit tests for {@link MultiLockAsyncMediaCodecAdapter}. */
|
||||
@LooperMode(LEGACY)
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class MultiLockAsyncMediaCodecAdapterTest {
|
||||
private MultiLockAsyncMediaCodecAdapter adapter;
|
||||
private MediaCodec codec;
|
||||
private MediaCodec.BufferInfo bufferInfo;
|
||||
private TestHandlerThread handlerThread;
|
||||
|
||||
@Before
|
||||
public void setUp() throws IOException {
|
||||
codec = MediaCodec.createByCodecName("h264");
|
||||
handlerThread = new TestHandlerThread("TestHandlerThread");
|
||||
adapter =
|
||||
new MultiLockAsyncMediaCodecAdapter(
|
||||
codec, /* enableAsynchronousQueueing= */ false, C.TRACK_TYPE_VIDEO, handlerThread);
|
||||
adapter.setCodecStartRunnable(() -> {});
|
||||
bufferInfo = new MediaCodec.BufferInfo();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
adapter.shutdown();
|
||||
|
||||
assertThat(TestHandlerThread.INSTANCES_STARTED.get()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void startAndShutdown_works() {
|
||||
adapter.start();
|
||||
adapter.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueInputBufferIndex_withAfterFlushFailed_throwsException()
|
||||
throws InterruptedException {
|
||||
AtomicInteger codecStartCalls = new AtomicInteger(0);
|
||||
adapter.setCodecStartRunnable(
|
||||
() -> {
|
||||
if (codecStartCalls.incrementAndGet() == 2) {
|
||||
throw new IllegalStateException("codec#start() exception");
|
||||
}
|
||||
});
|
||||
adapter.start();
|
||||
adapter.flush();
|
||||
|
||||
Shadows.shadowOf(handlerThread.getLooper()).idle();
|
||||
assertThrows(IllegalStateException.class, () -> adapter.dequeueInputBufferIndex());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueInputBufferIndex_withoutInputBuffer_returnsTryAgainLater() {
|
||||
adapter.start();
|
||||
|
||||
assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueInputBufferIndex_withInputBuffer_returnsInputBuffer() {
|
||||
adapter.start();
|
||||
adapter.onInputBufferAvailable(codec, 0);
|
||||
|
||||
assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueInputBufferIndex_withPendingFlush_returnsTryAgainLater() {
|
||||
adapter.start();
|
||||
adapter.onInputBufferAvailable(codec, 0);
|
||||
adapter.flush();
|
||||
|
||||
assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueInputBufferIndex_withFlushCompletedAndInputBuffer_returnsInputBuffer()
|
||||
throws InterruptedException {
|
||||
adapter.start();
|
||||
Looper looper = handlerThread.getLooper();
|
||||
Handler handler = new Handler(looper);
|
||||
// Enqueue 10 callbacks from codec
|
||||
for (int i = 0; i < 10; i++) {
|
||||
int bufferIndex = i;
|
||||
handler.post(() -> adapter.onInputBufferAvailable(codec, bufferIndex));
|
||||
}
|
||||
adapter.flush(); // Enqueues a flush event after the onInputBufferAvailable callbacks
|
||||
// Enqueue another onInputBufferAvailable after the flush event
|
||||
handler.post(() -> adapter.onInputBufferAvailable(codec, 10));
|
||||
|
||||
// Wait until all tasks have been handled
|
||||
Shadows.shadowOf(handlerThread.getLooper()).idle();
|
||||
assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(10);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueInputBufferIndex_withMediaCodecError_throwsException() {
|
||||
adapter.start();
|
||||
adapter.onMediaCodecError(new IllegalStateException("error from codec"));
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> adapter.dequeueInputBufferIndex());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void dequeueOutputBufferIndex_withInternalException_throwsException()
|
||||
throws InterruptedException {
|
||||
AtomicInteger codecStartCalls = new AtomicInteger(0);
|
||||
adapter.setCodecStartRunnable(
|
||||
() -> {
|
||||
if (codecStartCalls.incrementAndGet() == 2) {
|
||||
throw new RuntimeException("codec#start() exception");
|
||||
}
|
||||
});
|
||||
adapter.start();
|
||||
adapter.flush();
|
||||
|
||||
Shadows.shadowOf(handlerThread.getLooper()).idle();
|
||||
assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueOutputBufferIndex_withoutInputBuffer_returnsTryAgainLater() {
|
||||
adapter.start();
|
||||
|
||||
assertThat(adapter.dequeueOutputBufferIndex(bufferInfo))
|
||||
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueOutputBufferIndex_withOutputBuffer_returnsOutputBuffer() {
|
||||
adapter.start();
|
||||
MediaCodec.BufferInfo enqueuedBufferInfo = new MediaCodec.BufferInfo();
|
||||
adapter.onOutputBufferAvailable(codec, 0, enqueuedBufferInfo);
|
||||
|
||||
assertThat(adapter.dequeueOutputBufferIndex((bufferInfo))).isEqualTo(0);
|
||||
assertBufferInfosEqual(enqueuedBufferInfo, bufferInfo);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueOutputBufferIndex_withPendingFlush_returnsTryAgainLater() {
|
||||
adapter.start();
|
||||
adapter.dequeueOutputBufferIndex(bufferInfo);
|
||||
adapter.flush();
|
||||
|
||||
assertThat(adapter.dequeueOutputBufferIndex(bufferInfo))
|
||||
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueOutputBufferIndex_withFlushCompletedAndOutputBuffer_returnsOutputBuffer()
|
||||
throws InterruptedException {
|
||||
adapter.start();
|
||||
Looper looper = handlerThread.getLooper();
|
||||
Handler handler = new Handler(looper);
|
||||
// Enqueue 10 callbacks from codec
|
||||
for (int i = 0; i < 10; i++) {
|
||||
int bufferIndex = i;
|
||||
MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo();
|
||||
outBufferInfo.presentationTimeUs = i;
|
||||
handler.post(() -> adapter.onOutputBufferAvailable(codec, bufferIndex, outBufferInfo));
|
||||
}
|
||||
adapter.flush(); // Enqueues a flush event after the onOutputBufferAvailable callbacks
|
||||
// Enqueue another onOutputBufferAvailable after the flush event
|
||||
MediaCodec.BufferInfo lastBufferInfo = new MediaCodec.BufferInfo();
|
||||
lastBufferInfo.presentationTimeUs = 10;
|
||||
handler.post(() -> adapter.onOutputBufferAvailable(codec, 10, lastBufferInfo));
|
||||
|
||||
// Wait until all tasks have been handled
|
||||
Shadows.shadowOf(handlerThread.getLooper()).idle();
|
||||
assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)).isEqualTo(10);
|
||||
assertBufferInfosEqual(lastBufferInfo, bufferInfo);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueOutputBufferIndex_withMediaCodecError_throwsException() {
|
||||
adapter.start();
|
||||
adapter.onMediaCodecError(new IllegalStateException("error from codec"));
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getOutputFormat_withoutFormatReceived_throwsException() {
|
||||
adapter.start();
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> adapter.getOutputFormat());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getOutputFormat_withMultipleFormats_returnsCorrectFormat() {
|
||||
adapter.start();
|
||||
MediaFormat[] formats = new MediaFormat[10];
|
||||
for (int i = 0; i < formats.length; i++) {
|
||||
formats[i] = new MediaFormat();
|
||||
adapter.onOutputFormatChanged(codec, formats[i]);
|
||||
}
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
assertThat(adapter.dequeueOutputBufferIndex(bufferInfo))
|
||||
.isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
|
||||
assertThat(adapter.getOutputFormat()).isEqualTo(formats[i]);
|
||||
// A subsequent call to getOutputFormat() should return the previously fetched format
|
||||
assertThat(adapter.getOutputFormat()).isEqualTo(formats[i]);
|
||||
}
|
||||
assertThat(adapter.dequeueOutputBufferIndex(bufferInfo))
|
||||
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getOutputFormat_afterFlush_returnsPreviousFormat() {
|
||||
MediaFormat format = new MediaFormat();
|
||||
adapter.start();
|
||||
adapter.onOutputFormatChanged(codec, format);
|
||||
|
||||
assertThat(adapter.dequeueOutputBufferIndex(bufferInfo))
|
||||
.isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
|
||||
assertThat(adapter.getOutputFormat()).isEqualTo(format);
|
||||
|
||||
adapter.flush();
|
||||
Shadows.shadowOf(handlerThread.getLooper()).idle();
|
||||
assertThat(adapter.getOutputFormat()).isEqualTo(format);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flush_multipleTimes_onlyLastFlushExecutes() {
|
||||
AtomicInteger codecStartCalls = new AtomicInteger(0);
|
||||
adapter.setCodecStartRunnable(() -> codecStartCalls.incrementAndGet());
|
||||
adapter.start();
|
||||
Looper looper = handlerThread.getLooper();
|
||||
Handler handler = new Handler(looper);
|
||||
handler.post(() -> adapter.onInputBufferAvailable(codec, 0));
|
||||
adapter.flush(); // Enqueues a flush event
|
||||
handler.post(() -> adapter.onInputBufferAvailable(codec, 2));
|
||||
AtomicInteger milestoneCount = new AtomicInteger(0);
|
||||
handler.post(() -> milestoneCount.incrementAndGet());
|
||||
adapter.flush(); // Enqueues a second flush event
|
||||
handler.post(() -> adapter.onInputBufferAvailable(codec, 3));
|
||||
|
||||
// Progress the looper until the milestoneCount is increased:
|
||||
// adapter.start() called codec.start() but first flush event should have been a no-op
|
||||
ShadowLooper shadowLooper = shadowOf(looper);
|
||||
while (milestoneCount.get() < 1) {
|
||||
shadowLooper.runOneTask();
|
||||
}
|
||||
assertThat(codecStartCalls.get()).isEqualTo(1);
|
||||
|
||||
shadowLooper.idle();
|
||||
assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(3);
|
||||
assertThat(codecStartCalls.get()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flush_andImmediatelyShutdown_flushIsNoOp() {
|
||||
AtomicInteger codecStartCalls = new AtomicInteger(0);
|
||||
adapter.setCodecStartRunnable(() -> codecStartCalls.incrementAndGet());
|
||||
adapter.start();
|
||||
// Grab reference to Looper before shutting down the adapter otherwise handlerThread.getLooper()
|
||||
// might return null.
|
||||
Looper looper = handlerThread.getLooper();
|
||||
adapter.flush();
|
||||
adapter.shutdown();
|
||||
|
||||
Shadows.shadowOf(looper).idle();
|
||||
// Only adapter.start() called codec#start()
|
||||
assertThat(codecStartCalls.get()).isEqualTo(1);
|
||||
}
|
||||
|
||||
private static class TestHandlerThread extends HandlerThread {
|
||||
|
||||
private static final AtomicLong INSTANCES_STARTED = new AtomicLong(0);
|
||||
|
||||
public TestHandlerThread(String name) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void start() {
|
||||
super.start();
|
||||
INSTANCES_STARTED.incrementAndGet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean quit() {
|
||||
boolean quit = super.quit();
|
||||
INSTANCES_STARTED.decrementAndGet();
|
||||
return quit;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user