• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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