diff --git a/libraries/effect/src/androidTest/java/androidx/media3/effect/AlphaScaleShaderProgramPixelTest.java b/libraries/effect/src/androidTest/java/androidx/media3/effect/AlphaScaleShaderProgramPixelTest.java index 6dc6989a58..f46a6a6746 100644 --- a/libraries/effect/src/androidTest/java/androidx/media3/effect/AlphaScaleShaderProgramPixelTest.java +++ b/libraries/effect/src/androidTest/java/androidx/media3/effect/AlphaScaleShaderProgramPixelTest.java @@ -17,11 +17,11 @@ package androidx.media3.effect; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.test.utils.BitmapPixelTestUtil.MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE; -import static androidx.media3.test.utils.BitmapPixelTestUtil.createArgb8888BitmapFromFocusedGlFramebuffer; import static androidx.media3.test.utils.BitmapPixelTestUtil.createGlTextureFromBitmap; +import static androidx.media3.test.utils.BitmapPixelTestUtil.createUnpremultipliedArgb8888BitmapFromFocusedGlFramebuffer; import static androidx.media3.test.utils.BitmapPixelTestUtil.getBitmapAveragePixelAbsoluteDifferenceArgb8888; import static androidx.media3.test.utils.BitmapPixelTestUtil.maybeSaveTestBitmap; -import static androidx.media3.test.utils.BitmapPixelTestUtil.readBitmap; +import static androidx.media3.test.utils.BitmapPixelTestUtil.readBitmapUnpremultipliedAlpha; import static androidx.test.core.app.ApplicationProvider.getApplicationContext; import static com.google.common.truth.Truth.assertThat; @@ -86,7 +86,7 @@ public final class AlphaScaleShaderProgramPixelTest { eglContext = GlUtil.createEglContext(eglDisplay); placeholderEglSurface = GlUtil.createFocusedPlaceholderEglSurface(eglContext, eglDisplay); - Bitmap inputBitmap = readBitmap(ORIGINAL_PNG_ASSET_PATH); + Bitmap inputBitmap = readBitmapUnpremultipliedAlpha(ORIGINAL_PNG_ASSET_PATH); inputWidth = inputBitmap.getWidth(); inputHeight = inputBitmap.getHeight(); inputTexId = createGlTextureFromBitmap(inputBitmap); @@ -123,12 +123,13 @@ public final class AlphaScaleShaderProgramPixelTest { public void noOpAlpha_matchesGoldenFile() throws Exception { alphaScaleShaderProgram = new AlphaScale(1.0f).toGlShaderProgram(context, /* useHdr= */ false); Size outputSize = alphaScaleShaderProgram.configure(inputWidth, inputHeight); - Bitmap expectedBitmap = readBitmap(ORIGINAL_PNG_ASSET_PATH); + Bitmap expectedBitmap = readBitmapUnpremultipliedAlpha(ORIGINAL_PNG_ASSET_PATH); maybeSaveTestBitmap(testId, /* bitmapLabel= */ "input", expectedBitmap, /* path= */ null); alphaScaleShaderProgram.drawFrame(inputTexId, /* presentationTimeUs= */ 0); Bitmap actualBitmap = - createArgb8888BitmapFromFocusedGlFramebuffer(outputSize.getWidth(), outputSize.getHeight()); + createUnpremultipliedArgb8888BitmapFromFocusedGlFramebuffer( + outputSize.getWidth(), outputSize.getHeight()); // TODO(b/207848601): Switch to using proper tooling for testing against golden data. maybeSaveTestBitmap(testId, /* bitmapLabel= */ "actual", actualBitmap, /* path= */ null); @@ -142,11 +143,12 @@ public final class AlphaScaleShaderProgramPixelTest { public void zeroAlpha_matchesGoldenFile() throws Exception { alphaScaleShaderProgram = new AlphaScale(0.0f).toGlShaderProgram(context, /* useHdr= */ false); Size outputSize = alphaScaleShaderProgram.configure(inputWidth, inputHeight); - Bitmap expectedBitmap = readBitmap(ZERO_ALPHA_PNG_ASSET_PATH); + Bitmap expectedBitmap = readBitmapUnpremultipliedAlpha(ZERO_ALPHA_PNG_ASSET_PATH); alphaScaleShaderProgram.drawFrame(inputTexId, /* presentationTimeUs= */ 0); Bitmap actualBitmap = - createArgb8888BitmapFromFocusedGlFramebuffer(outputSize.getWidth(), outputSize.getHeight()); + createUnpremultipliedArgb8888BitmapFromFocusedGlFramebuffer( + outputSize.getWidth(), outputSize.getHeight()); // TODO(b/207848601): Switch to using proper tooling for testing against golden data. maybeSaveTestBitmap(testId, /* bitmapLabel= */ "actual", actualBitmap, /* path= */ null); @@ -160,11 +162,12 @@ public final class AlphaScaleShaderProgramPixelTest { public void decreaseAlpha_matchesGoldenFile() throws Exception { alphaScaleShaderProgram = new AlphaScale(0.5f).toGlShaderProgram(context, /* useHdr= */ false); Size outputSize = alphaScaleShaderProgram.configure(inputWidth, inputHeight); - Bitmap expectedBitmap = readBitmap(DECREASE_ALPHA_PNG_ASSET_PATH); + Bitmap expectedBitmap = readBitmapUnpremultipliedAlpha(DECREASE_ALPHA_PNG_ASSET_PATH); alphaScaleShaderProgram.drawFrame(inputTexId, /* presentationTimeUs= */ 0); Bitmap actualBitmap = - createArgb8888BitmapFromFocusedGlFramebuffer(outputSize.getWidth(), outputSize.getHeight()); + createUnpremultipliedArgb8888BitmapFromFocusedGlFramebuffer( + outputSize.getWidth(), outputSize.getHeight()); // TODO(b/207848601): Switch to using proper tooling for testing against golden data. maybeSaveTestBitmap(testId, /* bitmapLabel= */ "actual", actualBitmap, /* path= */ null); @@ -178,16 +181,17 @@ public final class AlphaScaleShaderProgramPixelTest { public void increaseAlpha_matchesGoldenFile() throws Exception { alphaScaleShaderProgram = new AlphaScale(1.5f).toGlShaderProgram(context, /* useHdr= */ false); Size outputSize = alphaScaleShaderProgram.configure(inputWidth, inputHeight); - Bitmap expectedBitmap = readBitmap(INCREASE_ALPHA_PNG_ASSET_PATH); + Bitmap expectedBitmap = readBitmapUnpremultipliedAlpha(INCREASE_ALPHA_PNG_ASSET_PATH); alphaScaleShaderProgram.drawFrame(inputTexId, /* presentationTimeUs= */ 0); Bitmap actualBitmap = - createArgb8888BitmapFromFocusedGlFramebuffer(outputSize.getWidth(), outputSize.getHeight()); + createUnpremultipliedArgb8888BitmapFromFocusedGlFramebuffer( + outputSize.getWidth(), outputSize.getHeight()); // TODO(b/207848601): Switch to using proper tooling for testing against golden data. maybeSaveTestBitmap(testId, /* bitmapLabel= */ "actual", actualBitmap, /* path= */ null); float averagePixelAbsoluteDifference = getBitmapAveragePixelAbsoluteDifferenceArgb8888(expectedBitmap, actualBitmap, testId); - assertThat(averagePixelAbsoluteDifference).isAtMost(0); + assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); } } diff --git a/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/input_grayscale_0s.png b/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/input_grayscale_0s.png index 408a580f35..49e77f850e 100644 Binary files a/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/input_grayscale_0s.png and b/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/input_grayscale_0s.png differ diff --git a/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/input_rotate180_1s.png b/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/input_rotate180_1s.png index 239f8275d5..0c757a5ec2 100644 Binary files a/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/input_rotate180_1s.png and b/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/input_rotate180_1s.png differ diff --git a/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_0s_0s.png b/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_0s_0s.png index 15592aae95..644d5e5b98 100644 Binary files a/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_0s_0s.png and b/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_0s_0s.png differ diff --git a/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_0s_1s.png b/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_0s_1s.png index 2ab1eba553..6e17f28c0a 100644 Binary files a/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_0s_1s.png and b/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_0s_1s.png differ diff --git a/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_1s_0s.png b/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_1s_0s.png index e8f123aaaf..f64679045e 100644 Binary files a/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_1s_0s.png and b/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_1s_0s.png differ diff --git a/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_1s_1s.png b/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_1s_1s.png index 27dc4eacfe..333075fb90 100644 Binary files a/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_1s_1s.png and b/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_1s_1s.png differ diff --git a/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_2s_0s.png b/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_2s_0s.png index 4fb28e9d0b..3eda1940e3 100644 Binary files a/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_2s_0s.png and b/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_2s_0s.png differ diff --git a/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_2s_1s.png b/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_2s_1s.png index 1a0700022f..662ddbecce 100644 Binary files a/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_2s_1s.png and b/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_2s_1s.png differ diff --git a/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_2s_2s.png b/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_2s_2s.png index 1cbc4a3c8b..ced21d088d 100644 Binary files a/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_2s_2s.png and b/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_2s_2s.png differ diff --git a/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_3s_0s.png b/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_3s_0s.png index 0b1370828a..210f55e1a9 100644 Binary files a/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_3s_0s.png and b/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_3s_0s.png differ diff --git a/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_3s_2s.png b/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_3s_2s.png index b35956753b..68ebe5f1f9 100644 Binary files a/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_3s_2s.png and b/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_3s_2s.png differ diff --git a/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_3s_3s.png b/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_3s_3s.png index 3f8c6a7b71..ba950deafd 100644 Binary files a/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_3s_3s.png and b/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_3s_3s.png differ diff --git a/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_4s_0s.png b/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_4s_0s.png index a65cdc6e7b..149b70b655 100644 Binary files a/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_4s_0s.png and b/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_4s_0s.png differ diff --git a/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_4s_4s.png b/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_4s_4s.png index a49e5d31d5..e2c94c660a 100644 Binary files a/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_4s_4s.png and b/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_4s_4s.png differ diff --git a/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame/electrical_colors/decrease_alpha.png b/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame/electrical_colors/decrease_alpha.png index 38a5ca5ca1..ebf2c6d286 100644 Binary files a/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame/electrical_colors/decrease_alpha.png and b/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame/electrical_colors/decrease_alpha.png differ diff --git a/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame/electrical_colors/increase_alpha.png b/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame/electrical_colors/increase_alpha.png index 399d2d5cdb..a48baa93e5 100644 Binary files a/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame/electrical_colors/increase_alpha.png and b/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame/electrical_colors/increase_alpha.png differ diff --git a/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame/electrical_colors/partial_alpha.png b/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame/electrical_colors/partial_alpha.png deleted file mode 100644 index 5b54a8206c..0000000000 Binary files a/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame/electrical_colors/partial_alpha.png and /dev/null differ diff --git a/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame/electrical_colors/zero_alpha.png b/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame/electrical_colors/zero_alpha.png index af807ef8de..826f016e9c 100644 Binary files a/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame/electrical_colors/zero_alpha.png and b/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame/electrical_colors/zero_alpha.png differ diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/BitmapPixelTestUtil.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/BitmapPixelTestUtil.java index 8be01ff37a..3bdb2b12c2 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/BitmapPixelTestUtil.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/BitmapPixelTestUtil.java @@ -17,6 +17,7 @@ package androidx.media3.test.utils; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkState; +import static androidx.media3.common.util.Util.SDK_INT; import static androidx.test.core.app.ApplicationProvider.getApplicationContext; import static com.google.common.truth.Truth.assertThat; import static java.lang.Math.abs; @@ -48,6 +49,7 @@ import java.io.OutputStream; import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.util.Arrays; +import org.junit.AssumptionViolatedException; /** Utilities for pixel tests. */ // TODO(b/263395272): After the bug is fixed and dependent tests are moved back to media3.effect, @@ -123,6 +125,8 @@ public class BitmapPixelTestUtil { * @return A {@link Bitmap}. * @throws IOException If the bitmap can't be read. */ + // TODO: b/295523484 - Update all tests using readBitmap to instead use + // readBitmapUnpremultipliedAlpha, and rename readBitmapUnpremultipliedAlpha back to readBitmap. public static Bitmap readBitmap(String assetString) throws IOException { Bitmap bitmap; try (InputStream inputStream = getApplicationContext().getAssets().open(assetString)) { @@ -131,6 +135,25 @@ public class BitmapPixelTestUtil { return bitmap; } + /** + * Reads a bitmap with unpremultiplied alpha from the specified asset location. + * + * @param assetString Relative path to the asset within the assets directory. + * @return A {@link Bitmap}. + * @throws IOException If the bitmap can't be read. + */ + @RequiresApi(19) // BitmapFactory.Options#inPremultiplied. + public static Bitmap readBitmapUnpremultipliedAlpha(String assetString) throws IOException { + Bitmap bitmap; + try (InputStream inputStream = getApplicationContext().getAssets().open(assetString)) { + // Media3 expected bitmaps are generated from OpenGL, which uses non-premultiplied colors. + BitmapFactory.Options bitmapOptions = new BitmapFactory.Options(); + bitmapOptions.inPremultiplied = false; + bitmap = BitmapFactory.decodeStream(inputStream, /* outPadding= */ null, bitmapOptions); + } + return checkNotNull(bitmap); + } + /** * Returns a bitmap with the same information as the provided alpha/red/green/blue 8-bits per * component image. @@ -360,8 +383,8 @@ public class BitmapPixelTestUtil { Class testStorageClass = Class.forName("androidx.test.services.storage.TestStorage"); Method method = testStorageClass.getMethod("openOutputFile", String.class); Object testStorage = testStorageClass.getDeclaredConstructor().newInstance(); - OutputStream outputStream = (OutputStream) method.invoke(testStorage, fileName); - bitmap.compress(Bitmap.CompressFormat.PNG, /* quality= */ 100, checkNotNull(outputStream)); + OutputStream outputStream = checkNotNull((OutputStream) method.invoke(testStorage, fileName)); + bitmap.compress(Bitmap.CompressFormat.PNG, /* quality= */ 100, outputStream); } catch (ClassNotFoundException e) { // Do nothing } catch (Exception e) { @@ -375,22 +398,56 @@ public class BitmapPixelTestUtil { * *

This method may block until any previously called OpenGL commands are complete. * + *

This method incorrectly marks the output Bitmap as {@link Bitmap#isPremultiplied() + * premultiplied}, even though OpenGL typically outputs only non-premultiplied alpha. Use {@link + * #createUnpremultipliedArgb8888BitmapFromFocusedGlFramebuffer} to properly handle alpha. + * * @param width The width of the pixel rectangle to read. * @param height The height of the pixel rectangle to read. * @return A {@link Bitmap} with the framebuffer's values. */ + // TODO: b/295523484 - Update all tests using createArgb8888BitmapFromFocusedGlFramebuffer to + // instead use createUnpremultipliedArgb8888BitmapFromFocusedGlFramebuffer, and rename + // createUnpremultipliedArgb8888BitmapFromFocusedGlFramebuffer back to + // createArgb8888BitmapFromFocusedGlFramebuffer. Also, apply + // setPremultiplied(false) to createBitmapFromFocusedGlFrameBuffer. + @RequiresApi(17) // #flipBitmapVertically. public static Bitmap createArgb8888BitmapFromFocusedGlFramebuffer(int width, int height) throws GlUtil.GlException { return createBitmapFromFocusedGlFrameBuffer( width, height, /* pixelSize= */ 4, GLES20.GL_UNSIGNED_BYTE, Bitmap.Config.ARGB_8888); } + /** + * Creates a {@link Bitmap.Config#ARGB_8888} bitmap with the values of the focused OpenGL + * framebuffer. + * + *

This method may block until any previously called OpenGL commands are complete. + * + * @param width The width of the pixel rectangle to read. + * @param height The height of the pixel rectangle to read. + * @return A {@link Bitmap} with the framebuffer's values. + */ + @RequiresApi(19) // Bitmap#setPremultiplied. + public static Bitmap createUnpremultipliedArgb8888BitmapFromFocusedGlFramebuffer( + int width, int height) throws GlUtil.GlException { + Bitmap bitmap = + createBitmapFromFocusedGlFrameBuffer( + width, height, /* pixelSize= */ 4, GLES20.GL_UNSIGNED_BYTE, Bitmap.Config.ARGB_8888); + bitmap.setPremultiplied(false); // OpenGL represents colors as unpremultiplied. + return bitmap; + } + /** * Creates a {@link Bitmap.Config#RGBA_F16} bitmap with the values of the focused OpenGL * framebuffer. * *

This method may block until any previously called OpenGL commands are complete. * + *

This method incorrectly marks the output Bitmap as {@link Bitmap#isPremultiplied() + * premultiplied}, even though OpenGL typically outputs only non-premultiplied alpha. Call {@link + * Bitmap#setPremultiplied} with {@code false} on the output bitmap to properly handle alpha. + * * @param width The width of the pixel rectangle to read. * @param height The height of the pixel rectangle to read. * @return A {@link Bitmap} with the framebuffer's values. @@ -402,6 +459,7 @@ public class BitmapPixelTestUtil { width, height, /* pixelSize= */ 8, GLES30.GL_HALF_FLOAT, Bitmap.Config.RGBA_F16); } + @RequiresApi(17) // #flipBitmapVertically. private static Bitmap createBitmapFromFocusedGlFrameBuffer( int width, int height, int pixelSize, int glReadPixelsFormat, Bitmap.Config bitmapConfig) throws GlUtil.GlException { @@ -424,6 +482,7 @@ public class BitmapPixelTestUtil { * @param bitmap A {@link Bitmap}. * @return The identifier of the newly created texture. */ + @RequiresApi(17) // #flipBitmapVertically. public static int createGlTextureFromBitmap(Bitmap bitmap) throws GlUtil.GlException { int texId = GlUtil.createTexture( @@ -436,17 +495,36 @@ public class BitmapPixelTestUtil { return texId; } + @RequiresApi(17) // Bitmap#isPremultiplied. public static Bitmap flipBitmapVertically(Bitmap bitmap) { + boolean wasPremultiplied = bitmap.isPremultiplied(); + if (!wasPremultiplied) { + if (SDK_INT >= 19) { + // Bitmap.createBitmap must be called on a premultiplied bitmap. + bitmap.setPremultiplied(true); + } else { + throw new AssumptionViolatedException( + "bitmap is not premultiplied and Bitmap.setPremultiplied is not supported under API 19." + + " unpremultiplied bitmaps cannot be flipped"); + } + } + Matrix flip = new Matrix(); flip.postScale(1f, -1f); - return Bitmap.createBitmap( - bitmap, - /* x= */ 0, - /* y= */ 0, - bitmap.getWidth(), - bitmap.getHeight(), - flip, - /* filter= */ true); + + Bitmap flippedBitmap = + Bitmap.createBitmap( + bitmap, + /* x= */ 0, + /* y= */ 0, + bitmap.getWidth(), + bitmap.getHeight(), + flip, + /* filter= */ true); + if (SDK_INT >= 19) { + flippedBitmap.setPremultiplied(wasPremultiplied); + } + return flippedBitmap; } private BitmapPixelTestUtil() {} diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/TextureBitmapReader.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/TextureBitmapReader.java index ec21a20a96..87060fd2b8 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/TextureBitmapReader.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/TextureBitmapReader.java @@ -16,12 +16,14 @@ package androidx.media3.test.utils; +import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkStateNotNull; import android.graphics.Bitmap; import android.os.Build; import android.view.Surface; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.media3.common.GlTextureInfo; import androidx.media3.common.VideoFrameProcessingException; import androidx.media3.common.util.GlUtil; @@ -35,6 +37,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * {@inheritDoc} * *

Reads from an OpenGL texture. Only for use on physical devices. + * + *

For images with alpha, this method incorrectly marks the output Bitmap as {@link + * Bitmap#isPremultiplied() premultiplied}, even though OpenGL typically outputs only + * non-premultiplied alpha. */ @UnstableApi public final class TextureBitmapReader implements VideoFrameProcessorTestRunner.BitmapReader { @@ -75,8 +81,18 @@ public final class TextureBitmapReader implements VideoFrameProcessorTestRunner. /** * Reads the given {@code outputTexture}. * - *

The read result can be fetched by calling one of me {@link #getBitmap} methods. + *

The read result can be fetched by calling one of the {@link #getBitmap} methods. + * + *

This implementation incorrectly marks the output Bitmap as {@link Bitmap#isPremultiplied() + * premultiplied}, even though OpenGL typically outputs only non-premultiplied alpha. Use {@link + * #readBitmapUnpremultipliedAlpha} to properly handle alpha. */ + // TODO: b/295523484 - In createBitmapFromCurrentGlFrameBuffer, call + // createUnpremultipliedArgb8888BitmapFromFocusedGlFramebuffer instead of + // createArgb8888BitmapFromFocusedGlFramebuffer, so that TextureBitmapReader always reads bitmaps + // as unpremultiplied alpha. Then, remove this method (as we'll already be using premultiplied + // alpha). + @RequiresApi(17) // BitmapPixelTestUtil#createArgb8888BitmapFromFocusedGlFramebuffer. public void readBitmap(GlTextureInfo outputTexture, long presentationTimeUs) throws VideoFrameProcessingException { try { @@ -91,6 +107,28 @@ public final class TextureBitmapReader implements VideoFrameProcessorTestRunner. } } + /** + * Reads the given {@code outputTexture} as one with unpremultiplied alpha. + * + *

The read result can be fetched by calling one of the {@link #getBitmap} methods. + */ + @RequiresApi(19) // BitmapPixelTestUtil#createArgb8888BitmapFromFocusedGlFramebuffer. + public void readBitmapUnpremultipliedAlpha(GlTextureInfo outputTexture, long presentationTimeUs) + throws VideoFrameProcessingException { + checkState(!useHighPrecisionColorComponents); + try { + GlUtil.focusFramebufferUsingCurrentContext( + outputTexture.fboId, outputTexture.width, outputTexture.height); + outputBitmap = + BitmapPixelTestUtil.createUnpremultipliedArgb8888BitmapFromFocusedGlFramebuffer( + outputTexture.width, outputTexture.height); + outputTimestampsToBitmaps.put(presentationTimeUs, outputBitmap); + } catch (GlUtil.GlException e) { + throw new VideoFrameProcessingException(e); + } + } + + @RequiresApi(17) // BitmapPixelTestUtil#createArgb8888BitmapFromFocusedGlFramebuffer. private static Bitmap createBitmapFromCurrentGlFrameBuffer( int width, int height, boolean useHighPrecisionColorComponents) throws GlUtil.GlException { if (!useHighPrecisionColorComponents) { diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/DefaultVideoCompositorPixelTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/DefaultVideoCompositorPixelTest.java index 3e4af8db0d..67f0b9b38a 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/DefaultVideoCompositorPixelTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/DefaultVideoCompositorPixelTest.java @@ -18,7 +18,7 @@ package androidx.media3.transformer; import static androidx.media3.common.util.Util.SDK_INT; import static androidx.media3.test.utils.BitmapPixelTestUtil.MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE; import static androidx.media3.test.utils.BitmapPixelTestUtil.maybeSaveTestBitmap; -import static androidx.media3.test.utils.BitmapPixelTestUtil.readBitmap; +import static androidx.media3.test.utils.BitmapPixelTestUtil.readBitmapUnpremultipliedAlpha; import static androidx.media3.test.utils.VideoFrameProcessorTestRunner.VIDEO_FRAME_PROCESSING_WAIT_MS; import static androidx.test.core.app.ApplicationProvider.getApplicationContext; import static com.google.common.truth.Truth.assertThat; @@ -474,7 +474,7 @@ public final class DefaultVideoCompositorPixelTest { } outputTimestampsToBitmaps.put( presentationTimeUs, - BitmapPixelTestUtil.createArgb8888BitmapFromFocusedGlFramebuffer( + BitmapPixelTestUtil.createUnpremultipliedArgb8888BitmapFromFocusedGlFramebuffer( outputTexture.width, outputTexture.height)); releaseOutputTextureCallback.release(presentationTimeUs); }, @@ -519,7 +519,7 @@ public final class DefaultVideoCompositorPixelTest { inputVideoFrameProcessorTestRunners .get(inputId) .queueInputBitmap( - readBitmap(ORIGINAL_PNG_ASSET_PATH), + readBitmapUnpremultipliedAlpha(ORIGINAL_PNG_ASSET_PATH), /* durationUs= */ durationSec * C.MICROS_PER_SECOND, /* offsetToAddUs= */ offsetToAddSec * C.MICROS_PER_SECOND, /* frameRate= */ frameRate); @@ -613,7 +613,8 @@ public final class DefaultVideoCompositorPixelTest { releaseOutputTextureCallback, long syncObject) -> { GlUtil.awaitSyncObject(syncObject); - textureBitmapReader.readBitmap(outputTexture, presentationTimeUs); + textureBitmapReader.readBitmapUnpremultipliedAlpha( + outputTexture, presentationTimeUs); videoCompositor.queueInputTexture( inputId, outputTexture, presentationTimeUs, releaseOutputTextureCallback); }, @@ -703,7 +704,7 @@ public final class DefaultVideoCompositorPixelTest { maybeSaveTestBitmap(testId, actualBitmapLabel, actualBitmap, /* path= */ null); float averagePixelAbsoluteDifference = BitmapPixelTestUtil.getBitmapAveragePixelAbsoluteDifferenceArgb8888( - readBitmap(expectedBitmapAssetPath), actualBitmap, testId); + readBitmapUnpremultipliedAlpha(expectedBitmapAssetPath), actualBitmap, testId); assertWithMessage("Pixel difference for bitmapLabel = " + actualBitmapLabel) .that(averagePixelAbsoluteDifference) .isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE_WITH_OVERLAY);