diff --git a/libraries/effect/src/main/java/androidx/media3/effect/PreviewingSingleInputVideoGraph.java b/libraries/effect/src/main/java/androidx/media3/effect/PreviewingSingleInputVideoGraph.java
index fcaa96e894..10cff7faed 100644
--- a/libraries/effect/src/main/java/androidx/media3/effect/PreviewingSingleInputVideoGraph.java
+++ b/libraries/effect/src/main/java/androidx/media3/effect/PreviewingSingleInputVideoGraph.java
@@ -40,6 +40,18 @@ public final class PreviewingSingleInputVideoGraph extends SingleInputVideoGraph
private final VideoFrameProcessor.Factory videoFrameProcessorFactory;
+ /**
+ * Creates a new factory that uses the {@link DefaultVideoFrameProcessor.Factory} with its
+ * default values.
+ */
+ public Factory() {
+ this(new DefaultVideoFrameProcessor.Factory.Builder().build());
+ }
+
+ /**
+ * Creates an instance that uses the supplied {@code videoFrameProcessorFactory} to create
+ * {@link VideoFrameProcessor} instances.
+ */
public Factory(VideoFrameProcessor.Factory videoFrameProcessorFactory) {
this.videoFrameProcessorFactory = videoFrameProcessorFactory;
}
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/CompositingVideoSinkProvider.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/CompositingVideoSinkProvider.java
index 31e83cd478..87ffb8b6ae 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/CompositingVideoSinkProvider.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/CompositingVideoSinkProvider.java
@@ -26,7 +26,6 @@ import android.util.Pair;
import android.view.Surface;
import androidx.annotation.FloatRange;
import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
import androidx.media3.common.C;
import androidx.media3.common.ColorInfo;
import androidx.media3.common.DebugViewProvider;
@@ -47,6 +46,8 @@ import androidx.media3.common.util.TimestampIterator;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.ExoPlaybackException;
+import com.google.common.base.Supplier;
+import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableList;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
@@ -63,6 +64,89 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/* package */ final class CompositingVideoSinkProvider
implements VideoSinkProvider, VideoGraph.Listener, VideoFrameRenderControl.FrameRenderer {
+ /** A builder for {@link CompositingVideoSinkProvider} instances. */
+ public static final class Builder {
+ private final Context context;
+
+ private VideoFrameProcessor.@MonotonicNonNull Factory videoFrameProcessorFactory;
+ private PreviewingVideoGraph.@MonotonicNonNull Factory previewingVideoGraphFactory;
+ private @MonotonicNonNull VideoFrameReleaseControl videoFrameReleaseControl;
+ private boolean built;
+
+ /** Creates a builder with the supplied {@linkplain Context application context}. */
+ public Builder(Context context) {
+ this.context = context;
+ }
+
+ /**
+ * Sets the {@link VideoFrameProcessor.Factory} that will be used for creating {@link
+ * VideoFrameProcessor} instances.
+ *
+ *
By default, the {@code DefaultVideoFrameProcessor.Factory} with its default values will be
+ * used.
+ *
+ * @param videoFrameProcessorFactory The {@link VideoFrameProcessor.Factory}.
+ * @return This builder, for convenience.
+ */
+ public Builder setVideoFrameProcessorFactory(
+ VideoFrameProcessor.Factory videoFrameProcessorFactory) {
+ this.videoFrameProcessorFactory = videoFrameProcessorFactory;
+ return this;
+ }
+
+ /**
+ * Sets the {@link PreviewingVideoGraph.Factory} that will be used for creating {@link
+ * PreviewingVideoGraph} instances.
+ *
+ *
By default, the {@code PreviewingSingleInputVideoGraph.Factory} will be used.
+ *
+ * @param previewingVideoGraphFactory The {@link PreviewingVideoGraph.Factory}.
+ * @return This builder, for convenience.
+ */
+ public Builder setPreviewingVideoGraphFactory(
+ PreviewingVideoGraph.Factory previewingVideoGraphFactory) {
+ this.previewingVideoGraphFactory = previewingVideoGraphFactory;
+ return this;
+ }
+
+ /**
+ * Sets the {@link VideoFrameReleaseControl} that will be used.
+ *
+ * @param videoFrameReleaseControl The {@link VideoFrameReleaseControl}.
+ * @return This builder, for convenience.
+ */
+ public Builder setVideoFrameReleaseControl(VideoFrameReleaseControl videoFrameReleaseControl) {
+ this.videoFrameReleaseControl = videoFrameReleaseControl;
+ return this;
+ }
+
+ /**
+ * Builds the {@link CompositingVideoSinkProvider}.
+ *
+ *
A {@link VideoFrameReleaseControl} must be set with {@link
+ * #setVideoFrameReleaseControl(VideoFrameReleaseControl)} otherwise this method throws {@link
+ * IllegalStateException}.
+ *
+ *
This method must be called at most once and will throw an {@link IllegalStateException} if
+ * it has already been called.
+ */
+ public CompositingVideoSinkProvider build() {
+ checkState(!built);
+
+ if (previewingVideoGraphFactory == null) {
+ if (videoFrameProcessorFactory == null) {
+ videoFrameProcessorFactory = new ReflectiveDefaultVideoFrameProcessorFactory();
+ }
+ previewingVideoGraphFactory =
+ new ReflectivePreviewingSingleInputVideoGraphFactory(videoFrameProcessorFactory);
+ }
+ CompositingVideoSinkProvider compositingVideoSinkProvider =
+ new CompositingVideoSinkProvider(this);
+ built = true;
+ return compositingVideoSinkProvider;
+ }
+ }
+
private static final Executor NO_OP_EXECUTOR = runnable -> {};
private final Context context;
@@ -83,25 +167,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private int pendingFlushCount;
private boolean released;
- /** Creates a new instance. */
- public CompositingVideoSinkProvider(
- Context context,
- VideoFrameProcessor.Factory videoFrameProcessorFactory,
- VideoFrameReleaseControl videoFrameReleaseControl) {
- this(
- context,
- new ReflectivePreviewingSingleInputVideoGraphFactory(videoFrameProcessorFactory),
- videoFrameReleaseControl);
- }
-
- @VisibleForTesting
- /* package */ CompositingVideoSinkProvider(
- Context context,
- PreviewingVideoGraph.Factory previewingVideoGraphFactory,
- VideoFrameReleaseControl videoFrameReleaseControl) {
- this.context = context;
- this.previewingVideoGraphFactory = previewingVideoGraphFactory;
- this.videoFrameReleaseControl = videoFrameReleaseControl;
+ private CompositingVideoSinkProvider(Builder builder) {
+ this.context = builder.context;
+ this.previewingVideoGraphFactory = checkStateNotNull(builder.previewingVideoGraphFactory);
+ videoFrameReleaseControl = checkStateNotNull(builder.videoFrameReleaseControl);
@SuppressWarnings("nullness:assignment")
VideoFrameRenderControl.@Initialized FrameRenderer thisRef = this;
videoFrameRenderControl =
@@ -682,4 +751,56 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
}
}
+
+ /**
+ * Delays reflection for loading a {@linkplain VideoFrameProcessor.Factory
+ * DefaultVideoFrameProcessor.Factory} instance.
+ */
+ private static final class ReflectiveDefaultVideoFrameProcessorFactory
+ implements VideoFrameProcessor.Factory {
+ private static final Supplier
+ VIDEO_FRAME_PROCESSOR_FACTORY_SUPPLIER =
+ Suppliers.memoize(
+ () -> {
+ try {
+ // TODO: b/284964524- Add LINT and proguard checks for media3.effect reflection.
+ Class> defaultVideoFrameProcessorFactoryBuilderClass =
+ Class.forName(
+ "androidx.media3.effect.DefaultVideoFrameProcessor$Factory$Builder");
+ Object builder =
+ defaultVideoFrameProcessorFactoryBuilderClass
+ .getConstructor()
+ .newInstance();
+ return (VideoFrameProcessor.Factory)
+ checkNotNull(
+ defaultVideoFrameProcessorFactoryBuilderClass
+ .getMethod("build")
+ .invoke(builder));
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
+ }
+ });
+
+ @Override
+ public VideoFrameProcessor create(
+ Context context,
+ DebugViewProvider debugViewProvider,
+ ColorInfo inputColorInfo,
+ ColorInfo outputColorInfo,
+ boolean renderFramesAutomatically,
+ Executor listenerExecutor,
+ VideoFrameProcessor.Listener listener)
+ throws VideoFrameProcessingException {
+ return VIDEO_FRAME_PROCESSOR_FACTORY_SUPPLIER
+ .get()
+ .create(
+ context,
+ debugViewProvider,
+ inputColorInfo,
+ outputColorInfo,
+ renderFramesAutomatically,
+ listenerExecutor,
+ listener);
+ }
+ }
}
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java
index 836be4fe47..aebf6cd98c 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java
@@ -46,14 +46,11 @@ import androidx.annotation.DoNotInline;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.media3.common.C;
-import androidx.media3.common.ColorInfo;
-import androidx.media3.common.DebugViewProvider;
import androidx.media3.common.DrmInitData;
import androidx.media3.common.Effect;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.PlaybackException;
-import androidx.media3.common.VideoFrameProcessingException;
import androidx.media3.common.VideoFrameProcessor;
import androidx.media3.common.VideoSize;
import androidx.media3.common.util.Clock;
@@ -80,13 +77,10 @@ import androidx.media3.exoplayer.mediacodec.MediaCodecSelector;
import androidx.media3.exoplayer.mediacodec.MediaCodecUtil;
import androidx.media3.exoplayer.mediacodec.MediaCodecUtil.DecoderQueryException;
import androidx.media3.exoplayer.video.VideoRendererEventListener.EventDispatcher;
-import com.google.common.base.Supplier;
-import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.MoreExecutors;
import java.nio.ByteBuffer;
import java.util.List;
-import java.util.concurrent.Executor;
import org.checkerframework.checker.initialization.qual.Initialized;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
@@ -349,7 +343,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
eventListener,
maxDroppedFramesToNotify,
assumedMinimumCodecOperatingRate,
- new ReflectiveDefaultVideoFrameProcessorFactory());
+ /* videoFrameProcessorFactory= */ null);
}
/**
@@ -385,7 +379,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
@Nullable VideoRendererEventListener eventListener,
int maxDroppedFramesToNotify,
float assumedMinimumCodecOperatingRate,
- VideoFrameProcessor.Factory videoFrameProcessorFactory) {
+ @Nullable VideoFrameProcessor.Factory videoFrameProcessorFactory) {
super(
C.TRACK_TYPE_VIDEO,
codecAdapterFactory,
@@ -401,9 +395,13 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
this.context, /* frameTimingEvaluator= */ thisRef, allowedJoiningTimeMs);
videoFrameReleaseInfo = new VideoFrameReleaseControl.FrameReleaseInfo();
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
- videoSinkProvider =
- new CompositingVideoSinkProvider(
- context, videoFrameProcessorFactory, videoFrameReleaseControl);
+ CompositingVideoSinkProvider.Builder compositingVideoSinkProvider =
+ new CompositingVideoSinkProvider.Builder(context)
+ .setVideoFrameReleaseControl(videoFrameReleaseControl);
+ if (videoFrameProcessorFactory != null) {
+ compositingVideoSinkProvider.setVideoFrameProcessorFactory(videoFrameProcessorFactory);
+ }
+ videoSinkProvider = compositingVideoSinkProvider.build();
deviceNeedsNoPostProcessWorkaround = deviceNeedsNoPostProcessWorkaround();
scalingMode = C.VIDEO_SCALING_MODE_DEFAULT;
decodedVideoSize = VideoSize.UNKNOWN;
@@ -1891,58 +1889,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
return new MediaCodecVideoDecoderException(cause, codecInfo, displaySurface);
}
- /**
- * Delays reflection for loading a {@linkplain VideoFrameProcessor.Factory
- * DefaultVideoFrameProcessor} instance.
- */
- private static final class ReflectiveDefaultVideoFrameProcessorFactory
- implements VideoFrameProcessor.Factory {
- private static final Supplier
- VIDEO_FRAME_PROCESSOR_FACTORY_SUPPLIER =
- Suppliers.memoize(
- () -> {
- try {
- // TODO: b/284964524- Add LINT and proguard checks for media3.effect reflection.
- Class> defaultVideoFrameProcessorFactoryBuilderClass =
- Class.forName(
- "androidx.media3.effect.DefaultVideoFrameProcessor$Factory$Builder");
- Object builder =
- defaultVideoFrameProcessorFactoryBuilderClass
- .getConstructor()
- .newInstance();
- return (VideoFrameProcessor.Factory)
- checkNotNull(
- defaultVideoFrameProcessorFactoryBuilderClass
- .getMethod("build")
- .invoke(builder));
- } catch (Exception e) {
- throw new IllegalStateException(e);
- }
- });
-
- @Override
- public VideoFrameProcessor create(
- Context context,
- DebugViewProvider debugViewProvider,
- ColorInfo inputColorInfo,
- ColorInfo outputColorInfo,
- boolean renderFramesAutomatically,
- Executor listenerExecutor,
- VideoFrameProcessor.Listener listener)
- throws VideoFrameProcessingException {
- return VIDEO_FRAME_PROCESSOR_FACTORY_SUPPLIER
- .get()
- .create(
- context,
- debugViewProvider,
- inputColorInfo,
- outputColorInfo,
- renderFramesAutomatically,
- listenerExecutor,
- listener);
- }
- }
-
/**
* Returns a maximum video size to use when configuring a codec for {@code format} in a way that
* will allow possible adaptation to other compatible formats that are expected to have the same
diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/video/CompositingVideoSinkProviderTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/video/CompositingVideoSinkProviderTest.java
index f6d413486f..ead017c07b 100644
--- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/video/CompositingVideoSinkProviderTest.java
+++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/video/CompositingVideoSinkProviderTest.java
@@ -18,6 +18,7 @@ package androidx.media3.exoplayer.video;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import android.content.Context;
@@ -28,7 +29,6 @@ import androidx.media3.common.Format;
import androidx.media3.common.PreviewingVideoGraph;
import androidx.media3.common.VideoFrameProcessor;
import androidx.media3.common.VideoGraph;
-import androidx.media3.exoplayer.ExoPlaybackException;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
@@ -42,6 +42,30 @@ import org.mockito.Mockito;
@RunWith(AndroidJUnit4.class)
public final class CompositingVideoSinkProviderTest {
+ @Test
+ public void builder_withoutVideoFrameReleaseControl_throws() {
+ assertThrows(
+ IllegalStateException.class,
+ () ->
+ new CompositingVideoSinkProvider.Builder(ApplicationProvider.getApplicationContext())
+ .build());
+ }
+
+ @Test
+ public void builder_calledMultipleTimes_throws() {
+ CompositingVideoSinkProvider.Builder builder =
+ new CompositingVideoSinkProvider.Builder(ApplicationProvider.getApplicationContext())
+ .setVideoFrameReleaseControl(
+ new VideoFrameReleaseControl(
+ ApplicationProvider.getApplicationContext(),
+ mock(VideoFrameReleaseControl.FrameTimingEvaluator.class),
+ /* allowedJoiningTimeMs= */ 0));
+
+ builder.build();
+
+ assertThrows(IllegalStateException.class, builder::build);
+ }
+
@Test
public void initialize() throws VideoSink.VideoSinkException {
CompositingVideoSinkProvider provider = createCompositingVideoSinkProvider();
@@ -133,6 +157,7 @@ public final class CompositingVideoSinkProviderTest {
}
private static CompositingVideoSinkProvider createCompositingVideoSinkProvider() {
+ Context context = ApplicationProvider.getApplicationContext();
VideoFrameReleaseControl.FrameTimingEvaluator frameTimingEvaluator =
new VideoFrameReleaseControl.FrameTimingEvaluator() {
@Override
@@ -152,20 +177,16 @@ public final class CompositingVideoSinkProviderTest {
long positionUs,
long elapsedRealtimeUs,
boolean isLastFrame,
- boolean treatDroppedBuffersAsSkipped)
- throws ExoPlaybackException {
+ boolean treatDroppedBuffersAsSkipped) {
return false;
}
};
VideoFrameReleaseControl releaseControl =
- new VideoFrameReleaseControl(
- ApplicationProvider.getApplicationContext(),
- frameTimingEvaluator,
- /* allowedJoiningTimeMs= */ 0);
- return new CompositingVideoSinkProvider(
- ApplicationProvider.getApplicationContext(),
- new TestPreviewingVideoGraphFactory(),
- releaseControl);
+ new VideoFrameReleaseControl(context, frameTimingEvaluator, /* allowedJoiningTimeMs= */ 0);
+ return new CompositingVideoSinkProvider.Builder(context)
+ .setPreviewingVideoGraphFactory(new TestPreviewingVideoGraphFactory())
+ .setVideoFrameReleaseControl(releaseControl)
+ .build();
}
private static class TestPreviewingVideoGraphFactory implements PreviewingVideoGraph.Factory {