Reduce flakiness for MediaCodecVideoRendererTests past SDK 30

PiperOrigin-RevId: 715761006
This commit is contained in:
michaelkatz 2025-01-15 05:34:53 -08:00 committed by Copybara-Service
parent a2016f03c6
commit 62341f31f9
3 changed files with 93 additions and 13 deletions

View File

@ -74,8 +74,15 @@ import java.nio.ByteBuffer;
new HandlerThread(createQueueingThreadLabel(trackType))); new HandlerThread(createQueueingThreadLabel(trackType)));
} }
@VisibleForTesting /**
/* package */ Factory( * Creates an factory for {@link AsynchronousMediaCodecAdapter} instances.
*
* @param callbackThreadSupplier A supplier of {@link HandlerThread} used for {@link MediaCodec}
* callbacks invoked when buffers are available.
* @param queueingThreadSupplier A supplier of {@link HandlerThread} to use for queueing
* buffers.
*/
public Factory(
Supplier<HandlerThread> callbackThreadSupplier, Supplier<HandlerThread> callbackThreadSupplier,
Supplier<HandlerThread> queueingThreadSupplier) { Supplier<HandlerThread> queueingThreadSupplier) {
this.callbackThreadSupplier = callbackThreadSupplier; this.callbackThreadSupplier = callbackThreadSupplier;

View File

@ -19,12 +19,14 @@ import static java.lang.annotation.ElementType.TYPE_USE;
import android.content.Context; import android.content.Context;
import android.media.MediaCodec; import android.media.MediaCodec;
import android.os.HandlerThread;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.MimeTypes; import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.Log; import androidx.media3.common.util.Log;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import com.google.common.base.Supplier;
import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.IOException; import java.io.IOException;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
@ -57,6 +59,8 @@ public final class DefaultMediaCodecAdapterFactory implements MediaCodecAdapter.
private static final String TAG = "DMCodecAdapterFactory"; private static final String TAG = "DMCodecAdapterFactory";
@Nullable private final Context context; @Nullable private final Context context;
@Nullable private final Supplier<HandlerThread> callbackThreadSupplier;
@Nullable private final Supplier<HandlerThread> queueingThreadSupplier;
private @Mode int asynchronousMode; private @Mode int asynchronousMode;
private boolean asyncCryptoFlagEnabled; private boolean asyncCryptoFlagEnabled;
@ -69,6 +73,8 @@ public final class DefaultMediaCodecAdapterFactory implements MediaCodecAdapter.
asynchronousMode = MODE_DEFAULT; asynchronousMode = MODE_DEFAULT;
asyncCryptoFlagEnabled = false; asyncCryptoFlagEnabled = false;
context = null; context = null;
callbackThreadSupplier = null;
queueingThreadSupplier = null;
} }
/** /**
@ -77,9 +83,26 @@ public final class DefaultMediaCodecAdapterFactory implements MediaCodecAdapter.
* @param context A {@link Context}. * @param context A {@link Context}.
*/ */
public DefaultMediaCodecAdapterFactory(Context context) { public DefaultMediaCodecAdapterFactory(Context context) {
this(context, null, null);
}
/**
* Creates the default media codec adapter factory.
*
* @param context A {@link Context}.
* @param callbackThreadSupplier A supplier of {@link HandlerThread} used for {@link MediaCodec}
* callbacks invoked when buffers are available.
* @param queueingThreadSupplier A supplier of {@link HandlerThread} to use for queueing buffers.
*/
public DefaultMediaCodecAdapterFactory(
Context context,
@Nullable Supplier<HandlerThread> callbackThreadSupplier,
@Nullable Supplier<HandlerThread> queueingThreadSupplier) {
this.context = context; this.context = context;
asynchronousMode = MODE_DEFAULT; asynchronousMode = MODE_DEFAULT;
asyncCryptoFlagEnabled = false; asyncCryptoFlagEnabled = false;
this.callbackThreadSupplier = callbackThreadSupplier;
this.queueingThreadSupplier = queueingThreadSupplier;
} }
/** /**
@ -132,7 +155,10 @@ public final class DefaultMediaCodecAdapterFactory implements MediaCodecAdapter.
"Creating an asynchronous MediaCodec adapter for track type " "Creating an asynchronous MediaCodec adapter for track type "
+ Util.getTrackTypeString(trackType)); + Util.getTrackTypeString(trackType));
AsynchronousMediaCodecAdapter.Factory factory = AsynchronousMediaCodecAdapter.Factory factory =
new AsynchronousMediaCodecAdapter.Factory(trackType); callbackThreadSupplier != null && queueingThreadSupplier != null
? new AsynchronousMediaCodecAdapter.Factory(
callbackThreadSupplier, queueingThreadSupplier)
: new AsynchronousMediaCodecAdapter.Factory(trackType);
factory.experimentalSetAsyncCryptoFlagEnabled(asyncCryptoFlagEnabled); factory.experimentalSetAsyncCryptoFlagEnabled(asyncCryptoFlagEnabled);
return factory.createAdapter(configuration); return factory.createAdapter(configuration);
} }

View File

@ -38,6 +38,7 @@ import android.media.MediaCodecInfo.CodecProfileLevel;
import android.media.MediaFormat; import android.media.MediaFormat;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper; import android.os.Looper;
import android.os.PersistableBundle; import android.os.PersistableBundle;
import android.os.SystemClock; import android.os.SystemClock;
@ -61,6 +62,7 @@ import androidx.media3.exoplayer.RendererConfiguration;
import androidx.media3.exoplayer.analytics.PlayerId; import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.drm.DrmSessionEventListener; import androidx.media3.exoplayer.drm.DrmSessionEventListener;
import androidx.media3.exoplayer.drm.DrmSessionManager; import androidx.media3.exoplayer.drm.DrmSessionManager;
import androidx.media3.exoplayer.mediacodec.DefaultMediaCodecAdapterFactory;
import androidx.media3.exoplayer.mediacodec.MediaCodecAdapter; import androidx.media3.exoplayer.mediacodec.MediaCodecAdapter;
import androidx.media3.exoplayer.mediacodec.MediaCodecInfo; import androidx.media3.exoplayer.mediacodec.MediaCodecInfo;
import androidx.media3.exoplayer.mediacodec.MediaCodecSelector; import androidx.media3.exoplayer.mediacodec.MediaCodecSelector;
@ -89,6 +91,7 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
@ -106,7 +109,6 @@ import org.robolectric.shadows.ShadowLooper;
import org.robolectric.shadows.ShadowSystemClock; import org.robolectric.shadows.ShadowSystemClock;
/** Unit test for {@link MediaCodecVideoRenderer}. */ /** Unit test for {@link MediaCodecVideoRenderer}. */
@Config(sdk = 30) // TODO: b/382017156 - Remove this when the tests pass on API 31+.
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class MediaCodecVideoRendererTest { public class MediaCodecVideoRendererTest {
@Rule public final MockitoRule mockito = MockitoJUnit.rule(); @Rule public final MockitoRule mockito = MockitoJUnit.rule();
@ -146,6 +148,9 @@ public class MediaCodecVideoRendererTest {
/* forceDisableAdaptive= */ false, /* forceDisableAdaptive= */ false,
/* forceSecure= */ false); /* forceSecure= */ false);
private final AtomicReference<HandlerThread> callbackThread = new AtomicReference<>();
private final AtomicReference<HandlerThread> queueingThread = new AtomicReference<>();
private Looper testMainLooper; private Looper testMainLooper;
private Surface surface; private Surface surface;
private MediaCodecVideoRenderer mediaCodecVideoRenderer; private MediaCodecVideoRenderer mediaCodecVideoRenderer;
@ -170,12 +175,22 @@ public class MediaCodecVideoRendererTest {
/* vendor= */ false, /* vendor= */ false,
/* forceDisableAdaptive= */ false, /* forceDisableAdaptive= */ false,
/* forceSecure= */ false)); /* forceSecure= */ false));
mediaCodecVideoRenderer = mediaCodecVideoRenderer =
new MediaCodecVideoRenderer( new MediaCodecVideoRenderer(
ApplicationProvider.getApplicationContext(), ApplicationProvider.getApplicationContext(),
new DefaultMediaCodecAdapterFactory(
ApplicationProvider.getApplicationContext(),
() -> {
callbackThread.set(new HandlerThread("MCVRTest:MediaCodecAsyncAdapter"));
return callbackThread.get();
},
() -> {
queueingThread.set(new HandlerThread("MCVRTest:MediaCodecQueueingThread"));
return queueingThread.get();
}),
mediaCodecSelector, mediaCodecSelector,
/* allowedJoiningTimeMs= */ 0, /* allowedJoiningTimeMs= */ 0,
/* enableDecoderFallback= */ false,
/* eventHandler= */ new Handler(testMainLooper), /* eventHandler= */ new Handler(testMainLooper),
/* eventListener= */ eventListener, /* eventListener= */ eventListener,
/* maxDroppedFramesToNotify= */ 1) { /* maxDroppedFramesToNotify= */ 1) {
@ -230,11 +245,15 @@ public class MediaCodecVideoRendererTest {
mediaCodecVideoRenderer.start(); mediaCodecVideoRenderer.start();
mediaCodecVideoRenderer.render(0, SystemClock.elapsedRealtime() * 1000); mediaCodecVideoRenderer.render(0, SystemClock.elapsedRealtime() * 1000);
for (int i = 0; i < 5; i++) {
mediaCodecVideoRenderer.render(40_000, SystemClock.elapsedRealtime() * 1000); mediaCodecVideoRenderer.render(40_000, SystemClock.elapsedRealtime() * 1000);
maybeIdleAsynchronousMediaCodecAdapterThreads();
}
mediaCodecVideoRenderer.setCurrentStreamFinal(); mediaCodecVideoRenderer.setCurrentStreamFinal();
int posUs = 80_001; // Ensures buffer will be 30_001us late. int posUs = 80_001; // Ensures buffer will be 30_001us late.
while (!mediaCodecVideoRenderer.isEnded()) { while (!mediaCodecVideoRenderer.isEnded()) {
mediaCodecVideoRenderer.render(posUs, SystemClock.elapsedRealtime() * 1000); mediaCodecVideoRenderer.render(posUs, SystemClock.elapsedRealtime() * 1000);
maybeIdleAsynchronousMediaCodecAdapterThreads();
posUs += 40_000; posUs += 40_000;
} }
shadowOf(testMainLooper).idle(); shadowOf(testMainLooper).idle();
@ -273,7 +292,9 @@ public class MediaCodecVideoRendererTest {
mediaCodecVideoRenderer.start(); mediaCodecVideoRenderer.start();
mediaCodecVideoRenderer.render(0, SystemClock.elapsedRealtime() * 1000); mediaCodecVideoRenderer.render(0, SystemClock.elapsedRealtime() * 1000);
while (decoderCounters.renderedOutputBufferCount == 0) {
mediaCodecVideoRenderer.render(10_000, SystemClock.elapsedRealtime() * 1000); mediaCodecVideoRenderer.render(10_000, SystemClock.elapsedRealtime() * 1000);
}
// Ensure existing buffer will be 1 second late and new (not yet read) buffers are available // Ensure existing buffer will be 1 second late and new (not yet read) buffers are available
// to be skipped and to skip to in the input stream. // to be skipped and to skip to in the input stream.
int posUs = 1_020_000; int posUs = 1_020_000;
@ -322,13 +343,16 @@ public class MediaCodecVideoRendererTest {
/* positionUs= */ 0, /* positionUs= */ 0,
/* joining= */ true, /* joining= */ true,
/* mayRenderStartOfStream= */ false, /* mayRenderStartOfStream= */ false,
/* startPositionUs= */ 50_000, /* startPositionUs= */ 0,
/* offsetUs= */ 0, /* offsetUs= */ 0,
/* mediaPeriodId= */ new MediaSource.MediaPeriodId(new Object())); /* mediaPeriodId= */ new MediaSource.MediaPeriodId(new Object()));
mediaCodecVideoRenderer.start(); mediaCodecVideoRenderer.start();
mediaCodecVideoRenderer.setCurrentStreamFinal(); mediaCodecVideoRenderer.setCurrentStreamFinal();
for (int i = 0; i < 5; i++) {
mediaCodecVideoRenderer.render(0, SystemClock.elapsedRealtime() * 1000); mediaCodecVideoRenderer.render(0, SystemClock.elapsedRealtime() * 1000);
maybeIdleAsynchronousMediaCodecAdapterThreads();
}
int posUs = 20_001; // Ensures buffer will be 29_999us early. int posUs = 20_001; // Ensures buffer will be 29_999us early.
mediaCodecVideoRenderer.render(posUs, SystemClock.elapsedRealtime() * 1000); mediaCodecVideoRenderer.render(posUs, SystemClock.elapsedRealtime() * 1000);
shadowOf(testMainLooper).idle(); shadowOf(testMainLooper).idle();
@ -360,12 +384,15 @@ public class MediaCodecVideoRendererTest {
/* positionUs= */ 0, /* positionUs= */ 0,
/* joining= */ true, /* joining= */ true,
/* mayRenderStartOfStream= */ false, /* mayRenderStartOfStream= */ false,
/* startPositionUs= */ 50_000, /* startPositionUs= */ 0,
/* offsetUs= */ 0, /* offsetUs= */ 0,
/* mediaPeriodId= */ new MediaSource.MediaPeriodId(new Object())); /* mediaPeriodId= */ new MediaSource.MediaPeriodId(new Object()));
mediaCodecVideoRenderer.setCurrentStreamFinal(); mediaCodecVideoRenderer.setCurrentStreamFinal();
for (int i = 0; i < 5; i++) {
mediaCodecVideoRenderer.render(0, SystemClock.elapsedRealtime() * 1000); mediaCodecVideoRenderer.render(0, SystemClock.elapsedRealtime() * 1000);
maybeIdleAsynchronousMediaCodecAdapterThreads();
}
int posUs = 20_001; // Ensures buffer will be 29_999us early. int posUs = 20_001; // Ensures buffer will be 29_999us early.
for (int i = 0; i < 3; i++) { for (int i = 0; i < 3; i++) {
mediaCodecVideoRenderer.render(posUs, SystemClock.elapsedRealtime() * 1000); mediaCodecVideoRenderer.render(posUs, SystemClock.elapsedRealtime() * 1000);
@ -876,9 +903,10 @@ public class MediaCodecVideoRendererTest {
int positionUs = 20_000; int positionUs = 20_000;
do { do {
ShadowSystemClock.advanceBy(10, TimeUnit.MILLISECONDS); ShadowSystemClock.advanceBy(2, TimeUnit.MILLISECONDS);
mediaCodecVideoRenderer.render(positionUs, msToUs(SystemClock.elapsedRealtime())); mediaCodecVideoRenderer.render(positionUs, msToUs(SystemClock.elapsedRealtime()));
positionUs += 10_000; maybeIdleAsynchronousMediaCodecAdapterThreads();
positionUs += 2_000;
} while (!mediaCodecVideoRenderer.isEnded()); } while (!mediaCodecVideoRenderer.isEnded());
shadowOf(testMainLooper).idle(); shadowOf(testMainLooper).idle();
@ -956,12 +984,15 @@ public class MediaCodecVideoRendererTest {
new MediaSource.MediaPeriodId(new Object())); new MediaSource.MediaPeriodId(new Object()));
for (int i = 0; i < 10; i++) { for (int i = 0; i < 10; i++) {
mediaCodecVideoRenderer.render(/* positionUs= */ 0, SystemClock.elapsedRealtime() * 1000); mediaCodecVideoRenderer.render(/* positionUs= */ 0, SystemClock.elapsedRealtime() * 1000);
maybeIdleAsynchronousMediaCodecAdapterThreads();
} }
shadowOf(testMainLooper).idle(); shadowOf(testMainLooper).idle();
verify(eventListener).onRenderedFirstFrame(eq(surface), /* renderTimeMs= */ anyLong()); verify(eventListener).onRenderedFirstFrame(eq(surface), /* renderTimeMs= */ anyLong());
} }
@Config(minSdk = 30)
@Test @Test
public void enable_withoutMayRenderStartOfStream_doesNotRenderFirstFrameBeforeStart() public void enable_withoutMayRenderStartOfStream_doesNotRenderFirstFrameBeforeStart()
throws Exception { throws Exception {
@ -987,6 +1018,7 @@ public class MediaCodecVideoRendererTest {
new MediaSource.MediaPeriodId(new Object())); new MediaSource.MediaPeriodId(new Object()));
for (int i = 0; i < 10; i++) { for (int i = 0; i < 10; i++) {
mediaCodecVideoRenderer.render(/* positionUs= */ 0, SystemClock.elapsedRealtime() * 1000); mediaCodecVideoRenderer.render(/* positionUs= */ 0, SystemClock.elapsedRealtime() * 1000);
maybeIdleAsynchronousMediaCodecAdapterThreads();
} }
shadowOf(testMainLooper).idle(); shadowOf(testMainLooper).idle();
@ -1018,6 +1050,7 @@ public class MediaCodecVideoRendererTest {
mediaCodecVideoRenderer.start(); mediaCodecVideoRenderer.start();
for (int i = 0; i < 10; i++) { for (int i = 0; i < 10; i++) {
mediaCodecVideoRenderer.render(/* positionUs= */ 0, SystemClock.elapsedRealtime() * 1000); mediaCodecVideoRenderer.render(/* positionUs= */ 0, SystemClock.elapsedRealtime() * 1000);
maybeIdleAsynchronousMediaCodecAdapterThreads();
} }
shadowOf(testMainLooper).idle(); shadowOf(testMainLooper).idle();
@ -1051,6 +1084,7 @@ public class MediaCodecVideoRendererTest {
new MediaSource.MediaPeriodId(new Object())); new MediaSource.MediaPeriodId(new Object()));
for (int i = 0; i < 10; i++) { for (int i = 0; i < 10; i++) {
mediaCodecVideoRenderer.render(/* positionUs= */ 0, SystemClock.elapsedRealtime() * 1000); mediaCodecVideoRenderer.render(/* positionUs= */ 0, SystemClock.elapsedRealtime() * 1000);
maybeIdleAsynchronousMediaCodecAdapterThreads();
} }
shadowOf(testMainLooper).idle(); shadowOf(testMainLooper).idle();
@ -1100,6 +1134,7 @@ public class MediaCodecVideoRendererTest {
for (int i = 0; i < 10; i++) { for (int i = 0; i < 10; i++) {
mediaCodecVideoRenderer.render( mediaCodecVideoRenderer.render(
/* positionUs= */ i * 10, SystemClock.elapsedRealtime() * 1000); /* positionUs= */ i * 10, SystemClock.elapsedRealtime() * 1000);
maybeIdleAsynchronousMediaCodecAdapterThreads();
if (!replacedStream && mediaCodecVideoRenderer.hasReadStreamToEnd()) { if (!replacedStream && mediaCodecVideoRenderer.hasReadStreamToEnd()) {
mediaCodecVideoRenderer.replaceStream( mediaCodecVideoRenderer.replaceStream(
new Format[] {VIDEO_H264}, new Format[] {VIDEO_H264},
@ -1164,6 +1199,7 @@ public class MediaCodecVideoRendererTest {
for (int i = 0; i < 10; i++) { for (int i = 0; i < 10; i++) {
mediaCodecVideoRenderer.render( mediaCodecVideoRenderer.render(
/* positionUs= */ i * 10, SystemClock.elapsedRealtime() * 1000); /* positionUs= */ i * 10, SystemClock.elapsedRealtime() * 1000);
maybeIdleAsynchronousMediaCodecAdapterThreads();
if (!replacedStream && mediaCodecVideoRenderer.hasReadStreamToEnd()) { if (!replacedStream && mediaCodecVideoRenderer.hasReadStreamToEnd()) {
mediaCodecVideoRenderer.replaceStream( mediaCodecVideoRenderer.replaceStream(
new Format[] {VIDEO_H264}, new Format[] {VIDEO_H264},
@ -1212,6 +1248,7 @@ public class MediaCodecVideoRendererTest {
// Render at the original start position. // Render at the original start position.
for (int i = 0; i < 10; i++) { for (int i = 0; i < 10; i++) {
mediaCodecVideoRenderer.render(/* positionUs= */ 1000, SystemClock.elapsedRealtime() * 1000); mediaCodecVideoRenderer.render(/* positionUs= */ 1000, SystemClock.elapsedRealtime() * 1000);
maybeIdleAsynchronousMediaCodecAdapterThreads();
} }
// Reset the position to before the original start position and render at this position. // Reset the position to before the original start position and render at this position.
@ -1221,6 +1258,7 @@ public class MediaCodecVideoRendererTest {
fakeSampleStream.writeData(/* startPositionUs= */ 500); fakeSampleStream.writeData(/* startPositionUs= */ 500);
for (int i = 0; i < 10; i++) { for (int i = 0; i < 10; i++) {
mediaCodecVideoRenderer.render(/* positionUs= */ 500, SystemClock.elapsedRealtime() * 1000); mediaCodecVideoRenderer.render(/* positionUs= */ 500, SystemClock.elapsedRealtime() * 1000);
maybeIdleAsynchronousMediaCodecAdapterThreads();
} }
// Assert that we rendered the first frame after the reset. // Assert that we rendered the first frame after the reset.
@ -1573,6 +1611,15 @@ public class MediaCodecVideoRendererTest {
assertThat(surfacesSet).containsExactly(newSurface); assertThat(surfacesSet).containsExactly(newSurface);
} }
private void maybeIdleAsynchronousMediaCodecAdapterThreads() {
if (queueingThread.get() != null) {
shadowOf(queueingThread.get().getLooper()).idle();
}
if (callbackThread.get() != null) {
shadowOf(callbackThread.get().getLooper()).idle();
}
}
private static CodecCapabilities createCodecCapabilities(int profile, int level) { private static CodecCapabilities createCodecCapabilities(int profile, int level) {
CodecCapabilities capabilities = new CodecCapabilities(); CodecCapabilities capabilities = new CodecCapabilities();
capabilities.profileLevels = new CodecProfileLevel[] {new CodecProfileLevel()}; capabilities.profileLevels = new CodecProfileLevel[] {new CodecProfileLevel()};