mirror of
https://github.com/androidx/media.git
synced 2025-05-04 22:20:47 +08:00
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:
parent
2462aeb443
commit
3156fbfc6e
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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 ≥ 21; if the API level
|
||||
* is ≤ 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() {}
|
||||
}
|
||||
}
|
||||
|
@ -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() {}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user