mirror of
https://github.com/samsonjs/media.git
synced 2026-04-08 11:45:51 +00:00
Refactor AsynchronousMediaCoderAdapter
Refactor the AsynchronousMediaCoderAdapter and move the callback thread out of the adapter so that implementation of async callback and and async queueing are consistent design-wise. PiperOrigin-RevId: 338637837
This commit is contained in:
parent
4783c329cc
commit
485949b56c
8 changed files with 794 additions and 647 deletions
|
|
@ -19,32 +19,25 @@ package com.google.android.exoplayer2.mediacodec;
|
|||
import android.media.MediaCodec;
|
||||
import android.media.MediaCrypto;
|
||||
import android.media.MediaFormat;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.view.Surface;
|
||||
import androidx.annotation.GuardedBy;
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.decoder.CryptoInfo;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
/**
|
||||
* A {@link MediaCodecAdapter} that operates the underlying {@link MediaCodec} in asynchronous mode
|
||||
* and routes {@link MediaCodec.Callback} callbacks on a dedicated thread that is managed
|
||||
* internally.
|
||||
*
|
||||
* <p>This adapter supports queueing input buffers asynchronously.
|
||||
* A {@link MediaCodecAdapter} that operates the underlying {@link MediaCodec} in asynchronous mode,
|
||||
* routes {@link MediaCodec.Callback} callbacks on a dedicated thread that is managed internally,
|
||||
* and queues input buffers asynchronously.
|
||||
*/
|
||||
@RequiresApi(23)
|
||||
/* package */ final class AsynchronousMediaCodecAdapter extends MediaCodec.Callback
|
||||
implements MediaCodecAdapter {
|
||||
/* package */ final class AsynchronousMediaCodecAdapter implements MediaCodecAdapter {
|
||||
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
|
|
@ -56,24 +49,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
private static final int STATE_STARTED = 2;
|
||||
private static final int STATE_SHUT_DOWN = 3;
|
||||
|
||||
private final Object lock;
|
||||
|
||||
@GuardedBy("lock")
|
||||
private final MediaCodecAsyncCallback mediaCodecAsyncCallback;
|
||||
|
||||
private final MediaCodec codec;
|
||||
private final HandlerThread handlerThread;
|
||||
private @MonotonicNonNull Handler handler;
|
||||
|
||||
@GuardedBy("lock")
|
||||
private long pendingFlushCount;
|
||||
|
||||
private @State int state;
|
||||
private final AsynchronousMediaCodecCallback asynchronousMediaCodecCallback;
|
||||
private final AsynchronousMediaCodecBufferEnqueuer bufferEnqueuer;
|
||||
|
||||
@GuardedBy("lock")
|
||||
@Nullable
|
||||
private IllegalStateException internalException;
|
||||
@State private int state;
|
||||
|
||||
/**
|
||||
* Creates an instance that wraps the specified {@link MediaCodec}.
|
||||
|
|
@ -85,21 +64,15 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
/* package */ AsynchronousMediaCodecAdapter(MediaCodec codec, int trackType) {
|
||||
this(
|
||||
codec,
|
||||
trackType,
|
||||
new HandlerThread(createCallbackThreadLabel(trackType)),
|
||||
new HandlerThread(createQueueingThreadLabel(trackType)));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
/* package */ AsynchronousMediaCodecAdapter(
|
||||
MediaCodec codec,
|
||||
int trackType,
|
||||
HandlerThread callbackThread,
|
||||
HandlerThread enqueueingThread) {
|
||||
this.lock = new Object();
|
||||
this.mediaCodecAsyncCallback = new MediaCodecAsyncCallback();
|
||||
MediaCodec codec, HandlerThread callbackThread, HandlerThread enqueueingThread) {
|
||||
this.codec = codec;
|
||||
this.handlerThread = callbackThread;
|
||||
this.asynchronousMediaCodecCallback = new AsynchronousMediaCodecCallback(callbackThread);
|
||||
this.bufferEnqueuer = new AsynchronousMediaCodecBufferEnqueuer(codec, enqueueingThread);
|
||||
this.state = STATE_CREATED;
|
||||
}
|
||||
|
|
@ -110,9 +83,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
@Nullable Surface surface,
|
||||
@Nullable MediaCrypto crypto,
|
||||
int flags) {
|
||||
handlerThread.start();
|
||||
handler = new Handler(handlerThread.getLooper());
|
||||
codec.setCallback(this, handler);
|
||||
asynchronousMediaCodecCallback.initialize(codec);
|
||||
codec.configure(mediaFormat, surface, crypto, flags);
|
||||
state = STATE_CONFIGURED;
|
||||
}
|
||||
|
|
@ -138,60 +109,40 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
|
||||
@Override
|
||||
public int dequeueInputBufferIndex() {
|
||||
synchronized (lock) {
|
||||
if (isFlushing()) {
|
||||
return MediaCodec.INFO_TRY_AGAIN_LATER;
|
||||
} else {
|
||||
maybeThrowException();
|
||||
return mediaCodecAsyncCallback.dequeueInputBufferIndex();
|
||||
}
|
||||
}
|
||||
return asynchronousMediaCodecCallback.dequeueInputBufferIndex();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) {
|
||||
synchronized (lock) {
|
||||
if (isFlushing()) {
|
||||
return MediaCodec.INFO_TRY_AGAIN_LATER;
|
||||
} else {
|
||||
maybeThrowException();
|
||||
return mediaCodecAsyncCallback.dequeueOutputBufferIndex(bufferInfo);
|
||||
}
|
||||
}
|
||||
return asynchronousMediaCodecCallback.dequeueOutputBufferIndex(bufferInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaFormat getOutputFormat() {
|
||||
synchronized (lock) {
|
||||
return mediaCodecAsyncCallback.getOutputFormat();
|
||||
}
|
||||
return asynchronousMediaCodecCallback.getOutputFormat();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() {
|
||||
synchronized (lock) {
|
||||
bufferEnqueuer.flush();
|
||||
codec.flush();
|
||||
++pendingFlushCount;
|
||||
Util.castNonNull(handler).post(this::onFlushCompleted);
|
||||
}
|
||||
// The order of calls is important:
|
||||
// First, flush the bufferEnqueuer to stop queueing input buffers.
|
||||
// Second, flush the codec to stop producing available input/output buffers.
|
||||
// Third, flush the callback after flushing the codec so that in-flight callbacks are discarded.
|
||||
bufferEnqueuer.flush();
|
||||
codec.flush();
|
||||
// When flushAsync() is completed, start the codec again.
|
||||
asynchronousMediaCodecCallback.flushAsync(/* onFlushCompleted= */ codec::start);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
synchronized (lock) {
|
||||
if (state == STATE_STARTED) {
|
||||
bufferEnqueuer.shutdown();
|
||||
}
|
||||
if (state == STATE_CONFIGURED || state == STATE_STARTED) {
|
||||
handlerThread.quit();
|
||||
mediaCodecAsyncCallback.flush();
|
||||
// Leave the adapter in a flushing state so that
|
||||
// it will not dequeue anything.
|
||||
++pendingFlushCount;
|
||||
asynchronousMediaCodecCallback.shutdown();
|
||||
}
|
||||
state = STATE_SHUT_DOWN;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -199,86 +150,14 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
return codec;
|
||||
}
|
||||
|
||||
// Called from the handler thread.
|
||||
|
||||
@Override
|
||||
public void onInputBufferAvailable(MediaCodec codec, int index) {
|
||||
synchronized (lock) {
|
||||
mediaCodecAsyncCallback.onInputBufferAvailable(codec, index);
|
||||
}
|
||||
@VisibleForTesting
|
||||
/* package */ void onError(MediaCodec.CodecException error) {
|
||||
asynchronousMediaCodecCallback.onError(codec, error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {
|
||||
synchronized (lock) {
|
||||
mediaCodecAsyncCallback.onOutputBufferAvailable(codec, index, info);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(MediaCodec codec, MediaCodec.CodecException e) {
|
||||
synchronized (lock) {
|
||||
mediaCodecAsyncCallback.onError(codec, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
|
||||
synchronized (lock) {
|
||||
mediaCodecAsyncCallback.onOutputFormatChanged(codec, format);
|
||||
}
|
||||
}
|
||||
|
||||
private void onFlushCompleted() {
|
||||
synchronized (lock) {
|
||||
onFlushCompletedSynchronized();
|
||||
}
|
||||
}
|
||||
|
||||
@GuardedBy("lock")
|
||||
private void onFlushCompletedSynchronized() {
|
||||
if (state == STATE_SHUT_DOWN) {
|
||||
return;
|
||||
}
|
||||
|
||||
--pendingFlushCount;
|
||||
if (pendingFlushCount > 0) {
|
||||
// Another flush() has been called.
|
||||
return;
|
||||
} else if (pendingFlushCount < 0) {
|
||||
// This should never happen.
|
||||
internalException = new IllegalStateException();
|
||||
return;
|
||||
}
|
||||
|
||||
mediaCodecAsyncCallback.flush();
|
||||
try {
|
||||
codec.start();
|
||||
} catch (IllegalStateException e) {
|
||||
internalException = e;
|
||||
} catch (Exception e) {
|
||||
internalException = new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@GuardedBy("lock")
|
||||
private boolean isFlushing() {
|
||||
return pendingFlushCount > 0;
|
||||
}
|
||||
|
||||
@GuardedBy("lock")
|
||||
private void maybeThrowException() {
|
||||
maybeThrowInternalException();
|
||||
mediaCodecAsyncCallback.maybeThrowMediaCodecException();
|
||||
}
|
||||
|
||||
@GuardedBy("lock")
|
||||
private void maybeThrowInternalException() {
|
||||
if (internalException != null) {
|
||||
IllegalStateException e = internalException;
|
||||
internalException = null;
|
||||
throw e;
|
||||
}
|
||||
@VisibleForTesting
|
||||
/* package */ void onOutputFormatChanged(MediaFormat format) {
|
||||
asynchronousMediaCodecCallback.onOutputFormatChanged(codec, format);
|
||||
}
|
||||
|
||||
private static String createCallbackThreadLabel(int trackType) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,325 @@
|
|||
/*
|
||||
* Copyright (C) 2020 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.util.Assertions.checkState;
|
||||
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaFormat;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import androidx.annotation.GuardedBy;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import com.google.android.exoplayer2.util.IntArrayQueue;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.util.ArrayDeque;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
/** A {@link MediaCodec.Callback} that routes callbacks on a separate thread. */
|
||||
@RequiresApi(23)
|
||||
/* package */ final class AsynchronousMediaCodecCallback extends MediaCodec.Callback {
|
||||
private final Object lock;
|
||||
|
||||
private final HandlerThread callbackThread;
|
||||
private @MonotonicNonNull Handler handler;
|
||||
|
||||
@GuardedBy("lock")
|
||||
private final IntArrayQueue availableInputBuffers;
|
||||
|
||||
@GuardedBy("lock")
|
||||
private final IntArrayQueue availableOutputBuffers;
|
||||
|
||||
@GuardedBy("lock")
|
||||
private final ArrayDeque<MediaCodec.BufferInfo> bufferInfos;
|
||||
|
||||
@GuardedBy("lock")
|
||||
private final ArrayDeque<MediaFormat> formats;
|
||||
|
||||
@GuardedBy("lock")
|
||||
@Nullable
|
||||
private MediaFormat currentFormat;
|
||||
|
||||
@GuardedBy("lock")
|
||||
@Nullable
|
||||
private MediaFormat pendingOutputFormat;
|
||||
|
||||
@GuardedBy("lock")
|
||||
@Nullable
|
||||
private MediaCodec.CodecException mediaCodecException;
|
||||
|
||||
@GuardedBy("lock")
|
||||
private long pendingFlushCount;
|
||||
|
||||
@GuardedBy("lock")
|
||||
private boolean shutDown;
|
||||
|
||||
@GuardedBy("lock")
|
||||
@Nullable
|
||||
private IllegalStateException internalException;
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
*
|
||||
* @param callbackThread The thread that will be used for routing the {@link MediaCodec}
|
||||
* callbacks. The thread must not be started.
|
||||
*/
|
||||
/* package */ AsynchronousMediaCodecCallback(HandlerThread callbackThread) {
|
||||
this.lock = new Object();
|
||||
this.callbackThread = callbackThread;
|
||||
this.availableInputBuffers = new IntArrayQueue();
|
||||
this.availableOutputBuffers = new IntArrayQueue();
|
||||
this.bufferInfos = new ArrayDeque<>();
|
||||
this.formats = new ArrayDeque<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the callback on {@code codec} and starts the background callback thread.
|
||||
*
|
||||
* <p>Make sure to call {@link #shutdown()} to stop the background thread and release its
|
||||
* resources.
|
||||
*
|
||||
* @see MediaCodec#setCallback(MediaCodec.Callback, Handler)
|
||||
*/
|
||||
public void initialize(MediaCodec codec) {
|
||||
checkState(handler == null);
|
||||
|
||||
callbackThread.start();
|
||||
Handler handler = new Handler(callbackThread.getLooper());
|
||||
codec.setCallback(this, handler);
|
||||
// Initialize this.handler at the very end ensuring the callback in not considered configured
|
||||
// if MediaCodec raises an exception.
|
||||
this.handler = handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuts down this instance.
|
||||
*
|
||||
* <p>This method will stop the callback thread. After calling it, callbacks will no longer be
|
||||
* handled and dequeue methods will return {@link MediaCodec#INFO_TRY_AGAIN_LATER}.
|
||||
*/
|
||||
public void shutdown() {
|
||||
synchronized (lock) {
|
||||
shutDown = true;
|
||||
callbackThread.quit();
|
||||
flushInternal();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next available input buffer index or {@link MediaCodec#INFO_TRY_AGAIN_LATER} if no
|
||||
* such buffer exists.
|
||||
*/
|
||||
public int dequeueInputBufferIndex() {
|
||||
synchronized (lock) {
|
||||
if (isFlushingOrShutdown()) {
|
||||
return MediaCodec.INFO_TRY_AGAIN_LATER;
|
||||
} else {
|
||||
maybeThrowException();
|
||||
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) {
|
||||
synchronized (lock) {
|
||||
if (isFlushingOrShutdown()) {
|
||||
return MediaCodec.INFO_TRY_AGAIN_LATER;
|
||||
} else {
|
||||
maybeThrowException();
|
||||
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 IllegalStateException If called before {@link #dequeueOutputBufferIndex} has returned
|
||||
* {@link MediaCodec#INFO_OUTPUT_FORMAT_CHANGED}.
|
||||
*/
|
||||
public MediaFormat getOutputFormat() {
|
||||
synchronized (lock) {
|
||||
if (currentFormat == null) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
return currentFormat;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates a flush asynchronously, which will be completed on the callback thread. When the
|
||||
* flush is complete, it will trigger {@code onFlushCompleted} from the callback thread.
|
||||
*
|
||||
* @param onFlushCompleted A {@link Runnable} that will be called when flush is completed. {@code
|
||||
* onFlushCompleted} will be called from the scallback thread, therefore it should execute
|
||||
* synchronized and thread-safe code.
|
||||
*/
|
||||
public void flushAsync(Runnable onFlushCompleted) {
|
||||
synchronized (lock) {
|
||||
++pendingFlushCount;
|
||||
Util.castNonNull(handler).post(() -> this.onFlushCompleted(onFlushCompleted));
|
||||
}
|
||||
}
|
||||
|
||||
// Called from the callback thread.
|
||||
|
||||
@Override
|
||||
public void onInputBufferAvailable(@NonNull MediaCodec codec, int index) {
|
||||
synchronized (lock) {
|
||||
availableInputBuffers.add(index);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOutputBufferAvailable(
|
||||
@NonNull MediaCodec codec, int index, @NonNull MediaCodec.BufferInfo info) {
|
||||
synchronized (lock) {
|
||||
if (pendingOutputFormat != null) {
|
||||
addOutputFormat(pendingOutputFormat);
|
||||
pendingOutputFormat = null;
|
||||
}
|
||||
availableOutputBuffers.add(index);
|
||||
bufferInfos.add(info);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(@NonNull MediaCodec codec, @NonNull MediaCodec.CodecException e) {
|
||||
synchronized (lock) {
|
||||
mediaCodecException = e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOutputFormatChanged(@NonNull MediaCodec codec, @NonNull MediaFormat format) {
|
||||
synchronized (lock) {
|
||||
addOutputFormat(format);
|
||||
pendingOutputFormat = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void onFlushCompleted(Runnable onFlushCompleted) {
|
||||
synchronized (lock) {
|
||||
onFlushCompletedSynchronized(onFlushCompleted);
|
||||
}
|
||||
}
|
||||
|
||||
@GuardedBy("lock")
|
||||
private void onFlushCompletedSynchronized(Runnable onFlushCompleted) {
|
||||
if (shutDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
--pendingFlushCount;
|
||||
if (pendingFlushCount > 0) {
|
||||
// Another flush() has been called.
|
||||
return;
|
||||
} else if (pendingFlushCount < 0) {
|
||||
// This should never happen.
|
||||
setInternalException(new IllegalStateException());
|
||||
return;
|
||||
}
|
||||
flushInternal();
|
||||
try {
|
||||
onFlushCompleted.run();
|
||||
} catch (IllegalStateException e) {
|
||||
setInternalException(e);
|
||||
} catch (Exception e) {
|
||||
setInternalException(new IllegalStateException(e));
|
||||
}
|
||||
}
|
||||
|
||||
/** Flushes all available input and output buffers and any error that was previously set. */
|
||||
@GuardedBy("lock")
|
||||
private void flushInternal() {
|
||||
pendingOutputFormat = formats.isEmpty() ? null : formats.getLast();
|
||||
availableInputBuffers.clear();
|
||||
availableOutputBuffers.clear();
|
||||
bufferInfos.clear();
|
||||
formats.clear();
|
||||
mediaCodecException = null;
|
||||
}
|
||||
|
||||
@GuardedBy("lock")
|
||||
private boolean isFlushingOrShutdown() {
|
||||
return pendingFlushCount > 0 || shutDown;
|
||||
}
|
||||
|
||||
@GuardedBy("lock")
|
||||
private void addOutputFormat(MediaFormat mediaFormat) {
|
||||
availableOutputBuffers.add(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
|
||||
formats.add(mediaFormat);
|
||||
}
|
||||
|
||||
@GuardedBy("lock")
|
||||
private void maybeThrowException() {
|
||||
maybeThrowInternalException();
|
||||
maybeThrowMediaCodecException();
|
||||
}
|
||||
|
||||
@GuardedBy("lock")
|
||||
private void maybeThrowInternalException() {
|
||||
if (internalException != null) {
|
||||
IllegalStateException e = internalException;
|
||||
internalException = null;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@GuardedBy("lock")
|
||||
private void maybeThrowMediaCodecException() {
|
||||
if (mediaCodecException != null) {
|
||||
MediaCodec.CodecException codecException = mediaCodecException;
|
||||
mediaCodecException = null;
|
||||
throw codecException;
|
||||
}
|
||||
}
|
||||
|
||||
private void setInternalException(IllegalStateException e) {
|
||||
synchronized (lock) {
|
||||
internalException = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,157 +0,0 @@
|
|||
/*
|
||||
* 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.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 MediaFormat pendingOutputFormat;
|
||||
@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 IllegalStateException If called before {@link #dequeueOutputBufferIndex} has 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() {
|
||||
pendingOutputFormat = formats.isEmpty() ? null : formats.getLast();
|
||||
availableInputBuffers.clear();
|
||||
availableOutputBuffers.clear();
|
||||
bufferInfos.clear();
|
||||
formats.clear();
|
||||
mediaCodecException = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInputBufferAvailable(MediaCodec mediaCodec, int index) {
|
||||
availableInputBuffers.add(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOutputBufferAvailable(
|
||||
MediaCodec mediaCodec, int index, MediaCodec.BufferInfo bufferInfo) {
|
||||
if (pendingOutputFormat != null) {
|
||||
addOutputFormat(pendingOutputFormat);
|
||||
pendingOutputFormat = null;
|
||||
}
|
||||
availableOutputBuffers.add(index);
|
||||
bufferInfos.add(bufferInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(MediaCodec mediaCodec, MediaCodec.CodecException e) {
|
||||
onMediaCodecError(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOutputFormatChanged(MediaCodec mediaCodec, MediaFormat mediaFormat) {
|
||||
addOutputFormat(mediaFormat);
|
||||
pendingOutputFormat = null;
|
||||
}
|
||||
|
||||
@VisibleForTesting()
|
||||
void onMediaCodecError(IllegalStateException e) {
|
||||
mediaCodecException = e;
|
||||
}
|
||||
|
||||
private void addOutputFormat(MediaFormat mediaFormat) {
|
||||
availableOutputBuffers.add(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
|
||||
formats.add(mediaFormat);
|
||||
}
|
||||
}
|
||||
|
|
@ -1067,7 +1067,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||
codecOperatingRate = CODEC_OPERATING_RATE_UNSET;
|
||||
}
|
||||
|
||||
MediaCodecAdapter codecAdapter = null;
|
||||
@Nullable MediaCodecAdapter codecAdapter = null;
|
||||
try {
|
||||
codecInitializingTimestamp = SystemClock.elapsedRealtime();
|
||||
TraceUtil.beginSection("createCodec:" + codecName);
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import android.media.MediaCodec;
|
|||
import android.media.MediaFormat;
|
||||
import android.os.HandlerThread;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Constructor;
|
||||
import org.junit.After;
|
||||
|
|
@ -38,18 +37,16 @@ import org.robolectric.shadows.ShadowLooper;
|
|||
public class AsynchronousMediaCodecAdapterTest {
|
||||
private AsynchronousMediaCodecAdapter adapter;
|
||||
private MediaCodec codec;
|
||||
private TestHandlerThread callbackThread;
|
||||
private HandlerThread callbackThread;
|
||||
private HandlerThread queueingThread;
|
||||
private MediaCodec.BufferInfo bufferInfo;
|
||||
|
||||
@Before
|
||||
public void setUp() throws IOException {
|
||||
codec = MediaCodec.createByCodecName("h264");
|
||||
callbackThread = new TestHandlerThread("TestCallbackThread");
|
||||
callbackThread = new HandlerThread("TestCallbackThread");
|
||||
queueingThread = new HandlerThread("TestQueueingThread");
|
||||
adapter =
|
||||
new AsynchronousMediaCodecAdapter(
|
||||
codec, /* trackType= */ C.TRACK_TYPE_VIDEO, callbackThread, queueingThread);
|
||||
adapter = new AsynchronousMediaCodecAdapter(codec, callbackThread, queueingThread);
|
||||
bufferInfo = new MediaCodec.BufferInfo();
|
||||
}
|
||||
|
||||
|
|
@ -57,8 +54,6 @@ public class AsynchronousMediaCodecAdapterTest {
|
|||
public void tearDown() {
|
||||
adapter.shutdown();
|
||||
codec.release();
|
||||
|
||||
assertThat(callbackThread.hasQuit()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -85,39 +80,7 @@ public class AsynchronousMediaCodecAdapterTest {
|
|||
assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueInputBufferIndex_withPendingFlush_returnsTryAgainLater() {
|
||||
adapter.configure(
|
||||
createMediaFormat("foo"), /* surface= */ null, /* crypto= */ null, /* flags= */ 0);
|
||||
adapter.start();
|
||||
|
||||
// After adapter.start(), the ShadowMediaCodec offers input buffer 0. We run all currently
|
||||
// enqueued messages and pause the looper so that flush is not completed.
|
||||
ShadowLooper shadowLooper = shadowOf(callbackThread.getLooper());
|
||||
shadowLooper.idle();
|
||||
shadowLooper.pause();
|
||||
adapter.flush();
|
||||
|
||||
assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueInputBufferIndex_withFlushCompletedAndInputBuffer_returnsInputBuffer() {
|
||||
adapter.configure(
|
||||
createMediaFormat("foo"), /* surface= */ null, /* crypto= */ null, /* flags= */ 0);
|
||||
adapter.start();
|
||||
// After adapter.start(), the ShadowMediaCodec offers input buffer 0. We advance the looper to
|
||||
// make sure all messages have been propagated to the adapter.
|
||||
ShadowLooper shadowLooper = shadowOf(callbackThread.getLooper());
|
||||
shadowLooper.idle();
|
||||
|
||||
adapter.flush();
|
||||
// Progress the looper to complete flush(): the adapter should call codec.start(), triggering
|
||||
// the ShadowMediaCodec to offer input buffer 0.
|
||||
shadowLooper.idle();
|
||||
|
||||
assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueInputBufferIndex_withMediaCodecError_throwsException() throws Exception {
|
||||
|
|
@ -128,7 +91,7 @@ public class AsynchronousMediaCodecAdapterTest {
|
|||
adapter.start();
|
||||
|
||||
// Set an error directly on the adapter (not through the looper).
|
||||
adapter.onError(codec, createCodecException());
|
||||
adapter.onError(createCodecException());
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> adapter.dequeueInputBufferIndex());
|
||||
}
|
||||
|
|
@ -192,25 +155,6 @@ public class AsynchronousMediaCodecAdapterTest {
|
|||
assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)).isEqualTo(index);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueOutputBufferIndex_withPendingFlush_returnsTryAgainLater() {
|
||||
adapter.configure(
|
||||
createMediaFormat("foo"), /* surface= */ null, /* crypto= */ null, /* flags= */ 0);
|
||||
adapter.start();
|
||||
// After start(), the ShadowMediaCodec offers input buffer 0, which is available only if we
|
||||
// progress the adapter's looper.
|
||||
ShadowLooper shadowLooper = shadowOf(callbackThread.getLooper());
|
||||
shadowLooper.idle();
|
||||
|
||||
// Flush enqueues a task in the looper, but we will pause the looper to leave flush()
|
||||
// in an incomplete state.
|
||||
shadowLooper.pause();
|
||||
adapter.flush();
|
||||
|
||||
assertThat(adapter.dequeueOutputBufferIndex(bufferInfo))
|
||||
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueOutputBufferIndex_withMediaCodecError_throwsException() throws Exception {
|
||||
// Pause the looper so that we interact with the adapter from this thread only.
|
||||
|
|
@ -220,7 +164,7 @@ public class AsynchronousMediaCodecAdapterTest {
|
|||
adapter.start();
|
||||
|
||||
// Set an error directly on the adapter.
|
||||
adapter.onError(codec, createCodecException());
|
||||
adapter.onError(createCodecException());
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo));
|
||||
}
|
||||
|
|
@ -266,8 +210,8 @@ public class AsynchronousMediaCodecAdapterTest {
|
|||
// progress the adapter's looper.
|
||||
shadowOf(callbackThread.getLooper()).idle();
|
||||
|
||||
// Add another format directly on the adapter.
|
||||
adapter.onOutputFormatChanged(codec, createMediaFormat("format2"));
|
||||
// Add another format on the adapter.
|
||||
adapter.onOutputFormatChanged(createMediaFormat("format2"));
|
||||
|
||||
assertThat(adapter.dequeueOutputBufferIndex(bufferInfo))
|
||||
.isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
|
||||
|
|
@ -314,22 +258,4 @@ public class AsynchronousMediaCodecAdapterTest {
|
|||
return constructor.newInstance(
|
||||
/* errorCode= */ 0, /* actionCode= */ 0, /* detailMessage= */ "error from codec");
|
||||
}
|
||||
|
||||
private static class TestHandlerThread extends HandlerThread {
|
||||
private boolean quit;
|
||||
|
||||
TestHandlerThread(String label) {
|
||||
super(label);
|
||||
}
|
||||
|
||||
public boolean hasQuit() {
|
||||
return quit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean quit() {
|
||||
quit = true;
|
||||
return super.quit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ import com.google.android.exoplayer2.decoder.CryptoInfo;
|
|||
import com.google.android.exoplayer2.util.ConditionVariable;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
|
|
@ -65,7 +64,7 @@ public class AsynchronousMediaCodecBufferEnqueuerTest {
|
|||
enqueuer.shutdown();
|
||||
codec.stop();
|
||||
codec.release();
|
||||
assertThat(TestHandlerThread.INSTANCES_STARTED.get()).isEqualTo(0);
|
||||
assertThat(!handlerThread.hasStarted() || handlerThread.hasQuit()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -221,25 +220,31 @@ public class AsynchronousMediaCodecBufferEnqueuerTest {
|
|||
}
|
||||
|
||||
private static class TestHandlerThread extends HandlerThread {
|
||||
private static final AtomicLong INSTANCES_STARTED = new AtomicLong(0);
|
||||
private boolean started;
|
||||
private boolean quit;
|
||||
|
||||
TestHandlerThread(String name) {
|
||||
super(name);
|
||||
TestHandlerThread(String label) {
|
||||
super(label);
|
||||
}
|
||||
|
||||
public boolean hasStarted() {
|
||||
return started;
|
||||
}
|
||||
|
||||
public boolean hasQuit() {
|
||||
return quit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void start() {
|
||||
super.start();
|
||||
INSTANCES_STARTED.incrementAndGet();
|
||||
started = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean quit() {
|
||||
boolean quit = super.quit();
|
||||
if (quit) {
|
||||
INSTANCES_STARTED.decrementAndGet();
|
||||
}
|
||||
return quit;
|
||||
quit = true;
|
||||
return super.quit();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,418 @@
|
|||
/*
|
||||
* Copyright (C) 2020 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.testutil.TestUtil.assertBufferInfosEqual;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.robolectric.Shadows.shadowOf;
|
||||
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaFormat;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.Looper;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.shadows.ShadowLooper;
|
||||
|
||||
/** Unit tests for {@link AsynchronousMediaCodecCallback}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class AsynchronousMediaCodecCallbackTest {
|
||||
|
||||
private AsynchronousMediaCodecCallback asynchronousMediaCodecCallback;
|
||||
private TestHandlerThread callbackThread;
|
||||
private MediaCodec codec;
|
||||
|
||||
@Before
|
||||
public void setUp() throws IOException {
|
||||
callbackThread = new TestHandlerThread("TestCallbackThread");
|
||||
codec = MediaCodec.createByCodecName("h264");
|
||||
asynchronousMediaCodecCallback = new AsynchronousMediaCodecCallback(callbackThread);
|
||||
asynchronousMediaCodecCallback.initialize(codec);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
codec.release();
|
||||
asynchronousMediaCodecCallback.shutdown();
|
||||
|
||||
assertThat(callbackThread.hasQuit()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeInputBufferIndex_afterCreation_returnsTryAgain() {
|
||||
assertThat(asynchronousMediaCodecCallback.dequeueInputBufferIndex())
|
||||
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeInputBufferIndex_returnsEnqueuedBuffers() {
|
||||
// Send two input buffers to the callback.
|
||||
asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 0);
|
||||
asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 1);
|
||||
|
||||
assertThat(asynchronousMediaCodecCallback.dequeueInputBufferIndex()).isEqualTo(0);
|
||||
assertThat(asynchronousMediaCodecCallback.dequeueInputBufferIndex()).isEqualTo(1);
|
||||
assertThat(asynchronousMediaCodecCallback.dequeueInputBufferIndex())
|
||||
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeInputBufferIndex_withPendingFlush_returnsTryAgain() {
|
||||
Looper callbackThreadLooper = callbackThread.getLooper();
|
||||
AtomicBoolean flushCompleted = new AtomicBoolean();
|
||||
// Pause the callback thread so that flush() never completes.
|
||||
shadowOf(callbackThreadLooper).pause();
|
||||
|
||||
// Send two input buffers to the callback and then flush().
|
||||
asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 0);
|
||||
asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 1);
|
||||
asynchronousMediaCodecCallback.flushAsync(
|
||||
/* onFlushCompleted= */ () -> flushCompleted.set(true));
|
||||
assertThat(flushCompleted.get()).isFalse();
|
||||
assertThat(asynchronousMediaCodecCallback.dequeueInputBufferIndex())
|
||||
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeInputBufferIndex_afterFlush_returnsTryAgain() {
|
||||
Looper callbackThreadLooper = callbackThread.getLooper();
|
||||
AtomicBoolean flushCompleted = new AtomicBoolean();
|
||||
|
||||
// Send two input buffers to the callback and then flush().
|
||||
asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 0);
|
||||
asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 1);
|
||||
asynchronousMediaCodecCallback.flushAsync(
|
||||
/* onFlushCompleted= */ () -> flushCompleted.set(true));
|
||||
// Progress the callback thread so that flush() completes.
|
||||
shadowOf(callbackThreadLooper).idle();
|
||||
|
||||
assertThat(flushCompleted.get()).isTrue();
|
||||
assertThat(asynchronousMediaCodecCallback.dequeueInputBufferIndex())
|
||||
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeInputBufferIndex_afterFlushAndNewInputBuffer_returnsEnqueuedBuffer() {
|
||||
Looper callbackThreadLooper = callbackThread.getLooper();
|
||||
AtomicBoolean flushCompleted = new AtomicBoolean();
|
||||
|
||||
// Send two input buffers to the callback, then flush(), then send
|
||||
// another input buffer.
|
||||
asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 0);
|
||||
asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 1);
|
||||
asynchronousMediaCodecCallback.flushAsync(
|
||||
/* onFlushCompleted= */ () -> flushCompleted.set(true));
|
||||
// Progress the callback thread so that flush() completes.
|
||||
shadowOf(callbackThreadLooper).idle();
|
||||
asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 2);
|
||||
|
||||
assertThat(flushCompleted.get()).isTrue();
|
||||
assertThat(asynchronousMediaCodecCallback.dequeueInputBufferIndex()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueInputBufferIndex_afterShutdown_returnsTryAgainLater() {
|
||||
asynchronousMediaCodecCallback.onInputBufferAvailable(codec, /* index= */ 1);
|
||||
|
||||
asynchronousMediaCodecCallback.shutdown();
|
||||
|
||||
assertThat(asynchronousMediaCodecCallback.dequeueInputBufferIndex())
|
||||
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueInputBufferIndex_afterOnErrorCallback_throwsError() throws Exception {
|
||||
asynchronousMediaCodecCallback.onError(codec, createCodecException());
|
||||
|
||||
assertThrows(
|
||||
MediaCodec.CodecException.class,
|
||||
() -> asynchronousMediaCodecCallback.dequeueInputBufferIndex());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueInputBufferIndex_afterFlushCompletedWithError_throwsError() throws Exception {
|
||||
MediaCodec.CodecException codecException = createCodecException();
|
||||
asynchronousMediaCodecCallback.flushAsync(
|
||||
() -> {
|
||||
throw codecException;
|
||||
});
|
||||
shadowOf(callbackThread.getLooper()).idle();
|
||||
|
||||
assertThrows(
|
||||
MediaCodec.CodecException.class,
|
||||
() -> asynchronousMediaCodecCallback.dequeueInputBufferIndex());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeOutputBufferIndex_afterCreation_returnsTryAgain() {
|
||||
MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo();
|
||||
assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(outBufferInfo))
|
||||
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeOutputBufferIndex_returnsEnqueuedBuffers() {
|
||||
// Send two output buffers to the callback.
|
||||
MediaCodec.BufferInfo bufferInfo1 = new MediaCodec.BufferInfo();
|
||||
asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 0, bufferInfo1);
|
||||
MediaCodec.BufferInfo bufferInfo2 = new MediaCodec.BufferInfo();
|
||||
bufferInfo2.set(1, 1, 1, 1);
|
||||
asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 1, bufferInfo2);
|
||||
|
||||
MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo();
|
||||
assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(outBufferInfo)).isEqualTo(0);
|
||||
assertBufferInfosEqual(bufferInfo1, outBufferInfo);
|
||||
assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(outBufferInfo)).isEqualTo(1);
|
||||
assertBufferInfosEqual(bufferInfo2, outBufferInfo);
|
||||
assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(outBufferInfo))
|
||||
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeOutputBufferIndex_withPendingFlush_returnsTryAgain() {
|
||||
Looper callbackThreadLooper = callbackThread.getLooper();
|
||||
AtomicBoolean flushCompleted = new AtomicBoolean();
|
||||
// Pause the callback thread so that flush() never completes.
|
||||
shadowOf(callbackThreadLooper).pause();
|
||||
|
||||
// Send two output buffers to the callback and then flush().
|
||||
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
||||
asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 0, bufferInfo);
|
||||
asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 1, bufferInfo);
|
||||
asynchronousMediaCodecCallback.flushAsync(
|
||||
/* onFlushCompleted= */ () -> flushCompleted.set(true));
|
||||
|
||||
assertThat(flushCompleted.get()).isFalse();
|
||||
assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(new MediaCodec.BufferInfo()))
|
||||
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeOutputBufferIndex_afterFlush_returnsTryAgain() {
|
||||
Looper callbackThreadLooper = callbackThread.getLooper();
|
||||
AtomicBoolean flushCompleted = new AtomicBoolean();
|
||||
|
||||
// Send two output buffers to the callback and then flush().
|
||||
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
||||
asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 0, bufferInfo);
|
||||
asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 1, bufferInfo);
|
||||
asynchronousMediaCodecCallback.flushAsync(
|
||||
/* onFlushCompleted= */ () -> flushCompleted.set(true));
|
||||
// Progress the callback looper so that flush() completes.
|
||||
shadowOf(callbackThreadLooper).idle();
|
||||
|
||||
assertThat(flushCompleted.get()).isTrue();
|
||||
assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(new MediaCodec.BufferInfo()))
|
||||
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeOutputBufferIndex_afterFlushAndNewOutputBuffers_returnsEnqueueBuffer() {
|
||||
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
||||
Looper callbackThreadLooper = callbackThread.getLooper();
|
||||
AtomicBoolean flushCompleted = new AtomicBoolean();
|
||||
|
||||
// Send two output buffers to the callback, then flush(), then send
|
||||
// another output buffer.
|
||||
asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 0, bufferInfo);
|
||||
asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 1, bufferInfo);
|
||||
asynchronousMediaCodecCallback.flushAsync(
|
||||
/* onFlushCompleted= */ () -> flushCompleted.set(true));
|
||||
// Progress the callback looper so that flush() completes.
|
||||
shadowOf(callbackThreadLooper).idle();
|
||||
asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 2, bufferInfo);
|
||||
MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo();
|
||||
|
||||
assertThat(flushCompleted.get()).isTrue();
|
||||
assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(outBufferInfo)).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeOutputBufferIndex_withPendingOutputFormat_returnsPendingOutputFormat() {
|
||||
MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo();
|
||||
Looper callbackThreadLooper = callbackThread.getLooper();
|
||||
AtomicBoolean flushCompleted = new AtomicBoolean();
|
||||
|
||||
asynchronousMediaCodecCallback.onOutputFormatChanged(codec, new MediaFormat());
|
||||
asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, /* index= */ 0, outBufferInfo);
|
||||
MediaFormat pendingMediaFormat = new MediaFormat();
|
||||
asynchronousMediaCodecCallback.onOutputFormatChanged(codec, pendingMediaFormat);
|
||||
// flush() should not discard the last format.
|
||||
asynchronousMediaCodecCallback.flushAsync(
|
||||
/* onFlushCompleted= */ () -> flushCompleted.set(true));
|
||||
// Progress the callback looper so that flush() completes.
|
||||
shadowOf(callbackThreadLooper).idle();
|
||||
// Right after flush(), we send an output buffer: the pending output format should be
|
||||
// dequeued first.
|
||||
asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, /* index= */ 1, outBufferInfo);
|
||||
|
||||
assertThat(flushCompleted.get()).isTrue();
|
||||
assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(outBufferInfo))
|
||||
.isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
|
||||
assertThat(asynchronousMediaCodecCallback.getOutputFormat()).isEqualTo(pendingMediaFormat);
|
||||
assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(outBufferInfo)).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeOutputBufferIndex_withPendingOutputFormatAndNewFormat_returnsNewFormat() {
|
||||
MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo();
|
||||
Looper callbackThreadLooper = callbackThread.getLooper();
|
||||
AtomicBoolean flushCompleted = new AtomicBoolean();
|
||||
|
||||
asynchronousMediaCodecCallback.onOutputFormatChanged(codec, new MediaFormat());
|
||||
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
||||
asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, /* index= */ 0, bufferInfo);
|
||||
MediaFormat pendingMediaFormat = new MediaFormat();
|
||||
asynchronousMediaCodecCallback.onOutputFormatChanged(codec, pendingMediaFormat);
|
||||
// flush() should not discard the last format.
|
||||
asynchronousMediaCodecCallback.flushAsync(
|
||||
/* onFlushCompleted= */ () -> flushCompleted.set(true));
|
||||
// Progress the callback looper so that flush() completes.
|
||||
shadowOf(callbackThreadLooper).idle();
|
||||
// The first callback after flush() is a new MediaFormat, it should overwrite the pending
|
||||
// format.
|
||||
MediaFormat newFormat = new MediaFormat();
|
||||
asynchronousMediaCodecCallback.onOutputFormatChanged(codec, newFormat);
|
||||
asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, /* index= */ 1, bufferInfo);
|
||||
|
||||
assertThat(flushCompleted.get()).isTrue();
|
||||
assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(outBufferInfo))
|
||||
.isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
|
||||
assertThat(asynchronousMediaCodecCallback.getOutputFormat()).isEqualTo(newFormat);
|
||||
assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(outBufferInfo)).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueOutputBufferIndex_afterShutdown_returnsTryAgainLater() {
|
||||
asynchronousMediaCodecCallback.onOutputBufferAvailable(
|
||||
codec, /* index= */ 1, new MediaCodec.BufferInfo());
|
||||
|
||||
asynchronousMediaCodecCallback.shutdown();
|
||||
|
||||
assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(new MediaCodec.BufferInfo()))
|
||||
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueOutputBufferIndex_afterOnErrorCallback_throwsError() throws Exception {
|
||||
asynchronousMediaCodecCallback.onError(codec, createCodecException());
|
||||
|
||||
assertThrows(
|
||||
MediaCodec.CodecException.class,
|
||||
() -> asynchronousMediaCodecCallback.dequeueOutputBufferIndex(new MediaCodec.BufferInfo()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueOutputBufferIndex_afterFlushCompletedWithError_throwsError() throws Exception {
|
||||
MediaCodec.CodecException codecException = createCodecException();
|
||||
asynchronousMediaCodecCallback.flushAsync(
|
||||
() -> {
|
||||
throw codecException;
|
||||
});
|
||||
shadowOf(callbackThread.getLooper()).idle();
|
||||
|
||||
assertThrows(
|
||||
MediaCodec.CodecException.class,
|
||||
() -> asynchronousMediaCodecCallback.dequeueOutputBufferIndex(new MediaCodec.BufferInfo()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getOutputFormat_onNewInstance_raisesException() {
|
||||
try {
|
||||
asynchronousMediaCodecCallback.getOutputFormat();
|
||||
fail();
|
||||
} catch (IllegalStateException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getOutputFormat_afterOnOutputFormatCalled_returnsFormat() {
|
||||
MediaFormat format = new MediaFormat();
|
||||
asynchronousMediaCodecCallback.onOutputFormatChanged(codec, format);
|
||||
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
||||
|
||||
assertThat(asynchronousMediaCodecCallback.dequeueOutputBufferIndex(bufferInfo))
|
||||
.isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
|
||||
assertThat(asynchronousMediaCodecCallback.getOutputFormat()).isEqualTo(format);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getOutputFormat_afterFlush_returnsCurrentFormat() {
|
||||
MediaFormat format = new MediaFormat();
|
||||
Looper callbackThreadLooper = callbackThread.getLooper();
|
||||
AtomicBoolean flushCompleted = new AtomicBoolean();
|
||||
|
||||
asynchronousMediaCodecCallback.onOutputFormatChanged(codec, format);
|
||||
asynchronousMediaCodecCallback.dequeueOutputBufferIndex(new MediaCodec.BufferInfo());
|
||||
asynchronousMediaCodecCallback.flushAsync(
|
||||
/* onFlushCompleted= */ () -> flushCompleted.set(true));
|
||||
// Progress the callback looper so that flush() completes.
|
||||
shadowOf(callbackThreadLooper).idle();
|
||||
|
||||
assertThat(flushCompleted.get()).isTrue();
|
||||
assertThat(asynchronousMediaCodecCallback.getOutputFormat()).isEqualTo(format);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flush_withPendingFlush_onlyLastFlushCompletes() {
|
||||
ShadowLooper callbackLooperShadow = shadowOf(callbackThread.getLooper());
|
||||
callbackLooperShadow.pause();
|
||||
AtomicInteger flushCompleted = new AtomicInteger();
|
||||
|
||||
asynchronousMediaCodecCallback.flushAsync(/* onFlushCompleted= */ () -> flushCompleted.set(1));
|
||||
asynchronousMediaCodecCallback.flushAsync(/* onFlushCompleted= */ () -> flushCompleted.set(2));
|
||||
callbackLooperShadow.idle();
|
||||
|
||||
assertThat(flushCompleted.get()).isEqualTo(2);
|
||||
}
|
||||
|
||||
/** Reflectively create a {@link MediaCodec.CodecException}. */
|
||||
private static MediaCodec.CodecException createCodecException() throws Exception {
|
||||
Constructor<MediaCodec.CodecException> constructor =
|
||||
MediaCodec.CodecException.class.getDeclaredConstructor(
|
||||
Integer.TYPE, Integer.TYPE, String.class);
|
||||
return constructor.newInstance(
|
||||
/* errorCode= */ 0, /* actionCode= */ 0, /* detailMessage= */ "error from codec");
|
||||
}
|
||||
|
||||
private static class TestHandlerThread extends HandlerThread {
|
||||
private boolean quit;
|
||||
|
||||
TestHandlerThread(String label) {
|
||||
super(label);
|
||||
}
|
||||
|
||||
public boolean hasQuit() {
|
||||
return quit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean quit() {
|
||||
quit = true;
|
||||
return super.quit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,249 +0,0 @@
|
|||
/*
|
||||
* 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.testutil.TestUtil.assertBufferInfosEqual;
|
||||
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);
|
||||
assertBufferInfosEqual(bufferInfo1, outBufferInfo);
|
||||
assertThat(mediaCodecAsyncCallback.dequeueOutputBufferIndex(outBufferInfo)).isEqualTo(1);
|
||||
assertBufferInfosEqual(bufferInfo2, outBufferInfo);
|
||||
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 dequeOutputBufferIndex_withPendingOutputFormat_returnsPendingOutputFormat() {
|
||||
MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo();
|
||||
|
||||
mediaCodecAsyncCallback.onOutputFormatChanged(codec, new MediaFormat());
|
||||
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
||||
mediaCodecAsyncCallback.onOutputBufferAvailable(codec, /* index= */ 0, bufferInfo);
|
||||
MediaFormat pendingMediaFormat = new MediaFormat();
|
||||
mediaCodecAsyncCallback.onOutputFormatChanged(codec, pendingMediaFormat);
|
||||
// Flush should not discard the last format.
|
||||
mediaCodecAsyncCallback.flush();
|
||||
// First callback after flush is an output buffer, pending output format should be pushed first.
|
||||
mediaCodecAsyncCallback.onOutputBufferAvailable(codec, /* index= */ 1, bufferInfo);
|
||||
|
||||
assertThat(mediaCodecAsyncCallback.dequeueOutputBufferIndex(outBufferInfo))
|
||||
.isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
|
||||
assertThat(mediaCodecAsyncCallback.getOutputFormat()).isEqualTo(pendingMediaFormat);
|
||||
assertThat(mediaCodecAsyncCallback.dequeueOutputBufferIndex(outBufferInfo)).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeOutputBufferIndex_withPendingOutputFormatAndNewFormat_returnsNewFormat() {
|
||||
mediaCodecAsyncCallback.onOutputFormatChanged(codec, new MediaFormat());
|
||||
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
||||
mediaCodecAsyncCallback.onOutputBufferAvailable(codec, /* index= */ 0, bufferInfo);
|
||||
MediaFormat pendingMediaFormat = new MediaFormat();
|
||||
mediaCodecAsyncCallback.onOutputFormatChanged(codec, pendingMediaFormat);
|
||||
// Flush should not discard the last format
|
||||
mediaCodecAsyncCallback.flush();
|
||||
// The first callback after flush is a new MediaFormat, it should overwrite the pending format.
|
||||
MediaFormat newFormat = new MediaFormat();
|
||||
mediaCodecAsyncCallback.onOutputFormatChanged(codec, newFormat);
|
||||
mediaCodecAsyncCallback.onOutputBufferAvailable(codec, /* index= */ 1, bufferInfo);
|
||||
MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo();
|
||||
|
||||
assertThat(mediaCodecAsyncCallback.dequeueOutputBufferIndex(outBufferInfo))
|
||||
.isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
|
||||
assertThat(mediaCodecAsyncCallback.getOutputFormat()).isEqualTo(newFormat);
|
||||
assertThat(mediaCodecAsyncCallback.dequeueOutputBufferIndex(outBufferInfo)).isEqualTo(1);
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue