diff --git a/libraries/effect/src/main/java/androidx/media3/effect/DefaultVideoFrameProcessor.java b/libraries/effect/src/main/java/androidx/media3/effect/DefaultVideoFrameProcessor.java index 2a6523c2f7..86a269c538 100644 --- a/libraries/effect/src/main/java/androidx/media3/effect/DefaultVideoFrameProcessor.java +++ b/libraries/effect/src/main/java/androidx/media3/effect/DefaultVideoFrameProcessor.java @@ -15,6 +15,7 @@ */ package androidx.media3.effect; +import static androidx.annotation.VisibleForTesting.PACKAGE_PRIVATE; import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkState; @@ -63,12 +64,21 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @UnstableApi public final class DefaultVideoFrameProcessor implements VideoFrameProcessor { + /** Listener interface for texture output. */ + @VisibleForTesting(otherwise = PACKAGE_PRIVATE) + public interface TextureOutputListener { + /** Called when a texture has been rendered to. */ + void onTextureRendered(GlTextureInfo outputTexture, long presentationTimeUs) + throws GlUtil.GlException; + } + /** A factory for {@link DefaultVideoFrameProcessor} instances. */ public static final class Factory implements VideoFrameProcessor.Factory { /** A builder for {@link DefaultVideoFrameProcessor.Factory} instances. */ public static final class Builder { private boolean enableColorTransfers; + @Nullable private TextureOutputListener textureOutputListener; /** Creates an instance. */ public Builder() { @@ -81,25 +91,39 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor { *

If the input or output is HDR, this must be {@code true}. */ @CanIgnoreReturnValue - public DefaultVideoFrameProcessor.Factory.Builder setEnableColorTransfers( - boolean enableColorTransfers) { + public Builder setEnableColorTransfers(boolean enableColorTransfers) { this.enableColorTransfers = enableColorTransfers; return this; } + /** + * Sets the {@link TextureOutputListener}. + * + *

If set, the {@link VideoFrameProcessor} will output to an OpenGL texture. + */ + @VisibleForTesting + @CanIgnoreReturnValue + public Builder setOnTextureRenderedListener(TextureOutputListener textureOutputListener) { + this.textureOutputListener = textureOutputListener; + return this; + } + /** Builds an {@link DefaultVideoFrameProcessor.Factory} instance. */ public DefaultVideoFrameProcessor.Factory build() { - return new DefaultVideoFrameProcessor.Factory(enableColorTransfers); + return new DefaultVideoFrameProcessor.Factory(enableColorTransfers, textureOutputListener); } } /** Whether to transfer colors to an intermediate color space when applying effects. */ - public final boolean enableColorTransfers; + private final boolean enableColorTransfers; + + @Nullable private final TextureOutputListener textureOutputListener; private GlObjectsProvider glObjectsProvider = GlObjectsProvider.DEFAULT; - private boolean outputToTexture; - private Factory(boolean enableColorTransfers) { + private Factory( + boolean enableColorTransfers, @Nullable TextureOutputListener textureOutputListener) { + this.textureOutputListener = textureOutputListener; this.enableColorTransfers = enableColorTransfers; } @@ -109,7 +133,7 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor { */ @Deprecated public Factory() { - this(/* enableColorTransfers= */ true); + this(/* enableColorTransfers= */ true, /* textureOutputListener= */ null); } // TODO(276913828): Move this setter to the DefaultVideoFrameProcessor.Factory.Builder. @@ -119,26 +143,11 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor { *

The default value is {@link GlObjectsProvider#DEFAULT}. */ @Override - public DefaultVideoFrameProcessor.Factory setGlObjectsProvider( - GlObjectsProvider glObjectsProvider) { + public Factory setGlObjectsProvider(GlObjectsProvider glObjectsProvider) { this.glObjectsProvider = glObjectsProvider; return this; } - // TODO(276913828): Move this setter to the DefaultVideoFrameProcessor.Factory.Builder. - /** - * Sets whether to output to a texture for testing. - * - *

Must be called before {@link VideoFrameProcessor.Factory#create}. - * - *

The default value is {@code false}. - */ - @VisibleForTesting - public Factory setOutputToTexture(boolean outputToTexture) { - this.outputToTexture = outputToTexture; - return this; - } - /** * {@inheritDoc} * @@ -223,7 +232,7 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor { listenerExecutor, listener, glObjectsProvider, - outputToTexture)); + textureOutputListener)); try { return defaultVideoFrameProcessorFuture.get(); @@ -359,20 +368,6 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor { finalShaderProgramWrapper.setOutputSurfaceInfo(outputSurfaceInfo); } - /** - * Gets the output {@link GlTextureInfo}. - * - *

Should only be called if {@code outputToTexture} is true, and after a frame is available, as - * reported by the output {@linkplain #setOutputSurfaceInfo surface}'s {@link - * SurfaceTexture#setOnFrameAvailableListener}. Returns {@code null} if an output texture is not - * yet available. - */ - @VisibleForTesting - @Nullable - public GlTextureInfo getOutputTextureInfo() { - return finalShaderProgramWrapper.getOutputTextureInfo(); - } - @Override public void releaseOutputFrame(long releaseTimeNs) { checkState( @@ -468,7 +463,7 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor { Executor executor, Listener listener, GlObjectsProvider glObjectsProvider, - boolean outputToTexture) + @Nullable TextureOutputListener textureOutputListener) throws GlUtil.GlException, VideoFrameProcessingException { checkState(Thread.currentThread().getName().equals(THREAD_NAME)); @@ -513,7 +508,7 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor { executor, listener, glObjectsProvider, - outputToTexture); + textureOutputListener); setGlObjectProviderOnShaderPrograms(shaderPrograms, glObjectsProvider); VideoFrameProcessingTaskExecutor videoFrameProcessingTaskExecutor = new VideoFrameProcessingTaskExecutor(singleThreadExecutorService, listener); @@ -554,7 +549,7 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor { Executor executor, Listener listener, GlObjectsProvider glObjectsProvider, - boolean outputToTexture) + @Nullable TextureOutputListener textureOutputListener) throws VideoFrameProcessingException { ImmutableList.Builder shaderProgramListBuilder = new ImmutableList.Builder<>(); ImmutableList.Builder matrixTransformationListBuilder = @@ -640,7 +635,7 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor { executor, listener, glObjectsProvider, - outputToTexture)); + textureOutputListener)); return shaderProgramListBuilder.build(); } diff --git a/libraries/effect/src/main/java/androidx/media3/effect/FinalShaderProgramWrapper.java b/libraries/effect/src/main/java/androidx/media3/effect/FinalShaderProgramWrapper.java index 516ab60719..e654e1c55c 100644 --- a/libraries/effect/src/main/java/androidx/media3/effect/FinalShaderProgramWrapper.java +++ b/libraries/effect/src/main/java/androidx/media3/effect/FinalShaderProgramWrapper.java @@ -32,7 +32,6 @@ import android.view.SurfaceHolder; import android.view.SurfaceView; import androidx.annotation.GuardedBy; import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; import androidx.media3.common.C; import androidx.media3.common.ColorInfo; import androidx.media3.common.DebugViewProvider; @@ -84,7 +83,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private final float[] textureTransformMatrix; private final Queue streamOffsetUsQueue; private final Queue> availableFrames; - private final boolean outputToTexture; + @Nullable private final DefaultVideoFrameProcessor.TextureOutputListener textureOutputListener; private int inputWidth; private int inputHeight; @@ -123,7 +122,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; Executor videoFrameProcessorListenerExecutor, VideoFrameProcessor.Listener videoFrameProcessorListener, GlObjectsProvider glObjectsProvider, - boolean outputToTexture) { + @Nullable DefaultVideoFrameProcessor.TextureOutputListener textureOutputListener) { this.context = context; this.matrixTransformations = matrixTransformations; this.rgbMatrices = rgbMatrices; @@ -139,7 +138,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; this.videoFrameProcessorListenerExecutor = videoFrameProcessorListenerExecutor; this.videoFrameProcessorListener = videoFrameProcessorListener; this.glObjectsProvider = glObjectsProvider; - this.outputToTexture = outputToTexture; + this.textureOutputListener = textureOutputListener; textureTransformMatrix = GlUtil.create4x4IdentityMatrix(); streamOffsetUsQueue = new ConcurrentLinkedQueue<>(); @@ -310,7 +309,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; GlTextureInfo inputTexture, long presentationTimeUs, long releaseTimeNs) { try { maybeRenderFrameToOutputSurface(inputTexture, presentationTimeUs, releaseTimeNs); - if (outputToTexture && defaultShaderProgram != null) { + if (textureOutputListener != null && defaultShaderProgram != null) { renderFrameToOutputTexture(inputTexture, presentationTimeUs); } @@ -364,12 +363,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; outputTexture.fboId, outputTexture.width, outputTexture.height); GlUtil.clearOutputFrame(); checkNotNull(defaultShaderProgram).drawFrame(inputTexture.texId, presentationTimeUs); - } - - @VisibleForTesting - @Nullable - /* package */ GlTextureInfo getOutputTextureInfo() { - return outputTexture; + checkNotNull(textureOutputListener).onTextureRendered(outputTexture, presentationTimeUs); } /** @@ -444,7 +438,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; if (defaultShaderProgram == null) { DefaultShaderProgram defaultShaderProgram = createDefaultShaderProgramForOutputSurface(outputSurfaceInfo); - if (outputToTexture) { + if (textureOutputListener != null) { configureOutputTexture( checkNotNull(outputSizeBeforeSurfaceTransformation).getWidth(), checkNotNull(outputSizeBeforeSurfaceTransformation).getHeight()); diff --git a/libraries/effect/src/main/java/androidx/media3/effect/VideoFrameProcessingTask.java b/libraries/effect/src/main/java/androidx/media3/effect/VideoFrameProcessingTask.java index 77feee2cae..d62e78195c 100644 --- a/libraries/effect/src/main/java/androidx/media3/effect/VideoFrameProcessingTask.java +++ b/libraries/effect/src/main/java/androidx/media3/effect/VideoFrameProcessingTask.java @@ -15,9 +15,6 @@ */ package androidx.media3.effect; -import static androidx.annotation.VisibleForTesting.PACKAGE_PRIVATE; - -import androidx.annotation.VisibleForTesting; import androidx.media3.common.VideoFrameProcessingException; import androidx.media3.common.util.GlUtil; import androidx.media3.common.util.UnstableApi; @@ -27,8 +24,7 @@ import androidx.media3.common.util.UnstableApi; * VideoFrameProcessingException}. */ @UnstableApi -@VisibleForTesting(otherwise = PACKAGE_PRIVATE) -public interface VideoFrameProcessingTask { +/* package */ interface VideoFrameProcessingTask { /** Runs the task. */ void run() throws VideoFrameProcessingException, GlUtil.GlException; } diff --git a/libraries/effect/src/main/java/androidx/media3/effect/VideoFrameProcessingTaskExecutor.java b/libraries/effect/src/main/java/androidx/media3/effect/VideoFrameProcessingTaskExecutor.java index 37f2968885..274aabea2e 100644 --- a/libraries/effect/src/main/java/androidx/media3/effect/VideoFrameProcessingTaskExecutor.java +++ b/libraries/effect/src/main/java/androidx/media3/effect/VideoFrameProcessingTaskExecutor.java @@ -15,12 +15,10 @@ */ package androidx.media3.effect; -import static androidx.annotation.VisibleForTesting.PACKAGE_PRIVATE; import static java.util.concurrent.TimeUnit.MILLISECONDS; import androidx.annotation.GuardedBy; import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; import androidx.media3.common.VideoFrameProcessingException; import androidx.media3.common.VideoFrameProcessor; import androidx.media3.common.util.UnstableApi; @@ -48,8 +46,7 @@ import java.util.concurrent.RejectedExecutionException; * equal priority are executed in FIFO order. */ @UnstableApi -@VisibleForTesting(otherwise = PACKAGE_PRIVATE) -public final class VideoFrameProcessingTaskExecutor { +/* package */ final class VideoFrameProcessingTaskExecutor { private final ExecutorService singleThreadExecutorService; private final VideoFrameProcessor.Listener listener; diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/VideoFrameProcessorTestRunner.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/VideoFrameProcessorTestRunner.java index f1f7125a80..0b1b8e8d77 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/VideoFrameProcessorTestRunner.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/VideoFrameProcessorTestRunner.java @@ -58,7 +58,7 @@ public final class VideoFrameProcessorTestRunner { private @MonotonicNonNull String testId; private VideoFrameProcessor.@MonotonicNonNull Factory videoFrameProcessorFactory; - private BitmapReader.@MonotonicNonNull Factory bitmapReaderFactory; + private @MonotonicNonNull BitmapReader bitmapReader; private @MonotonicNonNull String videoAssetPath; private @MonotonicNonNull String outputFileLabel; private @MonotonicNonNull ImmutableList effects; @@ -99,13 +99,13 @@ public final class VideoFrameProcessorTestRunner { } /** - * Sets the {@link BitmapReader.Factory}. + * Sets the {@link BitmapReader}. * - *

The default value is {@link SurfaceBitmapReader.Factory}. + *

The default value is a {@link SurfaceBitmapReader} instance. */ @CanIgnoreReturnValue - public Builder setBitmapReaderFactory(BitmapReader.Factory bitmapReaderFactory) { - this.bitmapReaderFactory = bitmapReaderFactory; + public Builder setBitmapReader(BitmapReader bitmapReader) { + this.bitmapReader = bitmapReader; return this; } @@ -218,7 +218,7 @@ public final class VideoFrameProcessorTestRunner { return new VideoFrameProcessorTestRunner( testId, videoFrameProcessorFactory, - bitmapReaderFactory == null ? new SurfaceBitmapReader.Factory() : bitmapReaderFactory, + bitmapReader == null ? new SurfaceBitmapReader() : bitmapReader, videoAssetPath, outputFileLabel == null ? "" : outputFileLabel, effects == null ? ImmutableList.of() : effects, @@ -250,7 +250,7 @@ public final class VideoFrameProcessorTestRunner { private VideoFrameProcessorTestRunner( String testId, VideoFrameProcessor.Factory videoFrameProcessorFactory, - BitmapReader.Factory bitmapReaderFactory, + BitmapReader bitmapReader, @Nullable String videoAssetPath, String outputFileLabel, ImmutableList effects, @@ -261,6 +261,7 @@ public final class VideoFrameProcessorTestRunner { OnOutputFrameAvailableListener onOutputFrameAvailableListener) throws VideoFrameProcessingException { this.testId = testId; + this.bitmapReader = bitmapReader; this.videoAssetPath = videoAssetPath; this.outputFileLabel = outputFileLabel; this.pixelWidthHeightRatio = pixelWidthHeightRatio; @@ -279,11 +280,9 @@ public final class VideoFrameProcessorTestRunner { new VideoFrameProcessor.Listener() { @Override public void onOutputSizeChanged(int width, int height) { - bitmapReader = - bitmapReaderFactory.create(checkNotNull(videoFrameProcessor), width, height); - Surface outputSurface = bitmapReader.getSurface(); - videoFrameProcessor.setOutputSurfaceInfo( - new SurfaceInfo(outputSurface, width, height)); + Surface outputSurface = bitmapReader.getSurface(width, height); + checkNotNull(videoFrameProcessor) + .setOutputSurfaceInfo(new SurfaceInfo(outputSurface, width, height)); } @Override @@ -360,12 +359,9 @@ public final class VideoFrameProcessorTestRunner { /** Reads a {@link Bitmap} from {@link VideoFrameProcessor} output. */ public interface BitmapReader { - interface Factory { - BitmapReader create(VideoFrameProcessor videoFrameProcessor, int width, int height); - } /** Returns the {@link VideoFrameProcessor} output {@link Surface}. */ - Surface getSurface(); + Surface getSurface(int width, int height); /** Returns the output {@link Bitmap}. */ Bitmap getBitmap(); @@ -378,26 +374,15 @@ public final class VideoFrameProcessorTestRunner { */ public static final class SurfaceBitmapReader implements VideoFrameProcessorTestRunner.BitmapReader { - public static final class Factory - implements VideoFrameProcessorTestRunner.BitmapReader.Factory { - @Override - public SurfaceBitmapReader create( - VideoFrameProcessor videoFrameProcessor, int width, int height) { - return new SurfaceBitmapReader(width, height); - } - } // ImageReader only supports SDR input. - private final ImageReader imageReader; - - @SuppressLint("WrongConstant") - private SurfaceBitmapReader(int width, int height) { - imageReader = - ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, /* maxImages= */ 1); - } + private @MonotonicNonNull ImageReader imageReader; @Override - public Surface getSurface() { + @SuppressLint("WrongConstant") + public Surface getSurface(int width, int height) { + imageReader = + ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, /* maxImages= */ 1); return imageReader.getSurface(); } diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/DefaultVideoFrameProcessorTextureOutputPixelTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/DefaultVideoFrameProcessorTextureOutputPixelTest.java index 83b49a1479..ace0bdfe47 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/DefaultVideoFrameProcessorTextureOutputPixelTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/DefaultVideoFrameProcessorTextureOutputPixelTest.java @@ -26,7 +26,6 @@ import android.graphics.Bitmap; import android.graphics.SurfaceTexture; import android.view.Surface; import androidx.media3.common.GlTextureInfo; -import androidx.media3.common.VideoFrameProcessor; import androidx.media3.common.util.GlUtil; import androidx.media3.effect.BitmapOverlay; import androidx.media3.effect.DefaultVideoFrameProcessor; @@ -105,13 +104,16 @@ public final class DefaultVideoFrameProcessorTextureOutputPixelTest { private VideoFrameProcessorTestRunner.Builder getDefaultFrameProcessorTestRunnerBuilder( String testId) { + TextureBitmapReader textureBitmapReader = new TextureBitmapReader(); DefaultVideoFrameProcessor.Factory defaultVideoFrameProcessorFactory = - new DefaultVideoFrameProcessor.Factory().setOutputToTexture(true); + new DefaultVideoFrameProcessor.Factory.Builder() + .setOnTextureRenderedListener(textureBitmapReader::readBitmapFromTexture) + .build(); return new VideoFrameProcessorTestRunner.Builder() .setTestId(testId) .setVideoFrameProcessorFactory(defaultVideoFrameProcessorFactory) .setVideoAssetPath(INPUT_SDR_MP4_ASSET_STRING) - .setBitmapReaderFactory(new TextureBitmapReader.Factory()); + .setBitmapReader(textureBitmapReader); } /** @@ -122,24 +124,11 @@ public final class DefaultVideoFrameProcessorTextureOutputPixelTest { private static final class TextureBitmapReader implements VideoFrameProcessorTestRunner.BitmapReader { // TODO(b/239172735): This outputs an incorrect black output image on emulators. - public static final class Factory - implements VideoFrameProcessorTestRunner.BitmapReader.Factory { - @Override - public TextureBitmapReader create( - VideoFrameProcessor videoFrameProcessor, int width, int height) { - return new TextureBitmapReader((DefaultVideoFrameProcessor) videoFrameProcessor); - } - } - private final DefaultVideoFrameProcessor defaultVideoFrameProcessor; private @MonotonicNonNull Bitmap outputBitmap; - private TextureBitmapReader(DefaultVideoFrameProcessor defaultVideoFrameProcessor) { - this.defaultVideoFrameProcessor = defaultVideoFrameProcessor; - } - @Override - public Surface getSurface() { + public Surface getSurface(int width, int height) { int texId; try { texId = GlUtil.createExternalTexture(); @@ -147,7 +136,6 @@ public final class DefaultVideoFrameProcessorTextureOutputPixelTest { throw new RuntimeException(e); } SurfaceTexture surfaceTexture = new SurfaceTexture(texId); - surfaceTexture.setOnFrameAvailableListener(this::onSurfaceTextureFrameAvailable); return new Surface(surfaceTexture); } @@ -156,15 +144,8 @@ public final class DefaultVideoFrameProcessorTextureOutputPixelTest { return checkStateNotNull(outputBitmap); } - private void onSurfaceTextureFrameAvailable(SurfaceTexture surfaceTexture) { - defaultVideoFrameProcessor - .getTaskExecutor() - .submitWithHighPriority(this::getBitmapFromTexture); - } - - private void getBitmapFromTexture() throws GlUtil.GlException { - GlTextureInfo outputTexture = checkNotNull(defaultVideoFrameProcessor.getOutputTextureInfo()); - + public void readBitmapFromTexture(GlTextureInfo outputTexture, long presentationTimeUs) + throws GlUtil.GlException { GlUtil.focusFramebufferUsingCurrentContext( outputTexture.fboId, outputTexture.width, outputTexture.height); outputBitmap =