Move MediaCodecAdapter out of MediaCodecRenderer

Move MediaCodeAdapter and implementations to separate
files and add unit tests for AsynchronousMediaCodecAdapter.

PiperOrigin-RevId: 284537185
This commit is contained in:
christosts 2019-12-09 13:57:36 +00:00 committed by Oliver Woodman
parent 2462aeb443
commit 3156fbfc6e
7 changed files with 610 additions and 191 deletions

View File

@ -0,0 +1,140 @@
/*
* 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.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 MediaCodecAsyncCallback mediaCodecAsyncCallback;
private final Handler handler;
private final MediaCodec codec;
@Nullable private IllegalStateException internalException;
private boolean flushing;
private Runnable onCodecStart;
/**
* 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) {
this.mediaCodecAsyncCallback = new MediaCodecAsyncCallback();
handler = new Handler(looper);
this.codec = codec;
this.codec.setCallback(mediaCodecAsyncCallback);
onCodecStart = () -> codec.start();
}
@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 {
onCodecStart.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 setOnCodecStart(Runnable onCodecStart) {
this.onCodecStart = onCodecStart;
}
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;
}
}

View File

@ -0,0 +1,79 @@
/*
* 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;
/**
* Abstracts {@link MediaCodec} operations.
*
* <p>{@code MediaCodecAdapter} offers a common interface to interact with a {@link MediaCodec}
* regardless of the {@link
* com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.MediaCodecOperationMode} the {@link
* MediaCodec} is operating in.
*
* @see com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.MediaCodecOperationMode
*/
/* package */ interface MediaCodecAdapter {
/**
* Returns the next available input buffer index from the underlying {@link MediaCodec} or {@link
* MediaCodec#INFO_TRY_AGAIN_LATER} if no such buffer exists.
*
* @throws {@link IllegalStateException} if the underlying {@link MediaCodec} raised an error.
*/
int dequeueInputBufferIndex();
/**
* Returns the next available output buffer index from the underlying {@link MediaCodec}. If the
* next available output is a MediaFormat change, it will return {@link
* MediaCodec#INFO_OUTPUT_FORMAT_CHANGED} and you should call {@link #getOutputFormat()} to get
* the format. If there is no available output, this method will return {@link
* MediaCodec#INFO_TRY_AGAIN_LATER}.
*
* @throws {@link IllegalStateException} if the underlying {@link MediaCodec} raised an error.
*/
int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo);
/**
* Gets the {@link MediaFormat} that was output from the {@link MediaCodec}.
*
* <p>Call this method if a previous call to {@link #dequeueOutputBufferIndex} returned {@link
* MediaCodec#INFO_OUTPUT_FORMAT_CHANGED}.
*/
MediaFormat getOutputFormat();
/**
* Flushes the {@code MediaCodecAdapter}.
*
* <p>Note: {@link #flush()} should also call any {@link MediaCodec} methods needed to flush the
* {@link MediaCodec}, i.e., {@link MediaCodec#flush()} and <em>optionally</em> {@link
* MediaCodec#start()}, if the {@link MediaCodec} operates in asynchronous mode.
*/
void flush();
/**
* Shutdown the {@code MediaCodecAdapter}.
*
* <p>Note: This method does not release the underlying {@link MediaCodec}. Make sure to call
* {@link #shutdown()} before stopping or releasing the underlying {@link MediaCodec} to ensure
* the adapter is fully shutdown before the {@link MediaCodec} stops executing. Otherwise, there
* is a risk the adapter might interact with a stopped or released {@link MediaCodec}.
*/
void shutdown();
}

View File

