1 /* 2 * Copyright 2025 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 17 package android.virtualdevice.cts.camera.util; 18 19 import static android.Manifest.permission.CAMERA; 20 import static android.graphics.ImageFormat.JPEG; 21 import static android.graphics.ImageFormat.YUV_420_888; 22 import static android.media.MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START; 23 import static android.opengl.EGL14.EGL_ALPHA_SIZE; 24 import static android.opengl.EGL14.EGL_BLUE_SIZE; 25 import static android.opengl.EGL14.EGL_CONTEXT_CLIENT_VERSION; 26 import static android.opengl.EGL14.EGL_DEFAULT_DISPLAY; 27 import static android.opengl.EGL14.EGL_GREEN_SIZE; 28 import static android.opengl.EGL14.EGL_NONE; 29 import static android.opengl.EGL14.EGL_NO_CONTEXT; 30 import static android.opengl.EGL14.EGL_NO_DISPLAY; 31 import static android.opengl.EGL14.EGL_NO_SURFACE; 32 import static android.opengl.EGL14.EGL_RED_SIZE; 33 import static android.opengl.EGL14.eglChooseConfig; 34 import static android.opengl.EGL14.eglCreateContext; 35 import static android.opengl.EGL14.eglDestroyContext; 36 import static android.opengl.EGL14.eglGetDisplay; 37 import static android.opengl.EGL14.eglGetError; 38 import static android.opengl.EGL14.eglInitialize; 39 import static android.opengl.EGL14.eglMakeCurrent; 40 import static android.opengl.GLES20.GL_MAX_TEXTURE_SIZE; 41 import static android.opengl.GLES20.glGetIntegerv; 42 43 import static androidx.test.core.app.ApplicationProvider.getApplicationContext; 44 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; 45 46 import static com.google.common.truth.Truth.assertThat; 47 import static com.google.common.truth.Truth.assertWithMessage; 48 49 import static org.junit.Assert.assertNotNull; 50 import static org.junit.Assert.fail; 51 import static org.junit.Assume.assumeFalse; 52 53 import android.companion.virtual.camera.VirtualCameraCallback; 54 import android.companion.virtual.camera.VirtualCameraConfig; 55 import android.companion.virtual.camera.VirtualCameraStreamConfig; 56 import android.content.Context; 57 import android.graphics.Bitmap; 58 import android.graphics.BitmapFactory; 59 import android.graphics.Canvas; 60 import android.graphics.Color; 61 import android.graphics.ImageDecoder; 62 import android.graphics.PixelFormat; 63 import android.hardware.camera2.CameraCharacteristics; 64 import android.hardware.camera2.cts.rs.BitmapUtils; 65 import android.media.Image; 66 import android.media.MediaMetadataRetriever; 67 import android.media.MediaPlayer; 68 import android.net.Uri; 69 import android.opengl.EGLConfig; 70 import android.opengl.EGLContext; 71 import android.opengl.EGLDisplay; 72 import android.os.Handler; 73 import android.os.HandlerThread; 74 import android.os.UserHandle; 75 import android.util.Log; 76 import android.view.Surface; 77 78 import androidx.annotation.ColorInt; 79 80 import com.google.common.collect.Iterables; 81 82 import java.io.File; 83 import java.io.FileNotFoundException; 84 import java.io.FileOutputStream; 85 import java.io.IOException; 86 import java.util.concurrent.CountDownLatch; 87 import java.util.concurrent.Executor; 88 import java.util.concurrent.TimeUnit; 89 import java.util.function.Consumer; 90 91 public final class VirtualCameraUtils { 92 public static final String BACK_CAMERA_ID = "0"; 93 public static final String FRONT_CAMERA_ID = "1"; 94 public static final CameraCharacteristics.Key<Integer> INFO_DEVICE_ID = 95 new CameraCharacteristics.Key<>("android.info.deviceId", int.class); 96 private static final long TIMEOUT_MILLIS = 2000L; 97 private static final int TEST_VIDEO_SEEK_TIME_MS = 2000; 98 private static final String TAG = "VirtualCameraUtils"; 99 createVirtualCameraConfig( int width, int height, int format, int maximumFramesPerSecond, int sensorOrientation, int lensFacing, String name, Executor executor, VirtualCameraCallback callback)100 public static VirtualCameraConfig createVirtualCameraConfig( 101 int width, int height, int format, int maximumFramesPerSecond, int sensorOrientation, 102 int lensFacing, String name, Executor executor, VirtualCameraCallback callback) { 103 return new VirtualCameraConfig.Builder(name) 104 .addStreamConfig(width, height, format, maximumFramesPerSecond) 105 .setVirtualCameraCallback(executor, callback) 106 .setSensorOrientation(sensorOrientation) 107 .setLensFacing(lensFacing) 108 .build(); 109 } 110 assertVirtualCameraConfig(VirtualCameraConfig config, int width, int height, int format, int maximumFramesPerSecond, int sensorOrientation, int lensFacing, String name)111 public static void assertVirtualCameraConfig(VirtualCameraConfig config, int width, int height, 112 int format, int maximumFramesPerSecond, int sensorOrientation, int lensFacing, 113 String name) { 114 assertThat(config.getName()).isEqualTo(name); 115 assertThat(config.getStreamConfigs()).hasSize(1); 116 VirtualCameraStreamConfig streamConfig = 117 Iterables.getOnlyElement(config.getStreamConfigs()); 118 assertThat(streamConfig.getWidth()).isEqualTo(width); 119 assertThat(streamConfig.getHeight()).isEqualTo(height); 120 assertThat(streamConfig.getFormat()).isEqualTo(format); 121 assertThat(streamConfig.getMaximumFramesPerSecond()).isEqualTo(maximumFramesPerSecond); 122 assertThat(config.getSensorOrientation()).isEqualTo(sensorOrientation); 123 assertThat(config.getLensFacing()).isEqualTo(lensFacing); 124 } 125 paintSurface(Surface surface, @ColorInt int color)126 public static void paintSurface(Surface surface, @ColorInt int color) { 127 Canvas canvas = surface.lockCanvas(null); 128 canvas.drawColor(color); 129 surface.unlockCanvasAndPost(canvas); 130 } 131 paintSurfaceRed(Surface surface)132 public static void paintSurfaceRed(Surface surface) { 133 paintSurface(surface, Color.RED); 134 } 135 toFormat(String str)136 public static int toFormat(String str) { 137 if (str.equals("YUV_420_888")) { 138 return YUV_420_888; 139 } 140 if (str.equals("RGBA_888")) { 141 return PixelFormat.RGBA_8888; 142 } 143 if (str.equals("JPEG")) { 144 return JPEG; 145 } 146 147 fail("Unknown pixel format string: " + str); 148 return PixelFormat.UNKNOWN; 149 } 150 151 /** 152 * Will write the image to disk so it can be pulled by the collector in case of error 153 * 154 * @see com.android.tradefed.device.metric.FilePullerLogCollector 155 */ writeImageToDisk(String imageName, Bitmap bitmap)156 private static void writeImageToDisk(String imageName, Bitmap bitmap) { 157 File dir = getApplicationContext().getFilesDir(); 158 // The FilePullerLogCollector only pulls image in png 159 File imageFile = new File(dir, imageName + ".png"); 160 try { 161 Log.i(TAG, "Saving image to disk at " + imageFile.getAbsolutePath()); 162 bitmap.compress(Bitmap.CompressFormat.PNG, 80, new FileOutputStream(imageFile)); 163 } catch (FileNotFoundException e) { 164 throw new RuntimeException(e); 165 } 166 } 167 168 /** 169 * @param generated Bitmap generated from the test. 170 * @param golden Golden bitmap to compare to. 171 * @param prefix Prefix for the image file generated in case of error. 172 */ assertImagesSimilar(Bitmap generated, Bitmap golden, String prefix, double maxDiff)173 public static void assertImagesSimilar(Bitmap generated, Bitmap golden, String prefix, 174 double maxDiff) { 175 boolean assertionPassed = false; 176 try { 177 double actual = BitmapUtils.calcDifferenceMetric(generated, golden); 178 assertWithMessage("Generated image does not match golden. " 179 + "Images have been saved to disk.").that(actual).isAtMost(maxDiff); 180 assertionPassed = true; 181 } finally { 182 if (!assertionPassed) { 183 writeImageToDisk(prefix + "_generated", generated); 184 writeImageToDisk(prefix + "_golden", golden); 185 } 186 } 187 } 188 189 public static class VideoRenderer implements Consumer<Surface> { 190 private final MediaPlayer mPlayer; 191 private final CountDownLatch mLatch; 192 private final Uri mUri; 193 VideoRenderer(int resId)194 public VideoRenderer(int resId) { 195 String path = 196 "android.resource://" + getApplicationContext().getPackageName() + "/" + resId; 197 mUri = Uri.parse(path); 198 mPlayer = MediaPlayer.create(getApplicationContext(), mUri); 199 mLatch = new CountDownLatch(1); 200 201 mPlayer.setOnInfoListener((mp, what, extra) -> { 202 if (what == MEDIA_INFO_VIDEO_RENDERING_START) { 203 mLatch.countDown(); 204 return true; 205 } 206 return false; 207 }); 208 } 209 210 @Override accept(Surface surface)211 public void accept(Surface surface) { 212 mPlayer.setSurface(surface); 213 mPlayer.seekTo(TEST_VIDEO_SEEK_TIME_MS); 214 mPlayer.start(); 215 try { 216 // Block until media player has drawn the first video frame 217 assertWithMessage("Media player did not notify first frame on time") 218 .that(mLatch.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) 219 .isTrue(); 220 } catch (InterruptedException e) { 221 throw new RuntimeException(e); 222 } 223 } 224 getGoldenBitmap()225 public Bitmap getGoldenBitmap() { 226 // Get the frame at a specific time (in microseconds) or the first frame 227 try (MediaMetadataRetriever goldenRetriever = new MediaMetadataRetriever()) { 228 goldenRetriever.setDataSource(getApplicationContext(), mUri); 229 Bitmap frame = 230 goldenRetriever.getFrameAtTime( 231 TEST_VIDEO_SEEK_TIME_MS, MediaMetadataRetriever.OPTION_CLOSEST); 232 assertNotNull("Can't extract golden frame for test video.", frame); 233 return frame; 234 } catch (IOException e) { 235 throw new RuntimeException(e); 236 } 237 } 238 } 239 loadBitmapFromRaw(int rawResId)240 public static Bitmap loadBitmapFromRaw(int rawResId) { 241 BitmapFactory.Options options = new BitmapFactory.Options(); 242 options.inScaled = false; 243 return BitmapFactory.decodeResource(getApplicationContext().getResources(), 244 rawResId, options); 245 } 246 jpegImageToBitmap(Image image)247 public static Bitmap jpegImageToBitmap(Image image) throws IOException { 248 assertThat(image.getFormat()).isEqualTo(JPEG); 249 return ImageDecoder.decodeBitmap( 250 ImageDecoder.createSource(image.getPlanes()[0].getBuffer())).copy( 251 Bitmap.Config.ARGB_8888, false); 252 } 253 grantCameraPermission(int deviceId)254 public static void grantCameraPermission(int deviceId) { 255 Context deviceContext = getInstrumentation().getTargetContext() 256 .createDeviceContext(deviceId); 257 deviceContext.getPackageManager().grantRuntimePermission("android.virtualdevice.cts.camera", 258 CAMERA, UserHandle.of(deviceContext.getUserId())); 259 } 260 getMaximumTextureSize()261 public static int getMaximumTextureSize() { 262 EGLDisplay eglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY); 263 assumeFalse(eglDisplay.equals(EGL_NO_DISPLAY)); 264 int[] version = new int[2]; 265 if (!eglInitialize(eglDisplay, version, 0, version, 1)) { 266 throw new IllegalStateException( 267 "eglInitialize() returned false. Can't query maximum texture size\n " 268 + "eglGetError():" + eglGetError()); 269 } 270 271 int[] attribList = {EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, 272 EGL_ALPHA_SIZE, 8, EGL_NONE}; 273 274 EGLConfig[] configs = new EGLConfig[1]; 275 int[] numConfigs = new int[1]; 276 if (!eglChooseConfig( 277 eglDisplay, attribList, 0, configs, 0, configs.length, numConfigs, 0)) { 278 throw new IllegalStateException( 279 "eglChooseConfig() returned false. Can't query maximum texture size\n" 280 + "eglGetError():" + eglGetError()); 281 } 282 283 284 int[] attrib2_list = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE}; 285 EGLContext eglContext = eglCreateContext(eglDisplay, configs[0], EGL_NO_CONTEXT, 286 attrib2_list, 0); 287 eglMakeCurrent(eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, eglContext); 288 289 int[] maxSize = new int[1]; 290 glGetIntegerv(GL_MAX_TEXTURE_SIZE, maxSize, 0); 291 292 eglDestroyContext(eglDisplay, eglContext); 293 294 return maxSize[0]; 295 } 296 297 /** 298 * Creates a new Handler with a thread named with the provided suffix. 299 */ createHandler(String threadSuffix)300 public static Handler createHandler(String threadSuffix) { 301 HandlerThread handlerThread = new HandlerThread("VirtualCameraTestHandler_" + threadSuffix); 302 handlerThread.start(); 303 return new Handler(handlerThread.getLooper()); 304 } 305 VirtualCameraUtils()306 private VirtualCameraUtils() { 307 } 308 } 309