diff --git a/libraries/common/src/main/java/androidx/media3/common/FrameProcessor.java b/libraries/common/src/main/java/androidx/media3/common/FrameProcessor.java index f7d52052e7..4b926c8ccb 100644 --- a/libraries/common/src/main/java/androidx/media3/common/FrameProcessor.java +++ b/libraries/common/src/main/java/androidx/media3/common/FrameProcessor.java @@ -127,20 +127,25 @@ public interface FrameProcessor { /** * Provides an input {@link Bitmap} to the {@link FrameProcessor}. * + *

This method should only be used for when the {@link FrameProcessor} was created with {@link + * C#TRACK_TYPE_IMAGE} as the {@code inputTrackType}. + * *

Can be called on any thread. * * @param inputBitmap The {@link Bitmap} queued to the {@link FrameProcessor}. * @param durationUs The duration for which to display the {@code inputBitmap}, in microseconds. * @param frameRate The frame rate at which to display the {@code inputBitmap}, in frames per - * second. */ - // TODO(b/262693274): Remove duration & frameRate parameters when EditedMediaItem can be signalled - // down to the processors. - void queueInputBitmap(Bitmap inputBitmap, long durationUs, int frameRate); + // TODO(b/262693274): Remove duration and frameRate parameters when EditedMediaItem can be + // signalled down to the processors. + void queueInputBitmap(Bitmap inputBitmap, long durationUs, float frameRate); /** * Returns the input {@link Surface}, where {@link FrameProcessor} consumes input frames from. * + *

This method should only be used for when the {@link FrameProcessor} was created with {@link + * C#TRACK_TYPE_VIDEO} as the {@code inputTrackType}. + * *

Can be called on any thread. */ Surface getInputSurface(); @@ -167,6 +172,9 @@ public interface FrameProcessor { * *

Must be called before rendering a frame to the frame processor's input surface. * + *

This method should only be used for when the {@link FrameProcessor} was created with {@link + * C#TRACK_TYPE_VIDEO} as the {@code inputTrackType}. + * *

Can be called on any thread. * * @throws IllegalStateException If called after {@link #signalEndOfInput()} or before {@link @@ -178,6 +186,9 @@ public interface FrameProcessor { * Returns the number of input frames that have been {@linkplain #registerInputFrame() registered} * but not processed off the {@linkplain #getInputSurface() input surface} yet. * + *

This method should only be used for when the {@link FrameProcessor} was created with {@link + * C#TRACK_TYPE_VIDEO} as the {@code inputTrackType}. + * *

Can be called on any thread. */ int getPendingInputFrameCount(); @@ -235,6 +246,9 @@ public interface FrameProcessor { *

All the frames that are {@linkplain #registerInputFrame() registered} prior to calling this * method are no longer considered to be registered when this method returns. * + *

This method should only be used for when the {@link FrameProcessor} was created with {@link + * C#TRACK_TYPE_VIDEO} as the {@code inputTrackType}. + * *

{@link Listener} methods invoked prior to calling this method should be ignored. */ void flush(); diff --git a/libraries/effect/src/androidTest/java/androidx/media3/effect/GlEffectsFrameProcessorPixelTest.java b/libraries/effect/src/androidTest/java/androidx/media3/effect/GlEffectsFrameProcessorPixelTest.java index 32c5c4f97e..d9111813e9 100644 --- a/libraries/effect/src/androidTest/java/androidx/media3/effect/GlEffectsFrameProcessorPixelTest.java +++ b/libraries/effect/src/androidTest/java/androidx/media3/effect/GlEffectsFrameProcessorPixelTest.java @@ -15,6 +15,7 @@ */ package androidx.media3.effect; +import static androidx.media3.common.C.TRACK_TYPE_IMAGE; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkStateNotNull; import static androidx.media3.effect.OverlayShaderProgramPixelTest.OVERLAY_PNG_ASSET_PATH; @@ -54,6 +55,8 @@ import org.junit.runner.RunWith; public final class GlEffectsFrameProcessorPixelTest { public static final String ORIGINAL_PNG_ASSET_PATH = "media/bitmap/sample_mp4_first_frame/electrical_colors/original.png"; + public static final String WRAPPED_CROP_PNG_ASSET_PATH = + "media/bitmap/sample_mp4_first_frame/electrical_colors/image_input_with_wrapped_crop.png"; // This file is generated on a Pixel 7, because the emulator isn't able to generate this file. public static final String BITMAP_OVERLAY_PNG_ASSET_PATH = "media/bitmap/sample_mp4_first_frame/electrical_colors/overlay_bitmap_FrameProcessor.png"; @@ -102,6 +105,50 @@ public final class GlEffectsFrameProcessorPixelTest { assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); } + @Test + public void noEffects_withImageInput_matchesGoldenFile() throws Exception { + String testId = "noEffects_withImageInput_matchesGoldenFile"; + frameProcessorTestRunner = + getDefaultFrameProcessorTestRunnerBuilder(testId) + .setInputTrackType(TRACK_TYPE_IMAGE) + .build(); + Bitmap expectedBitmap = readBitmap(ORIGINAL_PNG_ASSET_PATH); + + Bitmap actualBitmap = frameProcessorTestRunner.processImageFrameAndEnd(expectedBitmap); + + // TODO(b/207848601): switch to using proper tooling for testing against golden data. + float averagePixelAbsoluteDifference = + getBitmapAveragePixelAbsoluteDifferenceArgb8888(expectedBitmap, actualBitmap, testId); + assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); + } + + @Test + public void wrappedCrop_withImageInput_matchesGoldenFile() throws Exception { + String testId = "wrappedCrop_withImageInput_matchesGoldenFile"; + frameProcessorTestRunner = + getDefaultFrameProcessorTestRunnerBuilder(testId) + .setInputTrackType(TRACK_TYPE_IMAGE) + .setEffects( + new GlEffectWrapper( + new Crop( + /* left= */ -0.5f, + /* right= */ 0.5f, + /* bottom= */ -0.5f, + /* top= */ 0.5f))) + .build(); + Bitmap originalBitmap = readBitmap(ORIGINAL_PNG_ASSET_PATH); + Bitmap expectedBitmap = readBitmap(WRAPPED_CROP_PNG_ASSET_PATH); + + Bitmap actualBitmap = frameProcessorTestRunner.processImageFrameAndEnd(originalBitmap); + + // TODO(b/207848601): switch to using proper tooling for testing against golden data. + float averagePixelAbsoluteDifference = + getBitmapAveragePixelAbsoluteDifferenceArgb8888(expectedBitmap, actualBitmap, testId); + assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); + } + // TODO(b/262693274): Once texture deletion is added to InternalTextureManager.java, add a test + // queuing multiple input bitmaps to ensure successfully completion without errors. + @Test public void noEffects_withFrameCache_matchesGoldenFile() throws Exception { String testId = "noEffects_withFrameCache_matchesGoldenFile"; @@ -238,9 +285,10 @@ public final class GlEffectsFrameProcessorPixelTest { frameProcessorTestRunner = getDefaultFrameProcessorTestRunnerBuilder(testId) .setEffects( - new Crop(/* left= */ -.5f, /* right= */ .5f, /* bottom= */ -.5f, /* top= */ .5f), + new Crop( + /* left= */ -0.5f, /* right= */ 0.5f, /* bottom= */ -0.5f, /* top= */ 0.5f), Presentation.createForAspectRatio( - /* aspectRatio= */ .5f, Presentation.LAYOUT_SCALE_TO_FIT)) + /* aspectRatio= */ 0.5f, Presentation.LAYOUT_SCALE_TO_FIT)) .build(); Bitmap expectedBitmap = readBitmap(CROP_THEN_ASPECT_RATIO_PNG_ASSET_PATH); diff --git a/libraries/effect/src/main/assets/shaders/fragment_shader_transformation_sdr_internal_es2.glsl b/libraries/effect/src/main/assets/shaders/fragment_shader_transformation_sdr_internal_es2.glsl new file mode 100644 index 0000000000..5681862133 --- /dev/null +++ b/libraries/effect/src/main/assets/shaders/fragment_shader_transformation_sdr_internal_es2.glsl @@ -0,0 +1,103 @@ +#version 100 +// Copyright 2023 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +// ES 2 fragment shader that: +// 1. Samples from an input texture created from an internal texture (e.g. a +// texture created from a bitmap), with uTexSampler copying from this texture +// to the current output. +// 2. Transforms the electrical colors to optical colors using the SMPTE 170M +// EOTF. +// 3. Applies a 4x4 RGB color matrix to change the pixel colors. +// 4. Outputs as requested by uOutputColorTransfer. Use COLOR_TRANSFER_LINEAR +// for outputting to intermediate shaders, or COLOR_TRANSFER_SDR_VIDEO to +// output electrical colors via an OETF (e.g. to an encoder). + +precision mediump float; +uniform sampler2D uTexSampler; +uniform mat4 uRgbMatrix; +varying vec2 vTexSamplingCoord; +// C.java#ColorTransfer value. +// Only COLOR_TRANSFER_LINEAR and COLOR_TRANSFER_SDR_VIDEO are allowed. +uniform int uOutputColorTransfer; + +const float inverseGamma = 0.4500; +const float gamma = 1.0 / inverseGamma; + +// Transforms a single channel from electrical to optical SDR using the SMPTE +// 170M OETF. +float smpte170mEotfSingleChannel(float electricalChannel) { + // Specification: + // https://www.itu.int/rec/R-REC-BT.1700-0-200502-I/en + return electricalChannel < 0.0812 + ? electricalChannel / 4.500 + : pow((electricalChannel + 0.099) / 1.099, gamma); +} + +// Transforms electrical to optical SDR using the SMPTE 170M EOTF. +vec3 smpte170mEotf(vec3 electricalColor) { + return vec3( + smpte170mEotfSingleChannel(electricalColor.r), + smpte170mEotfSingleChannel(electricalColor.g), + smpte170mEotfSingleChannel(electricalColor.b)); +} + +// Transforms a single channel from optical to electrical SDR. +float smpte170mOetfSingleChannel(float opticalChannel) { + // Specification: + // https://www.itu.int/rec/R-REC-BT.1700-0-200502-I/en + return opticalChannel < 0.018 + ? opticalChannel * 4.500 + : 1.099 * pow(opticalChannel, inverseGamma) - 0.099; +} + +// Transforms optical SDR colors to electrical SDR using the SMPTE 170M OETF. +vec3 smpte170mOetf(vec3 opticalColor) { + return vec3( + smpte170mOetfSingleChannel(opticalColor.r), + smpte170mOetfSingleChannel(opticalColor.g), + smpte170mOetfSingleChannel(opticalColor.b)); +} + +// Applies the appropriate OETF to convert linear optical signals to nonlinear +// electrical signals. Input and output are both normalized to [0, 1]. +highp vec3 applyOetf(highp vec3 linearColor) { + // LINT.IfChange(color_transfer) + const int COLOR_TRANSFER_LINEAR = 1; + const int COLOR_TRANSFER_SDR_VIDEO = 3; + if (uOutputColorTransfer == COLOR_TRANSFER_LINEAR) { + return linearColor; + } else if (uOutputColorTransfer == COLOR_TRANSFER_SDR_VIDEO) { + return smpte170mOetf(linearColor); + } else { + // Output red as an obviously visible error. + return vec3(1.0, 0.0, 0.0); + } +} + +void main() { + vec2 vTexSamplingCoordFlipped = + vec2(vTexSamplingCoord.x, 1.0 - vTexSamplingCoord.y); + // Whereas the Android system uses the top-left corner as (0,0) of the + // coordinate system, OpenGL uses the bottom-left corner as (0,0), so the + // texture gets flipped. We flip the texture vertically to ensure the + // orientation of the output is correct. + vec4 inputColor = texture2D(uTexSampler, vTexSamplingCoordFlipped); + vec3 linearInputColor = smpte170mEotf(inputColor.rgb); + + vec4 transformedColors = uRgbMatrix * vec4(linearInputColor, 1); + + gl_FragColor = vec4(applyOetf(transformedColors.rgb), inputColor.a); +} diff --git a/libraries/effect/src/main/java/androidx/media3/effect/FinalMatrixShaderProgramWrapper.java b/libraries/effect/src/main/java/androidx/media3/effect/FinalMatrixShaderProgramWrapper.java index c6e45a30d9..31cb6c9524 100644 --- a/libraries/effect/src/main/java/androidx/media3/effect/FinalMatrixShaderProgramWrapper.java +++ b/libraries/effect/src/main/java/androidx/media3/effect/FinalMatrixShaderProgramWrapper.java @@ -71,7 +71,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private final EGLDisplay eglDisplay; private final EGLContext eglContext; private final DebugViewProvider debugViewProvider; - private final boolean sampleFromExternalTexture; + private final boolean sampleFromInputTexture; + private final boolean isInputExternal; private final ColorInfo inputColorInfo; private final ColorInfo outputColorInfo; private final boolean releaseFramesAutomatically; @@ -106,9 +107,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ImmutableList matrixTransformations, ImmutableList rgbMatrices, DebugViewProvider debugViewProvider, - boolean sampleFromExternalTexture, ColorInfo inputColorInfo, ColorInfo outputColorInfo, + boolean sampleFromInputTexture, + boolean isInputExternal, boolean releaseFramesAutomatically, Executor frameProcessorListenerExecutor, FrameProcessor.Listener frameProcessorListener) { @@ -118,7 +120,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; this.eglDisplay = eglDisplay; this.eglContext = eglContext; this.debugViewProvider = debugViewProvider; - this.sampleFromExternalTexture = sampleFromExternalTexture; + this.sampleFromInputTexture = sampleFromInputTexture; + this.isInputExternal = isInputExternal; this.inputColorInfo = inputColorInfo; this.outputColorInfo = outputColorInfo; this.releaseFramesAutomatically = releaseFramesAutomatically; @@ -402,14 +405,24 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; MatrixShaderProgram matrixShaderProgram; ImmutableList expandedMatrixTransformations = matrixTransformationListBuilder.build(); - if (sampleFromExternalTexture) { - matrixShaderProgram = - MatrixShaderProgram.createWithExternalSampler( - context, - expandedMatrixTransformations, - rgbMatrices, - /* inputColorInfo= */ inputColorInfo, - /* outputColorInfo= */ outputColorInfo); + if (sampleFromInputTexture) { + if (isInputExternal) { + matrixShaderProgram = + MatrixShaderProgram.createWithExternalSampler( + context, + expandedMatrixTransformations, + rgbMatrices, + inputColorInfo, + outputColorInfo); + } else { + matrixShaderProgram = + MatrixShaderProgram.createWithInternalSampler( + context, + expandedMatrixTransformations, + rgbMatrices, + inputColorInfo, + outputColorInfo); + } } else { matrixShaderProgram = MatrixShaderProgram.createApplyingOetf( diff --git a/libraries/effect/src/main/java/androidx/media3/effect/GlEffectsFrameProcessor.java b/libraries/effect/src/main/java/androidx/media3/effect/GlEffectsFrameProcessor.java index 16eb2c74d7..8ed4eadc64 100644 --- a/libraries/effect/src/main/java/androidx/media3/effect/GlEffectsFrameProcessor.java +++ b/libraries/effect/src/main/java/androidx/media3/effect/GlEffectsFrameProcessor.java @@ -16,6 +16,7 @@ package androidx.media3.effect; import static androidx.media3.common.util.Assertions.checkArgument; +import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkStateNotNull; import static com.google.common.collect.Iterables.getLast; @@ -108,6 +109,7 @@ public final class GlEffectsFrameProcessor implements FrameProcessor { checkArgument(inputColorInfo.colorTransfer != C.COLOR_TRANSFER_LINEAR); checkArgument(outputColorInfo.isValid()); checkArgument(outputColorInfo.colorTransfer != C.COLOR_TRANSFER_LINEAR); + checkArgument(inputTrackType == C.TRACK_TYPE_VIDEO || inputTrackType == C.TRACK_TYPE_IMAGE); if (inputColorInfo.colorSpace != outputColorInfo.colorSpace || ColorInfo.isTransferHdr(inputColorInfo) != ColorInfo.isTransferHdr(outputColorInfo)) { @@ -134,6 +136,7 @@ public final class GlEffectsFrameProcessor implements FrameProcessor { debugViewProvider, inputColorInfo, outputColorInfo, + /* isInputExternal= */ inputTrackType == C.TRACK_TYPE_VIDEO, releaseFramesAutomatically, singleThreadExecutorService, listenerExecutor, @@ -167,6 +170,7 @@ public final class GlEffectsFrameProcessor implements FrameProcessor { DebugViewProvider debugViewProvider, ColorInfo inputColorInfo, ColorInfo outputColorInfo, + boolean isInputExternal, boolean releaseFramesAutomatically, ExecutorService singleThreadExecutorService, Executor executor, @@ -208,6 +212,7 @@ public final class GlEffectsFrameProcessor implements FrameProcessor { debugViewProvider, inputColorInfo, outputColorInfo, + isInputExternal, releaseFramesAutomatically, executor, listener); @@ -219,6 +224,7 @@ public final class GlEffectsFrameProcessor implements FrameProcessor { return new GlEffectsFrameProcessor( eglDisplay, eglContext, + isInputExternal, frameProcessingTaskExecutor, shaderPrograms, releaseFramesAutomatically); @@ -243,6 +249,7 @@ public final class GlEffectsFrameProcessor implements FrameProcessor { DebugViewProvider debugViewProvider, ColorInfo inputColorInfo, ColorInfo outputColorInfo, + boolean isInputExternal, boolean releaseFramesAutomatically, Executor executor, Listener listener) @@ -251,7 +258,7 @@ public final class GlEffectsFrameProcessor implements FrameProcessor { ImmutableList.Builder matrixTransformationListBuilder = new ImmutableList.Builder<>(); ImmutableList.Builder rgbMatrixListBuilder = new ImmutableList.Builder<>(); - boolean sampleFromExternalTexture = true; + boolean sampleFromInputTexture = true; ColorInfo linearColorInfo = new ColorInfo( outputColorInfo.colorSpace, outputColorInfo.colorRange, C.COLOR_TRANSFER_LINEAR, null); @@ -275,16 +282,18 @@ public final class GlEffectsFrameProcessor implements FrameProcessor { matrixTransformationListBuilder.build(); ImmutableList rgbMatrices = rgbMatrixListBuilder.build(); boolean isOutputTransferHdr = ColorInfo.isTransferHdr(outputColorInfo); - if (!matrixTransformations.isEmpty() || !rgbMatrices.isEmpty() || sampleFromExternalTexture) { + if (!matrixTransformations.isEmpty() || !rgbMatrices.isEmpty() || sampleFromInputTexture) { MatrixShaderProgram matrixShaderProgram; - if (sampleFromExternalTexture) { - matrixShaderProgram = - MatrixShaderProgram.createWithExternalSampler( - context, - matrixTransformations, - rgbMatrices, - /* inputColorInfo= */ inputColorInfo, - /* outputColorInfo= */ linearColorInfo); + if (sampleFromInputTexture) { + if (isInputExternal) { + matrixShaderProgram = + MatrixShaderProgram.createWithExternalSampler( + context, matrixTransformations, rgbMatrices, inputColorInfo, linearColorInfo); + } else { + matrixShaderProgram = + MatrixShaderProgram.createWithInternalSampler( + context, matrixTransformations, rgbMatrices, inputColorInfo, linearColorInfo); + } } else { matrixShaderProgram = MatrixShaderProgram.create( @@ -293,7 +302,7 @@ public final class GlEffectsFrameProcessor implements FrameProcessor { shaderProgramListBuilder.add(matrixShaderProgram); matrixTransformationListBuilder = new ImmutableList.Builder<>(); rgbMatrixListBuilder = new ImmutableList.Builder<>(); - sampleFromExternalTexture = false; + sampleFromInputTexture = false; } shaderProgramListBuilder.add(glEffect.toGlShaderProgram(context, isOutputTransferHdr)); } @@ -306,9 +315,10 @@ public final class GlEffectsFrameProcessor implements FrameProcessor { matrixTransformationListBuilder.build(), rgbMatrixListBuilder.build(), debugViewProvider, - sampleFromExternalTexture, - /* inputColorInfo= */ sampleFromExternalTexture ? inputColorInfo : linearColorInfo, + /* inputColorInfo= */ sampleFromInputTexture ? inputColorInfo : linearColorInfo, outputColorInfo, + sampleFromInputTexture, + isInputExternal, releaseFramesAutomatically, executor, listener)); @@ -343,8 +353,10 @@ public final class GlEffectsFrameProcessor implements FrameProcessor { private final EGLDisplay eglDisplay; private final EGLContext eglContext; private final FrameProcessingTaskExecutor frameProcessingTaskExecutor; - private final ExternalTextureManager inputExternalTextureManager; - private final Surface inputSurface; + private @MonotonicNonNull InternalTextureManager inputInternalTextureManager; + private @MonotonicNonNull ExternalTextureManager inputExternalTextureManager; + // TODO(262693274): Move this variable to ExternalTextureManager. + private @MonotonicNonNull Surface inputExternalSurface; private final boolean releaseFramesAutomatically; private final FinalMatrixShaderProgramWrapper finalShaderProgramWrapper; private final ImmutableList allShaderPrograms; @@ -361,6 +373,7 @@ public final class GlEffectsFrameProcessor implements FrameProcessor { private GlEffectsFrameProcessor( EGLDisplay eglDisplay, EGLContext eglContext, + boolean isInputExternal, FrameProcessingTaskExecutor frameProcessingTaskExecutor, ImmutableList shaderPrograms, boolean releaseFramesAutomatically) @@ -372,14 +385,23 @@ public final class GlEffectsFrameProcessor implements FrameProcessor { this.releaseFramesAutomatically = releaseFramesAutomatically; checkState(!shaderPrograms.isEmpty()); - checkState(shaderPrograms.get(0) instanceof ExternalShaderProgram); checkState(getLast(shaderPrograms) instanceof FinalMatrixShaderProgramWrapper); - ExternalShaderProgram inputExternalShaderProgram = - (ExternalShaderProgram) shaderPrograms.get(0); - inputExternalTextureManager = - new ExternalTextureManager(inputExternalShaderProgram, frameProcessingTaskExecutor); - inputExternalShaderProgram.setInputListener(inputExternalTextureManager); - inputSurface = new Surface(inputExternalTextureManager.getSurfaceTexture()); + + GlShaderProgram inputShaderProgram = shaderPrograms.get(0); + + if (isInputExternal) { + checkState(inputShaderProgram instanceof ExternalShaderProgram); + inputExternalTextureManager = + new ExternalTextureManager( + (ExternalShaderProgram) inputShaderProgram, frameProcessingTaskExecutor); + inputShaderProgram.setInputListener(inputExternalTextureManager); + inputExternalSurface = new Surface(inputExternalTextureManager.getSurfaceTexture()); + } else { + inputInternalTextureManager = + new InternalTextureManager(inputShaderProgram, frameProcessingTaskExecutor); + inputShaderProgram.setInputListener(inputInternalTextureManager); + } + finalShaderProgramWrapper = (FinalMatrixShaderProgramWrapper) getLast(shaderPrograms); allShaderPrograms = shaderPrograms; previousStreamOffsetUs = C.TIME_UNSET; @@ -400,19 +422,27 @@ public final class GlEffectsFrameProcessor implements FrameProcessor { * call this method after instantiation to ensure that buffers are handled at full resolution. See * {@link SurfaceTexture#setDefaultBufferSize(int, int)} for more information. * + *

This method should only be used for when the {@link FrameProcessor} was created with {@link + * C#TRACK_TYPE_VIDEO} as the {@code inputTrackType}. + * * @param width The default width for input buffers, in pixels. * @param height The default height for input buffers, in pixels. */ public void setInputDefaultBufferSize(int width, int height) { - inputExternalTextureManager.getSurfaceTexture().setDefaultBufferSize(width, height); + checkNotNull(inputExternalTextureManager) + .getSurfaceTexture() + .setDefaultBufferSize(width, height); } @Override - public void queueInputBitmap(Bitmap inputBitmap, long durationUs, int frameRate) {} + public void queueInputBitmap(Bitmap inputBitmap, long durationUs, float frameRate) { + checkNotNull(inputInternalTextureManager) + .queueInputBitmap(inputBitmap, durationUs, frameRate, /* useHdr= */ false); + } @Override public Surface getInputSurface() { - return inputSurface; + return checkNotNull(inputExternalSurface); } @Override @@ -431,12 +461,12 @@ public final class GlEffectsFrameProcessor implements FrameProcessor { checkStateNotNull( nextInputFrameInfo, "setInputFrameInfo must be called before registering input frames"); - inputExternalTextureManager.registerInputFrame(nextInputFrameInfo); + checkNotNull(inputExternalTextureManager).registerInputFrame(nextInputFrameInfo); } @Override public int getPendingInputFrameCount() { - return inputExternalTextureManager.getPendingFrameCount(); + return checkNotNull(inputExternalTextureManager).getPendingFrameCount(); } @Override @@ -457,7 +487,12 @@ public final class GlEffectsFrameProcessor implements FrameProcessor { public void signalEndOfInput() { checkState(!inputStreamEnded); inputStreamEnded = true; - frameProcessingTaskExecutor.submit(inputExternalTextureManager::signalEndOfInput); + if (inputInternalTextureManager != null) { + frameProcessingTaskExecutor.submit(inputInternalTextureManager::signalEndOfInput); + } + if (inputExternalTextureManager != null) { + frameProcessingTaskExecutor.submit(inputExternalTextureManager::signalEndOfInput); + } } @Override @@ -465,7 +500,7 @@ public final class GlEffectsFrameProcessor implements FrameProcessor { try { frameProcessingTaskExecutor.flush(); CountDownLatch latch = new CountDownLatch(1); - inputExternalTextureManager.setOnFlushCompleteListener(latch::countDown); + checkNotNull(inputExternalTextureManager).setOnFlushCompleteListener(latch::countDown); frameProcessingTaskExecutor.submit(finalShaderProgramWrapper::flush); latch.await(); inputExternalTextureManager.setOnFlushCompleteListener(null); @@ -483,8 +518,10 @@ public final class GlEffectsFrameProcessor implements FrameProcessor { Thread.currentThread().interrupt(); throw new IllegalStateException(unexpected); } - inputExternalTextureManager.release(); - inputSurface.release(); + if (inputExternalTextureManager != null) { + inputExternalTextureManager.release(); + checkNotNull(inputExternalSurface).release(); + } } /** diff --git a/libraries/effect/src/main/java/androidx/media3/effect/InternalTextureManager.java b/libraries/effect/src/main/java/androidx/media3/effect/InternalTextureManager.java new file mode 100644 index 0000000000..fa6edfe6b0 --- /dev/null +++ b/libraries/effect/src/main/java/androidx/media3/effect/InternalTextureManager.java @@ -0,0 +1,159 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.effect; + +import static androidx.media3.common.util.Assertions.checkNotNull; +import static java.lang.Math.round; + +import android.graphics.Bitmap; +import android.opengl.GLES20; +import android.opengl.GLUtils; +import androidx.annotation.WorkerThread; +import androidx.media3.common.C; +import androidx.media3.common.FrameProcessingException; +import androidx.media3.common.FrameProcessor; +import androidx.media3.common.util.GlUtil; +import androidx.media3.common.util.UnstableApi; +import java.util.Queue; +import java.util.concurrent.LinkedBlockingQueue; + +/** Forwards a frame produced from a {@link Bitmap} to a {@link GlShaderProgram} for consumption. */ +@UnstableApi +/* package */ class InternalTextureManager implements GlShaderProgram.InputListener { + private final GlShaderProgram shaderProgram; + private final FrameProcessingTaskExecutor frameProcessingTaskExecutor; + private final Queue pendingBitmaps; + + private int downstreamShaderProgramCapacity; + private int availableFrameCount; + + private long currentPresentationTimeUs; + private long totalDurationUs; + private boolean inputEnded; + + public InternalTextureManager( + GlShaderProgram shaderProgram, FrameProcessingTaskExecutor frameProcessingTaskExecutor) { + this.shaderProgram = shaderProgram; + this.frameProcessingTaskExecutor = frameProcessingTaskExecutor; + pendingBitmaps = new LinkedBlockingQueue<>(); + } + + @Override + public void onReadyToAcceptInputFrame() { + frameProcessingTaskExecutor.submit( + () -> { + downstreamShaderProgramCapacity++; + maybeQueueToShaderProgram(); + }); + } + + @Override + public void onInputFrameProcessed(TextureInfo inputTexture) { + // TODO(b/262693274): Delete texture when last duplicate of the frame comes back from the shader + // program and change to only allocate one texId at a time. A change to method signature to + // include presentationTimeUs will probably be needed to do this. + frameProcessingTaskExecutor.submit( + () -> { + if (availableFrameCount == 0) { + signalEndOfInput(); + } + }); + } + + public void queueInputBitmap( + Bitmap inputBitmap, long durationUs, float frameRate, boolean useHdr) { + frameProcessingTaskExecutor.submit( + () -> setupBitmap(inputBitmap, durationUs, frameRate, useHdr)); + } + + @WorkerThread + private void setupBitmap(Bitmap bitmap, long durationUs, float frameRate, boolean useHdr) + throws FrameProcessingException { + if (inputEnded) { + return; + } + try { + int bitmapTexId = + GlUtil.createTexture( + bitmap.getWidth(), bitmap.getHeight(), /* useHighPrecisionColorComponents= */ useHdr); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, bitmapTexId); + GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, /* level= */ 0, bitmap, /* border= */ 0); + GlUtil.checkGlError(); + + TextureInfo textureInfo = + new TextureInfo( + bitmapTexId, /* fboId= */ C.INDEX_UNSET, bitmap.getWidth(), bitmap.getHeight()); + int timeIncrementUs = round(C.MICROS_PER_SECOND / frameRate); + availableFrameCount += round((frameRate * durationUs) / C.MICROS_PER_SECOND); + totalDurationUs += durationUs; + pendingBitmaps.add( + new BitmapFrameSequenceInfo(textureInfo, timeIncrementUs, totalDurationUs)); + } catch (GlUtil.GlException e) { + throw FrameProcessingException.from(e); + } + maybeQueueToShaderProgram(); + } + + @WorkerThread + private void maybeQueueToShaderProgram() { + if (inputEnded || availableFrameCount == 0 || downstreamShaderProgramCapacity == 0) { + return; + } + availableFrameCount--; + downstreamShaderProgramCapacity--; + + BitmapFrameSequenceInfo currentFrame = checkNotNull(pendingBitmaps.peek()); + shaderProgram.queueInputFrame(currentFrame.textureInfo, currentPresentationTimeUs); + + currentPresentationTimeUs += currentFrame.timeIncrementUs; + if (currentPresentationTimeUs >= currentFrame.endPresentationTimeUs) { + pendingBitmaps.remove(); + } + } + + /** + * Signals the end of the input. + * + * @see FrameProcessor#signalEndOfInput() + */ + public void signalEndOfInput() { + frameProcessingTaskExecutor.submit( + () -> { + if (inputEnded) { + return; + } + inputEnded = true; + shaderProgram.signalEndOfCurrentInputStream(); + }); + } + + /** + * Value class specifying information to generate all the frames associated with a specific {@link + * Bitmap}. + */ + private static final class BitmapFrameSequenceInfo { + public final TextureInfo textureInfo; + public final long timeIncrementUs; + public final long endPresentationTimeUs; + + public BitmapFrameSequenceInfo( + TextureInfo textureInfo, long timeIncrementUs, long endPresentationTimeUs) { + this.textureInfo = textureInfo; + this.timeIncrementUs = timeIncrementUs; + this.endPresentationTimeUs = endPresentationTimeUs; + } + } +} diff --git a/libraries/effect/src/main/java/androidx/media3/effect/MatrixShaderProgram.java b/libraries/effect/src/main/java/androidx/media3/effect/MatrixShaderProgram.java index f14a96089f..250a06f8d2 100644 --- a/libraries/effect/src/main/java/androidx/media3/effect/MatrixShaderProgram.java +++ b/libraries/effect/src/main/java/androidx/media3/effect/MatrixShaderProgram.java @@ -69,6 +69,8 @@ import java.util.List; "shaders/fragment_shader_transformation_external_yuv_es3.glsl"; private static final String FRAGMENT_SHADER_TRANSFORMATION_SDR_EXTERNAL_PATH = "shaders/fragment_shader_transformation_sdr_external_es2.glsl"; + private static final String FRAGMENT_SHADER_TRANSFORMATION_SDR_INTERNAL_PATH = + "shaders/fragment_shader_transformation_sdr_internal_es2.glsl"; private static final ImmutableList NDC_SQUARE = ImmutableList.of( new float[] {-1, -1, 0, 1}, @@ -163,6 +165,48 @@ import java.util.List; useHdr); } + /** + * Creates a new instance. + * + *

Input will be sampled from an internal (i.e. regular) texture. + * + *

Applies the {@linkplain ColorInfo#colorTransfer inputColorInfo EOTF} to convert from + * electrical color input, to intermediate optical {@link GlShaderProgram} color output, before + * {@code matrixTransformations} and {@code rgbMatrices} are applied. Also applies the {@linkplain + * ColorInfo#colorTransfer outputColorInfo OETF}, if needed, to convert back to an electrical + * color output. + * + * @param context The {@link Context}. + * @param matrixTransformations The {@link GlMatrixTransformation GlMatrixTransformations} to + * apply to each frame in order. Can be empty to apply no vertex transformations. + * @param rgbMatrices The {@link RgbMatrix RgbMatrices} to apply to each frame in order. Can be + * empty to apply no color transformations. + * @param inputColorInfo The input electrical (nonlinear) {@link ColorInfo}. + * @param outputColorInfo The output electrical (nonlinear) or optical (linear) {@link ColorInfo}. + * If this is an optical color, it must be BT.2020 if {@code inputColorInfo} is {@linkplain + * ColorInfo#isTransferHdr(ColorInfo) HDR}, and RGB BT.709 if not. + * @throws FrameProcessingException If a problem occurs while reading shader files or an OpenGL + * operation fails or is unsupported. + */ + public static MatrixShaderProgram createWithInternalSampler( + Context context, + List matrixTransformations, + List rgbMatrices, + ColorInfo inputColorInfo, + ColorInfo outputColorInfo) + throws FrameProcessingException { + checkState( + !ColorInfo.isTransferHdr(inputColorInfo), + "MatrixShaderProgram doesn't support HDR internal sampler input yet."); + GlProgram glProgram = + createGlProgram( + context, + VERTEX_SHADER_TRANSFORMATION_PATH, + FRAGMENT_SHADER_TRANSFORMATION_SDR_INTERNAL_PATH); + return createWithSampler( + glProgram, matrixTransformations, rgbMatrices, inputColorInfo, outputColorInfo); + } + /** * Creates a new instance. * @@ -204,50 +248,12 @@ import java.util.List; isInputTransferHdr ? FRAGMENT_SHADER_TRANSFORMATION_EXTERNAL_YUV_ES3_PATH : FRAGMENT_SHADER_TRANSFORMATION_SDR_EXTERNAL_PATH; - GlProgram glProgram = createGlProgram(context, vertexShaderFilePath, fragmentShaderFilePath); - - @C.ColorTransfer int outputColorTransfer = outputColorInfo.colorTransfer; - if (isInputTransferHdr) { - checkArgument(inputColorInfo.colorSpace == C.COLOR_SPACE_BT2020); - - // In HDR editing mode the decoder output is sampled in YUV. - if (!GlUtil.isYuvTargetExtensionSupported()) { - throw new FrameProcessingException( - "The EXT_YUV_target extension is required for HDR editing input."); - } - glProgram.setFloatsUniform( - "uYuvToRgbColorTransform", - inputColorInfo.colorRange == C.COLOR_RANGE_FULL - ? BT2020_FULL_RANGE_YUV_TO_RGB_COLOR_TRANSFORM_MATRIX - : BT2020_LIMITED_RANGE_YUV_TO_RGB_COLOR_TRANSFORM_MATRIX); - - checkArgument(ColorInfo.isTransferHdr(inputColorInfo)); - glProgram.setIntUniform("uInputColorTransfer", inputColorInfo.colorTransfer); - // TODO(b/239735341): Add a setBooleanUniform method to GlProgram. - glProgram.setIntUniform( - "uApplyHdrToSdrToneMapping", - /* value= */ (outputColorInfo.colorSpace != C.COLOR_SPACE_BT2020) ? 1 : 0); - checkArgument( - outputColorTransfer != Format.NO_VALUE && outputColorTransfer != C.COLOR_TRANSFER_SDR); - glProgram.setIntUniform("uOutputColorTransfer", outputColorTransfer); - } else { - checkArgument( - outputColorInfo.colorSpace != C.COLOR_SPACE_BT2020, - "Converting from SDR to HDR is not supported."); - checkArgument(inputColorInfo.colorSpace == outputColorInfo.colorSpace); - checkArgument( - outputColorTransfer == C.COLOR_TRANSFER_SDR - || outputColorTransfer == C.COLOR_TRANSFER_LINEAR); - // The SDR shader automatically applies an COLOR_TRANSFER_SDR EOTF. - glProgram.setIntUniform("uOutputColorTransfer", outputColorTransfer); - } - - return new MatrixShaderProgram( - glProgram, - ImmutableList.copyOf(matrixTransformations), - ImmutableList.copyOf(rgbMatrices), - outputColorInfo.colorTransfer, - isInputTransferHdr); + return createWithSampler( + createGlProgram(context, vertexShaderFilePath, fragmentShaderFilePath), + matrixTransformations, + rgbMatrices, + inputColorInfo, + outputColorInfo); } /** @@ -305,6 +311,58 @@ import java.util.List; outputIsHdr); } + private static MatrixShaderProgram createWithSampler( + GlProgram glProgram, + List matrixTransformations, + List rgbMatrices, + ColorInfo inputColorInfo, + ColorInfo outputColorInfo) + throws FrameProcessingException { + boolean isInputTransferHdr = ColorInfo.isTransferHdr(inputColorInfo); + @C.ColorTransfer int outputColorTransfer = outputColorInfo.colorTransfer; + if (isInputTransferHdr) { + checkArgument(inputColorInfo.colorSpace == C.COLOR_SPACE_BT2020); + + // In HDR editing mode the decoder output is sampled in YUV. + if (!GlUtil.isYuvTargetExtensionSupported()) { + throw new FrameProcessingException( + "The EXT_YUV_target extension is required for HDR editing input."); + } + glProgram.setFloatsUniform( + "uYuvToRgbColorTransform", + inputColorInfo.colorRange == C.COLOR_RANGE_FULL + ? BT2020_FULL_RANGE_YUV_TO_RGB_COLOR_TRANSFORM_MATRIX + : BT2020_LIMITED_RANGE_YUV_TO_RGB_COLOR_TRANSFORM_MATRIX); + + checkArgument(ColorInfo.isTransferHdr(inputColorInfo)); + glProgram.setIntUniform("uInputColorTransfer", inputColorInfo.colorTransfer); + // TODO(b/239735341): Add a setBooleanUniform method to GlProgram. + glProgram.setIntUniform( + "uApplyHdrToSdrToneMapping", + /* value= */ (outputColorInfo.colorSpace != C.COLOR_SPACE_BT2020) ? 1 : 0); + checkArgument( + outputColorTransfer != Format.NO_VALUE && outputColorTransfer != C.COLOR_TRANSFER_SDR); + glProgram.setIntUniform("uOutputColorTransfer", outputColorTransfer); + } else { + checkArgument( + outputColorInfo.colorSpace != C.COLOR_SPACE_BT2020, + "Converting from SDR to HDR is not supported."); + checkArgument(inputColorInfo.colorSpace == outputColorInfo.colorSpace); + checkArgument( + outputColorTransfer == C.COLOR_TRANSFER_SDR + || outputColorTransfer == C.COLOR_TRANSFER_LINEAR); + // The SDR shader automatically applies an COLOR_TRANSFER_SDR EOTF. + glProgram.setIntUniform("uOutputColorTransfer", outputColorTransfer); + } + + return new MatrixShaderProgram( + glProgram, + ImmutableList.copyOf(matrixTransformations), + ImmutableList.copyOf(rgbMatrices), + outputColorInfo.colorTransfer, + isInputTransferHdr); + } + /** * Creates a new instance. * diff --git a/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame/electrical_colors/image_input_with_wrapped_crop.png b/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame/electrical_colors/image_input_with_wrapped_crop.png new file mode 100644 index 0000000000..cbc2a47a6c Binary files /dev/null and b/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame/electrical_colors/image_input_with_wrapped_crop.png differ diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FrameProcessorTestRunner.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FrameProcessorTestRunner.java index 173bc45d84..1240eade1f 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FrameProcessorTestRunner.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FrameProcessorTestRunner.java @@ -62,10 +62,12 @@ public final class FrameProcessorTestRunner { private float pixelWidthHeightRatio; private @MonotonicNonNull ColorInfo inputColorInfo; private @MonotonicNonNull ColorInfo outputColorInfo; + private @C.TrackType int inputTrackType; /** Creates a new instance with default values. */ public Builder() { pixelWidthHeightRatio = DEFAULT_PIXEL_WIDTH_HEIGHT_RATIO; + inputTrackType = C.TRACK_TYPE_VIDEO; } /** @@ -168,6 +170,16 @@ public final class FrameProcessorTestRunner { this.outputColorInfo = outputColorInfo; return this; } + /** + * Sets the input track type. See {@link FrameProcessor.Factory#create}. + * + *

The default value is {@link C#TRACK_TYPE_VIDEO}. + */ + @CanIgnoreReturnValue + public Builder setInputTrackType(@C.TrackType int inputTrackType) { + this.inputTrackType = inputTrackType; + return this; + } public FrameProcessorTestRunner build() throws FrameProcessingException { checkStateNotNull(testId, "testId must be set."); @@ -182,7 +194,8 @@ public final class FrameProcessorTestRunner { effects == null ? ImmutableList.of() : effects, pixelWidthHeightRatio, inputColorInfo == null ? ColorInfo.SDR_BT709_LIMITED : inputColorInfo, - outputColorInfo == null ? ColorInfo.SDR_BT709_LIMITED : outputColorInfo); + outputColorInfo == null ? ColorInfo.SDR_BT709_LIMITED : outputColorInfo, + inputTrackType); } } @@ -211,7 +224,8 @@ public final class FrameProcessorTestRunner { ImmutableList effects, float pixelWidthHeightRatio, ColorInfo inputColorInfo, - ColorInfo outputColorInfo) + ColorInfo outputColorInfo, + @C.TrackType int inputTrackType) throws FrameProcessingException { this.testId = testId; this.videoAssetPath = videoAssetPath; @@ -226,7 +240,7 @@ public final class FrameProcessorTestRunner { DebugViewProvider.NONE, inputColorInfo, outputColorInfo, - C.TRACK_TYPE_VIDEO, + inputTrackType, /* releaseFramesAutomatically= */ true, MoreExecutors.directExecutor(), new FrameProcessor.Listener() { @@ -278,6 +292,19 @@ public final class FrameProcessorTestRunner { } }, frameProcessor.getInputSurface()); + return endFrameProcessingAndGetImage(); + } + + public Bitmap processImageFrameAndEnd(Bitmap inputBitmap) throws Exception { + frameProcessor.setInputFrameInfo( + new FrameInfo.Builder(inputBitmap.getWidth(), inputBitmap.getHeight()) + .setPixelWidthHeightRatio(pixelWidthHeightRatio) + .build()); + frameProcessor.queueInputBitmap(inputBitmap, C.MICROS_PER_SECOND, /* frameRate= */ 1); + return endFrameProcessingAndGetImage(); + } + + private Bitmap endFrameProcessingAndGetImage() throws Exception { frameProcessor.signalEndOfInput(); Thread.sleep(FRAME_PROCESSING_WAIT_MS);