mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +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.MediaCodec;
|
||||||
import android.media.MediaCrypto;
|
import android.media.MediaCrypto;
|
||||||
import android.media.MediaFormat;
|
import android.media.MediaFormat;
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.HandlerThread;
|
import android.os.HandlerThread;
|
||||||
import android.view.Surface;
|
import android.view.Surface;
|
||||||
import androidx.annotation.GuardedBy;
|
|
||||||
import androidx.annotation.IntDef;
|
import androidx.annotation.IntDef;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
import androidx.annotation.VisibleForTesting;
|
import androidx.annotation.VisibleForTesting;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.decoder.CryptoInfo;
|
import com.google.android.exoplayer2.decoder.CryptoInfo;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
|
||||||
import java.lang.annotation.Documented;
|
import java.lang.annotation.Documented;
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link MediaCodecAdapter} that operates the underlying {@link MediaCodec} in asynchronous mode
|
* 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
|
* routes {@link MediaCodec.Callback} callbacks on a dedicated thread that is managed internally,
|
||||||
* internally.
|
* and queues input buffers asynchronously.
|
||||||
*
|
|
||||||
* <p>This adapter supports queueing input buffers asynchronously.
|
|
||||||
*/
|
*/
|
||||||
@RequiresApi(23)
|
@RequiresApi(23)
|
||||||
/* package */ final class AsynchronousMediaCodecAdapter extends MediaCodec.Callback
|
/* package */ final class AsynchronousMediaCodecAdapter implements MediaCodecAdapter {
|
||||||
implements MediaCodecAdapter {
|
|
||||||
|
|
||||||
@Documented
|
@Documented
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
@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_STARTED = 2;
|
||||||
private static final int STATE_SHUT_DOWN = 3;
|
private static final int STATE_SHUT_DOWN = 3;
|
||||||
|
|
||||||
private final Object lock;
|
|
||||||
|
|
||||||
@GuardedBy("lock")
|
|
||||||
private final MediaCodecAsyncCallback mediaCodecAsyncCallback;
|
|
||||||
|
|
||||||
private final MediaCodec codec;
|
private final MediaCodec codec;
|
||||||
private final HandlerThread handlerThread;
|
private final AsynchronousMediaCodecCallback asynchronousMediaCodecCallback;
|
||||||
private @MonotonicNonNull Handler handler;
|
|
||||||
|
|
||||||
@GuardedBy("lock")
|
|
||||||
private long pendingFlushCount;
|
|
||||||
|
|
||||||
private @State int state;
|
|
||||||
private final AsynchronousMediaCodecBufferEnqueuer bufferEnqueuer;
|
private final AsynchronousMediaCodecBufferEnqueuer bufferEnqueuer;
|
||||||
|
@State private int state;
|
||||||
@GuardedBy("lock")
|
|
||||||
@Nullable
|
|
||||||
private IllegalStateException internalException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an instance that wraps the specified {@link MediaCodec}.
|
* 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) {
|
/* package */ AsynchronousMediaCodecAdapter(MediaCodec codec, int trackType) {
|
||||||
this(
|
this(
|
||||||
codec,
|
codec,
|
||||||
trackType,
|
|
||||||
new HandlerThread(createCallbackThreadLabel(trackType)),
|
new HandlerThread(createCallbackThreadLabel(trackType)),
|
||||||
new HandlerThread(createQueueingThreadLabel(trackType)));
|
new HandlerThread(createQueueingThreadLabel(trackType)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
/* package */ AsynchronousMediaCodecAdapter(
|
/* package */ AsynchronousMediaCodecAdapter(
|
||||||
MediaCodec codec,
|
MediaCodec codec, HandlerThread callbackThread, HandlerThread enqueueingThread) {
|
||||||
int trackType,
|
|
||||||
HandlerThread callbackThread,
|
|
||||||
HandlerThread enqueueingThread) {
|
|
||||||
this.lock = new Object();
|
|
||||||
this.mediaCodecAsyncCallback = new MediaCodecAsyncCallback();
|
|
||||||
this.codec = codec;
|
this.codec = codec;
|
||||||
this.handlerThread = callbackThread;
|
this.asynchronousMediaCodecCallback = new AsynchronousMediaCodecCallback(callbackThread);
|
||||||
this.bufferEnqueuer = new AsynchronousMediaCodecBufferEnqueuer(codec, enqueueingThread);
|
this.bufferEnqueuer = new AsynchronousMediaCodecBufferEnqueuer(codec, enqueueingThread);
|
||||||
this.state = STATE_CREATED;
|
this.state = STATE_CREATED;
|
||||||
}
|
}
|
||||||
|
|
@ -110,9 +83,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
@Nullable Surface surface,
|
@Nullable Surface surface,
|
||||||
@Nullable MediaCrypto crypto,
|
@Nullable MediaCrypto crypto,
|
||||||
int flags) {
|
int flags) {
|
||||||
handlerThread.start();
|
asynchronousMediaCodecCallback.initialize(codec);
|
||||||
handler = new Handler(handlerThread.getLooper());
|
|
||||||
codec.setCallback(this, handler);
|
|
||||||
codec.configure(mediaFormat, surface, crypto, flags);
|
codec.configure(mediaFormat, surface, crypto, flags);
|
||||||
state = STATE_CONFIGURED;
|
state = STATE_CONFIGURED;
|
||||||
}
|
}
|
||||||
|
|
@ -138,60 +109,40 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int dequeueInputBufferIndex() {
|
public int dequeueInputBufferIndex() {
|
||||||
synchronized (lock) {
|
return asynchronousMediaCodecCallback.dequeueInputBufferIndex();
|
||||||
if (isFlushing()) {
|
|
||||||
return MediaCodec.INFO_TRY_AGAIN_LATER;
|
|
||||||
} else {
|
|
||||||
maybeThrowException();
|
|
||||||
return mediaCodecAsyncCallback.dequeueInputBufferIndex();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) {
|
public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) {
|
||||||
synchronized (lock) {
|
return asynchronousMediaCodecCallback.dequeueOutputBufferIndex(bufferInfo);
|
||||||
if (isFlushing()) {
|
|
||||||
return MediaCodec.INFO_TRY_AGAIN_LATER;
|
|
||||||
} else {
|
|
||||||
maybeThrowException();
|
|
||||||
return mediaCodecAsyncCallback.dequeueOutputBufferIndex(bufferInfo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MediaFormat getOutputFormat() {
|
public MediaFormat getOutputFormat() {
|
||||||
synchronized (lock) {
|
return asynchronousMediaCodecCallback.getOutputFormat();
|
||||||
return mediaCodecAsyncCallback.getOutputFormat();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void flush() {
|
public void flush() {
|
||||||
synchronized (lock) {
|
// The order of calls is important:
|
||||||
bufferEnqueuer.flush();
|
// First, flush the bufferEnqueuer to stop queueing input buffers.
|
||||||
codec.flush();
|
// Second, flush the codec to stop producing available input/output buffers.
|
||||||
++pendingFlushCount;
|
// Third, flush the callback after flushing the codec so that in-flight callbacks are discarded.
|
||||||
Util.castNonNull(handler).post(this::onFlushCompleted);
|
bufferEnqueuer.flush();
|
||||||
}
|
codec.flush();
|
||||||
|
// When flushAsync() is completed, start the codec again.
|
||||||
|
asynchronousMediaCodecCallback.flushAsync(/* onFlushCompleted= */ codec::start);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void shutdown() {
|
public void shutdown() {
|
||||||
synchronized (lock) {
|
|
||||||
if (state == STATE_STARTED) {
|
if (state == STATE_STARTED) {
|
||||||
bufferEnqueuer.shutdown();
|
bufferEnqueuer.shutdown();
|
||||||
}
|
}
|
||||||
if (state == STATE_CONFIGURED || state == STATE_STARTED) {
|
if (state == STATE_CONFIGURED || state == STATE_STARTED) {
|
||||||
handlerThread.quit();
|
asynchronousMediaCodecCallback.shutdown();
|
||||||
mediaCodecAsyncCallback.flush();
|
|
||||||
// Leave the adapter in a flushing state so that
|
|
||||||
// it will not dequeue anything.
|
|
||||||
++pendingFlushCount;
|
|
||||||
}
|
}
|
||||||
state = STATE_SHUT_DOWN;
|
state = STATE_SHUT_DOWN;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -199,86 +150,14 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
return codec;
|
return codec;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called from the handler thread.
|
@VisibleForTesting
|
||||||
|
/* package */ void onError(MediaCodec.CodecException error) {
|
||||||
@Override
|
asynchronousMediaCodecCallback.onError(codec, error);
|
||||||
public void onInputBufferAvailable(MediaCodec codec, int index) {
|
|
||||||
synchronized (lock) {
|
|
||||||
mediaCodecAsyncCallback.onInputBufferAvailable(codec, index);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@VisibleForTesting
|
||||||
public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {
|
/* package */ void onOutputFormatChanged(MediaFormat format) {
|
||||||
synchronized (lock) {
|
asynchronousMediaCodecCallback.onOutputFormatChanged(codec, format);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String createCallbackThreadLabel(int trackType) {
|
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;
|
codecOperatingRate = CODEC_OPERATING_RATE_UNSET;
|
||||||
}
|
}
|
||||||
|
|
||||||
MediaCodecAdapter codecAdapter = null;
|
@Nullable MediaCodecAdapter codecAdapter = null;
|
||||||
try {
|
try {
|
||||||
codecInitializingTimestamp = SystemClock.elapsedRealtime();
|
codecInitializingTimestamp = SystemClock.elapsedRealtime();
|
||||||
TraceUtil.beginSection("createCodec:" + codecName);
|
TraceUtil.beginSection("createCodec:" + codecName);
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ import android.media.MediaCodec;
|
||||||
import android.media.MediaFormat;
|
import android.media.MediaFormat;
|
||||||
import android.os.HandlerThread;
|
import android.os.HandlerThread;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.android.exoplayer2.C;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.reflect.Constructor;
|
import java.lang.reflect.Constructor;
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
|
|
@ -38,18 +37,16 @@ import org.robolectric.shadows.ShadowLooper;
|
||||||
public class AsynchronousMediaCodecAdapterTest {
|
public class AsynchronousMediaCodecAdapterTest {
|
||||||
private AsynchronousMediaCodecAdapter adapter;
|
private AsynchronousMediaCodecAdapter adapter;
|
||||||
private MediaCodec codec;
|
private MediaCodec codec;
|
||||||
private TestHandlerThread callbackThread;
|
private HandlerThread callbackThread;
|
||||||
private HandlerThread queueingThread;
|
private HandlerThread queueingThread;
|
||||||
private MediaCodec.BufferInfo bufferInfo;
|
private MediaCodec.BufferInfo bufferInfo;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setUp() throws IOException {
|
public void setUp() throws IOException {
|
||||||
codec = MediaCodec.createByCodecName("h264");
|
codec = MediaCodec.createByCodecName("h264");
|
||||||
callbackThread = new TestHandlerThread("TestCallbackThread");
|
callbackThread = new HandlerThread("TestCallbackThread");
|
||||||
queueingThread = new HandlerThread("TestQueueingThread");
|
queueingThread = new HandlerThread("TestQueueingThread");
|
||||||
adapter =
|
adapter = new AsynchronousMediaCodecAdapter(codec, callbackThread, queueingThread);
|
||||||
new AsynchronousMediaCodecAdapter(
|
|
||||||
codec, /* trackType= */ C.TRACK_TYPE_VIDEO, callbackThread, queueingThread);
|
|
||||||
bufferInfo = new MediaCodec.BufferInfo();
|
bufferInfo = new MediaCodec.BufferInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,8 +54,6 @@ public class AsynchronousMediaCodecAdapterTest {
|
||||||
public void tearDown() {
|
public void tearDown() {
|
||||||
adapter.shutdown();
|
adapter.shutdown();
|
||||||
codec.release();
|
codec.release();
|
||||||
|
|
||||||
assertThat(callbackThread.hasQuit()).isTrue();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -85,39 +80,7 @@ public class AsynchronousMediaCodecAdapterTest {
|
||||||
assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(0);
|
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
|
@Test
|
||||||
public void dequeueInputBufferIndex_withMediaCodecError_throwsException() throws Exception {
|
public void dequeueInputBufferIndex_withMediaCodecError_throwsException() throws Exception {
|
||||||
|
|
@ -128,7 +91,7 @@ public class AsynchronousMediaCodecAdapterTest {
|
||||||
adapter.start();
|
adapter.start();
|
||||||
|
|
||||||
// Set an error directly on the adapter (not through the looper).
|
// Set an error directly on the adapter (not through the looper).
|
||||||
adapter.onError(codec, createCodecException());
|
adapter.onError(createCodecException());
|
||||||
|
|
||||||
assertThrows(IllegalStateException.class, () -> adapter.dequeueInputBufferIndex());
|
assertThrows(IllegalStateException.class, () -> adapter.dequeueInputBufferIndex());
|
||||||
}
|
}
|
||||||
|
|
@ -192,25 +155,6 @@ public class AsynchronousMediaCodecAdapterTest {
|
||||||
assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)).isEqualTo(index);
|
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
|
@Test
|
||||||
public void dequeueOutputBufferIndex_withMediaCodecError_throwsException() throws Exception {
|
public void dequeueOutputBufferIndex_withMediaCodecError_throwsException() throws Exception {
|
||||||
// Pause the looper so that we interact with the adapter from this thread only.
|
// Pause the looper so that we interact with the adapter from this thread only.
|
||||||
|
|
@ -220,7 +164,7 @@ public class AsynchronousMediaCodecAdapterTest {
|
||||||
adapter.start();
|
adapter.start();
|
||||||
|
|
||||||
// Set an error directly on the adapter.
|
// Set an error directly on the adapter.
|
||||||
adapter.onError(codec, createCodecException());
|
adapter.onError(createCodecException());
|
||||||
|
|
||||||
assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo));
|
assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo));
|
||||||
}
|
}
|
||||||
|
|
@ -266,8 +210,8 @@ public class AsynchronousMediaCodecAdapterTest {
|
||||||
// progress the adapter's looper.
|
// progress the adapter's looper.
|
||||||
shadowOf(callbackThread.getLooper()).idle();
|
shadowOf(callbackThread.getLooper()).idle();
|
||||||
|
|
||||||
// Add another format directly on the adapter.
|
// Add another format on the adapter.
|
||||||
adapter.onOutputFormatChanged(codec, createMediaFormat("format2"));
|
adapter.onOutputFormatChanged(createMediaFormat("format2"));
|
||||||
|
|
||||||
assertThat(adapter.dequeueOutputBufferIndex(bufferInfo))
|
assertThat(adapter.dequeueOutputBufferIndex(bufferInfo))
|
||||||
.isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
|
.isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
|
||||||
|
|
@ -314,22 +258,4 @@ public class AsynchronousMediaCodecAdapterTest {
|
||||||
return constructor.newInstance(
|
return constructor.newInstance(
|
||||||
/* errorCode= */ 0, /* actionCode= */ 0, /* detailMessage= */ "error from codec");
|
/* 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 com.google.android.exoplayer2.util.ConditionVariable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
|
|
@ -65,7 +64,7 @@ public class AsynchronousMediaCodecBufferEnqueuerTest {
|
||||||
enqueuer.shutdown();
|
enqueuer.shutdown();
|
||||||
codec.stop();
|
codec.stop();
|
||||||
codec.release();
|
codec.release();
|
||||||
assertThat(TestHandlerThread.INSTANCES_STARTED.get()).isEqualTo(0);
|
assertThat(!handlerThread.hasStarted() || handlerThread.hasQuit()).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -221,25 +220,31 @@ public class AsynchronousMediaCodecBufferEnqueuerTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class TestHandlerThread extends HandlerThread {
|
private static class TestHandlerThread extends HandlerThread {
|
||||||
private static final AtomicLong INSTANCES_STARTED = new AtomicLong(0);
|
private boolean started;
|
||||||
|
private boolean quit;
|
||||||
|
|
||||||
TestHandlerThread(String name) {
|
TestHandlerThread(String label) {
|
||||||
super(name);
|
super(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasStarted() {
|
||||||
|
return started;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasQuit() {
|
||||||
|
return quit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized void start() {
|
public synchronized void start() {
|
||||||
super.start();
|
super.start();
|
||||||
INSTANCES_STARTED.incrementAndGet();
|
started = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean quit() {
|
public boolean quit() {
|
||||||
boolean quit = super.quit();
|
quit = true;
|
||||||
if (quit) {
|
return super.quit();
|
||||||
INSTANCES_STARTED.decrementAndGet();
|
|
||||||
}
|
|
||||||
return 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