@ -23,13 +23,10 @@ import android.media.MediaCrypto;
import android.media.MediaCryptoException;
import android.media.MediaFormat;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import androidx.annotation.CheckResult;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.BaseRenderer;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
@ -193,6 +190,24 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
}
/** The modes to operate the {@link MediaCodec}. */
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
MediaCodecOperationMode.SYNCHRONOUS,
MediaCodecOperationMode.ASYNCHRONOUS_PLAYBACK_THREAD
})
public @interface MediaCodecOperationMode {
/** Operates the {@link MediaCodec} in synchronous mode. */
int SYNCHRONOUS = 0;
/**
* Operates the {@link MediaCodec} in asynchronous mode and routes {@link MediaCodec.Callback}
* callbacks to the playback Thread.
*/
int ASYNCHRONOUS_PLAYBACK_THREAD = 1;
}
/** Indicates no codec operating rate should be set. */
protected static final float CODEC_OPERATING_RATE_UNSET = -1;
@ -293,50 +308,6 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
})
private @interface AdaptationWorkaroundMode {}
/**
* Abstracts {@link MediaCodec} operations that differ whether a {@link MediaCodec} is used in
* synchronous or asynchronous mode.
*/
private interface MediaCodecAdapter {
/**
* Returns the next available input buffer index from the underlying {@link MediaCodec} or
* {@link MediaCodec#INFO_TRY_AGAIN_LATER} if no such buffer exists.
*
* @throws {@link IllegalStateException} if the underling {@link MediaCodec} raised an error.
*/
int dequeueInputBufferIndex();
/**
* Returns the next available output buffer index from the underlying {@link MediaCodec}. If the
* next available output is a MediaFormat change, it will return {@link
* MediaCodec#INFO_OUTPUT_FORMAT_CHANGED} and you should call {@link #getOutputFormat()} to get
* the format. If there is no available output, this method will return {@link
* MediaCodec#INFO_TRY_AGAIN_LATER}.
*
* @throws {@link IllegalStateException} if the underling {@link MediaCodec} raised an error.
*/
int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo);
/**
* Gets the {@link MediaFormat} that was output from the {@link MediaCodec}.
*
* <p>Call this method if a previous call to {@link #dequeueOutputBufferIndex} returned {@link
* MediaCodec#INFO_OUTPUT_FORMAT_CHANGED}.
*/
MediaFormat getOutputFormat();
/** Flushes the {@code MediaCodecAdapter}. */
void flush();
/**
* Shutdown the {@code MediaCodecAdapter}.
*
* <p>Note: it does not release the underlying codec.
*/
void shutdown();
}
/**
* The adaptation workaround is never used.
*/
@ -424,7 +395,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
private boolean skipMediaCodecStopOnRelease;
private boolean pendingOutputEndOfStream;
private boolean useMediaCodecInAsyncMode;
private @MediaCodecOperationMode int mediaCodecOperationMode;
protected DecoderCounters decoderCounters;
@ -470,6 +441,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
codecOperatingRate = CODEC_OPERATING_RATE_UNSET;
rendererOperatingRate = 1f;
renderTimeLimitMs = C.TIME_UNSET;
mediaCodecOperationMode = MediaCodecOperationMode.SYNCHRONOUS;
}
/**
@ -503,16 +475,25 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
/**
* Use the underlying {@link MediaCodec} in asynchronous mode to obtain available input and output
* buffers.
* Set the mode of operation of the underlying {@link MediaCodec}.
*
* <p>This method is experimental, and will be renamed or removed in a future release. It should
* only be called before the renderer is used.
*
* @param enabled enable of disable the feature.
* @param mode the mode of the MediaCodec. The supported modes are:
* <ul>
* <li>{@link MediaCodecOperationMode#SYNCHRONOUS}: The {@link MediaCodec} will operate in
* synchronous mode.
* <li>{@link MediaCodecOperationMode#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 &ge; 21; if the API level
* is &le; 20, the operation mode will be set to {@link
* MediaCodecOperationMode#SYNCHRONOUS}.
* </ul>
* By default, the operation mode is set to {@link MediaCodecOperationMode#SYNCHRONOUS}.
*/
public void experimental_setUseMediaCodecInAsyncMode(boolean enabled) {
useMediaCodecInAsyncMode = enabled;
public void experimental_setMediaCodecOperationMode(@MediaCodecOperationMode int mode) {
mediaCodecOperationMode = mode;
}
@Override
@ -709,6 +690,10 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
availableCodecInfos = null;
codecInfo = null;
codecFormat = null;
if (codecAdapter != null) {
codecAdapter.shutdown();
codecAdapter = null;
}
resetInputBuffer();
resetOutputBuffer();
resetCodecBuffers();
@ -730,10 +715,6 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
} finally {
codec = null;
if (codecAdapter != null) {
codecAdapter.shutdown();
codecAdapter = null;
}
try {
if (mediaCrypto != null) {
mediaCrypto.release();
@ -982,7 +963,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
codecInitializingTimestamp = SystemClock.elapsedRealtime();
TraceUtil.beginSection("createCodec:" + codecName);
codec = MediaCodec.createByCodecName(codecName);
if (useMediaCodecInAsyncMode && Util.SDK_INT >= 21) {
if (mediaCodecOperationMode == MediaCodecOperationMode.ASYNCHRONOUS_PLAYBACK_THREAD
&& Util.SDK_INT >= 21) {
codecAdapter = new AsynchronousMediaCodecAdapter(codec);
} else {
codecAdapter = new SynchronousMediaCodecAdapter(codec, getDequeueOutputBufferTimeoutUs());
@ -2021,124 +2003,4 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
return Util.SDK_INT <= 18 && format.channelCount == 1
&& "OMX.MTK.AUDIO.DECODER.MP3".equals(name);
}
@RequiresApi(21)
private static class AsynchronousMediaCodecAdapter implements MediaCodecAdapter {
private MediaCodecAsyncCallback mediaCodecAsyncCallback;
private final Handler handler;
private final MediaCodec codec;
@Nullable private IllegalStateException internalException;
private boolean flushing;
public AsynchronousMediaCodecAdapter(MediaCodec codec) {
mediaCodecAsyncCallback = new MediaCodecAsyncCallback();
handler = new Handler(Looper.myLooper());
this.codec = codec;
this.codec.setCallback(mediaCodecAsyncCallback);
}
@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();
}
private void onCompleteFlush() {
flushing = false;
mediaCodecAsyncCallback.flush();
try {
codec.start();
} catch (IllegalStateException e) {
// Catch IllegalStateException directly so that we don't have to wrap it
internalException = e;
} catch (Exception e) {
internalException = new IllegalStateException(e);
}
}
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;
}
}
private static class SynchronousMediaCodecAdapter implements MediaCodecAdapter {
private final MediaCodec codec;
private final long dequeueOutputBufferTimeoutMs;
public SynchronousMediaCodecAdapter(MediaCodec mediaCodec, long dequeueOutputBufferTimeoutMs) {
this.codec = mediaCodec;
this.dequeueOutputBufferTimeoutMs = dequeueOutputBufferTimeoutMs;
}
@Override
public int dequeueInputBufferIndex() {
return codec.dequeueInputBuffer(0);
}
@Override
public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) {
return codec.dequeueOutputBuffer(bufferInfo, dequeueOutputBufferTimeoutMs);
}
@Override
public MediaFormat getOutputFormat() {
return codec.getOutputFormat();
}
@Override
public void flush() {
codec.flush();
}
@Override
public void shutdown() {}
}
}

View File

@ -0,0 +1,56 @@
/*
* 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;
/**
* A {@link MediaCodecAdapter} that operates the underlying {@link MediaCodec} in synchronous mode.
*/
/* package */ final class SynchronousMediaCodecAdapter implements MediaCodecAdapter {
private final MediaCodec codec;
private final long dequeueOutputBufferTimeoutMs;
public SynchronousMediaCodecAdapter(MediaCodec mediaCodec, long dequeueOutputBufferTimeoutMs) {
this.codec = mediaCodec;
this.dequeueOutputBufferTimeoutMs = dequeueOutputBufferTimeoutMs;
}
@Override
public int dequeueInputBufferIndex() {
return codec.dequeueInputBuffer(0);
}
@Override
public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) {
return codec.dequeueOutputBuffer(bufferInfo, dequeueOutputBufferTimeoutMs);
}
@Override
public MediaFormat getOutputFormat() {
return codec.getOutputFormat();
}
@Override
public void flush() {
codec.flush();
}
@Override
public void shutdown() {}
}

View File

@ -0,0 +1,235 @@
/*
* 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 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.AtomicBoolean;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit tests for {@link AsynchronousMediaCodecAdapter}. */
@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("TestHandlerThread");
handlerThread.start();
looper = handlerThread.getLooper();
codec = MediaCodec.createByCodecName("h264");
adapter = new AsynchronousMediaCodecAdapter(codec, looper);
bufferInfo = new MediaCodec.BufferInfo();
}
@After
public void tearDown() {
handlerThread.quit();
}
@Test
public void dequeueInputBufferIndex_withoutInputBuffer_returnsTryAgainLater() {
assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
}
@Test
public void dequeueInputBufferIndex_withInputBuffer_returnsInputBuffer() {
adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 0);
assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(0);
}
@Test
public void dequeueInputBufferIndex_whileFlushing_returnsTryAgainLater() {
adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 0);
adapter.flush();
adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 1);
assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
}
@Test
public void dequeueInputBufferIndex_afterFlushCompletes_returnsNextInputBuffer()
throws InterruptedException {
// Disable calling codec.start() after flush() completes to avoid receiving buffers from the
// shadow codec impl
adapter.setOnCodecStart(() -> {});
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));
assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue();
assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(1);
}
@Test
public void dequeueInputBufferIndex_afterFlushCompletesWithError_throwsException()
throws InterruptedException {
adapter.setOnCodecStart(
() -> {
throw new IllegalStateException("codec#start() exception");
});
adapter.flush();
assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue();
try {
adapter.dequeueInputBufferIndex();
fail();
} catch (IllegalStateException expected) {
}
}
@Test
public void dequeueOutputBufferIndex_withoutOutputBuffer_returnsTryAgainLater() {
assertThat(adapter.dequeueOutputBufferIndex(bufferInfo))
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
}
@Test
public void dequeueOutputBufferIndex_withOutputBuffer_returnsOutputBuffer() {
MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo();
outBufferInfo.presentationTimeUs = 10;
adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 0, outBufferInfo);
assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)).isEqualTo(0);
assertThat(areEqual(bufferInfo, outBufferInfo)).isTrue();
}
@Test
public void dequeueOutputBufferIndex_whileFlushing_returnsTryAgainLater() {
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()
throws InterruptedException {
// Disable calling codec.start() after flush() completes to avoid receiving buffers from the
// shadow codec impl
adapter.setOnCodecStart(() -> {});
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));
assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue();
assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)).isEqualTo(1);
assertThat(areEqual(bufferInfo, info1)).isTrue();
}
@Test
public void dequeueOutputBufferIndex_afterFlushCompletesWithError_throwsException()
throws InterruptedException {
adapter.setOnCodecStart(
() -> {
throw new RuntimeException("codec#start() exception");
});
adapter.flush();
assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue();
try {
adapter.dequeueOutputBufferIndex(bufferInfo);
fail();
} catch (IllegalStateException expected) {
}
}
@Test
public void getOutputFormat_withoutFormat_throwsException() {
try {
adapter.getOutputFormat();
fail();
} catch (IllegalStateException expected) {
}
}
@Test
public void getOutputFormat_withMultipleFormats_returnsFormatsInCorrectOrder() {
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() throws InterruptedException {
MediaFormat format = new MediaFormat();
adapter.getMediaCodecCallback().onOutputFormatChanged(codec, format);
adapter.dequeueOutputBufferIndex(bufferInfo);
adapter.flush();
assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue();
assertThat(adapter.getOutputFormat()).isEqualTo(format);
}
@Test
public void shutdown_withPendingFlush_cancelsFlush() throws InterruptedException {
AtomicBoolean onCodecStartCalled = new AtomicBoolean(false);
Runnable onCodecStart = () -> onCodecStartCalled.set(true);
adapter.setOnCodecStart(onCodecStart);
adapter.flush();
adapter.shutdown();
assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue();
assertThat(onCodecStartCalled.get()).isFalse();
}
}

