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