• 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.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