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>
|
* <ul>
|
||||||
* <li>{@link #OPERATION_MODE_SYNCHRONOUS}
|
* <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}
|
||||||
* <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_ASYNCHRONOUS_QUEUEING}
|
||||||
* <li>{@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK_ASYNCHRONOUS_QUEUEING}
|
|
||||||
* </ul>
|
* </ul>
|
||||||
*/
|
*/
|
||||||
@Documented
|
@Documented
|
||||||
@ -85,42 +82,27 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
|
@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
|
||||||
@IntDef({
|
@IntDef({
|
||||||
OPERATION_MODE_SYNCHRONOUS,
|
OPERATION_MODE_SYNCHRONOUS,
|
||||||
OPERATION_MODE_ASYNCHRONOUS_PLAYBACK_THREAD,
|
|
||||||
OPERATION_MODE_ASYNCHRONOUS_DEDICATED_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_ASYNCHRONOUS_QUEUEING,
|
||||||
OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK_ASYNCHRONOUS_QUEUEING
|
|
||||||
})
|
})
|
||||||
public @interface MediaCodecOperationMode {}
|
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. */
|
/** Operates the {@link MediaCodec} in synchronous mode. */
|
||||||
public static final int OPERATION_MODE_SYNCHRONOUS = 0;
|
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}
|
* Operates the {@link MediaCodec} in asynchronous mode and routes {@link MediaCodec.Callback}
|
||||||
* callbacks to a dedicated thread.
|
* callbacks to a dedicated thread.
|
||||||
*/
|
*/
|
||||||
public static final int OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD = 2;
|
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
|
* Same as {@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD}, and offloads queueing to another
|
||||||
* thread.
|
* thread.
|
||||||
*/
|
*/
|
||||||
public static final int OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_ASYNCHRONOUS_QUEUEING = 4;
|
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. */
|
/** Thrown when a failure occurs instantiating a decoder. */
|
||||||
public static class DecoderInitializationException extends Exception {
|
public static class DecoderInitializationException extends Exception {
|
||||||
@ -488,25 +470,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
* <ul>
|
* <ul>
|
||||||
* <li>{@link #OPERATION_MODE_SYNCHRONOUS}: The {@link MediaCodec} will operate in
|
* <li>{@link #OPERATION_MODE_SYNCHRONOUS}: The {@link MediaCodec} will operate in
|
||||||
* synchronous mode.
|
* 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
|
* <li>{@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD}: The {@link MediaCodec} will
|
||||||
* operate in asynchronous mode and {@link MediaCodec.Callback} callbacks will be routed
|
* 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 ≤
|
* 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}.
|
* 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
|
* <li>{@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_ASYNCHRONOUS_QUEUEING}: Same as
|
||||||
* {@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD} and, in addition, input buffers
|
* {@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD} and, in addition, input buffers
|
||||||
* will be submitted to the {@link MediaCodec} in a separate thread.
|
* 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>
|
* </ul>
|
||||||
* By default, the operation mode is set to {@link
|
* By default, the operation mode is set to {@link
|
||||||
* MediaCodecRenderer#OPERATION_MODE_SYNCHRONOUS}.
|
* MediaCodecRenderer#OPERATION_MODE_SYNCHRONOUS}.
|
||||||
@ -1103,27 +1073,15 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
codecInitializingTimestamp = SystemClock.elapsedRealtime();
|
codecInitializingTimestamp = SystemClock.elapsedRealtime();
|
||||||
TraceUtil.beginSection("createCodec:" + codecName);
|
TraceUtil.beginSection("createCodec:" + codecName);
|
||||||
codec = MediaCodec.createByCodecName(codecName);
|
codec = MediaCodec.createByCodecName(codecName);
|
||||||
if (mediaCodecOperationMode == OPERATION_MODE_ASYNCHRONOUS_PLAYBACK_THREAD
|
if (mediaCodecOperationMode == OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD
|
||||||
&& Util.SDK_INT >= 21) {
|
|
||||||
codecAdapter = new AsynchronousMediaCodecAdapter(codec);
|
|
||||||
} else if (mediaCodecOperationMode == OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD
|
|
||||||
&& Util.SDK_INT >= 23) {
|
&& Util.SDK_INT >= 23) {
|
||||||
codecAdapter = new DedicatedThreadAsyncMediaCodecAdapter(codec, getTrackType());
|
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
|
} else if (mediaCodecOperationMode
|
||||||
== OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_ASYNCHRONOUS_QUEUEING
|
== OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_ASYNCHRONOUS_QUEUEING
|
||||||
&& Util.SDK_INT >= 23) {
|
&& Util.SDK_INT >= 23) {
|
||||||
codecAdapter =
|
codecAdapter =
|
||||||
new DedicatedThreadAsyncMediaCodecAdapter(
|
new DedicatedThreadAsyncMediaCodecAdapter(
|
||||||
codec, /* enableAsynchronousQueueing= */ true, getTrackType());
|
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 {
|
} else {
|
||||||
codecAdapter = new SynchronousMediaCodecAdapter(codec);
|
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