1 /* 2 * Copyright 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.google.android.exoplayer2.transformer; 17 18 import static androidx.test.core.app.ApplicationProvider.getApplicationContext; 19 import static com.google.common.truth.Truth.assertThat; 20 import static java.lang.Math.abs; 21 import static java.lang.Math.max; 22 23 import android.content.Context; 24 import android.graphics.Bitmap; 25 import android.graphics.BitmapFactory; 26 import android.graphics.Color; 27 import android.graphics.Matrix; 28 import android.graphics.PixelFormat; 29 import android.media.Image; 30 import android.opengl.GLES20; 31 import android.opengl.GLUtils; 32 import androidx.annotation.Nullable; 33 import com.google.android.exoplayer2.util.GlUtil; 34 import com.google.android.exoplayer2.util.Log; 35 import java.io.File; 36 import java.io.FileOutputStream; 37 import java.io.IOException; 38 import java.io.InputStream; 39 import java.nio.ByteBuffer; 40 41 /** 42 * Utilities for instrumentation tests for the {@link FrameProcessorChain} and {@link 43 * GlFrameProcessor GlFrameProcessors}. 44 */ 45 public class BitmapTestUtil { 46 47 private static final String TAG = "BitmapTestUtil"; 48 49 /* Expected first frames after transformation. */ 50 public static final String FIRST_FRAME_PNG_ASSET_STRING = 51 "media/bitmap/sample_mp4_first_frame.png"; 52 public static final String TRANSLATE_RIGHT_EXPECTED_OUTPUT_PNG_ASSET_STRING = 53 "media/bitmap/sample_mp4_first_frame_translate_right.png"; 54 public static final String SCALE_NARROW_EXPECTED_OUTPUT_PNG_ASSET_STRING = 55 "media/bitmap/sample_mp4_first_frame_scale_narrow.png"; 56 public static final String ROTATE_THEN_TRANSLATE_EXPECTED_OUTPUT_PNG_ASSET_STRING = 57 "media/bitmap/sample_mp4_first_frame_rotate_then_translate.png"; 58 public static final String TRANSLATE_THEN_ROTATE_EXPECTED_OUTPUT_PNG_ASSET_STRING = 59 "media/bitmap/sample_mp4_first_frame_translate_then_rotate.png"; 60 public static final String ROTATE_90_EXPECTED_OUTPUT_PNG_ASSET_STRING = 61 "media/bitmap/sample_mp4_first_frame_rotate90.png"; 62 public static final String REQUEST_OUTPUT_HEIGHT_EXPECTED_OUTPUT_PNG_ASSET_STRING = 63 "media/bitmap/sample_mp4_first_frame_request_output_height.png"; 64 public static final String ROTATE45_SCALE_TO_FIT_EXPECTED_OUTPUT_PNG_ASSET_STRING = 65 "media/bitmap/sample_mp4_first_frame_rotate_45_scale_to_fit.png"; 66 /** 67 * Maximum allowed average pixel difference between the expected and actual edited images in pixel 68 * difference-based tests. The value is chosen so that differences in decoder behavior across 69 * emulator versions don't affect whether the test passes for most emulators, but substantial 70 * distortions introduced by changes in the behavior of the {@link GlFrameProcessor 71 * GlFrameProcessors} will cause the test to fail. 72 * 73 * <p>To run pixel difference-based tests on physical devices, please use a value of 5f, rather 74 * than 0.1f. This higher value will ignore some very small errors, but will allow for some 75 * differences caused by graphics implementations to be ignored. When the difference is close to 76 * the threshold, manually inspect expected/actual bitmaps to confirm failure, as it's possible 77 * this is caused by a difference in the codec or graphics implementation as opposed to a {@link 78 * GlFrameProcessor} issue. 79 */ 80 public static final float MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE = 0.1f; 81 82 /** 83 * Reads a bitmap from the specified asset location. 84 * 85 * @param assetString Relative path to the asset within the assets directory. 86 * @return A {@link Bitmap}. 87 * @throws IOException If the bitmap can't be read. 88 */ readBitmap(String assetString)89 public static Bitmap readBitmap(String assetString) throws IOException { 90 Bitmap bitmap; 91 try (InputStream inputStream = getApplicationContext().getAssets().open(assetString)) { 92 bitmap = BitmapFactory.decodeStream(inputStream); 93 } 94 return bitmap; 95 } 96 97 /** 98 * Returns a bitmap with the same information as the provided alpha/red/green/blue 8-bits per 99 * component image. 100 */ createArgb8888BitmapFromRgba8888Image(Image image)101 public static Bitmap createArgb8888BitmapFromRgba8888Image(Image image) { 102 int width = image.getWidth(); 103 int height = image.getHeight(); 104 assertThat(image.getPlanes()).hasLength(1); 105 assertThat(image.getFormat()).isEqualTo(PixelFormat.RGBA_8888); 106 Image.Plane plane = image.getPlanes()[0]; 107 ByteBuffer buffer = plane.getBuffer(); 108 int[] colors = new int[width * height]; 109 for (int y = 0; y < height; y++) { 110 for (int x = 0; x < width; x++) { 111 int offset = y * plane.getRowStride() + x * plane.getPixelStride(); 112 int r = buffer.get(offset) & 0xFF; 113 int g = buffer.get(offset + 1) & 0xFF; 114 int b = buffer.get(offset + 2) & 0xFF; 115 int a = buffer.get(offset + 3) & 0xFF; 116 colors[y * width + x] = Color.argb(a, r, g, b); 117 } 118 } 119 return Bitmap.createBitmap(colors, width, height, Bitmap.Config.ARGB_8888); 120 } 121 122 /** 123 * Returns the average difference between the expected and actual bitmaps, calculated using the 124 * maximum difference across all color channels for each pixel, then divided by the total number 125 * of pixels in the image. The bitmap resolutions must match and they must use configuration 126 * {@link Bitmap.Config#ARGB_8888}. 127 * 128 * @param expected The expected {@link Bitmap}. 129 * @param actual The actual {@link Bitmap} produced by the test. 130 * @param testId The name of the test that produced the {@link Bitmap}, or {@code null} if the 131 * differences bitmap should not be saved to cache. 132 * @return The average of the maximum absolute pixel-wise differences between the expected and 133 * actual bitmaps. 134 */ getAveragePixelAbsoluteDifferenceArgb8888( Bitmap expected, Bitmap actual, @Nullable String testId)135 public static float getAveragePixelAbsoluteDifferenceArgb8888( 136 Bitmap expected, Bitmap actual, @Nullable String testId) { 137 int width = actual.getWidth(); 138 int height = actual.getHeight(); 139 assertThat(width).isEqualTo(expected.getWidth()); 140 assertThat(height).isEqualTo(expected.getHeight()); 141 assertThat(actual.getConfig()).isEqualTo(Bitmap.Config.ARGB_8888); 142 long sumMaximumAbsoluteDifferences = 0; 143 // Debug-only image diff without alpha. To use, set a breakpoint right before the method return 144 // to view the difference between the expected and actual bitmaps. A passing test should show 145 // an image that is completely black (color == 0). 146 Bitmap differencesBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 147 for (int y = 0; y < height; y++) { 148 for (int x = 0; x < width; x++) { 149 int actualColor = actual.getPixel(x, y); 150 int expectedColor = expected.getPixel(x, y); 151 152 int alphaDifference = abs(Color.alpha(actualColor) - Color.alpha(expectedColor)); 153 int redDifference = abs(Color.red(actualColor) - Color.red(expectedColor)); 154 int blueDifference = abs(Color.blue(actualColor) - Color.blue(expectedColor)); 155 int greenDifference = abs(Color.green(actualColor) - Color.green(expectedColor)); 156 differencesBitmap.setPixel(x, y, Color.rgb(redDifference, blueDifference, greenDifference)); 157 158 int maximumAbsoluteDifference = 0; 159 maximumAbsoluteDifference = max(maximumAbsoluteDifference, alphaDifference); 160 maximumAbsoluteDifference = max(maximumAbsoluteDifference, redDifference); 161 maximumAbsoluteDifference = max(maximumAbsoluteDifference, blueDifference); 162 maximumAbsoluteDifference = max(maximumAbsoluteDifference, greenDifference); 163 164 sumMaximumAbsoluteDifferences += maximumAbsoluteDifference; 165 } 166 } 167 if (testId != null) { 168 try { 169 saveTestBitmapToCacheDirectory( 170 testId, "diff", differencesBitmap, /* throwOnFailure= */ false); 171 } catch (IOException impossible) { 172 throw new IllegalStateException(impossible); 173 } 174 } 175 return (float) sumMaximumAbsoluteDifferences / (width * height); 176 } 177 178 /** 179 * Saves the {@link Bitmap} to the {@link Context#getCacheDir() cache directory} as a PNG. 180 * 181 * <p>File name will be {@code <testId>_<bitmapLabel>.png}. If {@code throwOnFailure} is {@code 182 * false}, any {@link IOException} will be caught and logged. 183 * 184 * @param testId Name of the test that produced the {@link Bitmap}. 185 * @param bitmapLabel Label to identify the bitmap. 186 * @param bitmap The {@link Bitmap} to save. 187 * @param throwOnFailure Whether to throw an exception if the bitmap can't be saved. 188 * @throws IOException If the bitmap can't be saved and {@code throwOnFailure} is {@code true}. 189 */ saveTestBitmapToCacheDirectory( String testId, String bitmapLabel, Bitmap bitmap, boolean throwOnFailure)190 public static void saveTestBitmapToCacheDirectory( 191 String testId, String bitmapLabel, Bitmap bitmap, boolean throwOnFailure) throws IOException { 192 File file = 193 new File( 194 getApplicationContext().getExternalCacheDir(), testId + "_" + bitmapLabel + ".png"); 195 try (FileOutputStream outputStream = new FileOutputStream(file)) { 196 bitmap.compress(Bitmap.CompressFormat.PNG, /* quality= */ 100, outputStream); 197 } catch (IOException e) { 198 if (throwOnFailure) { 199 throw e; 200 } else { 201 Log.e(TAG, "Could not write Bitmap to file path: " + file.getAbsolutePath(), e); 202 } 203 } 204 } 205 206 /** 207 * Creates a bitmap with the values of the current OpenGL framebuffer. 208 * 209 * <p>This method may block until any previously called OpenGL commands are complete. 210 * 211 * @param width The width of the pixel rectangle to read. 212 * @param height The height of the pixel rectangle to read. 213 * @return A {@link Bitmap} with the framebuffer's values. 214 */ createArgb8888BitmapFromCurrentGlFramebuffer(int width, int height)215 public static Bitmap createArgb8888BitmapFromCurrentGlFramebuffer(int width, int height) { 216 ByteBuffer rgba8888Buffer = ByteBuffer.allocateDirect(width * height * 4); 217 GLES20.glReadPixels( 218 0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, rgba8888Buffer); 219 GlUtil.checkGlError(); 220 Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 221 // According to https://www.khronos.org/opengl/wiki/Pixel_Transfer#Endian_issues, 222 // the colors will have the order RGBA in client memory. This is what the bitmap expects: 223 // https://developer.android.com/reference/android/graphics/Bitmap.Config#ARGB_8888. 224 bitmap.copyPixelsFromBuffer(rgba8888Buffer); 225 // Flip the bitmap as its positive y-axis points down while OpenGL's positive y-axis points up. 226 return flipBitmapVertically(bitmap); 227 } 228 229 /** 230 * Creates a {@link GLES20#GL_TEXTURE_2D 2-dimensional OpenGL texture} with the bitmap's contents. 231 * 232 * @param bitmap A {@link Bitmap}. 233 * @return The identifier of the newly created texture. 234 */ createGlTextureFromBitmap(Bitmap bitmap)235 public static int createGlTextureFromBitmap(Bitmap bitmap) { 236 int texId = GlUtil.createTexture(bitmap.getWidth(), bitmap.getHeight()); 237 // Put the flipped bitmap in the OpenGL texture as the bitmap's positive y-axis points down 238 // while OpenGL's positive y-axis points up. 239 GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, flipBitmapVertically(bitmap), 0); 240 GlUtil.checkGlError(); 241 return texId; 242 } 243 flipBitmapVertically(Bitmap bitmap)244 private static Bitmap flipBitmapVertically(Bitmap bitmap) { 245 Matrix flip = new Matrix(); 246 flip.postScale(1f, -1f); 247 return Bitmap.createBitmap( 248 bitmap, 249 /* x= */ 0, 250 /* y= */ 0, 251 bitmap.getWidth(), 252 bitmap.getHeight(), 253 flip, 254 /* filter= */ true); 255 } 256 BitmapTestUtil()257 private BitmapTestUtil() {} 258 } 259