mirror of
https://github.com/samsonjs/media.git
synced 2026-04-01 10:35:48 +00:00
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
This commit is contained in:
parent
fcfc4eb5b6
commit
bf6e9c6144
3 changed files with 796 additions and 1 deletions
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,344 @@
|
|||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,437 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue