Commit 41d4a132 by christosts Committed by Andrew Lewis

Add configure() in MediaCodecAdapter

The correct order of initializing the MediaCodec should be (as per
documentation
https://developer.android.com/reference/android/media/MediaCodec#initialization)

"create -> setCallback -> configure -> start"

but the MediaCodecRenderer currently does

"create -> configure -> setCallback -> start"

MediaCodec implementations did not complain about this so far, but the
wrong sequence does not work with the MediaCodec in block mode (new mode
in Android R) and also the ShadowMediaCodec won't operate in
asynchronous mode otherwise. To initialize the MediaCodec in the correct
order, this commit adds configure() in the MediaCodecAdapter so the
MediaCodecRenderer can do:

adapter.configure(); // sets the callback and then configures the codec
adapter.start();     // starts the codec

PiperOrigin-RevId: 316127680
parent 0a617146
...@@ -32,6 +32,7 @@ import com.google.android.exoplayer2.PlayerMessage.Target; ...@@ -32,6 +32,7 @@ import com.google.android.exoplayer2.PlayerMessage.Target;
import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispatcher; import com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispatcher;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.mediacodec.MediaCodecAdapter;
import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo;
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer;
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
...@@ -289,7 +290,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media ...@@ -289,7 +290,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
@Override @Override
protected void configureCodec( protected void configureCodec(
MediaCodecInfo codecInfo, MediaCodecInfo codecInfo,
MediaCodec codec, MediaCodecAdapter codecAdapter,
Format format, Format format,
@Nullable MediaCrypto crypto, @Nullable MediaCrypto crypto,
float codecOperatingRate) { float codecOperatingRate) {
...@@ -301,7 +302,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media ...@@ -301,7 +302,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
&& !MimeTypes.AUDIO_RAW.equals(format.sampleMimeType); && !MimeTypes.AUDIO_RAW.equals(format.sampleMimeType);
MediaFormat mediaFormat = MediaFormat mediaFormat =
getMediaFormat(format, codecInfo.codecMimeType, codecMaxInputSize, codecOperatingRate); getMediaFormat(format, codecInfo.codecMimeType, codecMaxInputSize, codecOperatingRate);
codec.configure(mediaFormat, /* surface= */ null, crypto, /* flags= */ 0); codecAdapter.configure(mediaFormat, /* surface= */ null, crypto, /* flags= */ 0);
// Store the input MIME type if we're using the passthrough codec. // Store the input MIME type if we're using the passthrough codec.
passthroughFormat = passthroughEnabled ? format : null; passthroughFormat = passthroughEnabled ? format : null;
} }
......
...@@ -17,9 +17,13 @@ ...@@ -17,9 +17,13 @@
package com.google.android.exoplayer2.mediacodec; package com.google.android.exoplayer2.mediacodec;
import android.media.MediaCodec; import android.media.MediaCodec;
import android.media.MediaCrypto;
import android.media.MediaFormat; import android.media.MediaFormat;
import android.os.Handler; import android.os.Handler;
import android.os.HandlerThread; import android.os.HandlerThread;
import android.os.Looper;
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;
...@@ -45,22 +49,32 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -45,22 +49,32 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@IntDef({STATE_CREATED, STATE_STARTED, STATE_SHUT_DOWN}) @IntDef({STATE_CREATED, STATE_CONFIGURED, STATE_STARTED, STATE_SHUT_DOWN})
private @interface State {} private @interface State {}
private static final int STATE_CREATED = 0; private static final int STATE_CREATED = 0;
private static final int STATE_STARTED = 1; private static final int STATE_CONFIGURED = 1;
private static final int STATE_SHUT_DOWN = 2; 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 MediaCodecAsyncCallback mediaCodecAsyncCallback;
private final MediaCodec codec; private final MediaCodec codec;
private final HandlerThread handlerThread; private final HandlerThread handlerThread;
private @MonotonicNonNull Handler handler; private @MonotonicNonNull Handler handler;
@GuardedBy("lock")
private long pendingFlushCount; private long pendingFlushCount;
private @State int state; private @State int state;
private Runnable codecStartRunnable;
private final MediaCodecInputBufferEnqueuer bufferEnqueuer; private final MediaCodecInputBufferEnqueuer bufferEnqueuer;
@Nullable private IllegalStateException internalException;
@GuardedBy("lock")
@Nullable
private IllegalStateException internalException;
/** /**
* Creates an instance that wraps the specified {@link MediaCodec}. Instances created with this * Creates an instance that wraps the specified {@link MediaCodec}. Instances created with this
...@@ -101,46 +115,52 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -101,46 +115,52 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
boolean enableAsynchronousQueueing, boolean enableAsynchronousQueueing,
int trackType, int trackType,
HandlerThread handlerThread) { HandlerThread handlerThread) {
mediaCodecAsyncCallback = new MediaCodecAsyncCallback(); this.lock = new Object();
this.mediaCodecAsyncCallback = new MediaCodecAsyncCallback();
this.codec = codec; this.codec = codec;
this.handlerThread = handlerThread; this.handlerThread = handlerThread;
state = STATE_CREATED; this.bufferEnqueuer =
codecStartRunnable = codec::start; enableAsynchronousQueueing
if (enableAsynchronousQueueing) { ? new AsynchronousMediaCodecBufferEnqueuer(codec, trackType)
bufferEnqueuer = new AsynchronousMediaCodecBufferEnqueuer(codec, trackType); : new SynchronousMediaCodecBufferEnqueuer(this.codec);
} else { this.state = STATE_CREATED;
bufferEnqueuer = new SynchronousMediaCodecBufferEnqueuer(this.codec);
}
} }
@Override @Override
public synchronized void start() { public void configure(
@Nullable MediaFormat mediaFormat,
@Nullable Surface surface,
@Nullable MediaCrypto crypto,
int flags) {
handlerThread.start(); handlerThread.start();
handler = new Handler(handlerThread.getLooper()); handler = new Handler(handlerThread.getLooper());
codec.setCallback(this, handler); codec.setCallback(this, handler);
codec.configure(mediaFormat, surface, crypto, flags);
state = STATE_CONFIGURED;
}
@Override
public void start() {
bufferEnqueuer.start(); bufferEnqueuer.start();
codecStartRunnable.run(); codec.start();
state = STATE_STARTED; state = STATE_STARTED;
} }
@Override @Override
public void queueInputBuffer( public void queueInputBuffer(
int index, int offset, int size, long presentationTimeUs, int flags) { int index, int offset, int size, long presentationTimeUs, int flags) {
// This method does not need to be synchronized because it does not interact with the
// mediaCodecAsyncCallback.
bufferEnqueuer.queueInputBuffer(index, offset, size, presentationTimeUs, flags); bufferEnqueuer.queueInputBuffer(index, offset, size, presentationTimeUs, flags);
} }
@Override @Override
public void queueSecureInputBuffer( public void queueSecureInputBuffer(
int index, int offset, CryptoInfo info, long presentationTimeUs, int flags) { int index, int offset, CryptoInfo info, long presentationTimeUs, int flags) {
// This method does not need to be synchronized because it does not interact with the
// mediaCodecAsyncCallback.
bufferEnqueuer.queueSecureInputBuffer(index, offset, info, presentationTimeUs, flags); bufferEnqueuer.queueSecureInputBuffer(index, offset, info, presentationTimeUs, flags);
} }
@Override @Override
public synchronized int dequeueInputBufferIndex() { public int dequeueInputBufferIndex() {
synchronized (lock) {
if (isFlushing()) { if (isFlushing()) {
return MediaCodec.INFO_TRY_AGAIN_LATER; return MediaCodec.INFO_TRY_AGAIN_LATER;
} else { } else {
...@@ -148,9 +168,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -148,9 +168,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return mediaCodecAsyncCallback.dequeueInputBufferIndex(); return mediaCodecAsyncCallback.dequeueInputBufferIndex();
} }
} }
}
@Override @Override
public synchronized int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) {
synchronized (lock) {
if (isFlushing()) { if (isFlushing()) {
return MediaCodec.INFO_TRY_AGAIN_LATER; return MediaCodec.INFO_TRY_AGAIN_LATER;
} else { } else {
...@@ -158,64 +180,99 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -158,64 +180,99 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return mediaCodecAsyncCallback.dequeueOutputBufferIndex(bufferInfo); return mediaCodecAsyncCallback.dequeueOutputBufferIndex(bufferInfo);
} }
} }
}
@Override @Override
public synchronized MediaFormat getOutputFormat() { public MediaFormat getOutputFormat() {
synchronized (lock) {
return mediaCodecAsyncCallback.getOutputFormat(); return mediaCodecAsyncCallback.getOutputFormat();
} }
}
@Override @Override
public synchronized void flush() { public void flush() {
synchronized (lock) {
bufferEnqueuer.flush(); bufferEnqueuer.flush();
codec.flush(); codec.flush();
++pendingFlushCount; ++pendingFlushCount;
Util.castNonNull(handler).post(this::onFlushCompleted); Util.castNonNull(handler).post(this::onFlushCompleted);
} }
}
@Override @Override
public synchronized 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) {
handlerThread.quit(); handlerThread.quit();
mediaCodecAsyncCallback.flush(); 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
public synchronized void onInputBufferAvailable(MediaCodec codec, int index) { public MediaCodec getCodec() {
return codec;
}
// Called from the handler thread.
@Override
public void onInputBufferAvailable(MediaCodec codec, int index) {
synchronized (lock) {
mediaCodecAsyncCallback.onInputBufferAvailable(codec, index); mediaCodecAsyncCallback.onInputBufferAvailable(codec, index);
} }
}
@Override @Override
public synchronized void onOutputBufferAvailable( public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {
MediaCodec codec, int index, MediaCodec.BufferInfo info) { synchronized (lock) {
mediaCodecAsyncCallback.onOutputBufferAvailable(codec, index, info); mediaCodecAsyncCallback.onOutputBufferAvailable(codec, index, info);
} }
}
@Override @Override
public synchronized void onError(MediaCodec codec, MediaCodec.CodecException e) { public void onError(MediaCodec codec, MediaCodec.CodecException e) {
synchronized (lock) {
mediaCodecAsyncCallback.onError(codec, e); mediaCodecAsyncCallback.onError(codec, e);
} }
}
@Override @Override
public synchronized void onOutputFormatChanged(MediaCodec codec, MediaFormat format) { public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
synchronized (lock) {
mediaCodecAsyncCallback.onOutputFormatChanged(codec, format); mediaCodecAsyncCallback.onOutputFormatChanged(codec, format);
} }
}
@VisibleForTesting @VisibleForTesting
/* package */ void onMediaCodecError(IllegalStateException e) { /* package */ void onMediaCodecError(IllegalStateException e) {
synchronized (lock) {
mediaCodecAsyncCallback.onMediaCodecError(e); mediaCodecAsyncCallback.onMediaCodecError(e);
} }
}
@VisibleForTesting @VisibleForTesting
/* package */ void setCodecStartRunnable(Runnable codecStartRunnable) { @Nullable
this.codecStartRunnable = codecStartRunnable; /* package */ Looper getLooper() {
return handlerThread.getLooper();
}
private void onFlushCompleted() {
synchronized (lock) {
onFlushCompletedSynchronized();
}
} }
private synchronized void onFlushCompleted() { @GuardedBy("lock")
if (state != STATE_STARTED) { private void onFlushCompletedSynchronized() {
// The adapter has been shutdown. if (state == STATE_SHUT_DOWN) {
return; return;
} }
...@@ -231,7 +288,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -231,7 +288,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
mediaCodecAsyncCallback.flush(); mediaCodecAsyncCallback.flush();
try { try {
codecStartRunnable.run(); codec.start();
} catch (IllegalStateException e) { } catch (IllegalStateException e) {
internalException = e; internalException = e;
} catch (Exception e) { } catch (Exception e) {
...@@ -239,16 +296,19 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -239,16 +296,19 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
} }
private synchronized boolean isFlushing() { @GuardedBy("lock")
private boolean isFlushing() {
return pendingFlushCount > 0; return pendingFlushCount > 0;
} }
private synchronized void maybeThrowException() { @GuardedBy("lock")
private void maybeThrowException() {
maybeThrowInternalException(); maybeThrowInternalException();
mediaCodecAsyncCallback.maybeThrowMediaCodecException(); mediaCodecAsyncCallback.maybeThrowMediaCodecException();
} }
private synchronized void maybeThrowInternalException() { @GuardedBy("lock")
private void maybeThrowInternalException() {
if (internalException != null) { if (internalException != null) {
IllegalStateException e = internalException; IllegalStateException e = internalException;
internalException = null; internalException = null;
......
...@@ -17,7 +17,10 @@ ...@@ -17,7 +17,10 @@
package com.google.android.exoplayer2.mediacodec; package com.google.android.exoplayer2.mediacodec;
import android.media.MediaCodec; import android.media.MediaCodec;
import android.media.MediaCrypto;
import android.media.MediaFormat; import android.media.MediaFormat;
import android.view.Surface;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.decoder.CryptoInfo; import com.google.android.exoplayer2.decoder.CryptoInfo;
/** /**
...@@ -30,12 +33,24 @@ import com.google.android.exoplayer2.decoder.CryptoInfo; ...@@ -30,12 +33,24 @@ import com.google.android.exoplayer2.decoder.CryptoInfo;
* *
* @see com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.MediaCodecOperationMode * @see com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.MediaCodecOperationMode
*/ */
/* package */ interface MediaCodecAdapter { public interface MediaCodecAdapter {
/** /**
* Starts this instance. * Configures this adapter and the underlying {@link MediaCodec}. Needs to be called before {@link
* #start()}.
* *
* @see MediaCodec#start(). * @see MediaCodec#configure(MediaFormat, Surface, MediaCrypto, int)
*/
void configure(
@Nullable MediaFormat mediaFormat,
@Nullable Surface surface,
@Nullable MediaCrypto crypto,
int flags);
/**
* Starts this instance. Needs to be called after {@link #configure}.
*
* @see MediaCodec#start()
*/ */
void start(); void start();
...@@ -109,4 +124,7 @@ import com.google.android.exoplayer2.decoder.CryptoInfo; ...@@ -109,4 +124,7 @@ import com.google.android.exoplayer2.decoder.CryptoInfo;
* is a risk the adapter might interact with a stopped or released {@link MediaCodec}. * is a risk the adapter might interact with a stopped or released {@link MediaCodec}.
*/ */
void shutdown(); void shutdown();
/** Returns the {@link MediaCodec} instance of this adapter. */
MediaCodec getCodec();
} }
...@@ -532,7 +532,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { ...@@ -532,7 +532,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
* Configures a newly created {@link MediaCodec}. * Configures a newly created {@link MediaCodec}.
* *
* @param codecInfo Information about the {@link MediaCodec} being configured. * @param codecInfo Information about the {@link MediaCodec} being configured.
* @param codec The {@link MediaCodec} to configure. * @param codecAdapter The {@link MediaCodecAdapter} to configure.
* @param format The {@link Format} for which the codec is being configured. * @param format The {@link Format} for which the codec is being configured.
* @param crypto For drm protected playbacks, a {@link MediaCrypto} to use for decryption. * @param crypto For drm protected playbacks, a {@link MediaCrypto} to use for decryption.
* @param codecOperatingRate The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if * @param codecOperatingRate The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if
...@@ -540,7 +540,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { ...@@ -540,7 +540,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
*/ */
protected abstract void configureCodec( protected abstract void configureCodec(
MediaCodecInfo codecInfo, MediaCodecInfo codecInfo,
MediaCodec codec, MediaCodecAdapter codecAdapter,
Format format, Format format,
@Nullable MediaCrypto crypto, @Nullable MediaCrypto crypto,
float codecOperatingRate); float codecOperatingRate);
...@@ -1036,8 +1036,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { ...@@ -1036,8 +1036,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
/** /**
* Configures passthrough where no codec is used. Called instead of {@link * Configures passthrough where no codec is used. Called instead of {@link
* #configureCodec(MediaCodecInfo, MediaCodec, Format, MediaCrypto, float)} when no codec is used * #configureCodec(MediaCodecInfo, MediaCodecAdapter, Format, MediaCrypto, float)} when no codec
* in passthrough. * is used in passthrough.
*/ */
private void initPassthrough(Format format) { private void initPassthrough(Format format) {
disablePassthrough(); // In case of transition between 2 passthrough formats. disablePassthrough(); // In case of transition between 2 passthrough formats.
...@@ -1088,7 +1088,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { ...@@ -1088,7 +1088,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
TraceUtil.endSection(); TraceUtil.endSection();
TraceUtil.beginSection("configureCodec"); TraceUtil.beginSection("configureCodec");
configureCodec(codecInfo, codec, inputFormat, crypto, codecOperatingRate); configureCodec(codecInfo, codecAdapter, inputFormat, crypto, codecOperatingRate);
TraceUtil.endSection(); TraceUtil.endSection();
TraceUtil.beginSection("startCodec"); TraceUtil.beginSection("startCodec");
codecAdapter.start(); codecAdapter.start();
......
...@@ -17,7 +17,10 @@ ...@@ -17,7 +17,10 @@
package com.google.android.exoplayer2.mediacodec; package com.google.android.exoplayer2.mediacodec;
import android.media.MediaCodec; import android.media.MediaCodec;
import android.media.MediaCrypto;
import android.media.MediaFormat; import android.media.MediaFormat;
import android.view.Surface;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.decoder.CryptoInfo; import com.google.android.exoplayer2.decoder.CryptoInfo;
/** /**
...@@ -32,6 +35,15 @@ import com.google.android.exoplayer2.decoder.CryptoInfo; ...@@ -32,6 +35,15 @@ import com.google.android.exoplayer2.decoder.CryptoInfo;
} }
@Override @Override
public void configure(
@Nullable MediaFormat mediaFormat,
@Nullable Surface surface,
@Nullable MediaCrypto crypto,
int flags) {
codec.configure(mediaFormat, surface, crypto, flags);
}
@Override
public void start() { public void start() {
codec.start(); codec.start();
} }
...@@ -71,4 +83,9 @@ import com.google.android.exoplayer2.decoder.CryptoInfo; ...@@ -71,4 +83,9 @@ import com.google.android.exoplayer2.decoder.CryptoInfo;
@Override @Override
public void shutdown() {} public void shutdown() {}
@Override
public MediaCodec getCodec() {
return codec;
}
} }
...@@ -42,6 +42,7 @@ import com.google.android.exoplayer2.PlayerMessage.Target; ...@@ -42,6 +42,7 @@ import com.google.android.exoplayer2.PlayerMessage.Target;
import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.mediacodec.MediaCodecAdapter;
import com.google.android.exoplayer2.mediacodec.MediaCodecDecoderException; import com.google.android.exoplayer2.mediacodec.MediaCodecDecoderException;
import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo;
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer;
...@@ -552,7 +553,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -552,7 +553,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
@Override @Override
protected void configureCodec( protected void configureCodec(
MediaCodecInfo codecInfo, MediaCodecInfo codecInfo,
MediaCodec codec, MediaCodecAdapter codecAdapter,
Format format, Format format,
@Nullable MediaCrypto crypto, @Nullable MediaCrypto crypto,
float codecOperatingRate) { float codecOperatingRate) {
...@@ -575,9 +576,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -575,9 +576,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
} }
surface = dummySurface; surface = dummySurface;
} }
codec.configure(mediaFormat, surface, crypto, 0); codecAdapter.configure(mediaFormat, surface, crypto, 0);
if (Util.SDK_INT >= 23 && tunneling) { if (Util.SDK_INT >= 23 && tunneling) {
tunnelingOnFrameRenderedListener = new OnFrameRenderedListenerV23(codec); tunnelingOnFrameRenderedListener = new OnFrameRenderedListenerV23(codecAdapter.getCodec());
} }
} }
......
...@@ -16,32 +16,25 @@ ...@@ -16,32 +16,25 @@
package com.google.android.exoplayer2.mediacodec; 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 com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertThrows;
import static org.robolectric.Shadows.shadowOf; import static org.robolectric.Shadows.shadowOf;
import static org.robolectric.annotation.LooperMode.Mode.LEGACY; import static org.robolectric.annotation.LooperMode.Mode.PAUSED;
import android.media.MediaCodec; import android.media.MediaCodec;
import android.media.MediaFormat; import android.media.MediaFormat;
import android.os.Handler;
import android.os.HandlerThread; import android.os.HandlerThread;
import android.os.Looper;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;
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.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.robolectric.Shadows;
import org.robolectric.annotation.LooperMode; import org.robolectric.annotation.LooperMode;
import org.robolectric.shadows.ShadowLooper;
/** Unit tests for {@link AsynchronousMediaCodecAdapter}. */ /** Unit tests for {@link AsynchronousMediaCodecAdapter}. */
@LooperMode(LEGACY) @LooperMode(PAUSED)
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class AsynchronousMediaCodecAdapterTest { public class AsynchronousMediaCodecAdapterTest {
private AsynchronousMediaCodecAdapter adapter; private AsynchronousMediaCodecAdapter adapter;
...@@ -59,7 +52,6 @@ public class AsynchronousMediaCodecAdapterTest { ...@@ -59,7 +52,6 @@ public class AsynchronousMediaCodecAdapterTest {
/* enableAsynchronousQueueing= */ false, /* enableAsynchronousQueueing= */ false,
/* trackType= */ C.TRACK_TYPE_VIDEO, /* trackType= */ C.TRACK_TYPE_VIDEO,
handlerThread); handlerThread);
adapter.setCodecStartRunnable(() -> {});
bufferInfo = new MediaCodec.BufferInfo(); bufferInfo = new MediaCodec.BufferInfo();
} }
...@@ -67,51 +59,46 @@ public class AsynchronousMediaCodecAdapterTest { ...@@ -67,51 +59,46 @@ public class AsynchronousMediaCodecAdapterTest {
public void tearDown() { public void tearDown() {
adapter.shutdown(); adapter.shutdown();
assertThat(TestHandlerThread.INSTANCES_STARTED.get()).isEqualTo(0); assertThat(handlerThread.hasQuit()).isTrue();
}
@Test
public void startAndShutdown_works() {
adapter.start();
adapter.shutdown();
}
@Test
public void dequeueInputBufferIndex_withAfterFlushFailed_throwsException() {
AtomicInteger codecStartCalls = new AtomicInteger(0);
adapter.setCodecStartRunnable(
() -> {
if (codecStartCalls.incrementAndGet() == 2) {
throw new IllegalStateException("codec#start() exception");
}
});
adapter.start();
adapter.flush();
// Wait until all tasks have been handled.
Shadows.shadowOf(handlerThread.getLooper()).idle();
assertThrows(IllegalStateException.class, () -> adapter.dequeueInputBufferIndex());
} }
@Test @Test
public void dequeueInputBufferIndex_withoutInputBuffer_returnsTryAgainLater() { public void dequeueInputBufferIndex_withoutInputBuffer_returnsTryAgainLater() {
adapter.configure(
createMediaFormat("foo"), /* surface= */ null, /* crypto= */ null, /* flags= */ 0);
adapter.start(); adapter.start();
// After start(), the ShadowMediaCodec offers one input buffer, which is available only if we
// progress the adapter's looper. We don't progress the looper so that the buffer is not
// available.
assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
} }
@Test @Test
public void dequeueInputBufferIndex_withInputBuffer_returnsInputBuffer() { public void dequeueInputBufferIndex_withInputBuffer_returnsInputBuffer() {
adapter.configure(
createMediaFormat("foo"), /* surface= */ null, /* crypto= */ null, /* flags= */ 0);
adapter.start(); adapter.start();
adapter.onInputBufferAvailable(codec, 0); // After start(), the ShadowMediaCodec offers input buffer 0, which is available only if we
// progress the adapter's looper.
shadowOf(adapter.getLooper()).idle();
assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(0); assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(0);
} }
@Test @Test
public void dequeueInputBufferIndex_withPendingFlush_returnsTryAgainLater() { public void dequeueInputBufferIndex_withPendingFlush_returnsTryAgainLater() {
adapter.configure(
createMediaFormat("foo"), /* surface= */ null, /* crypto= */ null, /* flags= */ 0);
adapter.start(); adapter.start();
adapter.onInputBufferAvailable(codec, 0); // After start(), the ShadowMediaCodec offers input buffer 0, which is available only if we
// progress the adapter's looper.
shadowOf(adapter.getLooper()).idle();
// Flush enqueues a task in the looper, but we won't progress the looper to leave flush()
// in a pending state.
adapter.flush(); adapter.flush();
assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
...@@ -119,70 +106,96 @@ public class AsynchronousMediaCodecAdapterTest { ...@@ -119,70 +106,96 @@ public class AsynchronousMediaCodecAdapterTest {
@Test @Test
public void dequeueInputBufferIndex_withFlushCompletedAndInputBuffer_returnsInputBuffer() { public void dequeueInputBufferIndex_withFlushCompletedAndInputBuffer_returnsInputBuffer() {
adapter.configure(
createMediaFormat("foo"), /* surface= */ null, /* crypto= */ null, /* flags= */ 0);
adapter.start(); adapter.start();
Looper looper = handlerThread.getLooper(); // After start(), the ShadowMediaCodec offers input buffer 0, which is available only if we
Handler handler = new Handler(looper); // progress the adapter's looper.
// Enqueue 10 callbacks from codec shadowOf(adapter.getLooper()).idle();
for (int i = 0; i < 10; i++) {
int bufferIndex = i;
handler.post(() -> adapter.onInputBufferAvailable(codec, bufferIndex));
}
adapter.flush(); // Enqueues a flush event after the onInputBufferAvailable callbacks
// Enqueue another onInputBufferAvailable after the flush event
handler.post(() -> adapter.onInputBufferAvailable(codec, 10));
// Wait until all tasks have been handled. adapter.flush();
Shadows.shadowOf(handlerThread.getLooper()).idle(); // Progress the looper to complete flush(): the adapter should call codec.start(), triggering
assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(10); // the ShadowMediaCodec to offer input buffer 0.
shadowOf(adapter.getLooper()).idle();
assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(0);
} }
@Test @Test
public void dequeueInputBufferIndex_withMediaCodecError_throwsException() { public void dequeueInputBufferIndex_withMediaCodecError_throwsException() {
adapter.configure(
createMediaFormat("foo"), /* surface= */ null, /* crypto= */ null, /* flags= */ 0);
adapter.start(); adapter.start();
// Set an error directly on the adapter (not through the looper).
adapter.onMediaCodecError(new IllegalStateException("error from codec")); adapter.onMediaCodecError(new IllegalStateException("error from codec"));
assertThrows(IllegalStateException.class, () -> adapter.dequeueInputBufferIndex()); assertThrows(IllegalStateException.class, () -> adapter.dequeueInputBufferIndex());
} }
@Test @Test
public void dequeueOutputBufferIndex_withInternalException_throwsException() { public void dequeueInputBufferIndex_afterShutdown_returnsTryAgainLater() {
AtomicInteger codecStartCalls = new AtomicInteger(0); adapter.configure(
adapter.setCodecStartRunnable( createMediaFormat("foo"), /* surface= */ null, /* crypto= */ null, /* flags= */ 0);
() -> {
if (codecStartCalls.incrementAndGet() == 2) {
throw new RuntimeException("codec#start() exception");
}
});
adapter.start(); adapter.start();
adapter.flush(); // After start(), the ShadowMediaCodec offers input buffer 0, which is available only if we
// progress the adapter's looper.
shadowOf(adapter.getLooper()).idle();
// Wait until all tasks have been handled. adapter.shutdown();
Shadows.shadowOf(handlerThread.getLooper()).idle();
assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo)); assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
} }
@Test @Test
public void dequeueOutputBufferIndex_withoutInputBuffer_returnsTryAgainLater() { public void dequeueOutputBufferIndex_withoutOutputBuffer_returnsTryAgainLater() {
adapter.configure(
createMediaFormat("foo"), /* surface= */ null, /* crypto= */ null, /* flags= */ 0);
adapter.start(); adapter.start();
// After start(), the ShadowMediaCodec offers an output format change.
shadowOf(adapter.getLooper()).idle();
assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) assertThat(adapter.dequeueOutputBufferIndex(bufferInfo))
.isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
// Assert that output buffer is available.
assertThat(adapter.dequeueOutputBufferIndex(bufferInfo))
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
} }
@Test @Test
public void dequeueOutputBufferIndex_withOutputBuffer_returnsOutputBuffer() { public void dequeueOutputBufferIndex_withOutputBuffer_returnsOutputBuffer() {
adapter.configure(
createMediaFormat("foo"), /* surface= */ null, /* crypto= */ null, /* flags= */ 0);
adapter.start(); adapter.start();
MediaCodec.BufferInfo enqueuedBufferInfo = new MediaCodec.BufferInfo(); // After start(), the ShadowMediaCodec offers input buffer 0, which is available only if we
adapter.onOutputBufferAvailable(codec, 0, enqueuedBufferInfo); // progress the adapter's looper.
shadowOf(adapter.getLooper()).idle();
assertThat(adapter.dequeueOutputBufferIndex((bufferInfo))).isEqualTo(0); int index = adapter.dequeueInputBufferIndex();
assertBufferInfosEqual(enqueuedBufferInfo, bufferInfo); adapter.queueInputBuffer(index, 0, 0, 0, 0);
// Progress the looper so that the ShadowMediaCodec processes the input buffer.
shadowOf(adapter.getLooper()).idle();
// The ShadowMediaCodec will first offer an output format and then the output buffer.
assertThat(adapter.dequeueOutputBufferIndex(bufferInfo))
.isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
// Assert it's the ShadowMediaCodec's output format
assertThat(adapter.getOutputFormat().getByteBuffer("csd-0")).isNotNull();
assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)).isEqualTo(index);
} }
@Test @Test
public void dequeueOutputBufferIndex_withPendingFlush_returnsTryAgainLater() { public void dequeueOutputBufferIndex_withPendingFlush_returnsTryAgainLater() {
adapter.configure(
createMediaFormat("foo"), /* surface= */ null, /* crypto= */ null, /* flags= */ 0);
adapter.start(); adapter.start();
adapter.dequeueOutputBufferIndex(bufferInfo); // After start(), the ShadowMediaCodec offers input buffer 0, which is available only if we
// progress the adapter's looper.
shadowOf(adapter.getLooper()).idle();
// Flush enqueues a task in the looper, but we won't progress the looper to leave flush()
// in a pending state.
adapter.flush(); adapter.flush();
assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) assertThat(adapter.dequeueOutputBufferIndex(bufferInfo))
...@@ -190,82 +203,40 @@ public class AsynchronousMediaCodecAdapterTest { ...@@ -190,82 +203,40 @@ public class AsynchronousMediaCodecAdapterTest {
} }
@Test @Test
public void dequeueOutputBufferIndex_withFlushCompletedAndOutputBuffer_returnsOutputBuffer() {
adapter.start();
Looper looper = handlerThread.getLooper();
Handler handler = new Handler(looper);
// Enqueue 10 callbacks from codec
for (int i = 0; i < 10; i++) {
int bufferIndex = i;
MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo();
outBufferInfo.presentationTimeUs = i;
handler.post(() -> adapter.onOutputBufferAvailable(codec, bufferIndex, outBufferInfo));
}
adapter.flush(); // Enqueues a flush event after the onOutputBufferAvailable callbacks
// Enqueue another onOutputBufferAvailable after the flush event
MediaCodec.BufferInfo lastBufferInfo = new MediaCodec.BufferInfo();
lastBufferInfo.presentationTimeUs = 10;
handler.post(() -> adapter.onOutputBufferAvailable(codec, 10, lastBufferInfo));
// Wait until all tasks have been handled.
Shadows.shadowOf(handlerThread.getLooper()).idle();
assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)).isEqualTo(10);
assertBufferInfosEqual(lastBufferInfo, bufferInfo);
}
@Test
public void dequeueOutputBufferIndex_withMediaCodecError_throwsException() { public void dequeueOutputBufferIndex_withMediaCodecError_throwsException() {
adapter.configure(
createMediaFormat("foo"), /* surface= */ null, /* crypto= */ null, /* flags= */ 0);
adapter.start(); adapter.start();
// Set an error directly on the adapter.
adapter.onMediaCodecError(new IllegalStateException("error from codec")); adapter.onMediaCodecError(new IllegalStateException("error from codec"));
assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo)); assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo));
} }
@Test @Test
public void dequeueOutputBufferIndex_withPendingOutputFormat_returnsPendingOutputFormat() { public void dequeueOutputBufferIndex_afterShutdown_returnsTryAgainLater() {
adapter.start(); adapter.configure(
MediaCodec.BufferInfo outputBufferInfo = new MediaCodec.BufferInfo(); createMediaFormat("foo"), /* surface= */ null, /* crypto= */ null, /* flags= */ 0);
MediaFormat pendingMediaFormat = new MediaFormat();
adapter.onOutputFormatChanged(codec, new MediaFormat());
adapter.onOutputBufferAvailable(codec, /* index= */ 0, new MediaCodec.BufferInfo());
adapter.onOutputFormatChanged(codec, pendingMediaFormat);
adapter.onOutputBufferAvailable(codec, /* index= */ 1, new MediaCodec.BufferInfo());
// Flush should clear the output queue except from the last pending output format received.
adapter.flush();
shadowOf(handlerThread.getLooper()).idle();
adapter.onOutputBufferAvailable(codec, /* index= */ 2, new MediaCodec.BufferInfo());
assertThat(adapter.dequeueOutputBufferIndex(outputBufferInfo))
.isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
assertThat(adapter.getOutputFormat()).isEqualTo(pendingMediaFormat);
assertThat(adapter.dequeueOutputBufferIndex(outputBufferInfo)).isEqualTo(2);
}
@Test
public void dequeueOutputBufferIndex_withPendingAndNewOutputFormat_returnsNewOutputFormat() {
adapter.start(); adapter.start();
MediaCodec.BufferInfo outputBufferInfo = new MediaCodec.BufferInfo(); // After start(), the ShadowMediaCodec offers input buffer 0, which is available only if we
MediaFormat pendingMediaFormat = new MediaFormat(); // progress the adapter's looper.
MediaFormat latestOutputFormat = new MediaFormat(); shadowOf(adapter.getLooper()).idle();
adapter.onOutputFormatChanged(codec, new MediaFormat()); int index = adapter.dequeueInputBufferIndex();
adapter.onOutputBufferAvailable(codec, /* index= */ 0, new MediaCodec.BufferInfo()); adapter.queueInputBuffer(index, 0, 0, 0, 0);
adapter.onOutputFormatChanged(codec, pendingMediaFormat); // Progress the looper so that the ShadowMediaCodec processes the input buffer.
adapter.onOutputBufferAvailable(codec, /* index= */ 1, new MediaCodec.BufferInfo()); shadowOf(adapter.getLooper()).idle();
// Flush should clear the output queue except from the last pending output format received. adapter.shutdown();
adapter.flush();
shadowOf(handlerThread.getLooper()).idle();
// New output format should overwrite the pending format.
adapter.onOutputFormatChanged(codec, latestOutputFormat);
assertThat(adapter.dequeueOutputBufferIndex(outputBufferInfo)) assertThat(adapter.dequeueOutputBufferIndex(bufferInfo))
.isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED); .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
assertThat(adapter.getOutputFormat()).isEqualTo(latestOutputFormat);
} }
@Test @Test
public void getOutputFormat_withoutFormatReceived_throwsException() { public void getOutputFormat_withoutFormatReceived_throwsException() {
adapter.configure(
createMediaFormat("foo"), /* surface= */ null, /* crypto= */ null, /* flags= */ 0);
adapter.start(); adapter.start();
assertThrows(IllegalStateException.class, () -> adapter.getOutputFormat()); assertThrows(IllegalStateException.class, () -> adapter.getOutputFormat());
...@@ -273,107 +244,67 @@ public class AsynchronousMediaCodecAdapterTest { ...@@ -273,107 +244,67 @@ public class AsynchronousMediaCodecAdapterTest {
@Test @Test
public void getOutputFormat_withMultipleFormats_returnsCorrectFormat() { public void getOutputFormat_withMultipleFormats_returnsCorrectFormat() {
adapter.configure(
createMediaFormat("foo"), /* surface= */ null, /* crypto= */ null, /* flags= */ 0);
adapter.start(); adapter.start();
MediaFormat[] formats = new MediaFormat[10]; // After start(), the ShadowMediaCodec offers an output format, which is available only if we
for (int i = 0; i < formats.length; i++) { // progress the adapter's looper.
formats[i] = new MediaFormat(); shadowOf(adapter.getLooper()).idle();
adapter.onOutputFormatChanged(codec, formats[i]);
} // Add another format directly on the adapter.
adapter.onOutputFormatChanged(codec, createMediaFormat("format2"));
for (int i = 0; i < 10; i++) {
assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) assertThat(adapter.dequeueOutputBufferIndex(bufferInfo))
.isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED); .isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
assertThat(adapter.getOutputFormat()).isEqualTo(formats[i]); // The first format is the ShadowMediaCodec's output format.
// A subsequent call to getOutputFormat() should return the previously fetched format assertThat(adapter.getOutputFormat().getByteBuffer("csd-0")).isNotNull();
assertThat(adapter.getOutputFormat()).isEqualTo(formats[i]); assertThat(adapter.dequeueOutputBufferIndex(bufferInfo))
} .isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
// The 2nd format is the format we enqueued 'manually' above.
assertThat(adapter.getOutputFormat().getString("name")).isEqualTo("format2");
assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) assertThat(adapter.dequeueOutputBufferIndex(bufferInfo))
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
} }
@Test @Test
public void getOutputFormat_afterFlush_returnsPreviousFormat() { public void getOutputFormat_afterFlush_returnsPreviousFormat() {
MediaFormat format = new MediaFormat(); adapter.configure(
createMediaFormat("foo"), /* surface= */ null, /* crypto= */ null, /* flags= */ 0);
adapter.start(); adapter.start();
adapter.onOutputFormatChanged(codec, format); // After start(), the ShadowMediaCodec offers an output format, which is available only if we
// progress the adapter's looper.
assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) shadowOf(adapter.getLooper()).idle();
.isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
assertThat(adapter.getOutputFormat()).isEqualTo(format);
adapter.dequeueOutputBufferIndex(bufferInfo);
MediaFormat outputFormat = adapter.getOutputFormat();
// Flush the adapter and progress the looper so that flush is completed.
adapter.flush(); adapter.flush();
shadowOf(adapter.getLooper()).idle();
// Wait until all tasks have been handled. assertThat(adapter.getOutputFormat()).isEqualTo(outputFormat);
Shadows.shadowOf(handlerThread.getLooper()).idle();
assertThat(adapter.getOutputFormat()).isEqualTo(format);
}
@Test
public void flush_multipleTimes_onlyLastFlushExecutes() {
AtomicInteger codecStartCalls = new AtomicInteger(0);
adapter.setCodecStartRunnable(() -> codecStartCalls.incrementAndGet());
adapter.start();
Looper looper = handlerThread.getLooper();
Handler handler = new Handler(looper);
handler.post(() -> adapter.onInputBufferAvailable(codec, 0));
adapter.flush(); // Enqueues a flush event
handler.post(() -> adapter.onInputBufferAvailable(codec, 2));
AtomicInteger milestoneCount = new AtomicInteger(0);
handler.post(() -> milestoneCount.incrementAndGet());
adapter.flush(); // Enqueues a second flush event
handler.post(() -> adapter.onInputBufferAvailable(codec, 3));
// Progress the looper until the milestoneCount is increased.
// adapter.start() will call codec.start(). First flush event should not call codec.start().
ShadowLooper shadowLooper = shadowOf(looper);
while (milestoneCount.get() < 1) {
shadowLooper.runOneTask();
} }
assertThat(codecStartCalls.get()).isEqualTo(1);
// Wait until all tasks have been handled. private static MediaFormat createMediaFormat(String name) {
shadowLooper.idle(); MediaFormat format = new MediaFormat();
assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(3); format.setString("name", name);
assertThat(codecStartCalls.get()).isEqualTo(2); return format;
}
@Test
public void flush_andImmediatelyShutdown_flushIsNoOp() {
AtomicInteger onCodecStartCount = new AtomicInteger(0);
adapter.setCodecStartRunnable(() -> onCodecStartCount.incrementAndGet());
adapter.start();
// Grab reference to Looper before shutting down the adapter otherwise handlerThread.getLooper()
// might return null.
Looper looper = handlerThread.getLooper();
adapter.flush();
adapter.shutdown();
// Wait until all tasks have been handled.
Shadows.shadowOf(looper).idle();
// Only adapter.start() calls onCodecStart.
assertThat(onCodecStartCount.get()).isEqualTo(1);
} }
private static class TestHandlerThread extends HandlerThread { private static class TestHandlerThread extends HandlerThread {
private static final AtomicLong INSTANCES_STARTED = new AtomicLong(0); private boolean quit;
public TestHandlerThread(String name) { TestHandlerThread(String label) {
super(name); super(label);
} }
@Override public boolean hasQuit() {
public synchronized void start() { return quit;
super.start();
INSTANCES_STARTED.incrementAndGet();
} }
@Override @Override
public boolean quit() { public boolean quit() {
boolean quit = super.quit(); quit = true;
if (quit) { return super.quit();
INSTANCES_STARTED.decrementAndGet();
}
return quit;
} }
} }
} }
...@@ -27,6 +27,7 @@ import com.google.android.exoplayer2.Format; ...@@ -27,6 +27,7 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.mediacodec.MediaCodecAdapter;
import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo;
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
...@@ -107,7 +108,7 @@ import java.util.ArrayList; ...@@ -107,7 +108,7 @@ import java.util.ArrayList;
@Override @Override
protected void configureCodec( protected void configureCodec(
MediaCodecInfo codecInfo, MediaCodecInfo codecInfo,
MediaCodec codec, MediaCodecAdapter codecAdapter,
Format format, Format format,
MediaCrypto crypto, MediaCrypto crypto,
float operatingRate) { float operatingRate) {
...@@ -117,7 +118,7 @@ import java.util.ArrayList; ...@@ -117,7 +118,7 @@ import java.util.ArrayList;
// dropped frames allowed, this is not desired behavior. Hence we skip (rather than drop) // dropped frames allowed, this is not desired behavior. Hence we skip (rather than drop)
// frames up to the current playback position [Internal: b/66494991]. // frames up to the current playback position [Internal: b/66494991].
skipToPositionBeforeRenderingFirstFrame = getState() == Renderer.STATE_STARTED; skipToPositionBeforeRenderingFirstFrame = getState() == Renderer.STATE_STARTED;
super.configureCodec(codecInfo, codec, format, crypto, operatingRate); super.configureCodec(codecInfo, codecAdapter, format, crypto, operatingRate);
} }
@Override @Override
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment