diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecAdapter.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecAdapter.java index 225b80d2c7..205698fe9e 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecAdapter.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecAdapter.java @@ -74,8 +74,15 @@ import java.nio.ByteBuffer; 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 callbackThreadSupplier, Supplier queueingThreadSupplier) { this.callbackThreadSupplier = callbackThreadSupplier; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/DefaultMediaCodecAdapterFactory.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/DefaultMediaCodecAdapterFactory.java index a14b51d1d3..c7b732eb38 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/DefaultMediaCodecAdapterFactory.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/DefaultMediaCodecAdapterFactory.java @@ -19,12 +19,14 @@ import static java.lang.annotation.ElementType.TYPE_USE; import android.content.Context; import android.media.MediaCodec; +import android.os.HandlerThread; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.media3.common.MimeTypes; import androidx.media3.common.util.Log; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; +import com.google.common.base.Supplier; import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.io.IOException; import java.lang.annotation.Documented; @@ -57,6 +59,8 @@ public final class DefaultMediaCodecAdapterFactory implements MediaCodecAdapter. private static final String TAG = "DMCodecAdapterFactory"; @Nullable private final Context context; + @Nullable private final Supplier callbackThreadSupplier; + @Nullable private final Supplier queueingThreadSupplier; private @Mode int asynchronousMode; private boolean asyncCryptoFlagEnabled; @@ -69,6 +73,8 @@ public final class DefaultMediaCodecAdapterFactory implements MediaCodecAdapter. asynchronousMode = MODE_DEFAULT; asyncCryptoFlagEnabled = false; context = null; + callbackThreadSupplier = null; + queueingThreadSupplier = null; } /** @@ -77,9 +83,26 @@ public final class DefaultMediaCodecAdapterFactory implements MediaCodecAdapter. * @param context A {@link 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 callbackThreadSupplier, + @Nullable Supplier queueingThreadSupplier) { this.context = context; asynchronousMode = MODE_DEFAULT; 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 " + Util.getTrackTypeString(trackType)); AsynchronousMediaCodecAdapter.Factory factory = - new AsynchronousMediaCodecAdapter.Factory(trackType); + callbackThreadSupplier != null && queueingThreadSupplier != null + ? new AsynchronousMediaCodecAdapter.Factory( + callbackThreadSupplier, queueingThreadSupplier) + : new AsynchronousMediaCodecAdapter.Factory(trackType); factory.experimentalSetAsyncCryptoFlagEnabled(asyncCryptoFlagEnabled); return factory.createAdapter(configuration); } diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/video/MediaCodecVideoRendererTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/video/MediaCodecVideoRendererTest.java index 95bd73f5c0..4691ef1b60 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/video/MediaCodecVideoRendererTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/video/MediaCodecVideoRendererTest.java @@ -38,6 +38,7 @@ import android.media.MediaCodecInfo.CodecProfileLevel; import android.media.MediaFormat; import android.os.Bundle; import android.os.Handler; +import android.os.HandlerThread; import android.os.Looper; import android.os.PersistableBundle; import android.os.SystemClock; @@ -61,6 +62,7 @@ import androidx.media3.exoplayer.RendererConfiguration; import androidx.media3.exoplayer.analytics.PlayerId; import androidx.media3.exoplayer.drm.DrmSessionEventListener; import androidx.media3.exoplayer.drm.DrmSessionManager; +import androidx.media3.exoplayer.mediacodec.DefaultMediaCodecAdapterFactory; import androidx.media3.exoplayer.mediacodec.MediaCodecAdapter; import androidx.media3.exoplayer.mediacodec.MediaCodecInfo; import androidx.media3.exoplayer.mediacodec.MediaCodecSelector; @@ -89,6 +91,7 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import org.junit.After; import org.junit.Before; @@ -106,7 +109,6 @@ import org.robolectric.shadows.ShadowLooper; import org.robolectric.shadows.ShadowSystemClock; /** Unit test for {@link MediaCodecVideoRenderer}. */ -@Config(sdk = 30) // TODO: b/382017156 - Remove this when the tests pass on API 31+. @RunWith(AndroidJUnit4.class) public class MediaCodecVideoRendererTest { @Rule public final MockitoRule mockito = MockitoJUnit.rule(); @@ -146,6 +148,9 @@ public class MediaCodecVideoRendererTest { /* forceDisableAdaptive= */ false, /* forceSecure= */ false); + private final AtomicReference callbackThread = new AtomicReference<>(); + private final AtomicReference queueingThread = new AtomicReference<>(); + private Looper testMainLooper; private Surface surface; private MediaCodecVideoRenderer mediaCodecVideoRenderer; @@ -170,12 +175,22 @@ public class MediaCodecVideoRendererTest { /* vendor= */ false, /* forceDisableAdaptive= */ false, /* forceSecure= */ false)); - mediaCodecVideoRenderer = new MediaCodecVideoRenderer( 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, /* allowedJoiningTimeMs= */ 0, + /* enableDecoderFallback= */ false, /* eventHandler= */ new Handler(testMainLooper), /* eventListener= */ eventListener, /* maxDroppedFramesToNotify= */ 1) { @@ -230,11 +245,15 @@ public class MediaCodecVideoRendererTest { mediaCodecVideoRenderer.start(); mediaCodecVideoRenderer.render(0, SystemClock.elapsedRealtime() * 1000); - mediaCodecVideoRenderer.render(40_000, SystemClock.elapsedRealtime() * 1000); + for (int i = 0; i < 5; i++) { + mediaCodecVideoRenderer.render(40_000, SystemClock.elapsedRealtime() * 1000); + maybeIdleAsynchronousMediaCodecAdapterThreads(); + } mediaCodecVideoRenderer.setCurrentStreamFinal(); int posUs = 80_001; // Ensures buffer will be 30_001us late. while (!mediaCodecVideoRenderer.isEnded()) { mediaCodecVideoRenderer.render(posUs, SystemClock.elapsedRealtime() * 1000); + maybeIdleAsynchronousMediaCodecAdapterThreads(); posUs += 40_000; } shadowOf(testMainLooper).idle(); @@ -273,7 +292,9 @@ public class MediaCodecVideoRendererTest { mediaCodecVideoRenderer.start(); mediaCodecVideoRenderer.render(0, SystemClock.elapsedRealtime() * 1000); - mediaCodecVideoRenderer.render(10_000, SystemClock.elapsedRealtime() * 1000); + while (decoderCounters.renderedOutputBufferCount == 0) { + mediaCodecVideoRenderer.render(10_000, SystemClock.elapsedRealtime() * 1000); + } // 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. int posUs = 1_020_000; @@ -322,13 +343,16 @@ public class MediaCodecVideoRendererTest { /* positionUs= */ 0, /* joining= */ true, /* mayRenderStartOfStream= */ false, - /* startPositionUs= */ 50_000, + /* startPositionUs= */ 0, /* offsetUs= */ 0, /* mediaPeriodId= */ new MediaSource.MediaPeriodId(new Object())); mediaCodecVideoRenderer.start(); mediaCodecVideoRenderer.setCurrentStreamFinal(); - mediaCodecVideoRenderer.render(0, SystemClock.elapsedRealtime() * 1000); + for (int i = 0; i < 5; i++) { + mediaCodecVideoRenderer.render(0, SystemClock.elapsedRealtime() * 1000); + maybeIdleAsynchronousMediaCodecAdapterThreads(); + } int posUs = 20_001; // Ensures buffer will be 29_999us early. mediaCodecVideoRenderer.render(posUs, SystemClock.elapsedRealtime() * 1000); shadowOf(testMainLooper).idle(); @@ -360,12 +384,15 @@ public class MediaCodecVideoRendererTest { /* positionUs= */ 0, /* joining= */ true, /* mayRenderStartOfStream= */ false, - /* startPositionUs= */ 50_000, + /* startPositionUs= */ 0, /* offsetUs= */ 0, /* mediaPeriodId= */ new MediaSource.MediaPeriodId(new Object())); mediaCodecVideoRenderer.setCurrentStreamFinal(); - mediaCodecVideoRenderer.render(0, SystemClock.elapsedRealtime() * 1000); + for (int i = 0; i < 5; i++) { + mediaCodecVideoRenderer.render(0, SystemClock.elapsedRealtime() * 1000); + maybeIdleAsynchronousMediaCodecAdapterThreads(); + } int posUs = 20_001; // Ensures buffer will be 29_999us early. for (int i = 0; i < 3; i++) { mediaCodecVideoRenderer.render(posUs, SystemClock.elapsedRealtime() * 1000); @@ -876,9 +903,10 @@ public class MediaCodecVideoRendererTest { int positionUs = 20_000; do { - ShadowSystemClock.advanceBy(10, TimeUnit.MILLISECONDS); + ShadowSystemClock.advanceBy(2, TimeUnit.MILLISECONDS); mediaCodecVideoRenderer.render(positionUs, msToUs(SystemClock.elapsedRealtime())); - positionUs += 10_000; + maybeIdleAsynchronousMediaCodecAdapterThreads(); + positionUs += 2_000; } while (!mediaCodecVideoRenderer.isEnded()); shadowOf(testMainLooper).idle(); @@ -956,12 +984,15 @@ public class MediaCodecVideoRendererTest { new MediaSource.MediaPeriodId(new Object())); for (int i = 0; i < 10; i++) { mediaCodecVideoRenderer.render(/* positionUs= */ 0, SystemClock.elapsedRealtime() * 1000); + maybeIdleAsynchronousMediaCodecAdapterThreads(); } + shadowOf(testMainLooper).idle(); verify(eventListener).onRenderedFirstFrame(eq(surface), /* renderTimeMs= */ anyLong()); } + @Config(minSdk = 30) @Test public void enable_withoutMayRenderStartOfStream_doesNotRenderFirstFrameBeforeStart() throws Exception { @@ -987,6 +1018,7 @@ public class MediaCodecVideoRendererTest { new MediaSource.MediaPeriodId(new Object())); for (int i = 0; i < 10; i++) { mediaCodecVideoRenderer.render(/* positionUs= */ 0, SystemClock.elapsedRealtime() * 1000); + maybeIdleAsynchronousMediaCodecAdapterThreads(); } shadowOf(testMainLooper).idle(); @@ -1018,6 +1050,7 @@ public class MediaCodecVideoRendererTest { mediaCodecVideoRenderer.start(); for (int i = 0; i < 10; i++) { mediaCodecVideoRenderer.render(/* positionUs= */ 0, SystemClock.elapsedRealtime() * 1000); + maybeIdleAsynchronousMediaCodecAdapterThreads(); } shadowOf(testMainLooper).idle(); @@ -1051,6 +1084,7 @@ public class MediaCodecVideoRendererTest { new MediaSource.MediaPeriodId(new Object())); for (int i = 0; i < 10; i++) { mediaCodecVideoRenderer.render(/* positionUs= */ 0, SystemClock.elapsedRealtime() * 1000); + maybeIdleAsynchronousMediaCodecAdapterThreads(); } shadowOf(testMainLooper).idle(); @@ -1100,6 +1134,7 @@ public class MediaCodecVideoRendererTest { for (int i = 0; i < 10; i++) { mediaCodecVideoRenderer.render( /* positionUs= */ i * 10, SystemClock.elapsedRealtime() * 1000); + maybeIdleAsynchronousMediaCodecAdapterThreads(); if (!replacedStream && mediaCodecVideoRenderer.hasReadStreamToEnd()) { mediaCodecVideoRenderer.replaceStream( new Format[] {VIDEO_H264}, @@ -1164,6 +1199,7 @@ public class MediaCodecVideoRendererTest { for (int i = 0; i < 10; i++) { mediaCodecVideoRenderer.render( /* positionUs= */ i * 10, SystemClock.elapsedRealtime() * 1000); + maybeIdleAsynchronousMediaCodecAdapterThreads(); if (!replacedStream && mediaCodecVideoRenderer.hasReadStreamToEnd()) { mediaCodecVideoRenderer.replaceStream( new Format[] {VIDEO_H264}, @@ -1212,6 +1248,7 @@ public class MediaCodecVideoRendererTest { // Render at the original start position. for (int i = 0; i < 10; i++) { mediaCodecVideoRenderer.render(/* positionUs= */ 1000, SystemClock.elapsedRealtime() * 1000); + maybeIdleAsynchronousMediaCodecAdapterThreads(); } // 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); for (int i = 0; i < 10; i++) { mediaCodecVideoRenderer.render(/* positionUs= */ 500, SystemClock.elapsedRealtime() * 1000); + maybeIdleAsynchronousMediaCodecAdapterThreads(); } // Assert that we rendered the first frame after the reset. @@ -1573,6 +1611,15 @@ public class MediaCodecVideoRendererTest { 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) { CodecCapabilities capabilities = new CodecCapabilities(); capabilities.profileLevels = new CodecProfileLevel[] {new CodecProfileLevel()};