diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/BitmapTestUtil.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/BitmapTestUtil.java new file mode 100644 index 0000000000..0be2fde75b --- /dev/null +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/BitmapTestUtil.java @@ -0,0 +1,160 @@ +/* + * Copyright 2022 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.transformer; + +import static androidx.test.core.app.ApplicationProvider.getApplicationContext; +import static com.google.common.truth.Truth.assertThat; +import static java.lang.Math.abs; +import static java.lang.Math.max; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.graphics.PixelFormat; +import android.media.Image; +import androidx.media3.common.util.Log; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +/** + * Utilities for instrumentation tests for the {@link FrameEditor} and {@link GlFrameProcessor + * GlFrameProcessors}. + */ +public class BitmapTestUtil { + + private static final String TAG = "BitmapTestUtil"; + + /* Expected first frames after transformation. */ + public static final String FIRST_FRAME_PNG_ASSET_STRING = + "media/bitmap/sample_mp4_first_frame.png"; + public static final String TRANSLATE_RIGHT_EXPECTED_OUTPUT_PNG_ASSET_STRING = + "media/bitmap/sample_mp4_first_frame_translate_right.png"; + public static final String SCALE_NARROW_EXPECTED_OUTPUT_PNG_ASSET_STRING = + "media/bitmap/sample_mp4_first_frame_scale_narrow.png"; + public static final String ROTATE_90_EXPECTED_OUTPUT_PNG_ASSET_STRING = + "media/bitmap/sample_mp4_first_frame_rotate90.png"; + + /** + * Reads a bitmap 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. + */ + public static Bitmap readBitmap(String assetString) throws IOException { + Bitmap bitmap; + try (InputStream inputStream = getApplicationContext().getAssets().open(assetString)) { + bitmap = BitmapFactory.decodeStream(inputStream); + } + return bitmap; + } + + /** + * Returns a bitmap with the same information as the provided alpha/red/green/blue 8-bits per + * component image. + */ + public static Bitmap createArgb8888BitmapFromRgba8888Image(Image image) { + int width = image.getWidth(); + int height = image.getHeight(); + assertThat(image.getPlanes()).hasLength(1); + assertThat(image.getFormat()).isEqualTo(PixelFormat.RGBA_8888); + Image.Plane plane = image.getPlanes()[0]; + ByteBuffer buffer = plane.getBuffer(); + int[] colors = new int[width * height]; + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int offset = y * plane.getRowStride() + x * plane.getPixelStride(); + int r = buffer.get(offset) & 0xFF; + int g = buffer.get(offset + 1) & 0xFF; + int b = buffer.get(offset + 2) & 0xFF; + int a = buffer.get(offset + 3) & 0xFF; + colors[y * width + x] = (a << 24) + (r << 16) + (g << 8) + b; + } + } + return Bitmap.createBitmap(colors, width, height, Bitmap.Config.ARGB_8888); + } + + /** + * Returns the sum of the absolute differences between the expected and actual bitmaps, calculated + * using the maximum difference across all color channels for each pixel, then divided by the + * total number of pixels in the image. The bitmap resolutions must match and they must use + * configuration {@link Bitmap.Config#ARGB_8888}. + */ + public static float getAveragePixelAbsoluteDifferenceArgb8888(Bitmap expected, Bitmap actual) { + int width = actual.getWidth(); + int height = actual.getHeight(); + assertThat(width).isEqualTo(expected.getWidth()); + assertThat(height).isEqualTo(expected.getHeight()); + assertThat(actual.getConfig()).isEqualTo(Bitmap.Config.ARGB_8888); + long sumMaximumAbsoluteDifferences = 0; + // Debug-only image diff without alpha. To use, set a breakpoint right before the method return + // to view the difference between the expected and actual bitmaps. A passing test should show + // an image that is completely black (color == 0). + Bitmap debugDiff = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int actualColor = actual.getPixel(x, y); + int expectedColor = expected.getPixel(x, y); + + int alphaDifference = abs(Color.alpha(actualColor) - Color.alpha(expectedColor)); + int redDifference = abs(Color.red(actualColor) - Color.red(expectedColor)); + int blueDifference = abs(Color.blue(actualColor) - Color.blue(expectedColor)); + int greenDifference = abs(Color.green(actualColor) - Color.green(expectedColor)); + debugDiff.setPixel(x, y, Color.rgb(redDifference, blueDifference, greenDifference)); + + int maximumAbsoluteDifference = 0; + maximumAbsoluteDifference = max(maximumAbsoluteDifference, alphaDifference); + maximumAbsoluteDifference = max(maximumAbsoluteDifference, redDifference); + maximumAbsoluteDifference = max(maximumAbsoluteDifference, blueDifference); + maximumAbsoluteDifference = max(maximumAbsoluteDifference, greenDifference); + + sumMaximumAbsoluteDifferences += maximumAbsoluteDifference; + } + } + return (float) sumMaximumAbsoluteDifferences / (width * height); + } + + /** + * Saves the {@link Bitmap} to the {@link Context#getCacheDir() cache directory} as a PNG. + * + *

File name will be {@code _output.png}. If {@code throwOnFailure} is {@code false}, + * any {@link IOException} will be caught and logged. + * + * @param testId Name of the test that produced the {@link Bitmap}. + * @param bitmap The {@link Bitmap} to save. + * @param throwOnFailure Whether to throw an exception if the bitmap can't be saved. + * @throws IOException If the bitmap can't be saved and {@code throwOnFailure} is {@code true}. + */ + public static void saveTestBitmapToCacheDirectory( + String testId, Bitmap bitmap, boolean throwOnFailure) throws IOException { + File file = new File(getApplicationContext().getExternalCacheDir(), testId + "_output.png"); + try (FileOutputStream outputStream = new FileOutputStream(file)) { + bitmap.compress(Bitmap.CompressFormat.PNG, /* quality= */ 100, outputStream); + } catch (IOException e) { + if (throwOnFailure) { + throw e; + } else { + Log.e(TAG, "Could not write Bitmap to file path: " + file.getAbsolutePath(), e); + } + } + } + + private BitmapTestUtil() {} +} diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameEditorDataProcessingTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameEditorDataProcessingTest.java index 527f4568c1..ed81f59347 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameEditorDataProcessingTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameEditorDataProcessingTest.java @@ -16,16 +16,16 @@ package androidx.media3.transformer; import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.transformer.BitmapTestUtil.FIRST_FRAME_PNG_ASSET_STRING; +import static androidx.media3.transformer.BitmapTestUtil.ROTATE_90_EXPECTED_OUTPUT_PNG_ASSET_STRING; +import static androidx.media3.transformer.BitmapTestUtil.SCALE_NARROW_EXPECTED_OUTPUT_PNG_ASSET_STRING; +import static androidx.media3.transformer.BitmapTestUtil.TRANSLATE_RIGHT_EXPECTED_OUTPUT_PNG_ASSET_STRING; import static androidx.test.core.app.ApplicationProvider.getApplicationContext; import static com.google.common.truth.Truth.assertThat; -import static java.lang.Math.abs; -import static java.lang.Math.max; import android.content.Context; import android.content.res.AssetFileDescriptor; import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Color; import android.graphics.Matrix; import android.graphics.PixelFormat; import android.media.Image; @@ -35,12 +35,7 @@ import android.media.MediaExtractor; import android.media.MediaFormat; import androidx.annotation.Nullable; import androidx.media3.common.MimeTypes; -import androidx.media3.common.util.Log; import androidx.test.ext.junit.runners.AndroidJUnit4; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; import java.nio.ByteBuffer; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.junit.After; @@ -57,21 +52,8 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public final class FrameEditorDataProcessingTest { - private static final String TAG = "FrameEditorDataTest"; - - // Input MP4 file to transform. + /** Input video of which we only use the first frame. */ private static final String INPUT_MP4_ASSET_STRING = "media/mp4/sample.mp4"; - - /* Expected first frames after transformation. */ - private static final String NO_EDITS_EXPECTED_OUTPUT_PNG_ASSET_STRING = - "media/bitmap/sample_mp4_first_frame.png"; - private static final String TRANSLATE_RIGHT_EXPECTED_OUTPUT_PNG_ASSET_STRING = - "media/bitmap/sample_mp4_first_frame_translate_right.png"; - private static final String SCALE_NARROW_EXPECTED_OUTPUT_PNG_ASSET_STRING = - "media/bitmap/sample_mp4_first_frame_scale_narrow.png"; - private static final String ROTATE_90_EXPECTED_OUTPUT_PNG_ASSET_STRING = - "media/bitmap/sample_mp4_first_frame_rotate90.png"; - /** * Maximum allowed average pixel difference between the expected and actual edited images for the * test to pass. The value is chosen so that differences in decoder behavior across emulator @@ -107,16 +89,18 @@ public final class FrameEditorDataProcessingTest { public void processData_noEdits_producesExpectedOutput() throws Exception { Matrix identityMatrix = new Matrix(); setUpAndPrepareFirstFrame(identityMatrix); - Bitmap expectedBitmap = getBitmap(NO_EDITS_EXPECTED_OUTPUT_PNG_ASSET_STRING); + Bitmap expectedBitmap = BitmapTestUtil.readBitmap(FIRST_FRAME_PNG_ASSET_STRING); checkNotNull(frameEditor).processData(); Image editedImage = checkNotNull(frameEditorOutputImageReader).acquireLatestImage(); - Bitmap editedBitmap = getArgb8888BitmapForRgba8888Image(editedImage); + Bitmap editedBitmap = BitmapTestUtil.createArgb8888BitmapFromRgba8888Image(editedImage); + editedImage.close(); // TODO(b/207848601): switch to using proper tooling for testing against golden data. float averagePixelAbsoluteDifference = - getAveragePixelAbsoluteDifferenceArgb8888(expectedBitmap, editedBitmap); - saveTestBitmapToCacheDirectory("processData_noEdits", editedBitmap); + BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888(expectedBitmap, editedBitmap); + BitmapTestUtil.saveTestBitmapToCacheDirectory( + "processData_noEdits", editedBitmap, /* throwOnFailure= */ false); assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); } @@ -125,17 +109,19 @@ public final class FrameEditorDataProcessingTest { Matrix translateRightMatrix = new Matrix(); translateRightMatrix.postTranslate(/* dx= */ 1, /* dy= */ 0); setUpAndPrepareFirstFrame(translateRightMatrix); - Bitmap expectedBitmap = getBitmap(TRANSLATE_RIGHT_EXPECTED_OUTPUT_PNG_ASSET_STRING); + Bitmap expectedBitmap = + BitmapTestUtil.readBitmap(TRANSLATE_RIGHT_EXPECTED_OUTPUT_PNG_ASSET_STRING); checkNotNull(frameEditor).processData(); Image editedImage = checkNotNull(frameEditorOutputImageReader).acquireLatestImage(); - Bitmap editedBitmap = getArgb8888BitmapForRgba8888Image(editedImage); + Bitmap editedBitmap = BitmapTestUtil.createArgb8888BitmapFromRgba8888Image(editedImage); + editedImage.close(); - // TODO(b/207848601): switch to using proper tooling for testing against golden - // data.simple + // TODO(b/207848601): switch to using proper tooling for testing against golden data. float averagePixelAbsoluteDifference = - getAveragePixelAbsoluteDifferenceArgb8888(expectedBitmap, editedBitmap); - saveTestBitmapToCacheDirectory("processData_translateRight", editedBitmap); + BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888(expectedBitmap, editedBitmap); + BitmapTestUtil.saveTestBitmapToCacheDirectory( + "processData_translateRight", editedBitmap, /* throwOnFailure= */ false); assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); } @@ -144,16 +130,19 @@ public final class FrameEditorDataProcessingTest { Matrix scaleNarrowMatrix = new Matrix(); scaleNarrowMatrix.postScale(.5f, 1.2f); setUpAndPrepareFirstFrame(scaleNarrowMatrix); - Bitmap expectedBitmap = getBitmap(SCALE_NARROW_EXPECTED_OUTPUT_PNG_ASSET_STRING); + Bitmap expectedBitmap = + BitmapTestUtil.readBitmap(SCALE_NARROW_EXPECTED_OUTPUT_PNG_ASSET_STRING); checkNotNull(frameEditor).processData(); Image editedImage = checkNotNull(frameEditorOutputImageReader).acquireLatestImage(); - Bitmap editedBitmap = getArgb8888BitmapForRgba8888Image(editedImage); + Bitmap editedBitmap = BitmapTestUtil.createArgb8888BitmapFromRgba8888Image(editedImage); + editedImage.close(); // TODO(b/207848601): switch to using proper tooling for testing against golden data. float averagePixelAbsoluteDifference = - getAveragePixelAbsoluteDifferenceArgb8888(expectedBitmap, editedBitmap); - saveTestBitmapToCacheDirectory("processData_scaleNarrow", editedBitmap); + BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888(expectedBitmap, editedBitmap); + BitmapTestUtil.saveTestBitmapToCacheDirectory( + "processData_scaleNarrow", editedBitmap, /* throwOnFailure= */ false); assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); } @@ -165,16 +154,18 @@ public final class FrameEditorDataProcessingTest { Matrix rotate90Matrix = new Matrix(); rotate90Matrix.postRotate(/* degrees= */ 90); setUpAndPrepareFirstFrame(rotate90Matrix); - Bitmap expectedBitmap = getBitmap(ROTATE_90_EXPECTED_OUTPUT_PNG_ASSET_STRING); + Bitmap expectedBitmap = BitmapTestUtil.readBitmap(ROTATE_90_EXPECTED_OUTPUT_PNG_ASSET_STRING); checkNotNull(frameEditor).processData(); Image editedImage = checkNotNull(frameEditorOutputImageReader).acquireLatestImage(); - Bitmap editedBitmap = getArgb8888BitmapForRgba8888Image(editedImage); + Bitmap editedBitmap = BitmapTestUtil.createArgb8888BitmapFromRgba8888Image(editedImage); + editedImage.close(); // TODO(b/207848601): switch to using proper tooling for testing against golden data. float averagePixelAbsoluteDifference = - getAveragePixelAbsoluteDifferenceArgb8888(expectedBitmap, editedBitmap); - saveTestBitmapToCacheDirectory("processData_rotate90", editedBitmap); + BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888(expectedBitmap, editedBitmap); + BitmapTestUtil.saveTestBitmapToCacheDirectory( + "processData_rotate90", editedBitmap, /* throwOnFailure= */ false); assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); } @@ -257,94 +248,4 @@ public final class FrameEditorDataProcessingTest { } } } - - private Bitmap getBitmap(String expectedAssetString) throws IOException { - Bitmap bitmap; - try (InputStream inputStream = getApplicationContext().getAssets().open(expectedAssetString)) { - bitmap = BitmapFactory.decodeStream(inputStream); - } - return bitmap; - } - - /** - * Returns a bitmap with the same information as the provided alpha/red/green/blue 8-bits per - * component image. - */ - private static Bitmap getArgb8888BitmapForRgba8888Image(Image image) { - int width = image.getWidth(); - int height = image.getHeight(); - assertThat(image.getPlanes()).hasLength(1); - assertThat(image.getFormat()).isEqualTo(PixelFormat.RGBA_8888); - Image.Plane plane = image.getPlanes()[0]; - ByteBuffer buffer = plane.getBuffer(); - int[] colors = new int[width * height]; - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - int offset = y * plane.getRowStride() + x * plane.getPixelStride(); - int r = buffer.get(offset) & 0xFF; - int g = buffer.get(offset + 1) & 0xFF; - int b = buffer.get(offset + 2) & 0xFF; - int a = buffer.get(offset + 3) & 0xFF; - colors[y * width + x] = (a << 24) + (r << 16) + (g << 8) + b; - } - } - return Bitmap.createBitmap(colors, width, height, Bitmap.Config.ARGB_8888); - } - - /** - * Returns the sum of the absolute differences between the expected and actual bitmaps, calculated - * using the maximum difference across all color channels for each pixel, then divided by the - * total number of pixels in the image. The bitmap resolutions must match and they must use - * configuration {@link Bitmap.Config#ARGB_8888}. - */ - private static float getAveragePixelAbsoluteDifferenceArgb8888(Bitmap expected, Bitmap actual) { - int width = actual.getWidth(); - int height = actual.getHeight(); - assertThat(width).isEqualTo(expected.getWidth()); - assertThat(height).isEqualTo(expected.getHeight()); - assertThat(actual.getConfig()).isEqualTo(Bitmap.Config.ARGB_8888); - long sumMaximumAbsoluteDifferences = 0; - // Debug-only image diff without alpha. To use, set a breakpoint right before the method return - // to view the difference between the expected and actual bitmaps. A passing test should show - // an image that is completely black (color == 0). - Bitmap debugDiff = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - int actualColor = actual.getPixel(x, y); - int expectedColor = expected.getPixel(x, y); - - int alphaDifference = abs(Color.alpha(actualColor) - Color.alpha(expectedColor)); - int redDifference = abs(Color.red(actualColor) - Color.red(expectedColor)); - int blueDifference = abs(Color.blue(actualColor) - Color.blue(expectedColor)); - int greenDifference = abs(Color.green(actualColor) - Color.green(expectedColor)); - debugDiff.setPixel(x, y, Color.rgb(redDifference, blueDifference, greenDifference)); - - int maximumAbsoluteDifference = 0; - maximumAbsoluteDifference = max(maximumAbsoluteDifference, alphaDifference); - maximumAbsoluteDifference = max(maximumAbsoluteDifference, redDifference); - maximumAbsoluteDifference = max(maximumAbsoluteDifference, blueDifference); - maximumAbsoluteDifference = max(maximumAbsoluteDifference, greenDifference); - - sumMaximumAbsoluteDifferences += maximumAbsoluteDifference; - } - } - return (float) sumMaximumAbsoluteDifferences / (width * height); - } - - /** - * Saves the {@link Bitmap} to the {@link Context#getCacheDir() cache directory} as a PNG. - * - *

File name will be {@code _output.png}. - * - * @param testId Name of the test that produced the {@link Bitmap}. - * @param bitmap The {@link Bitmap} to save. - */ - private static void saveTestBitmapToCacheDirectory(String testId, Bitmap bitmap) { - File file = new File(getApplicationContext().getExternalCacheDir(), testId + "_output.png"); - try (FileOutputStream outputStream = new FileOutputStream(file)) { - bitmap.compress(Bitmap.CompressFormat.PNG, /* quality= */ 100, outputStream); - } catch (IOException e) { - Log.e(TAG, "Could not write Bitmap to file path: " + file.getAbsolutePath(), e); - } - } }