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.companion.virtual.camera.VirtualCameraConfig.SENSOR_ORIENTATION_0; 20 import static android.graphics.ImageFormat.YUV_420_888; 21 import static android.hardware.camera2.CameraMetadata.LENS_FACING_FRONT; 22 import static android.virtualdevice.cts.camera.util.VirtualCameraUtils.createVirtualCameraConfig; 23 24 import static com.google.common.truth.Truth.assertThat; 25 26 import static org.junit.Assume.assumeNoException; 27 28 import android.companion.virtual.VirtualDeviceManager; 29 import android.companion.virtual.camera.VirtualCamera; 30 import android.companion.virtual.camera.VirtualCameraCallback; 31 import android.companion.virtual.camera.VirtualCameraConfig; 32 import android.content.Context; 33 import android.graphics.ImageFormat; 34 import android.graphics.PixelFormat; 35 import android.hardware.camera2.CameraAccessException; 36 import android.hardware.camera2.CameraCaptureSession; 37 import android.hardware.camera2.CameraCaptureSession.CaptureCallback; 38 import android.hardware.camera2.CameraDevice; 39 import android.hardware.camera2.CameraManager; 40 import android.hardware.camera2.CameraMetadata; 41 import android.hardware.camera2.CaptureFailure; 42 import android.hardware.camera2.CaptureRequest; 43 import android.hardware.camera2.TotalCaptureResult; 44 import android.hardware.camera2.params.OutputConfiguration; 45 import android.hardware.camera2.params.SessionConfiguration; 46 import android.media.Image; 47 import android.media.ImageReader; 48 import android.os.Handler; 49 import android.view.Surface; 50 51 import androidx.annotation.NonNull; 52 import androidx.annotation.Nullable; 53 54 import com.google.common.truth.Truth; 55 56 import org.junit.Assert; 57 import org.mockito.ArgumentCaptor; 58 import org.mockito.ArgumentMatchers; 59 import org.mockito.Captor; 60 import org.mockito.Mock; 61 import org.mockito.Mockito; 62 import org.mockito.MockitoAnnotations; 63 64 import java.util.ArrayList; 65 import java.util.List; 66 import java.util.Objects; 67 import java.util.concurrent.CountDownLatch; 68 import java.util.concurrent.Executor; 69 import java.util.concurrent.Executors; 70 import java.util.concurrent.TimeUnit; 71 import java.util.concurrent.atomic.AtomicReference; 72 import java.util.function.Consumer; 73 74 /** 75 * Helper class for testing capture scenarios with a virtual camera. 76 */ 77 public class VirtualCameraCaptureHelper { 78 public static final long TIMEOUT_MILLIS = 2000L; 79 public static final String CAMERA_NAME = "Virtual camera"; 80 public static final int CAMERA_WIDTH = 640; 81 public static final int CAMERA_HEIGHT = 480; 82 public static final int CAMERA_INPUT_FORMAT = PixelFormat.RGBA_8888; 83 public static final int CAMERA_MAX_FPS = 30; 84 85 private static final long FAILURE_TIMEOUT = 10000L; 86 private static final int IMAGE_READER_MAX_IMAGES = 2; 87 88 private final Handler mImageReaderHandler = VirtualCameraUtils.createHandler( 89 "image-reader-callback"); 90 private final Executor mCameraExecutor = Executors.newSingleThreadExecutor(); 91 @Mock 92 private CameraDevice.StateCallback mCameraStateCallback; 93 @Mock 94 private CameraCaptureSession.StateCallback mSessionStateCallback; 95 96 private TestCaptureCallback mCaptureCallback; 97 @Mock 98 private VirtualCameraCallback mVirtualCameraCallback; 99 @Captor 100 private ArgumentCaptor<CameraDevice> mCameraDeviceCaptor; 101 @Captor 102 private ArgumentCaptor<CameraCaptureSession> mCameraCaptureSessionCaptor; 103 @Captor 104 private ArgumentCaptor<Surface> mSurfaceCaptor; 105 106 @Nullable 107 private CameraManager mCameraManager = null; 108 @Nullable 109 private VirtualCamera mVirtualCamera = null; 110 @Nullable 111 private CameraDevice mCameraDevice = null; 112 @Nullable 113 private ImageReader mOutputReader = null; 114 @Nullable 115 private CameraCaptureSession mCaptureSession = null; 116 @Nullable 117 private VirtualDeviceManager.VirtualDevice mVirtualDevice = null; 118 119 /** 120 * Initialize the helper to work with the provided virtualDevice onto which the virtual camera 121 * will be created and the context used to open the camera for capture. 122 */ setUp(@onNull VirtualDeviceManager.VirtualDevice virtualDevice, @NonNull Context context)123 public void setUp(@NonNull VirtualDeviceManager.VirtualDevice virtualDevice, 124 @NonNull Context context) { 125 mCameraManager = Objects.requireNonNull(context).getSystemService(CameraManager.class); 126 mVirtualDevice = Objects.requireNonNull(virtualDevice); 127 MockitoAnnotations.initMocks(this); 128 } 129 130 /** 131 * Clean up resources after the test has been run 132 */ tearDown()133 public void tearDown() { 134 Mockito.reset(mCameraStateCallback, mSessionStateCallback, mVirtualCameraCallback); 135 if (mCameraDevice != null) { 136 mCameraDevice.close(); 137 mCameraDevice = null; 138 } 139 if (mVirtualCamera != null) { 140 mVirtualCamera.close(); 141 mVirtualCamera = null; 142 } 143 if (mOutputReader != null) { 144 mOutputReader.close(); 145 mOutputReader = null; 146 } 147 } 148 149 /** 150 * Create a virtual camera with default values. 151 */ createVirtualCamera()152 public void createVirtualCamera() { 153 createVirtualCamera(VirtualCameraCaptureHelper.CAMERA_WIDTH, 154 VirtualCameraCaptureHelper.CAMERA_HEIGHT, 155 VirtualCameraCaptureHelper.CAMERA_INPUT_FORMAT); 156 } 157 158 /** 159 * Create a virtual camera with the provided configuration 160 * 161 * @param inputWidth width of the input of this virtual camera 162 * @param inputHeight height of the input of this virtual camera 163 * @param inputFormat format of the input of this virtual camera 164 */ createVirtualCamera(int inputWidth, int inputHeight, int inputFormat)165 public void createVirtualCamera(int inputWidth, int inputHeight, int inputFormat) { 166 createVirtualCamera(inputWidth, inputHeight, inputFormat, 167 VirtualCameraCaptureHelper.CAMERA_MAX_FPS); 168 } 169 170 /** 171 * Create a virtual camera with the provided configuration 172 * 173 * @param inputWidth width of the input of this virtual camera 174 * @param inputHeight height of the input of this virtual camera 175 * @param inputFormat format of the input of this virtual camera 176 * @param fps fps of the input of this virtual camera 177 */ createVirtualCamera(int inputWidth, int inputHeight, int inputFormat, int fps)178 public void createVirtualCamera(int inputWidth, int inputHeight, int inputFormat, 179 int fps) { 180 Objects.requireNonNull(mVirtualDevice, 181 "mVirtualDevice must not be null when calling #createVirtualCamera()"); 182 VirtualCameraConfig config = createVirtualCameraConfig(inputWidth, inputHeight, 183 inputFormat, fps, SENSOR_ORIENTATION_0, LENS_FACING_FRONT, 184 VirtualCameraCaptureHelper.CAMERA_NAME, mCameraExecutor, 185 mVirtualCameraCallback); 186 try { 187 mVirtualCamera = mVirtualDevice.createVirtualCamera(config); 188 } catch (UnsupportedOperationException e) { 189 assumeNoException("Virtual camera is not available on this device", e); 190 } 191 } 192 193 /** 194 * Capture images using the provided {@link CaptureConfiguration} 195 * <p> 196 * The camera device and session will be automatically created if needed. 197 * 198 * @return The latest captured image. 199 */ captureImages(CaptureConfiguration config)200 public Image captureImages(CaptureConfiguration config) { 201 AtomicReference<Image> latestImageRef = new AtomicReference<>(null); 202 203 mCaptureCallback = new TestCaptureCallback(); 204 mCaptureCallback.mFailOnFailedCapture = config.mFailOnCaptureError; 205 206 try { 207 ImageReader reader = getOrCreateOutputReader(config); 208 CameraCaptureSession cameraCaptureSession = getOrCreateCaptureSession(reader); 209 CameraDevice cameraDevice = cameraCaptureSession.getDevice(); 210 config.mInputSurfaceConsumer.accept(getInputSurface()); 211 212 CaptureRequest.Builder request = cameraDevice.createCaptureRequest( 213 CameraDevice.TEMPLATE_PREVIEW); 214 config.mRequestBuilderModifier.accept(request); 215 request.addTarget(reader.getSurface()); 216 217 CountDownLatch imageReaderLatch = new CountDownLatch(config.mImageCount); 218 reader.setOnImageAvailableListener(imageReader -> { 219 Image latestImage = latestImageRef.get(); 220 if (latestImage != null) { 221 latestImage.close(); 222 } 223 latestImageRef.set(imageReader.acquireLatestImage()); 224 imageReaderLatch.countDown(); 225 }, mImageReaderHandler); 226 227 for (int i = 0; i < config.mImageCount; i++) { 228 cameraCaptureSession.captureSingleRequest(request.build(), mCameraExecutor, 229 mCaptureCallback); 230 } 231 232 if (!config.mVerifyCaptureComplete) { 233 return reader.acquireLatestImage(); 234 } 235 236 verifyCaptureComplete(config.mImageCount); 237 Truth.assertWithMessage("Timeout waiting for image reader result").that( 238 imageReaderLatch.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)).isTrue(); 239 Image image = latestImageRef.getAndSet(null); 240 ImageSubject.assertThat(image).isNotNull(); 241 return image; 242 } catch (CameraAccessException | InterruptedException e) { 243 throw new RuntimeException(e); 244 } finally { 245 Image image = latestImageRef.getAndSet(null); 246 if (image != null) { 247 image.close(); 248 } 249 } 250 } 251 252 /** 253 * Returns the {@link CameraDevice} corresponding to the virtual camera. 254 */ getOrOpenCameraDevice()255 public CameraDevice getOrOpenCameraDevice() { 256 try { 257 if (mCameraDevice != null) { 258 return mCameraDevice; 259 } 260 Objects.requireNonNull(mVirtualCamera, 261 "mVirtualCamera must not be null when calling this method."); 262 Objects.requireNonNull(mCameraManager, 263 "mCameraManager must not be null when calling this method."); 264 mCameraManager.openCamera(getVirtualCameraId(mVirtualCamera), mCameraExecutor, 265 mCameraStateCallback); 266 Mockito.verify(mCameraStateCallback, Mockito.timeout(TIMEOUT_MILLIS)).onOpened( 267 mCameraDeviceCaptor.capture()); 268 mCameraDevice = mCameraDeviceCaptor.getValue(); 269 return mCameraDevice; 270 } catch (CameraAccessException e) { 271 throw new RuntimeException(e); 272 } 273 } 274 getInputSurface()275 private Surface getInputSurface() { 276 Surface surface = mSurfaceCaptor.getValue(); 277 assertThat(surface.isValid()).isTrue(); 278 return surface; 279 } 280 getOrCreateCaptureSession(ImageReader reader)281 private CameraCaptureSession getOrCreateCaptureSession(ImageReader reader) 282 throws CameraAccessException { 283 if (mCaptureSession != null) { 284 return mCaptureSession; 285 } 286 CameraDevice cameraDevice = getOrOpenCameraDevice(); 287 OutputConfiguration outputConfiguration = new OutputConfiguration(reader.getSurface()); 288 cameraDevice.createCaptureSession( 289 new SessionConfiguration(SessionConfiguration.SESSION_REGULAR, 290 List.of(outputConfiguration), mCameraExecutor, mSessionStateCallback)); 291 Mockito.verify(mSessionStateCallback, Mockito.timeout(TIMEOUT_MILLIS)).onConfigured( 292 mCameraCaptureSessionCaptor.capture()); 293 Mockito.verify(mVirtualCameraCallback, 294 Mockito.timeout(TIMEOUT_MILLIS)).onStreamConfigured(ArgumentMatchers.anyInt(), 295 mSurfaceCaptor.capture(), ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt(), 296 ArgumentMatchers.anyInt()); 297 mCaptureSession = mCameraCaptureSessionCaptor.getValue(); 298 return mCaptureSession; 299 } 300 getOrCreateOutputReader(CaptureConfiguration config)301 private ImageReader getOrCreateOutputReader(CaptureConfiguration config) { 302 if (mOutputReader != null && (config.mOutputFormat != mOutputReader.getImageFormat() 303 || config.mHeight != mOutputReader.getHeight() 304 || mOutputReader.getWidth() != config.mWidth)) { 305 mOutputReader.close(); 306 mOutputReader = null; 307 } 308 309 if (mOutputReader == null) { 310 mOutputReader = ImageReader.newInstance(config.mWidth, config.mHeight, 311 config.mOutputFormat, IMAGE_READER_MAX_IMAGES); 312 } 313 return mOutputReader; 314 } 315 verifyCaptureComplete(int imageCount)316 private void verifyCaptureComplete(int imageCount) { 317 Mockito.verify(mVirtualCameraCallback, 318 Mockito.timeout(TIMEOUT_MILLIS).atLeast(imageCount)).onProcessCaptureRequest( 319 ArgumentMatchers.anyInt(), ArgumentMatchers.anyLong()); 320 mCaptureCallback.waitForCaptures(imageCount, TIMEOUT_MILLIS); 321 } 322 323 /** 324 * Check that the capture has failed at least one time and never succeeded. 325 */ verifyCaptureFailed()326 public void verifyCaptureFailed() { 327 mCaptureCallback.waitForCaptures(1, FAILURE_TIMEOUT); 328 assertThat(mCaptureCallback.getFailedCaptureCount()).isEqualTo(1); 329 assertThat(mCaptureCallback.getCaptureResults()).isEmpty(); 330 } 331 getVirtualCameraId(VirtualCamera virtualCamera)332 private static String getVirtualCameraId(VirtualCamera virtualCamera) { 333 return switch (virtualCamera.getConfig().getLensFacing()) { 334 case LENS_FACING_FRONT -> VirtualCameraUtils.FRONT_CAMERA_ID; 335 case CameraMetadata.LENS_FACING_BACK -> VirtualCameraUtils.BACK_CAMERA_ID; 336 default -> virtualCamera.getId(); 337 }; 338 } 339 340 /** 341 * Returns a {@link Mock} of {@link CaptureCallback} 342 */ getCaptureCallback()343 public CaptureCallback getCaptureCallback() { 344 return mCaptureCallback; 345 } 346 347 /** 348 * Returns a {@link Mock} of {@link VirtualCameraCallback} 349 */ getVirtualCameraCallback()350 public VirtualCameraCallback getVirtualCameraCallback() { 351 return mVirtualCameraCallback; 352 } 353 getCaptureResults()354 public List<TotalCaptureResult> getCaptureResults() { 355 return mCaptureCallback.getCaptureResults(); 356 } 357 getLastResult()358 public TotalCaptureResult getLastResult() { 359 if (mCaptureCallback.getCaptureResults().isEmpty()) { 360 return null; 361 } 362 return mCaptureCallback.getCaptureResults().getLast(); 363 } 364 365 /** 366 * Holds the configuration used for {@link #captureImages(CaptureConfiguration)}. 367 * <p> 368 * The default configuration can be used as is, all setters are optional. 369 */ 370 public static final class CaptureConfiguration { 371 372 private int mImageCount = 1; 373 public boolean mFailOnCaptureError = true; 374 private boolean mVerifyCaptureComplete = true; 375 private Consumer<Surface> mInputSurfaceConsumer = (surface) -> { 376 }; 377 private Consumer<CaptureRequest.Builder> mRequestBuilderModifier = (request) -> { 378 }; 379 private int mWidth = CAMERA_WIDTH; 380 private int mHeight = CAMERA_HEIGHT; 381 private int mOutputFormat = YUV_420_888; 382 383 /** 384 * Set the number of image to capture 385 * <p> 386 * Default is 1. 387 */ setImageCount(int imageCount)388 public CaptureConfiguration setImageCount(int imageCount) { 389 mImageCount = imageCount; 390 return this; 391 } 392 393 /** 394 * Set the whether the successful completion of the capture should be checked 395 * <p> 396 * Default is true. 397 */ setVerifyCaptureComplete(boolean verifyCaptureComplete)398 public CaptureConfiguration setVerifyCaptureComplete(boolean verifyCaptureComplete) { 399 mVerifyCaptureComplete = verifyCaptureComplete; 400 return this; 401 } 402 403 /** 404 * Set the whether we should fail as soon as we get a capture error. 405 * <p> 406 * Default is true. 407 * 408 * @see CaptureCallback#onCaptureFailed(CameraCaptureSession, CaptureRequest, 409 * CaptureFailure) 410 */ setFailOnCaptureError(boolean failOnCaptureError)411 public CaptureConfiguration setFailOnCaptureError(boolean failOnCaptureError) { 412 mFailOnCaptureError = failOnCaptureError; 413 return this; 414 } 415 416 /** 417 * Set a consumer to write onto the input surface of the {@link VirtualCamera} 418 * <p> 419 * Default is no-op. 420 */ setInputSurfaceConsumer( Consumer<Surface> inputSurfaceConsumer)421 public CaptureConfiguration setInputSurfaceConsumer( 422 Consumer<Surface> inputSurfaceConsumer) { 423 mInputSurfaceConsumer = inputSurfaceConsumer; 424 return this; 425 } 426 427 /** 428 * Set the consumer that accepts a {@link CaptureRequest.Builder} and which can modify that 429 * request. 430 * <p> 431 * Default is no-op. 432 */ setRequestBuilderModifier( @ullable Consumer<CaptureRequest.Builder> requestBuilderModifier)433 public CaptureConfiguration setRequestBuilderModifier( 434 @Nullable Consumer<CaptureRequest.Builder> requestBuilderModifier) { 435 mRequestBuilderModifier = requestBuilderModifier; 436 return this; 437 } 438 439 /** 440 * Set the width of the capture surface and result 441 * <p> 442 * Default is {@link #CAMERA_WIDTH}. 443 */ setWidth(int width)444 public CaptureConfiguration setWidth(int width) { 445 mWidth = width; 446 return this; 447 } 448 449 /** 450 * Set the height of the capture surface and result 451 * <p> 452 * Default is {@link #CAMERA_WIDTH}. 453 */ setHeight(int height)454 public CaptureConfiguration setHeight(int height) { 455 mHeight = height; 456 return this; 457 } 458 459 /** 460 * Set the output format of the capture surface and result 461 * <p> 462 * Default is {@link ImageFormat#YUV_420_888}. 463 */ setOutputFormat(int outputFormat)464 public CaptureConfiguration setOutputFormat(int outputFormat) { 465 mOutputFormat = outputFormat; 466 return this; 467 } 468 } 469 470 private static class TestCaptureCallback extends CaptureCallback { 471 472 private final ArrayList<TotalCaptureResult> mCaptureResults = new ArrayList<>(); 473 private CountDownLatch mCaptureAndErrorLatch = new CountDownLatch(0); 474 private int mFailedCaptureCount = 0; 475 private boolean mFailOnFailedCapture = true; 476 477 @Override onCaptureFailed(@onNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureFailure failure)478 public void onCaptureFailed(@NonNull CameraCaptureSession session, 479 @NonNull CaptureRequest request, 480 @NonNull CaptureFailure failure) { 481 mFailedCaptureCount++; 482 mCaptureAndErrorLatch.countDown(); 483 if (!mFailOnFailedCapture) { 484 return; 485 } 486 synchronized (mCaptureResults) { 487 Assert.fail( 488 ("Unexpected capture failure for request %s. Failure frame: %d , reason " 489 + "%s, imageCaptured: %s. Before this error we received %d " 490 + "successful frames") 491 .formatted(request, failure.getFrameNumber(), 492 failureToString(failure), 493 failure.wasImageCaptured(), mCaptureResults.size())); 494 } 495 } 496 497 @NonNull failureToString(@onNull CaptureFailure failure)498 private static Object failureToString(@NonNull CaptureFailure failure) { 499 return switch (failure.getReason()) { 500 case CaptureFailure.REASON_ERROR -> "REASON_ERROR"; 501 case CaptureFailure.REASON_FLUSHED -> "REASON_FLUSHED"; 502 default -> failure.getReason(); 503 }; 504 } 505 506 @Override onCaptureCompleted(@onNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result)507 public void onCaptureCompleted(@NonNull CameraCaptureSession session, 508 @NonNull CaptureRequest request, 509 @NonNull TotalCaptureResult result) { 510 mCaptureResults.add(result); 511 mCaptureAndErrorLatch.countDown(); 512 } 513 getCaptureResults()514 public List<TotalCaptureResult> getCaptureResults() { 515 synchronized (mCaptureResults) { 516 return List.copyOf(mCaptureResults); 517 } 518 } 519 getFailedCaptureCount()520 public int getFailedCaptureCount() { 521 return mFailedCaptureCount; 522 } 523 waitForCaptures(int expectedCaptureNumber, long timeoutMillis)524 public void waitForCaptures(int expectedCaptureNumber, long timeoutMillis) { 525 int captureAndErrorCount; 526 synchronized (mCaptureResults) { 527 captureAndErrorCount = mCaptureResults.size() + mFailedCaptureCount; 528 } 529 if (captureAndErrorCount >= expectedCaptureNumber) { 530 return; 531 } 532 int missingCaptureCount = expectedCaptureNumber - captureAndErrorCount; 533 mCaptureAndErrorLatch = new CountDownLatch(missingCaptureCount); 534 try { 535 if (!mCaptureAndErrorLatch.await(timeoutMillis * missingCaptureCount, 536 TimeUnit.MILLISECONDS)) { 537 synchronized (mCaptureResults) { 538 captureAndErrorCount = mCaptureResults.size(); 539 } 540 Assert.fail( 541 ("Timed out waiting for capture. Expected: %d, received: %d " 542 + "successful captures and %d errors") 543 .formatted(expectedCaptureNumber, captureAndErrorCount, 544 mFailedCaptureCount)); 545 } 546 } catch (InterruptedException e) { 547 throw new RuntimeException("Interrupted while waiting for capture", e); 548 } 549 } 550 } 551 } 552