Commit bf6e9c61 by christosts Committed by Oliver Woodman

Add MultiLockAsyncMediaCodecAdapter

MultiLockAsyncMediaCodecAdapter is an implementation of the
MediaCodecAdapter that uses multiple locks to synchronize access to its
data compared to the single-lock approach used in
DedicatedThreadAsyncMediaCodecAdapter.

PiperOrigin-RevId: 285944702
parent fcfc4eb5
......@@ -197,7 +197,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
@IntDef({
MediaCodecOperationMode.SYNCHRONOUS,
MediaCodecOperationMode.ASYNCHRONOUS_PLAYBACK_THREAD,
MediaCodecOperationMode.ASYNCHRONOUS_DEDICATED_THREAD
MediaCodecOperationMode.ASYNCHRONOUS_DEDICATED_THREAD,
MediaCodecOperationMode.ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK
})
public @interface MediaCodecOperationMode {
......@@ -213,6 +214,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
* callbacks to a dedicated Thread.
*/
int 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.
*/
int ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK = 3;
}
/** Indicates no codec operating rate should be set. */
......@@ -481,6 +487,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
* 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
* MediaCodecOperationMode#SYNCHRONOUS}.
* <li>{@link MediaCodecOperationMode#ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK}: Same as
* {@link MediaCodecOperationMode#ASYNCHRONOUS_DEDICATED_THREAD} but it will internally
* use a finer grained locking mechanism for increased performance.
* </ul>
* By default, the operation mode is set to {@link MediaCodecOperationMode#SYNCHRONOUS}.
*/
......@@ -984,6 +993,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
&& Util.SDK_INT >= 23) {
codecAdapter = new DedicatedThreadAsyncMediaCodecAdapter(codec, getTrackType());
((DedicatedThreadAsyncMediaCodecAdapter) codecAdapter).start();
} else if (mediaCodecOperationMode
== MediaCodecOperationMode.ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK
&& Util.SDK_INT >= 23) {
codecAdapter = new MultiLockAsynchMediaCodecAdapter(codec, getTrackType());
((MultiLockAsynchMediaCodecAdapter) codecAdapter).start();
} else {
codecAdapter = new SynchronousMediaCodecAdapter(codec, getDequeueOutputBufferTimeoutUs());
}
......
/*
* 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.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.IntArrayQueue;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayDeque;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* A {@link 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.
*
* <p>After creating an instance, you need to call {@link #start()} to start the internal Thread.
*/
@RequiresApi(23)
/* package */ final class MultiLockAsynchMediaCodecAdapter extends MediaCodec.Callback
implements MediaCodecAdapter {
@IntDef({State.CREATED, State.STARTED, State.SHUT_DOWN})
private @interface State {
int CREATED = 0;
int STARTED = 1;
int 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")
@MonotonicNonNull
private MediaFormat currentFormat;
@GuardedBy("objectStateLock")
private long pendingFlush;
@GuardedBy("objectStateLock")
@Nullable
private IllegalStateException codecException;
@GuardedBy("objectStateLock")
private @State int state;
private final HandlerThread handlerThread;
@MonotonicNonNull private Handler handler;
private Runnable onCodecStart;
/** Creates a new instance that wraps the specified {@link MediaCodec}. */
/* package */ MultiLockAsynchMediaCodecAdapter(MediaCodec codec, int trackType) {
this(codec, new HandlerThread(createThreadLabel(trackType)));
}
@VisibleForTesting
/* package */ MultiLockAsynchMediaCodecAdapter(MediaCodec codec, 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;
state = State.CREATED;
this.handlerThread = handlerThread;
onCodecStart = codec::start;
}
/**
* Starts the operation of this instance.
*
* <p>After a call to this method, make sure to call {@link #shutdown()} to terminate the internal
* Thread. You can only call this method once during the lifetime of an instance; calling this
* method again will throw an {@link IllegalStateException}.
*
* @throws IllegalStateException If this method has been called already.
*/
public void start() {
synchronized (objectStateLock) {
Assertions.checkState(state == State.CREATED);
handlerThread.start();
handler = new Handler(handlerThread.getLooper());
codec.setCallback(this, handler);
state = State.STARTED;
}
}
@Override
public int dequeueInputBufferIndex() {
synchronized (objectStateLock) {
Assertions.checkState(state == State.STARTED);
if (isFlushing()) {
return MediaCodec.INFO_TRY_AGAIN_LATER;
} else {
maybeThrowException();
return dequeueAvailableInputBufferIndex();
}
}
}
@Override
public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) {
synchronized (objectStateLock) {
Assertions.checkState(state == State.STARTED);
if (isFlushing()) {
return MediaCodec.INFO_TRY_AGAIN_LATER;
} else {
maybeThrowException();
return dequeueAvailableOutputBufferIndex(bufferInfo);
}
}
}
@Override
public MediaFormat getOutputFormat() {
synchronized (objectStateLock) {
Assertions.checkState(state == State.STARTED);
if (currentFormat == null) {
throw new IllegalStateException();
}
return currentFormat;
}
}
@Override
public void flush() {
synchronized (objectStateLock) {
Assertions.checkState(state == State.STARTED);
codec.flush();
pendingFlush++;
Util.castNonNull(handler).post(this::onFlushComplete);
}
}
@Override
public void shutdown() {
synchronized (objectStateLock) {
if (state == State.STARTED) {
handlerThread.quit();
}
state = State.SHUT_DOWN;
}
}
@VisibleForTesting
/* package */ void setOnCodecStart(Runnable onCodecStart) {
this.onCodecStart = onCodecStart;
}
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(@NonNull MediaCodec codec, int index) {
synchronized (inputBufferLock) {
availableInputBuffers.add(index);
}
}
@Override
public void onOutputBufferAvailable(
@NonNull MediaCodec codec, int index, @NonNull MediaCodec.BufferInfo info) {
synchronized (outputBufferLock) {
availableOutputBuffers.add(index);
bufferInfos.add(info);
}
}
@Override
public void onError(@NonNull MediaCodec codec, @NonNull MediaCodec.CodecException e) {
onMediaCodecError(e);
}
@Override
public void onOutputFormatChanged(@NonNull MediaCodec codec, @NonNull 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 {
onCodecStart.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("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();
}
}
/*
* 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.mediacodec.MediaCodecTestUtils.areEqual;
import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.waitUntilAllEventsAreExecuted;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import static org.robolectric.Shadows.shadowOf;
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.TimeUnit;
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.ShadowLooper;
/** Unit tests for {@link MultiLockAsynchMediaCodecAdapter}. */
@RunWith(AndroidJUnit4.class)
public class MultiLockAsyncMediaCodecAdapterTest {
private MultiLockAsynchMediaCodecAdapter adapter;
private MediaCodec codec;
private MediaCodec.BufferInfo bufferInfo = null;
private MediaCodecAsyncCallback mediaCodecAsyncCallbackSpy;
private TestHandlerThread handlerThread;
@Before
public void setup() throws IOException {
codec = MediaCodec.createByCodecName("h264");
handlerThread = new TestHandlerThread("TestHandlerThread");
adapter = new MultiLockAsynchMediaCodecAdapter(codec, handlerThread);
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 start_calledTwice_throwsException() {
adapter.start();
try {
adapter.start();
fail();
} catch (IllegalStateException expected) {
}
}
@Test
public void dequeueInputBufferIndex_withoutStart_throwsException() {
try {
adapter.dequeueInputBufferIndex();
fail();
} catch (IllegalStateException expected) {
}
}
@Test
public void dequeueInputBufferIndex_afterShutdown_throwsException() {
adapter.start();
adapter.shutdown();
try {
adapter.dequeueInputBufferIndex();
fail();
} catch (IllegalStateException expected) {
}
}
@Test
public void dequeueInputBufferIndex_withAfterFlushFailed_throwsException()
throws InterruptedException {
adapter.setOnCodecStart(
() -> {
throw new IllegalStateException("codec#start() exception");
});
adapter.start();
adapter.flush();
assertThat(
waitUntilAllEventsAreExecuted(
handlerThread.getLooper(), /* time= */ 5, TimeUnit.SECONDS))
.isTrue();
try {
adapter.dequeueInputBufferIndex();
fail();
} catch (IllegalStateException expected) {
}
}
@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 {
// Disable calling codec.start() after flush to avoid receiving buffers from the
// shadow codec impl
adapter.setOnCodecStart(() -> {});
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
assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue();
assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(10);
}
@Test
public void dequeueInputBufferIndex_withMediaCodecError_throwsException() {
adapter.start();
adapter.onMediaCodecError(new IllegalStateException("error from codec"));
try {
adapter.dequeueInputBufferIndex();
fail();
} catch (IllegalStateException expected) {
}
}
@Test
public void dequeueOutputBufferIndex_withoutStart_throwsException() {
try {
adapter.dequeueOutputBufferIndex(bufferInfo);
fail();
} catch (IllegalStateException expected) {
}
}
@Test
public void dequeueOutputBufferIndex_afterShutdown_throwsException() {
adapter.start();
adapter.shutdown();
try {
adapter.dequeueOutputBufferIndex(bufferInfo);
fail();
} catch (IllegalStateException expected) {
}
}
@Test
public void dequeueOutputBufferIndex_withInternalException_throwsException()
throws InterruptedException {
adapter.setOnCodecStart(
() -> {
throw new RuntimeException("codec#start() exception");
});
adapter.start();
adapter.flush();
assertThat(
waitUntilAllEventsAreExecuted(
handlerThread.getLooper(), /* time= */ 5, TimeUnit.SECONDS))
.isTrue();
try {
adapter.dequeueOutputBufferIndex(bufferInfo);
fail();
} catch (IllegalStateException expected) {
}
}
@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);
assertThat(areEqual(bufferInfo, enqueuedBufferInfo)).isTrue();
}
@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
assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue();
assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)).isEqualTo(10);
assertThat(areEqual(bufferInfo, lastBufferInfo)).isTrue();
}
@Test
public void dequeueOutputBufferIndex_withMediaCodecError_throwsException() {
adapter.start();
adapter.onMediaCodecError(new IllegalStateException("error from codec"));
try {
adapter.dequeueOutputBufferIndex(bufferInfo);
fail();
} catch (IllegalStateException expected) {
}
}
@Test
public void getOutputFormat_withoutStart_throwsException() {
try {
adapter.getOutputFormat();
fail();
} catch (IllegalStateException expected) {
}
}
@Test
public void getOutputFormat_afterShutdown_throwsException() {
adapter.start();
adapter.shutdown();
try {
adapter.getOutputFormat();
fail();
} catch (IllegalStateException expected) {
}
}
@Test
public void getOutputFormat_withoutFormatReceived_throwsException() {
adapter.start();
try {
adapter.getOutputFormat();
fail();
} catch (IllegalStateException expected) {
}
}
@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() throws InterruptedException {
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();
assertThat(
waitUntilAllEventsAreExecuted(
handlerThread.getLooper(), /* time= */ 5, TimeUnit.SECONDS))
.isTrue();
assertThat(adapter.getOutputFormat()).isEqualTo(format);
}
@Test
public void flush_withoutStarted_throwsException() {
try {
adapter.flush();
} catch (IllegalStateException expected) {
}
}
@Test
public void flush_afterShutdown_throwsException() {
adapter.start();
adapter.shutdown();
try {
adapter.flush();
} catch (IllegalStateException expected) {
}
}
@Test
public void flush_multipleTimes_onlyLastFlushExecutes() throws InterruptedException {
AtomicInteger onCodecStartCount = new AtomicInteger(0);
adapter.setOnCodecStart(() -> onCodecStartCount.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 - first flush event
// should have been a no-op
ShadowLooper shadowLooper = shadowOf(looper);
while (milestoneCount.get() < 1) {
shadowLooper.runOneTask();
}
assertThat(onCodecStartCount.get()).isEqualTo(0);
assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue();
assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(3);
assertThat(onCodecStartCount.get()).isEqualTo(1);
}
@Test
public void flush_andImmediatelyShutdown_flushIsNoOp() throws InterruptedException {
AtomicInteger onCodecStartCount = new AtomicInteger(0);
adapter.setOnCodecStart(() -> onCodecStartCount.incrementAndGet());
adapter.start();
// Obtain looper when adapter is started.
Looper looper = handlerThread.getLooper();
adapter.flush();
adapter.shutdown();
assertThat(waitUntilAllEventsAreExecuted(looper, 5, TimeUnit.SECONDS)).isTrue();
// Only shutdown flushes the MediaCodecAsync handler.
assertThat(onCodecStartCount.get()).isEqualTo(0);
}
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;
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment