mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Enable MediaCodec asynchronous mode
Enable using MediaCodec in async mode. Expose experimental API to enable/disable the feature. PiperOrigin-RevId: 283309798
This commit is contained in:
parent
b68d19bceb
commit
aceba835cc
@ -0,0 +1,150 @@
|
||||
/*
|
||||
* 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 androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import com.google.android.exoplayer2.util.IntArrayQueue;
|
||||
import java.util.ArrayDeque;
|
||||
|
||||
/** Handles the asynchronous callbacks from {@link android.media.MediaCodec.Callback}. */
|
||||
@RequiresApi(21)
|
||||
/* package */ final class MediaCodecAsyncCallback extends MediaCodec.Callback {
|
||||
private final IntArrayQueue availableInputBuffers;
|
||||
private final IntArrayQueue availableOutputBuffers;
|
||||
private final ArrayDeque<MediaCodec.BufferInfo> bufferInfos;
|
||||
private final ArrayDeque<MediaFormat> formats;
|
||||
@Nullable private MediaFormat currentFormat;
|
||||
@Nullable private IllegalStateException mediaCodecException;
|
||||
|
||||
/** Creates a new MediaCodecAsyncCallback. */
|
||||
public MediaCodecAsyncCallback() {
|
||||
availableInputBuffers = new IntArrayQueue();
|
||||
availableOutputBuffers = new IntArrayQueue();
|
||||
bufferInfos = new ArrayDeque<>();
|
||||
formats = new ArrayDeque<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next available input buffer index or {@link MediaCodec#INFO_TRY_AGAIN_LATER} if no
|
||||
* such buffer exists.
|
||||
*/
|
||||
public int dequeueInputBufferIndex() {
|
||||
return availableInputBuffers.isEmpty()
|
||||
? MediaCodec.INFO_TRY_AGAIN_LATER
|
||||
: availableInputBuffers.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next available output buffer index. 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}.
|
||||
*/
|
||||
public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) {
|
||||
if (availableOutputBuffers.isEmpty()) {
|
||||
return MediaCodec.INFO_TRY_AGAIN_LATER;
|
||||
} else {
|
||||
int bufferIndex = availableOutputBuffers.remove();
|
||||
if (bufferIndex >= 0) {
|
||||
MediaCodec.BufferInfo nextBufferInfo = bufferInfos.remove();
|
||||
bufferInfo.set(
|
||||
nextBufferInfo.offset,
|
||||
nextBufferInfo.size,
|
||||
nextBufferInfo.presentationTimeUs,
|
||||
nextBufferInfo.flags);
|
||||
} else if (bufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
|
||||
currentFormat = formats.remove();
|
||||
}
|
||||
return bufferIndex;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link MediaFormat} signalled by the underlying {@link MediaCodec}.
|
||||
*
|
||||
* <p>Call this <b>after</b> {@link #dequeueOutputBufferIndex} returned {@link
|
||||
* MediaCodec#INFO_OUTPUT_FORMAT_CHANGED}.
|
||||
*
|
||||
* @throws {@link IllegalStateException} if you call this method before before {
|
||||
* @link #dequeueOutputBufferIndex} returned {@link MediaCodec#INFO_OUTPUT_FORMAT_CHANGED}.
|
||||
*/
|
||||
public MediaFormat getOutputFormat() throws IllegalStateException {
|
||||
if (currentFormat == null) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
return currentFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks and throws an {@link IllegalStateException} if an error was previously set on this
|
||||
* instance via {@link #onError}.
|
||||
*/
|
||||
public void maybeThrowMediaCodecException() throws IllegalStateException {
|
||||
IllegalStateException exception = mediaCodecException;
|
||||
mediaCodecException = null;
|
||||
|
||||
if (exception != null) {
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes the MediaCodecAsyncCallback. This method removes all available input and output buffers
|
||||
* and any error that was previously set.
|
||||
*/
|
||||
public void flush() {
|
||||
availableInputBuffers.clear();
|
||||
availableOutputBuffers.clear();
|
||||
bufferInfos.clear();
|
||||
formats.clear();
|
||||
mediaCodecException = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInputBufferAvailable(@NonNull MediaCodec mediaCodec, int i) {
|
||||
availableInputBuffers.add(i);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOutputBufferAvailable(
|
||||
@NonNull MediaCodec mediaCodec, int i, @NonNull MediaCodec.BufferInfo bufferInfo) {
|
||||
availableOutputBuffers.add(i);
|
||||
bufferInfos.add(bufferInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(@NonNull MediaCodec mediaCodec, @NonNull MediaCodec.CodecException e) {
|
||||
onMediaCodecError(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOutputFormatChanged(
|
||||
@NonNull MediaCodec mediaCodec, @NonNull MediaFormat mediaFormat) {
|
||||
availableOutputBuffers.add(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
|
||||
formats.add(mediaFormat);
|
||||
}
|
||||
|
||||
@VisibleForTesting()
|
||||
void onMediaCodecError(IllegalStateException e) {
|
||||
mediaCodecException = e;
|
||||
}
|
||||
}
|
@ -23,10 +23,13 @@ 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;
|
||||
@ -289,6 +292,51 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
ADAPTATION_WORKAROUND_MODE_ALWAYS
|
||||
})
|
||||
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.
|
||||
*/
|
||||
@ -336,6 +384,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
private long renderTimeLimitMs;
|
||||
private float rendererOperatingRate;
|
||||
@Nullable private MediaCodec codec;
|
||||
@Nullable private MediaCodecAdapter codecAdapter;
|
||||
@Nullable private Format codecFormat;
|
||||
private float codecOperatingRate;
|
||||
@Nullable private ArrayDeque<MediaCodecInfo> availableCodecInfos;
|
||||
@ -375,6 +424,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
private boolean skipMediaCodecStopOnRelease;
|
||||
private boolean pendingOutputEndOfStream;
|
||||
|
||||
private boolean useMediaCodecInAsyncMode;
|
||||
|
||||
protected DecoderCounters decoderCounters;
|
||||
|
||||
/**
|
||||
@ -451,6 +502,19 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
skipMediaCodecStopOnRelease = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the underlying {@link MediaCodec} in asynchronous mode to obtain available input and output
|
||||
* buffers.
|
||||
*
|
||||
* <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.
|
||||
*/
|
||||
public void experimental_setUseMediaCodecInAsyncMode(boolean enabled) {
|
||||
useMediaCodecInAsyncMode = enabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int supportsMixedMimeTypeAdaptation() {
|
||||
return ADAPTIVE_NOT_SEAMLESS;
|
||||
@ -664,6 +728,10 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
}
|
||||
} finally {
|
||||
codec = null;
|
||||
if (codecAdapter != null) {
|
||||
codecAdapter.shutdown();
|
||||
codecAdapter = null;
|
||||
}
|
||||
try {
|
||||
if (mediaCrypto != null) {
|
||||
mediaCrypto.release();
|
||||
@ -763,7 +831,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
return true;
|
||||
}
|
||||
|
||||
codec.flush();
|
||||
codecAdapter.flush();
|
||||
resetInputBuffer();
|
||||
resetOutputBuffer();
|
||||
codecHotswapDeadlineMs = C.TIME_UNSET;
|
||||
@ -908,10 +976,18 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
if (codecOperatingRate <= assumedMinimumCodecOperatingRate) {
|
||||
codecOperatingRate = CODEC_OPERATING_RATE_UNSET;
|
||||
}
|
||||
|
||||
MediaCodecAdapter codecAdapter = null;
|
||||
try {
|
||||
codecInitializingTimestamp = SystemClock.elapsedRealtime();
|
||||
TraceUtil.beginSection("createCodec:" + codecName);
|
||||
codec = MediaCodec.createByCodecName(codecName);
|
||||
if (useMediaCodecInAsyncMode && Util.SDK_INT >= 21) {
|
||||
codecAdapter = new AsynchronousMediaCodecAdapter(codec);
|
||||
} else {
|
||||
codecAdapter = new SynchronousMediaCodecAdapter(codec, getDequeueOutputBufferTimeoutUs());
|
||||
}
|
||||
|
||||
TraceUtil.endSection();
|
||||
TraceUtil.beginSection("configureCodec");
|
||||
configureCodec(codecInfo, codec, inputFormat, crypto, codecOperatingRate);
|
||||
@ -922,6 +998,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
codecInitializedTimestamp = SystemClock.elapsedRealtime();
|
||||
getCodecBuffers(codec);
|
||||
} catch (Exception e) {
|
||||
if (codecAdapter != null) {
|
||||
codecAdapter.shutdown();
|
||||
}
|
||||
if (codec != null) {
|
||||
resetCodecBuffers();
|
||||
codec.release();
|
||||
@ -930,6 +1009,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
}
|
||||
|
||||
this.codec = codec;
|
||||
this.codecAdapter = codecAdapter;
|
||||
this.codecInfo = codecInfo;
|
||||
this.codecOperatingRate = codecOperatingRate;
|
||||
codecFormat = inputFormat;
|
||||
@ -1036,7 +1116,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
}
|
||||
|
||||
if (inputIndex < 0) {
|
||||
inputIndex = codec.dequeueInputBuffer(0);
|
||||
inputIndex = codecAdapter.dequeueInputBufferIndex();
|
||||
if (inputIndex < 0) {
|
||||
return false;
|
||||
}
|
||||
@ -1489,8 +1569,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
int outputIndex;
|
||||
if (codecNeedsEosOutputExceptionWorkaround && codecReceivedEos) {
|
||||
try {
|
||||
outputIndex =
|
||||
codec.dequeueOutputBuffer(outputBufferInfo, getDequeueOutputBufferTimeoutUs());
|
||||
outputIndex = codecAdapter.dequeueOutputBufferIndex(outputBufferInfo);
|
||||
} catch (IllegalStateException e) {
|
||||
processEndOfStream();
|
||||
if (outputStreamEnded) {
|
||||
@ -1500,8 +1579,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
outputIndex =
|
||||
codec.dequeueOutputBuffer(outputBufferInfo, getDequeueOutputBufferTimeoutUs());
|
||||
outputIndex = codecAdapter.dequeueOutputBufferIndex(outputBufferInfo);
|
||||
}
|
||||
|
||||
if (outputIndex < 0) {
|
||||
@ -1599,7 +1677,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
|
||||
/** Processes a new output {@link MediaFormat}. */
|
||||
private void processOutputFormat() throws ExoPlaybackException {
|
||||
MediaFormat mediaFormat = codec.getOutputFormat();
|
||||
MediaFormat mediaFormat = codecAdapter.getOutputFormat();
|
||||
if (codecAdaptationWorkaroundMode != ADAPTATION_WORKAROUND_MODE_NEVER
|
||||
&& mediaFormat.getInteger(MediaFormat.KEY_WIDTH) == ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT
|
||||
&& mediaFormat.getInteger(MediaFormat.KEY_HEIGHT)
|
||||
@ -1944,4 +2022,123 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
&& "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,112 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
/**
|
||||
* Array-based unbounded queue for int primitives with amortized O(1) add and remove.
|
||||
*
|
||||
* <p>Use this class instead of a {@link java.util.Deque} to avoid boxing int primitives to {@link
|
||||
* Integer} instances.
|
||||
*/
|
||||
public final class IntArrayQueue {
|
||||
|
||||
/** Default capacity needs to be a power of 2. */
|
||||
private static int DEFAULT_INITIAL_CAPACITY = 16;
|
||||
|
||||
private int headIndex;
|
||||
private int tailIndex;
|
||||
private int size;
|
||||
private int[] data;
|
||||
private int wrapAroundMask;
|
||||
|
||||
public IntArrayQueue() {
|
||||
headIndex = 0;
|
||||
tailIndex = -1;
|
||||
size = 0;
|
||||
data = new int[DEFAULT_INITIAL_CAPACITY];
|
||||
wrapAroundMask = data.length - 1;
|
||||
}
|
||||
|
||||
/** Add a new item to the queue. */
|
||||
public void add(int value) {
|
||||
if (size == data.length) {
|
||||
doubleArraySize();
|
||||
}
|
||||
|
||||
tailIndex = (tailIndex + 1) & wrapAroundMask;
|
||||
data[tailIndex] = value;
|
||||
size++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an item from the queue.
|
||||
*
|
||||
* @throws {@link NoSuchElementException} if the queue is empty.
|
||||
*/
|
||||
public int remove() {
|
||||
if (size == 0) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
|
||||
int value = data[headIndex];
|
||||
headIndex = (headIndex + 1) & wrapAroundMask;
|
||||
size--;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Returns the number of items in the queue. */
|
||||
public int size() {
|
||||
return size;
|
||||
}
|
||||
|
||||
/** Returns whether the queue is empty. */
|
||||
public boolean isEmpty() {
|
||||
return size == 0;
|
||||
}
|
||||
|
||||
/** Clears the queue. */
|
||||
public void clear() {
|
||||
headIndex = 0;
|
||||
tailIndex = -1;
|
||||
size = 0;
|
||||
}
|
||||
|
||||
/** Returns the length of the backing array. */
|
||||
public int capacity() {
|
||||
return data.length;
|
||||
}
|
||||
|
||||
private void doubleArraySize() {
|
||||
int newCapacity = data.length << 1;
|
||||
if (newCapacity < 0) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
int[] newData = new int[newCapacity];
|
||||
int itemsToRight = data.length - headIndex;
|
||||
int itemsToLeft = headIndex;
|
||||
System.arraycopy(data, headIndex, newData, 0, itemsToRight);
|
||||
System.arraycopy(data, 0, newData, itemsToRight, itemsToLeft);
|
||||
|
||||
headIndex = 0;
|
||||
tailIndex = size - 1;
|
||||
data = newData;
|
||||
wrapAroundMask = data.length - 1;
|
||||
}
|
||||
}
|
@ -0,0 +1,220 @@
|
||||
/*
|
||||
* 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.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaFormat;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import java.io.IOException;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Unit tests for {@link MediaCodecAsyncCallback}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class MediaCodecAsyncCallbackTest {
|
||||
|
||||
private MediaCodecAsyncCallback mediaCodecAsyncCallback;
|
||||
private MediaCodec codec;
|
||||
|
||||
@Before
|
||||
public void setUp() throws IOException {
|
||||
mediaCodecAsyncCallback = new MediaCodecAsyncCallback();
|
||||
codec = MediaCodec.createByCodecName("h264");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeInputBufferIndex_afterCreation_returnsTryAgain() {
|
||||
assertThat(mediaCodecAsyncCallback.dequeueInputBufferIndex())
|
||||
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeInputBufferIndex_returnsEnqueuedBuffers() {
|
||||
// Send two input buffers to the mediaCodecAsyncCallback.
|
||||
mediaCodecAsyncCallback.onInputBufferAvailable(codec, 0);
|
||||
mediaCodecAsyncCallback.onInputBufferAvailable(codec, 1);
|
||||
|
||||
assertThat(mediaCodecAsyncCallback.dequeueInputBufferIndex()).isEqualTo(0);
|
||||
assertThat(mediaCodecAsyncCallback.dequeueInputBufferIndex()).isEqualTo(1);
|
||||
assertThat(mediaCodecAsyncCallback.dequeueInputBufferIndex())
|
||||
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeInputBufferIndex_afterFlush_returnsTryAgain() {
|
||||
// Send two input buffers to the mediaCodecAsyncCallback and then flush().
|
||||
mediaCodecAsyncCallback.onInputBufferAvailable(codec, 0);
|
||||
mediaCodecAsyncCallback.onInputBufferAvailable(codec, 1);
|
||||
mediaCodecAsyncCallback.flush();
|
||||
|
||||
assertThat(mediaCodecAsyncCallback.dequeueInputBufferIndex())
|
||||
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeInputBufferIndex_afterFlushAndNewInputBuffer_returnsEnqueuedBuffer() {
|
||||
// Send two input buffers to the mediaCodecAsyncCallback, then flush(), then send
|
||||
// another input buffer.
|
||||
mediaCodecAsyncCallback.onInputBufferAvailable(codec, 0);
|
||||
mediaCodecAsyncCallback.onInputBufferAvailable(codec, 1);
|
||||
mediaCodecAsyncCallback.flush();
|
||||
mediaCodecAsyncCallback.onInputBufferAvailable(codec, 2);
|
||||
|
||||
assertThat(mediaCodecAsyncCallback.dequeueInputBufferIndex()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeOutputBufferIndex_afterCreation_returnsTryAgain() {
|
||||
MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo();
|
||||
assertThat(mediaCodecAsyncCallback.dequeueOutputBufferIndex(outBufferInfo))
|
||||
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeOutputBufferIndex_returnsEnqueuedBuffers() {
|
||||
// Send two output buffers to the mediaCodecAsyncCallback.
|
||||
MediaCodec.BufferInfo bufferInfo1 = new MediaCodec.BufferInfo();
|
||||
mediaCodecAsyncCallback.onOutputBufferAvailable(codec, 0, bufferInfo1);
|
||||
|
||||
MediaCodec.BufferInfo bufferInfo2 = new MediaCodec.BufferInfo();
|
||||
bufferInfo2.set(1, 1, 1, 1);
|
||||
mediaCodecAsyncCallback.onOutputBufferAvailable(codec, 1, bufferInfo2);
|
||||
|
||||
MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo();
|
||||
|
||||
assertThat(mediaCodecAsyncCallback.dequeueOutputBufferIndex(outBufferInfo)).isEqualTo(0);
|
||||
assertThat(areEqual(outBufferInfo, bufferInfo1)).isTrue();
|
||||
assertThat(mediaCodecAsyncCallback.dequeueOutputBufferIndex(outBufferInfo)).isEqualTo(1);
|
||||
assertThat(areEqual(outBufferInfo, bufferInfo2)).isTrue();
|
||||
assertThat(mediaCodecAsyncCallback.dequeueOutputBufferIndex(outBufferInfo))
|
||||
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeOutputBufferIndex_afterFlush_returnsTryAgain() {
|
||||
// Send two output buffers to the mediaCodecAsyncCallback and then flush().
|
||||
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
||||
mediaCodecAsyncCallback.onOutputBufferAvailable(codec, 0, bufferInfo);
|
||||
mediaCodecAsyncCallback.onOutputBufferAvailable(codec, 1, bufferInfo);
|
||||
mediaCodecAsyncCallback.flush();
|
||||
MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo();
|
||||
|
||||
assertThat(mediaCodecAsyncCallback.dequeueOutputBufferIndex(outBufferInfo))
|
||||
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeOutputBufferIndex_afterFlushAndNewOutputBuffers_returnsEnqueueBuffer() {
|
||||
// Send two output buffers to the mediaCodecAsyncCallback, then flush(), then send
|
||||
// another output buffer.
|
||||
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
||||
mediaCodecAsyncCallback.onOutputBufferAvailable(codec, 0, bufferInfo);
|
||||
mediaCodecAsyncCallback.onOutputBufferAvailable(codec, 1, bufferInfo);
|
||||
mediaCodecAsyncCallback.flush();
|
||||
mediaCodecAsyncCallback.onOutputBufferAvailable(codec, 2, bufferInfo);
|
||||
MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo();
|
||||
|
||||
assertThat(mediaCodecAsyncCallback.dequeueOutputBufferIndex(outBufferInfo)).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getOutputFormat_onNewInstance_raisesException() {
|
||||
try {
|
||||
mediaCodecAsyncCallback.getOutputFormat();
|
||||
fail();
|
||||
} catch (IllegalStateException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getOutputFormat_afterOnOutputFormatCalled_returnsFormat() {
|
||||
MediaFormat format = new MediaFormat();
|
||||
mediaCodecAsyncCallback.onOutputFormatChanged(codec, format);
|
||||
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
||||
|
||||
assertThat(mediaCodecAsyncCallback.dequeueOutputBufferIndex(bufferInfo))
|
||||
.isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
|
||||
assertThat(mediaCodecAsyncCallback.getOutputFormat()).isEqualTo(format);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getOutputFormat_afterFlush_raisesCurrentFormat() {
|
||||
MediaFormat format = new MediaFormat();
|
||||
mediaCodecAsyncCallback.onOutputFormatChanged(codec, format);
|
||||
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
||||
mediaCodecAsyncCallback.dequeueOutputBufferIndex(bufferInfo);
|
||||
mediaCodecAsyncCallback.flush();
|
||||
|
||||
assertThat(mediaCodecAsyncCallback.getOutputFormat()).isEqualTo(format);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void maybeThrowExoPlaybackException_withoutErrorFromCodec_doesNotThrow() {
|
||||
mediaCodecAsyncCallback.maybeThrowMediaCodecException();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void maybeThrowExoPlaybackException_withErrorFromCodec_Throws() {
|
||||
IllegalStateException exception = new IllegalStateException();
|
||||
mediaCodecAsyncCallback.onMediaCodecError(exception);
|
||||
|
||||
try {
|
||||
mediaCodecAsyncCallback.maybeThrowMediaCodecException();
|
||||
fail();
|
||||
} catch (IllegalStateException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void maybeThrowExoPlaybackException_doesNotThrowTwice() {
|
||||
IllegalStateException exception = new IllegalStateException();
|
||||
mediaCodecAsyncCallback.onMediaCodecError(exception);
|
||||
|
||||
try {
|
||||
mediaCodecAsyncCallback.maybeThrowMediaCodecException();
|
||||
fail();
|
||||
} catch (IllegalStateException expected) {
|
||||
}
|
||||
|
||||
mediaCodecAsyncCallback.maybeThrowMediaCodecException();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void maybeThrowExoPlaybackException_afterFlush_doesNotThrow() {
|
||||
IllegalStateException exception = new IllegalStateException();
|
||||
mediaCodecAsyncCallback.onMediaCodecError(exception);
|
||||
mediaCodecAsyncCallback.flush();
|
||||
|
||||
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,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.util;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import java.util.NoSuchElementException;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Tests for {@link IntArrayQueue}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class IntArrayQueueTest {
|
||||
|
||||
@Test
|
||||
public void add_willDoubleCapacity() {
|
||||
IntArrayQueue queue = new IntArrayQueue();
|
||||
int capacity = queue.capacity();
|
||||
|
||||
for (int i = 0; i <= capacity; i++) {
|
||||
queue.add(i);
|
||||
}
|
||||
|
||||
assertThat(queue.capacity()).isEqualTo(2 * capacity);
|
||||
assertThat(queue.size()).isEqualTo(capacity + 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isEmpty_returnsTrueAfterConstruction() {
|
||||
IntArrayQueue queue = new IntArrayQueue();
|
||||
|
||||
assertThat(queue.isEmpty()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isEmpty_returnsFalseAfterAddition() {
|
||||
IntArrayQueue queue = new IntArrayQueue();
|
||||
queue.add(0);
|
||||
|
||||
assertThat(queue.isEmpty()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isEmpty_returnsFalseAfterRemoval() {
|
||||
IntArrayQueue queue = new IntArrayQueue();
|
||||
queue.add(0);
|
||||
queue.remove();
|
||||
|
||||
assertThat(queue.isEmpty()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void remove_onEmptyQueue_throwsException() {
|
||||
IntArrayQueue queue = new IntArrayQueue();
|
||||
|
||||
try {
|
||||
queue.remove();
|
||||
fail();
|
||||
} catch (NoSuchElementException expected) {
|
||||
// expected
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void remove_returnsCorrectItem() {
|
||||
IntArrayQueue queue = new IntArrayQueue();
|
||||
int value = 20;
|
||||
queue.add(value);
|
||||
|
||||
assertThat(queue.remove()).isEqualTo(value);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void remove_untilIsEmpty() {
|
||||
IntArrayQueue queue = new IntArrayQueue();
|
||||
for (int i = 0; i < 1024; i++) {
|
||||
queue.add(i);
|
||||
}
|
||||
|
||||
int expectedRemoved = 0;
|
||||
while (!queue.isEmpty()) {
|
||||
if (expectedRemoved == 15) {
|
||||
System.out.println("foo");
|
||||
}
|
||||
int removed = queue.remove();
|
||||
assertThat(removed).isEqualTo(expectedRemoved++);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void remove_withResize_returnsCorrectItem() {
|
||||
IntArrayQueue queue = new IntArrayQueue();
|
||||
int nextToAdd = 0;
|
||||
|
||||
while (queue.size() < queue.capacity()) {
|
||||
queue.add(nextToAdd++);
|
||||
}
|
||||
|
||||
queue.remove();
|
||||
queue.remove();
|
||||
|
||||
// This will force the queue to wrap-around and then resize
|
||||
int howManyToResize = queue.capacity() - queue.size() + 1;
|
||||
for (int i = 0; i < howManyToResize; i++) {
|
||||
queue.add(nextToAdd++);
|
||||
}
|
||||
|
||||
assertThat(queue.remove()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clear_resetsQueue() {
|
||||
IntArrayQueue queue = new IntArrayQueue();
|
||||
|
||||
// Add items until array re-sizes twice (capacity grows by 4)
|
||||
for (int i = 0; i < 1024; i++) {
|
||||
queue.add(i);
|
||||
}
|
||||
|
||||
queue.clear();
|
||||
|
||||
assertThat(queue.size()).isEqualTo(0);
|
||||
assertThat(queue.isEmpty()).isTrue();
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user