From 07ec1eaa480146942e8019a1db25661201a2f3e0 Mon Sep 17 00:00:00 2001 From: huangdarwin Date: Mon, 15 May 2023 14:43:30 +0100 Subject: [PATCH] Effect: Multiple Texture output Allow the VideoFrameProcessor to output multiple textures at a time, so that lifetime of textures is up to the consumer calling VFP.releaseOutputFrame. The FinalShaderProgramWrapper also has a new maxCapacity limit added, to ensure the a reasonable amount of textures is used and avoid using up memory. PiperOrigin-RevId: 532094256 --- .../media3/common/VideoFrameProcessor.java | 14 ++- .../effect/DefaultVideoFrameProcessor.java | 78 +++++++++++++--- .../effect/FinalShaderProgramWrapper.java | 93 +++++++++++++------ .../utils/VideoFrameProcessorTestRunner.java | 21 ++++- ...oFrameProcessorTextureOutputPixelTest.java | 44 ++++++--- 5 files changed, 188 insertions(+), 62 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/VideoFrameProcessor.java b/libraries/common/src/main/java/androidx/media3/common/VideoFrameProcessor.java index d22209f9cb..1ad966e5d0 100644 --- a/libraries/common/src/main/java/androidx/media3/common/VideoFrameProcessor.java +++ b/libraries/common/src/main/java/androidx/media3/common/VideoFrameProcessor.java @@ -145,7 +145,7 @@ public interface VideoFrameProcessor { */ void onError(VideoFrameProcessingException exception); - /** Called after the {@link VideoFrameProcessor} has produced its final output frame. */ + /** Called after the {@link VideoFrameProcessor} has rendered its final output frame. */ void onEnded(); } @@ -291,6 +291,18 @@ public interface VideoFrameProcessor { */ void renderOutputFrame(long renderTimeNs); + /** + * Releases resources associated with all output frames with presentation time less than or equal + * to {@code presentationTimeUs}. + * + *

Not needed for outputting to an {@linkplain #setOutputSurfaceInfo output surface}, but may + * be required for other outputs. + * + * @param presentationTimeUs The presentation time where all frames before and at this time should + * be released, in microseconds. + */ + void releaseOutputFrame(long presentationTimeUs); + /** * Informs the {@code VideoFrameProcessor} that no further input frames should be accepted. * 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 850d5b733e..16d1eba3be 100644 --- a/libraries/effect/src/main/java/androidx/media3/effect/DefaultVideoFrameProcessor.java +++ b/libraries/effect/src/main/java/androidx/media3/effect/DefaultVideoFrameProcessor.java @@ -31,6 +31,7 @@ import android.opengl.GLES20; import android.opengl.GLES30; import android.view.Surface; import androidx.annotation.GuardedBy; +import androidx.annotation.IntRange; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.media3.common.C; @@ -73,7 +74,7 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor { public interface TextureOutputListener { /** Called when a texture has been rendered to. */ void onTextureRendered(GlTextureInfo outputTexture, long presentationTimeUs) - throws GlUtil.GlException, VideoFrameProcessingException; + throws VideoFrameProcessingException; } /** A factory for {@link DefaultVideoFrameProcessor} instances. */ @@ -84,6 +85,7 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor { private boolean enableColorTransfers; private GlObjectsProvider glObjectsProvider; @Nullable private TextureOutputListener textureOutputListener; + private int textureOutputCapacity; /** Creates an instance. */ public Builder() { @@ -114,39 +116,55 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor { } /** - * Sets the {@link TextureOutputListener}. + * Sets texture output settings. * - *

If set, the {@link VideoFrameProcessor} will output to an OpenGL texture, accessible via - * {@link TextureOutputListener#onTextureRendered}. Otherwise, no texture will be rendered to. + *

If set, the {@link VideoFrameProcessor} will output to OpenGL textures, accessible via + * {@link TextureOutputListener#onTextureRendered}. Textures will stop being output when + * {@code textureOutputCapacity} is reached, until they're released via {@link + * #releaseOutputFrame}. Output textures must be released using {@link #releaseOutputFrame}. * - *

If an {@linkplain #setOutputSurfaceInfo output surface} is set, the texture output will - * be be adjusted as needed, to match the output surface's output. + *

If not set, there will be no texture output. + * + *

This must not be set if the {@linkplain #setOutputSurfaceInfo output surface info} is + * also set. + * + * @param textureOutputListener The {@link TextureOutputListener}. + * @param textureOutputCapacity The amount of output textures that may be allocated at a time + * before texture output blocks. Must be greater than or equal to 1. */ @VisibleForTesting @CanIgnoreReturnValue - public Builder setOnTextureRenderedListener(TextureOutputListener textureOutputListener) { + public Builder setTextureOutput( + TextureOutputListener textureOutputListener, + @IntRange(from = 1) int textureOutputCapacity) { + // TODO: http://b/262694346 - Add tests for multiple texture output. this.textureOutputListener = textureOutputListener; + checkArgument(textureOutputCapacity >= 1); + this.textureOutputCapacity = textureOutputCapacity; return this; } /** Builds an {@link DefaultVideoFrameProcessor.Factory} instance. */ public DefaultVideoFrameProcessor.Factory build() { return new DefaultVideoFrameProcessor.Factory( - enableColorTransfers, glObjectsProvider, textureOutputListener); + enableColorTransfers, glObjectsProvider, textureOutputListener, textureOutputCapacity); } } private final boolean enableColorTransfers; private final GlObjectsProvider glObjectsProvider; @Nullable private final TextureOutputListener textureOutputListener; + private final int textureOutputCapacity; private Factory( boolean enableColorTransfers, GlObjectsProvider glObjectsProvider, - @Nullable TextureOutputListener textureOutputListener) { + @Nullable TextureOutputListener textureOutputListener, + int textureOutputCapacity) { this.enableColorTransfers = enableColorTransfers; this.glObjectsProvider = glObjectsProvider; this.textureOutputListener = textureOutputListener; + this.textureOutputCapacity = textureOutputCapacity; } /** @@ -231,7 +249,8 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor { listenerExecutor, listener, glObjectsProvider, - textureOutputListener)); + textureOutputListener, + textureOutputCapacity)); try { return defaultVideoFrameProcessorFuture.get(); @@ -411,11 +430,23 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor { return checkNotNull(textureManager).getPendingFrameCount(); } + /** + * {@inheritDoc} + * + *

This must not be set on an instance where {@linkplain Factory.Builder#setTextureOutput + * texture output} is set. + */ @Override public void setOutputSurfaceInfo(@Nullable SurfaceInfo outputSurfaceInfo) { finalShaderProgramWrapper.setOutputSurfaceInfo(outputSurfaceInfo); } + /** + * {@inheritDoc} + * + *

This may also be used for rendering from an output texture, if a {@link + * TextureOutputListener} {@linkplain Factory.Builder#setTextureOutput is set} + */ @Override public void renderOutputFrame(long renderTimeNs) { checkState( @@ -425,6 +456,21 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor { () -> finalShaderProgramWrapper.renderOutputFrame(renderTimeNs)); } + /** + * {@inheritDoc} + * + *

If a {@link TextureOutputListener} {@linkplain Factory.Builder#setTextureOutput is set}, + * this must be called to release the output information stored in the {@link GlTextureInfo} + * instances. + */ + @Override + public void releaseOutputFrame(long presentationTimeUs) { + // TODO(b/262694346): Add Compositor system tests exercising this code path after GL texture + // input is possible. + videoFrameProcessingTaskExecutor.submit( + () -> finalShaderProgramWrapper.releaseOutputFrame(presentationTimeUs)); + } + @Override public void signalEndOfInput() { DebugTraceUtil.recordVideoFrameProcessorReceiveDecoderEos(); @@ -511,7 +557,8 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor { Executor executor, Listener listener, GlObjectsProvider glObjectsProvider, - @Nullable TextureOutputListener textureOutputListener) + @Nullable TextureOutputListener textureOutputListener, + int textureOutputCapacity) throws GlUtil.GlException, VideoFrameProcessingException { checkState(Thread.currentThread().getName().equals(THREAD_NAME)); @@ -568,7 +615,8 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor { executor, listener, glObjectsProvider, - textureOutputListener); + textureOutputListener, + textureOutputCapacity); inputSwitcher.registerInput(INPUT_TYPE_SURFACE); if (!ColorInfo.isTransferHdr(inputColorInfo)) { @@ -618,7 +666,8 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor { Executor executor, Listener listener, GlObjectsProvider glObjectsProvider, - @Nullable TextureOutputListener textureOutputListener) + @Nullable TextureOutputListener textureOutputListener, + int textureOutputCapacity) throws VideoFrameProcessingException { ImmutableList.Builder shaderProgramListBuilder = new ImmutableList.Builder<>(); ImmutableList.Builder matrixTransformationListBuilder = @@ -670,7 +719,8 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor { executor, listener, glObjectsProvider, - textureOutputListener)); + textureOutputListener, + textureOutputCapacity)); 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 20dacf686b..16eb525558 100644 --- a/libraries/effect/src/main/java/androidx/media3/effect/FinalShaderProgramWrapper.java +++ b/libraries/effect/src/main/java/androidx/media3/effect/FinalShaderProgramWrapper.java @@ -50,8 +50,8 @@ import java.util.concurrent.Executor; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** - * Wrapper around a {@link DefaultShaderProgram} that renders to the provided output surface or - * texture. + * Wrapper around a {@link DefaultShaderProgram} that renders to either the provided output surface + * or texture. * *

Also renders to a debug surface, if provided. * @@ -87,17 +87,20 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private final Executor videoFrameProcessorListenerExecutor; private final VideoFrameProcessor.Listener videoFrameProcessorListener; private final Queue> availableFrames; + private final Queue> outputTextures; @Nullable private final DefaultVideoFrameProcessor.TextureOutputListener textureOutputListener; + private final int textureOutputCapacity; private int inputWidth; private int inputHeight; + private int outputWidth; + private int outputHeight; @Nullable private DefaultShaderProgram defaultShaderProgram; @Nullable private SurfaceViewWrapper debugSurfaceViewWrapper; private GlObjectsProvider glObjectsProvider; private InputListener inputListener; private @MonotonicNonNull Size outputSizeBeforeSurfaceTransformation; @Nullable private SurfaceView debugSurfaceView; - @Nullable private GlTextureInfo outputTexture; @Nullable private OnInputStreamProcessedListener onInputStreamProcessedListener; private boolean frameProcessingStarted; @@ -125,7 +128,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; Executor videoFrameProcessorListenerExecutor, VideoFrameProcessor.Listener videoFrameProcessorListener, GlObjectsProvider glObjectsProvider, - @Nullable DefaultVideoFrameProcessor.TextureOutputListener textureOutputListener) { + @Nullable DefaultVideoFrameProcessor.TextureOutputListener textureOutputListener, + int textureOutputCapacity) { this.context = context; this.matrixTransformations = matrixTransformations; this.rgbMatrices = rgbMatrices; @@ -139,9 +143,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; this.videoFrameProcessorListener = videoFrameProcessorListener; this.glObjectsProvider = glObjectsProvider; this.textureOutputListener = textureOutputListener; + this.textureOutputCapacity = textureOutputCapacity; inputListener = new InputListener() {}; availableFrames = new ConcurrentLinkedQueue<>(); + outputTextures = new ConcurrentLinkedQueue<>(); } @Override @@ -155,7 +161,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public void setInputListener(InputListener inputListener) { this.inputListener = inputListener; - inputListener.onReadyToAcceptInputFrame(); + maybeOnReadyToAcceptInputFrame(); } @Override @@ -193,20 +199,40 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; frameProcessingStarted = true; videoFrameProcessorListenerExecutor.execute( () -> videoFrameProcessorListener.onOutputFrameAvailableForRendering(presentationTimeUs)); - if (renderFramesAutomatically) { - renderFrame(inputTexture, presentationTimeUs, /* renderTimeNs= */ presentationTimeUs * 1000); + if (textureOutputListener == null) { + if (renderFramesAutomatically) { + renderFrame( + inputTexture, presentationTimeUs, /* renderTimeNs= */ presentationTimeUs * 1000); + } else { + availableFrames.add(Pair.create(inputTexture, presentationTimeUs)); + } } else { - availableFrames.add(Pair.create(inputTexture, presentationTimeUs)); + checkState(outputTextures.size() < textureOutputCapacity); + renderFrame(inputTexture, presentationTimeUs, /* renderTimeNs= */ presentationTimeUs * 1000); } - inputListener.onReadyToAcceptInputFrame(); + maybeOnReadyToAcceptInputFrame(); } @Override public void releaseOutputFrame(GlTextureInfo outputTexture) { - // The final shader program writes to a surface so there is no texture to release. + // FinalShaderProgramWrapper cannot release output textures using GlTextureInfo. throw new UnsupportedOperationException(); } + public void releaseOutputFrame(long presentationTimeUs) throws VideoFrameProcessingException { + while (!outputTextures.isEmpty() + && checkNotNull(outputTextures.peek()).second <= presentationTimeUs) { + GlTextureInfo outputTexture = outputTextures.remove().first; + try { + GlUtil.deleteTexture(outputTexture.texId); + GlUtil.deleteFbo(outputTexture.fboId); + } catch (GlUtil.GlException exception) { + throw new VideoFrameProcessingException(exception); + } + maybeOnReadyToAcceptInputFrame(); + } + } + public void renderOutputFrame(long renderTimeNs) { frameProcessingStarted = true; checkState(!renderFramesAutomatically); @@ -226,7 +252,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; defaultShaderProgram.flush(); } inputListener.onFlush(); - inputListener.onReadyToAcceptInputFrame(); + maybeOnReadyToAcceptInputFrame(); } @Override @@ -235,8 +261,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; defaultShaderProgram.release(); } try { - if (outputTexture != null) { - GlTextureInfo outputTexture = checkNotNull(this.outputTexture); + while (!outputTextures.isEmpty()) { + GlTextureInfo outputTexture = outputTextures.remove().first; GlUtil.deleteTexture(outputTexture.texId); GlUtil.deleteFbo(outputTexture.fboId); } @@ -252,6 +278,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * @see VideoFrameProcessor#setOutputSurfaceInfo(SurfaceInfo) */ public synchronized void setOutputSurfaceInfo(@Nullable SurfaceInfo outputSurfaceInfo) { + checkState(textureOutputListener == null); if (Util.areEqual(this.outputSurfaceInfo, outputSurfaceInfo)) { return; } @@ -276,18 +303,23 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; this.outputSurfaceInfo = outputSurfaceInfo; } + private void maybeOnReadyToAcceptInputFrame() { + if (textureOutputListener == null || outputTextures.size() < textureOutputCapacity) { + inputListener.onReadyToAcceptInputFrame(); + } + } + private synchronized void renderFrame( GlTextureInfo inputTexture, long presentationTimeUs, long renderTimeNs) { try { if (renderTimeNs == VideoFrameProcessor.DROP_OUTPUT_FRAME || !ensureConfigured(inputTexture.width, inputTexture.height)) { inputListener.onInputFrameProcessed(inputTexture); - return; // Drop frames when requested, or there is no output surface. + return; // Drop frames when requested, or there is no output surface and output texture. } if (outputSurfaceInfo != null) { renderFrameToOutputSurface(inputTexture, presentationTimeUs, renderTimeNs); - } - if (textureOutputListener != null) { + } else if (textureOutputListener != null) { renderFrameToOutputTexture(inputTexture, presentationTimeUs); } } catch (VideoFrameProcessingException | GlUtil.GlException e) { @@ -331,12 +363,25 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private void renderFrameToOutputTexture(GlTextureInfo inputTexture, long presentationTimeUs) throws GlUtil.GlException, VideoFrameProcessingException { - GlTextureInfo outputTexture = checkNotNull(this.outputTexture); + // TODO(b/262694346): Use a texture pool instead of creating a new texture on every frame. + int outputTexId = + GlUtil.createTexture( + outputWidth, + outputHeight, + /* useHighPrecisionColorComponents= */ ColorInfo.isTransferHdr(outputColorInfo)); + GlTextureInfo outputTexture = + glObjectsProvider.createBuffersForTexture(outputTexId, outputWidth, outputHeight); + GlUtil.focusFramebufferUsingCurrentContext( outputTexture.fboId, outputTexture.width, outputTexture.height); GlUtil.clearOutputFrame(); checkNotNull(defaultShaderProgram).drawFrame(inputTexture.texId, presentationTimeUs); + // TODO(b/262694346): If Compositor's VFPs all use the same context, media3 should be able to + // avoid calling glFinish, and require the onTextureRendered listener to decide whether to + // glFinish. Consider removing glFinish and requiring onTextureRendered to handle + // synchronization. GLES20.glFinish(); + outputTextures.add(Pair.create(outputTexture, presentationTimeUs)); checkNotNull(textureOutputListener).onTextureRendered(outputTexture, presentationTimeUs); } @@ -381,11 +426,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; return false; } - int outputWidth = + outputWidth = outputSurfaceInfo == null ? outputSizeBeforeSurfaceTransformation.getWidth() : outputSurfaceInfo.width; - int outputHeight = + outputHeight = outputSurfaceInfo == null ? outputSizeBeforeSurfaceTransformation.getHeight() : outputSurfaceInfo.height; @@ -411,16 +456,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } this.debugSurfaceView = debugSurfaceView; - if (textureOutputListener != null) { - int outputTexId = - GlUtil.createTexture( - outputWidth, - outputHeight, - /* useHighPrecisionColorComponents= */ ColorInfo.isTransferHdr(outputColorInfo)); - outputTexture = - glObjectsProvider.createBuffersForTexture(outputTexId, outputWidth, outputHeight); - } - if (defaultShaderProgram != null && (outputSurfaceInfoChanged || inputSizeChanged)) { defaultShaderProgram.release(); defaultShaderProgram = null; 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 2331810c6a..f6b74328e9 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 @@ -285,13 +285,14 @@ public final class VideoFrameProcessorTestRunner { new VideoFrameProcessor.Listener() { @Override public void onOutputSizeChanged(int width, int height) { + boolean useHighPrecisionColorComponents = ColorInfo.isTransferHdr(outputColorInfo); @Nullable Surface outputSurface = bitmapReader.getSurface( width, height, - /* useHighPrecisionColorComponents= */ ColorInfo.isTransferHdr( - outputColorInfo)); + useHighPrecisionColorComponents, + checkNotNull(videoFrameProcessor)::releaseOutputFrame); if (outputSurface != null) { checkNotNull(videoFrameProcessor) .setOutputSurfaceInfo(new SurfaceInfo(outputSurface, width, height)); @@ -406,10 +407,18 @@ public final class VideoFrameProcessorTestRunner { /** Reads a {@link Bitmap} from {@link VideoFrameProcessor} output. */ public interface BitmapReader { + /** Wraps a callback for {@link VideoFrameProcessor#releaseOutputFrame}. */ + interface ReleaseOutputFrameListener { + void releaseOutputFrame(long releaseTimeUs); + } /** Returns the {@link VideoFrameProcessor} output {@link Surface}, if one is needed. */ @Nullable - Surface getSurface(int width, int height, boolean useHighPrecisionColorComponents); + Surface getSurface( + int width, + int height, + boolean useHighPrecisionColorComponents, + ReleaseOutputFrameListener listener); /** Returns the output {@link Bitmap}. */ Bitmap getBitmap(); @@ -429,7 +438,11 @@ public final class VideoFrameProcessorTestRunner { @Override @SuppressLint("WrongConstant") @Nullable - public Surface getSurface(int width, int height, boolean useHighPrecisionColorComponents) { + public Surface getSurface( + int width, + int height, + boolean useHighPrecisionColorComponents, + ReleaseOutputFrameListener listener) { 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 107464e9bb..eec751a942 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 @@ -138,10 +138,11 @@ public final class DefaultVideoFrameProcessorTextureOutputPixelTest { TextureBitmapReader consumersBitmapReader = new TextureBitmapReader(); DefaultVideoFrameProcessor.Factory defaultVideoFrameProcessorFactory = new DefaultVideoFrameProcessor.Factory.Builder() - .setOnTextureRenderedListener( + .setTextureOutput( (outputTexture, presentationTimeUs) -> inputTextureIntoVideoFrameProcessor( - testId, consumersBitmapReader, outputTexture, presentationTimeUs)) + testId, consumersBitmapReader, outputTexture, presentationTimeUs), + /* textureOutputCapacity= */ 1) .build(); VideoFrameProcessorTestRunner texIdProducingVideoFrameProcessorTestRunner = new VideoFrameProcessorTestRunner.Builder() @@ -206,10 +207,11 @@ public final class DefaultVideoFrameProcessorTextureOutputPixelTest { TextureBitmapReader consumersBitmapReader = new TextureBitmapReader(); DefaultVideoFrameProcessor.Factory defaultVideoFrameProcessorFactory = new DefaultVideoFrameProcessor.Factory.Builder() - .setOnTextureRenderedListener( + .setTextureOutput( (outputTexture, presentationTimeUs) -> inputTextureIntoVideoFrameProcessor( - testId, consumersBitmapReader, outputTexture, presentationTimeUs)) + testId, consumersBitmapReader, outputTexture, presentationTimeUs), + /* textureOutputCapacity= */ 1) .build(); VideoFrameProcessorTestRunner texIdProducingVideoFrameProcessorTestRunner = new VideoFrameProcessorTestRunner.Builder() @@ -380,7 +382,7 @@ public final class DefaultVideoFrameProcessorTextureOutputPixelTest { new DefaultGlObjectsProvider(GlUtil.getCurrentContext()); DefaultVideoFrameProcessor.Factory defaultVideoFrameProcessorFactory = new DefaultVideoFrameProcessor.Factory.Builder() - .setOnTextureRenderedListener(bitmapReader::readBitmapFromTexture) + .setTextureOutput(bitmapReader::readBitmapFromTexture, /* textureOutputCapacity= */ 1) .setGlObjectsProvider(contextSharingGlObjectsProvider) .build(); videoFrameProcessorTestRunner = @@ -405,7 +407,8 @@ public final class DefaultVideoFrameProcessorTextureOutputPixelTest { TextureBitmapReader textureBitmapReader = new TextureBitmapReader(); DefaultVideoFrameProcessor.Factory defaultVideoFrameProcessorFactory = new DefaultVideoFrameProcessor.Factory.Builder() - .setOnTextureRenderedListener(textureBitmapReader::readBitmapFromTexture) + .setTextureOutput( + textureBitmapReader::readBitmapFromTexture, /* textureOutputCapacity= */ 1) .build(); return new VideoFrameProcessorTestRunner.Builder() .setTestId(testId) @@ -422,13 +425,19 @@ public final class DefaultVideoFrameProcessorTextureOutputPixelTest { private static final class TextureBitmapReader implements BitmapReader { // TODO(b/239172735): This outputs an incorrect black output image on emulators. private boolean useHighPrecisionColorComponents; + private @MonotonicNonNull ReleaseOutputFrameListener releaseOutputFrameListener; private @MonotonicNonNull Bitmap outputBitmap; - @Override @Nullable - public Surface getSurface(int width, int height, boolean useHighPrecisionColorComponents) { + @Override + public Surface getSurface( + int width, + int height, + boolean useHighPrecisionColorComponents, + ReleaseOutputFrameListener releaseOutputFrameListener) { this.useHighPrecisionColorComponents = useHighPrecisionColorComponents; + this.releaseOutputFrameListener = releaseOutputFrameListener; return null; } @@ -438,12 +447,19 @@ public final class DefaultVideoFrameProcessorTextureOutputPixelTest { } public void readBitmapFromTexture(GlTextureInfo outputTexture, long presentationTimeUs) - throws GlUtil.GlException { - GlUtil.focusFramebufferUsingCurrentContext( - outputTexture.fboId, outputTexture.width, outputTexture.height); - outputBitmap = - createBitmapFromCurrentGlFrameBuffer( - outputTexture.width, outputTexture.height, useHighPrecisionColorComponents); + throws VideoFrameProcessingException { + try { + GlUtil.focusFramebufferUsingCurrentContext( + outputTexture.fboId, outputTexture.width, outputTexture.height); + outputBitmap = + createBitmapFromCurrentGlFrameBuffer( + outputTexture.width, outputTexture.height, useHighPrecisionColorComponents); + GlUtil.deleteTexture(outputTexture.texId); + GlUtil.deleteFbo(outputTexture.fboId); + } catch (GlUtil.GlException e) { + throw new VideoFrameProcessingException(e); + } + checkNotNull(releaseOutputFrameListener).releaseOutputFrame(presentationTimeUs); } private static Bitmap createBitmapFromCurrentGlFrameBuffer(