1 /* 2 * Copyright (C) 2018 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.camera.cts; 18 19 import static android.hardware.camera2.cts.CameraTestUtils.SESSION_CONFIGURE_TIMEOUT_MS; 20 import static android.hardware.camera2.cts.CameraTestUtils.CAPTURE_RESULT_TIMEOUT_MS; 21 import static android.hardware.camera2.cts.CameraTestUtils.SimpleCaptureCallback; 22 import static android.hardware.camera2.cts.CameraTestUtils.getValueNotNull; 23 24 import static androidx.heifwriter.HeifWriter.INPUT_MODE_SURFACE; 25 26 import static org.junit.Assert.assertEquals; 27 import static org.junit.Assert.assertTrue; 28 29 import android.graphics.ImageFormat; 30 import android.graphics.SurfaceTexture; 31 import android.hardware.camera2.CameraCharacteristics; 32 import android.hardware.camera2.CameraDevice; 33 import android.hardware.camera2.CaptureRequest; 34 import android.hardware.camera2.CaptureResult; 35 import android.hardware.camera2.cts.helpers.StaticMetadata; 36 import android.hardware.camera2.cts.testcases.Camera2AndroidTestCase; 37 import android.hardware.camera2.params.OutputConfiguration; 38 import android.media.MediaExtractor; 39 import android.media.MediaFormat; 40 import android.media.MediaMetadataRetriever; 41 import android.os.Environment; 42 import android.os.SystemClock; 43 import android.util.Log; 44 import android.util.Size; 45 import android.view.Surface; 46 47 import androidx.heifwriter.HeifWriter; 48 49 import com.android.compatibility.common.util.MediaUtils; 50 import com.android.ex.camera2.blocking.BlockingSessionCallback; 51 52 import java.io.File; 53 import java.io.IOException; 54 import java.util.ArrayList; 55 import java.util.Arrays; 56 import java.util.List; 57 58 public class HeifWriterTest extends Camera2AndroidTestCase { 59 private static final String TAG = HeifWriterTest.class.getSimpleName(); 60 private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE); 61 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 62 63 private String mFilePath; 64 private static final String OUTPUT_FILENAME = "output.heic"; 65 66 @Override setUp()67 public void setUp() throws Exception { 68 super.setUp(); 69 70 File filesDir = mContext.getPackageManager().isInstantApp() 71 ? mContext.getFilesDir() 72 : mContext.getExternalFilesDir(null); 73 74 mFilePath = filesDir.getPath(); 75 } 76 77 @Override tearDown()78 public void tearDown() throws Exception { 79 super.tearDown(); 80 } 81 testHeif()82 public void testHeif() throws Exception { 83 final int NUM_SINGLE_CAPTURE_TESTED = 3; 84 final int NUM_HEIC_CAPTURE_TESTED = 2; 85 final int SESSION_WARMUP_MS = 1000; 86 final int HEIF_STOP_TIMEOUT = 3000 * NUM_SINGLE_CAPTURE_TESTED; 87 88 if (!canEncodeHeic()) { 89 MediaUtils.skipTest("heic encoding is not supported on this device"); 90 return; 91 } 92 93 boolean sessionFailure = false; 94 Integer[] sessionStates = {BlockingSessionCallback.SESSION_READY, 95 BlockingSessionCallback.SESSION_CONFIGURE_FAILED}; 96 for (String id : mCameraIds) { 97 try { 98 Log.v(TAG, "Testing HEIF capture for Camera " + id); 99 openDevice(id); 100 101 Size[] availableSizes = mStaticInfo.getAvailableSizesForFormatChecked( 102 ImageFormat.PRIVATE, 103 StaticMetadata.StreamDirection.Output); 104 105 // for each resolution, test imageReader: 106 for (Size sz : availableSizes) { 107 HeifWriter heifWriter = null; 108 OutputConfiguration outConfig = null; 109 Surface latestSurface = null; 110 CaptureRequest.Builder reqStill = null; 111 int width = sz.getWidth(); 112 int height = sz.getHeight(); 113 for (int cap = 0; cap < NUM_HEIC_CAPTURE_TESTED; cap++) { 114 if (VERBOSE) { 115 Log.v(TAG, "Testing size " + sz.toString() + " format PRIVATE" 116 + " for camera " + mCamera.getId() + ". Iteration:" + cap); 117 } 118 119 try { 120 TestConfig.Builder builder = new TestConfig.Builder(/*useGrid*/false); 121 builder.setNumImages(NUM_SINGLE_CAPTURE_TESTED); 122 builder.setSize(sz); 123 String filename = "Cam" + id + "_" + width + "x" + height + 124 "_" + cap + ".heic"; 125 builder.setOutputPath( 126 new File(mFilePath, filename).getAbsolutePath()); 127 TestConfig config = builder.build(); 128 129 try { 130 heifWriter = new HeifWriter.Builder( 131 config.mOutputPath, 132 width, height, INPUT_MODE_SURFACE) 133 .setGridEnabled(config.mUseGrid) 134 .setMaxImages(config.mMaxNumImages) 135 .setQuality(config.mQuality) 136 .setPrimaryIndex(config.mNumImages - 1) 137 .setHandler(mHandler) 138 .build(); 139 } catch (IOException e) { 140 // Continue in case the size is not supported 141 sessionFailure = true; 142 Log.i(TAG, "Skip due to heifWriter creation failure: " 143 + e.getMessage()); 144 continue; 145 } 146 147 // First capture. Start capture session 148 latestSurface = heifWriter.getInputSurface(); 149 outConfig = new OutputConfiguration(latestSurface); 150 List<OutputConfiguration> configs = 151 new ArrayList<OutputConfiguration>(); 152 configs.add(outConfig); 153 154 SurfaceTexture preview = new SurfaceTexture(/*random int*/ 1); 155 Surface previewSurface = new Surface(preview); 156 preview.setDefaultBufferSize(640, 480); 157 configs.add(new OutputConfiguration(previewSurface)); 158 159 CaptureRequest.Builder reqPreview = mCamera.createCaptureRequest( 160 CameraDevice.TEMPLATE_PREVIEW); 161 reqPreview.addTarget(previewSurface); 162 163 reqStill = mCamera.createCaptureRequest( 164 CameraDevice.TEMPLATE_STILL_CAPTURE); 165 reqStill.addTarget(previewSurface); 166 reqStill.addTarget(latestSurface); 167 168 // Start capture session and preview 169 createSessionByConfigs(configs); 170 int state = mCameraSessionListener.getStateWaiter().waitForAnyOfStates( 171 Arrays.asList(sessionStates), SESSION_CONFIGURE_TIMEOUT_MS); 172 if (state == BlockingSessionCallback.SESSION_CONFIGURE_FAILED) { 173 // session configuration failure. Bail out due to known issue of 174 // HeifWriter INPUT_SURFACE mode support for camera. b/79699819 175 sessionFailure = true; 176 break; 177 } 178 startCapture(reqPreview.build(), /*repeating*/true, null, null); 179 180 SystemClock.sleep(SESSION_WARMUP_MS); 181 182 heifWriter.start(); 183 184 // Start capture. 185 CaptureRequest request = reqStill.build(); 186 SimpleCaptureCallback listener = new SimpleCaptureCallback(); 187 188 int numImages = config.mNumImages; 189 190 for (int i = 0; i < numImages; i++) { 191 startCapture(request, /*repeating*/false, listener, mHandler); 192 } 193 194 // Validate capture result. 195 CaptureResult result = validateCaptureResult( 196 ImageFormat.PRIVATE, sz, listener, numImages); 197 198 // TODO: convert capture results into EXIF and send to heifwriter 199 200 heifWriter.stop(HEIF_STOP_TIMEOUT); 201 202 verifyResult(config.mOutputPath, width, height, 203 config.mRotation, config.mUseGrid, 204 Math.min(numImages, config.mMaxNumImages)); 205 } finally { 206 if (heifWriter != null) { 207 heifWriter.close(); 208 heifWriter = null; 209 } 210 if (!sessionFailure) { 211 stopCapture(/*fast*/false); 212 } 213 } 214 } 215 216 if (sessionFailure) { 217 break; 218 } 219 } 220 } finally { 221 closeDevice(id); 222 } 223 } 224 } 225 canEncodeHeic()226 private static boolean canEncodeHeic() { 227 return MediaUtils.hasEncoder(MediaFormat.MIMETYPE_VIDEO_HEVC) 228 || MediaUtils.hasEncoder(MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC); 229 } 230 231 private static class TestConfig { 232 final boolean mUseGrid; 233 final int mMaxNumImages; 234 final int mNumImages; 235 final int mWidth; 236 final int mHeight; 237 final int mRotation; 238 final int mQuality; 239 final String mOutputPath; 240 TestConfig(boolean useGrid, int maxNumImages, int numImages, int width, int height, int rotation, int quality, String outputPath)241 TestConfig(boolean useGrid, int maxNumImages, int numImages, 242 int width, int height, int rotation, int quality, 243 String outputPath) { 244 mUseGrid = useGrid; 245 mMaxNumImages = maxNumImages; 246 mNumImages = numImages; 247 mWidth = width; 248 mHeight = height; 249 mRotation = rotation; 250 mQuality = quality; 251 mOutputPath = outputPath; 252 } 253 254 static class Builder { 255 final boolean mUseGrid; 256 int mMaxNumImages; 257 int mNumImages; 258 int mWidth; 259 int mHeight; 260 int mRotation; 261 final int mQuality; 262 String mOutputPath; 263 Builder(boolean useGrids)264 Builder(boolean useGrids) { 265 mUseGrid = useGrids; 266 mMaxNumImages = mNumImages = 4; 267 mWidth = 1920; 268 mHeight = 1080; 269 mRotation = 0; 270 mQuality = 100; 271 mOutputPath = new File(Environment.getExternalStorageDirectory(), 272 OUTPUT_FILENAME).getAbsolutePath(); 273 } 274 setNumImages(int numImages)275 Builder setNumImages(int numImages) { 276 mMaxNumImages = mNumImages = numImages; 277 return this; 278 } 279 setRotation(int rotation)280 Builder setRotation(int rotation) { 281 mRotation = rotation; 282 return this; 283 } 284 setSize(Size sz)285 Builder setSize(Size sz) { 286 mWidth = sz.getWidth(); 287 mHeight = sz.getHeight(); 288 return this; 289 } 290 setOutputPath(String path)291 Builder setOutputPath(String path) { 292 mOutputPath = path; 293 return this; 294 } 295 cleanupStaleOutputs()296 private void cleanupStaleOutputs() { 297 File outputFile = new File(mOutputPath); 298 if (outputFile.exists()) { 299 outputFile.delete(); 300 } 301 } 302 build()303 TestConfig build() { 304 cleanupStaleOutputs(); 305 return new TestConfig(mUseGrid, mMaxNumImages, mNumImages, 306 mWidth, mHeight, mRotation, mQuality, mOutputPath); 307 } 308 } 309 310 @Override toString()311 public String toString() { 312 return "TestConfig" 313 + ": mUseGrid " + mUseGrid 314 + ", mMaxNumImages " + mMaxNumImages 315 + ", mNumImages " + mNumImages 316 + ", mWidth " + mWidth 317 + ", mHeight " + mHeight 318 + ", mRotation " + mRotation 319 + ", mQuality " + mQuality 320 + ", mOutputPath " + mOutputPath; 321 } 322 } 323 verifyResult( String filename, int width, int height, int rotation, boolean useGrid, int numImages)324 private void verifyResult( 325 String filename, int width, int height, int rotation, boolean useGrid, int numImages) 326 throws Exception { 327 MediaMetadataRetriever retriever = new MediaMetadataRetriever(); 328 retriever.setDataSource(filename); 329 String hasImage = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_IMAGE); 330 if (!"yes".equals(hasImage)) { 331 throw new Exception("No images found in file " + filename); 332 } 333 assertEquals("Wrong image count", numImages, 334 Integer.parseInt(retriever.extractMetadata( 335 MediaMetadataRetriever.METADATA_KEY_IMAGE_COUNT))); 336 assertEquals("Wrong width", width, 337 Integer.parseInt(retriever.extractMetadata( 338 MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH))); 339 assertEquals("Wrong height", height, 340 Integer.parseInt(retriever.extractMetadata( 341 MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT))); 342 assertEquals("Wrong rotation", rotation, 343 Integer.parseInt(retriever.extractMetadata( 344 MediaMetadataRetriever.METADATA_KEY_IMAGE_ROTATION))); 345 retriever.release(); 346 347 if (useGrid) { 348 MediaExtractor extractor = new MediaExtractor(); 349 extractor.setDataSource(filename); 350 MediaFormat format = extractor.getTrackFormat(0); 351 int tileWidth = format.getInteger(MediaFormat.KEY_TILE_WIDTH); 352 int tileHeight = format.getInteger(MediaFormat.KEY_TILE_HEIGHT); 353 int gridRows = format.getInteger(MediaFormat.KEY_GRID_ROWS); 354 int gridCols = format.getInteger(MediaFormat.KEY_GRID_COLUMNS); 355 assertTrue("Wrong tile width or grid cols", 356 ((width + tileWidth - 1) / tileWidth) == gridCols); 357 assertTrue("Wrong tile height or grid rows", 358 ((height + tileHeight - 1) / tileHeight) == gridRows); 359 extractor.release(); 360 } 361 } 362 363 /** 364 * Validate capture results. 365 * 366 * @param format The format of this capture. 367 * @param size The capture size. 368 * @param listener The capture listener to get capture result callbacks. 369 * @return the last verified CaptureResult 370 */ validateCaptureResult( int format, Size size, SimpleCaptureCallback listener, int numFrameVerified)371 private CaptureResult validateCaptureResult( 372 int format, Size size, SimpleCaptureCallback listener, int numFrameVerified) { 373 CaptureResult result = null; 374 for (int i = 0; i < numFrameVerified; i++) { 375 result = listener.getCaptureResult(CAPTURE_RESULT_TIMEOUT_MS); 376 if (mStaticInfo.isCapabilitySupported( 377 CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_READ_SENSOR_SETTINGS)) { 378 Long exposureTime = getValueNotNull(result, CaptureResult.SENSOR_EXPOSURE_TIME); 379 Integer sensitivity = getValueNotNull(result, CaptureResult.SENSOR_SENSITIVITY); 380 mCollector.expectInRange( 381 String.format( 382 "Capture for format %d, size %s exposure time is invalid.", 383 format, size.toString()), 384 exposureTime, 385 mStaticInfo.getExposureMinimumOrDefault(), 386 mStaticInfo.getExposureMaximumOrDefault() 387 ); 388 mCollector.expectInRange( 389 String.format("Capture for format %d, size %s sensitivity is invalid.", 390 format, size.toString()), 391 sensitivity, 392 mStaticInfo.getSensitivityMinimumOrDefault(), 393 mStaticInfo.getSensitivityMaximumOrDefault() 394 ); 395 } 396 // TODO: add more key validations. 397 } 398 return result; 399 } 400 } 401