View File

@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.mediacodec;
import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.areEqual;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
@ -204,17 +205,4 @@ public class MediaCodecAsyncCallbackTest {
mediaCodecAsyncCallback.maybeThrowMediaCodecException();
}
/**
* Compares if two {@link android.media.MediaCodec.BufferInfo} are equal by inspecting {@link
* android.media.MediaCodec.BufferInfo#flags}, {@link android.media.MediaCodec.BufferInfo#size},
* {@link android.media.MediaCodec.BufferInfo#presentationTimeUs} and {@link
* android.media.MediaCodec.BufferInfo#offset}.
*/
private static boolean areEqual(MediaCodec.BufferInfo lhs, MediaCodec.BufferInfo rhs) {
return lhs.flags == rhs.flags
&& lhs.offset == rhs.offset
&& lhs.presentationTimeUs == rhs.presentationTimeUs
&& lhs.size == rhs.size;
}
}

View File

@ -0,0 +1,59 @@
/*
* 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 org.robolectric.Shadows.shadowOf;
import android.media.MediaCodec;
import android.os.Handler;
import android.os.Looper;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/** Testing utilities for MediaCodec related test classes */
public class MediaCodecTestUtils {
/**
* Compares if two {@link android.media.MediaCodec.BufferInfo} are equal by inspecting {@link
* android.media.MediaCodec.BufferInfo#flags}, {@link android.media.MediaCodec.BufferInfo#size},
* {@link android.media.MediaCodec.BufferInfo#presentationTimeUs} and {@link
* android.media.MediaCodec.BufferInfo#offset}.
*/
public static boolean areEqual(MediaCodec.BufferInfo lhs, MediaCodec.BufferInfo rhs) {
return lhs.flags == rhs.flags
&& lhs.offset == rhs.offset
&& lhs.presentationTimeUs == rhs.presentationTimeUs
&& lhs.size == rhs.size;
}
/**
* Blocks until all events of a shadow looper are executed or the specified time elapses.
*
* @param looper the shadow looper
* @param time the time to wait
* @param unit the time units
* @return true if all events are executed, false if the time elapsed.
* @throws InterruptedException if the Thread was interrupted while waiting.
*/
public static boolean waitUntilAllEventsAreExecuted(Looper looper, long time, TimeUnit unit)
throws InterruptedException {
Handler handler = new Handler(looper);
CountDownLatch latch = new CountDownLatch(1);
handler.post(() -> latch.countDown());
shadowOf(looper).idle();
return latch.await(time, unit);
}
}