blob: f729cc7ef799f09efe37794cea193fe0ec75e573 [file] [log] [blame]
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.camera.cts;
import static android.hardware.camera2.cts.CameraTestUtils.SESSION_CONFIGURE_TIMEOUT_MS;
import static android.hardware.camera2.cts.CameraTestUtils.CAPTURE_RESULT_TIMEOUT_MS;
import static android.hardware.camera2.cts.CameraTestUtils.SimpleCaptureCallback;
import static android.hardware.camera2.cts.CameraTestUtils.getValueNotNull;
import static androidx.heifwriter.HeifWriter.INPUT_MODE_SURFACE;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import android.graphics.ImageFormat;
import android.graphics.SurfaceTexture;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult;
import android.hardware.camera2.cts.helpers.StaticMetadata;
import android.hardware.camera2.cts.testcases.Camera2AndroidTestCase;
import android.hardware.camera2.params.OutputConfiguration;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.media.MediaMetadataRetriever;
import android.os.Environment;
import android.os.SystemClock;
import android.util.Log;
import android.util.Size;
import android.view.Surface;
import androidx.heifwriter.HeifWriter;
import com.android.compatibility.common.util.MediaUtils;
import com.android.ex.camera2.blocking.BlockingSessionCallback;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.junit.runners.Parameterized;
import org.junit.runner.RunWith;
import org.junit.Test;
@RunWith(Parameterized.class)
public class HeifWriterTest extends Camera2AndroidTestCase {
private static final String TAG = HeifWriterTest.class.getSimpleName();
private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private String mFilePath;
private static final String OUTPUT_FILENAME = "output.heic";
@Override
public void setUp() throws Exception {
super.setUp();
File filesDir = mContext.getPackageManager().isInstantApp()
? mContext.getFilesDir()
: mContext.getExternalFilesDir(null);
mFilePath = filesDir.getPath();
}
@Override
public void tearDown() throws Exception {
super.tearDown();
}
@Test
public void testHeif() throws Exception {
final int NUM_SINGLE_CAPTURE_TESTED = 3;
final int NUM_HEIC_CAPTURE_TESTED = 2;
final int SESSION_WARMUP_MS = 1000;
final int HEIF_STOP_TIMEOUT = 3000 * NUM_SINGLE_CAPTURE_TESTED;
if (!canEncodeHeic()) {
MediaUtils.skipTest("heic encoding is not supported on this device");
return;
}
boolean sessionFailure = false;
Integer[] sessionStates = {BlockingSessionCallback.SESSION_READY,
BlockingSessionCallback.SESSION_CONFIGURE_FAILED};
for (String id : mCameraIdsUnderTest) {
try {
Log.v(TAG, "Testing HEIF capture for Camera " + id);
openDevice(id);
Size[] availableSizes = mStaticInfo.getAvailableSizesForFormatChecked(
ImageFormat.PRIVATE,
StaticMetadata.StreamDirection.Output);
// for each resolution, test imageReader:
for (Size sz : availableSizes) {
HeifWriter heifWriter = null;
OutputConfiguration outConfig = null;
Surface latestSurface = null;
CaptureRequest.Builder reqStill = null;
int width = sz.getWidth();
int height = sz.getHeight();
for (int cap = 0; cap < NUM_HEIC_CAPTURE_TESTED; cap++) {
if (VERBOSE) {
Log.v(TAG, "Testing size " + sz.toString() + " format PRIVATE"
+ " for camera " + mCamera.getId() + ". Iteration:" + cap);
}
try {
TestConfig.Builder builder = new TestConfig.Builder(/*useGrid*/false);
builder.setNumImages(NUM_SINGLE_CAPTURE_TESTED);
builder.setSize(sz);
String filename = "Cam" + id + "_" + width + "x" + height +
"_" + cap + ".heic";
builder.setOutputPath(
new File(mFilePath, filename).getAbsolutePath());
TestConfig config = builder.build();
try {
heifWriter = new HeifWriter.Builder(
config.mOutputPath,
width, height, INPUT_MODE_SURFACE)
.setGridEnabled(config.mUseGrid)
.setMaxImages(config.mMaxNumImages)
.setQuality(config.mQuality)
.setPrimaryIndex(config.mNumImages - 1)
.setHandler(mHandler)
.build();
} catch (IOException e) {
// Continue in case the size is not supported
sessionFailure = true;
Log.i(TAG, "Skip due to heifWriter creation failure: "
+ e.getMessage());
continue;
}
// First capture. Start capture session
latestSurface = heifWriter.getInputSurface();
outConfig = new OutputConfiguration(latestSurface);
List<OutputConfiguration> configs =
new ArrayList<OutputConfiguration>();
configs.add(outConfig);
SurfaceTexture preview = new SurfaceTexture(/*random int*/ 1);
Surface previewSurface = new Surface(preview);
preview.setDefaultBufferSize(640, 480);
configs.add(new OutputConfiguration(previewSurface));
CaptureRequest.Builder reqPreview = mCamera.createCaptureRequest(
CameraDevice.TEMPLATE_PREVIEW);
reqPreview.addTarget(previewSurface);
reqStill = mCamera.createCaptureRequest(
CameraDevice.TEMPLATE_STILL_CAPTURE);
reqStill.addTarget(previewSurface);
reqStill.addTarget(latestSurface);
// Start capture session and preview
createSessionByConfigs(configs);
int state = mCameraSessionListener.getStateWaiter().waitForAnyOfStates(
Arrays.asList(sessionStates), SESSION_CONFIGURE_TIMEOUT_MS);
if (state == BlockingSessionCallback.SESSION_CONFIGURE_FAILED) {
// session configuration failure. Bail out due to known issue of
// HeifWriter INPUT_SURFACE mode support for camera. b/79699819
sessionFailure = true;
break;
}
startCapture(reqPreview.build(), /*repeating*/true, null, null);
SystemClock.sleep(SESSION_WARMUP_MS);
heifWriter.start();
// Start capture.
CaptureRequest request = reqStill.build();
SimpleCaptureCallback listener = new SimpleCaptureCallback();
int numImages = config.mNumImages;
for (int i = 0; i < numImages; i++) {
startCapture(request, /*repeating*/false, listener, mHandler);
}
// Validate capture result.
CaptureResult result = validateCaptureResult(
ImageFormat.PRIVATE, sz, listener, numImages);
// TODO: convert capture results into EXIF and send to heifwriter
heifWriter.stop(HEIF_STOP_TIMEOUT);
verifyResult(config.mOutputPath, width, height,
config.mRotation, config.mUseGrid,
Math.min(numImages, config.mMaxNumImages));
} finally {
if (heifWriter != null) {
heifWriter.close();
heifWriter = null;
}
if (!sessionFailure) {
stopCapture(/*fast*/false);
}
}
}
if (sessionFailure) {
break;
}
}
} finally {
closeDevice(id);
}
}
}
private static boolean canEncodeHeic() {
return MediaUtils.hasEncoder(MediaFormat.MIMETYPE_VIDEO_HEVC)
|| MediaUtils.hasEncoder(MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC);
}
private static class TestConfig {
final boolean mUseGrid;
final int mMaxNumImages;
final int mNumImages;
final int mWidth;
final int mHeight;
final int mRotation;
final int mQuality;
final String mOutputPath;
TestConfig(boolean useGrid, int maxNumImages, int numImages,
int width, int height, int rotation, int quality,
String outputPath) {
mUseGrid = useGrid;
mMaxNumImages = maxNumImages;
mNumImages = numImages;
mWidth = width;
mHeight = height;
mRotation = rotation;
mQuality = quality;
mOutputPath = outputPath;
}
static class Builder {
final boolean mUseGrid;
int mMaxNumImages;
int mNumImages;
int mWidth;
int mHeight;
int mRotation;
final int mQuality;
String mOutputPath;
Builder(boolean useGrids) {
mUseGrid = useGrids;
mMaxNumImages = mNumImages = 4;
mWidth = 1920;
mHeight = 1080;
mRotation = 0;
mQuality = 100;
mOutputPath = new File(Environment.getExternalStorageDirectory(),
OUTPUT_FILENAME).getAbsolutePath();
}
Builder setNumImages(int numImages) {
mMaxNumImages = mNumImages = numImages;
return this;
}
Builder setRotation(int rotation) {
mRotation = rotation;
return this;
}
Builder setSize(Size sz) {
mWidth = sz.getWidth();
mHeight = sz.getHeight();
return this;
}
Builder setOutputPath(String path) {
mOutputPath = path;
return this;
}
private void cleanupStaleOutputs() {
File outputFile = new File(mOutputPath);
if (outputFile.exists()) {
outputFile.delete();
}
}
TestConfig build() {
cleanupStaleOutputs();
return new TestConfig(mUseGrid, mMaxNumImages, mNumImages,
mWidth, mHeight, mRotation, mQuality, mOutputPath);
}
}
@Override
public String toString() {
return "TestConfig"
+ ": mUseGrid " + mUseGrid
+ ", mMaxNumImages " + mMaxNumImages
+ ", mNumImages " + mNumImages
+ ", mWidth " + mWidth
+ ", mHeight " + mHeight
+ ", mRotation " + mRotation
+ ", mQuality " + mQuality
+ ", mOutputPath " + mOutputPath;
}
}
private void verifyResult(
String filename, int width, int height, int rotation, boolean useGrid, int numImages)
throws Exception {
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
retriever.setDataSource(filename);
String hasImage = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_IMAGE);
if (!"yes".equals(hasImage)) {
throw new Exception("No images found in file " + filename);
}
assertEquals("Wrong image count", numImages,
Integer.parseInt(retriever.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_IMAGE_COUNT)));
assertEquals("Wrong width", width,
Integer.parseInt(retriever.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH)));
assertEquals("Wrong height", height,
Integer.parseInt(retriever.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT)));
assertEquals("Wrong rotation", rotation,
Integer.parseInt(retriever.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_IMAGE_ROTATION)));
retriever.release();
if (useGrid) {
MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(filename);
MediaFormat format = extractor.getTrackFormat(0);
int tileWidth = format.getInteger(MediaFormat.KEY_TILE_WIDTH);
int tileHeight = format.getInteger(MediaFormat.KEY_TILE_HEIGHT);
int gridRows = format.getInteger(MediaFormat.KEY_GRID_ROWS);
int gridCols = format.getInteger(MediaFormat.KEY_GRID_COLUMNS);
assertTrue("Wrong tile width or grid cols",
((width + tileWidth - 1) / tileWidth) == gridCols);
assertTrue("Wrong tile height or grid rows",
((height + tileHeight - 1) / tileHeight) == gridRows);
extractor.release();
}
}
/**
* Validate capture results.
*
* @param format The format of this capture.
* @param size The capture size.
* @param listener The capture listener to get capture result callbacks.
* @return the last verified CaptureResult
*/
private CaptureResult validateCaptureResult(
int format, Size size, SimpleCaptureCallback listener, int numFrameVerified) {
CaptureResult result = null;
for (int i = 0; i < numFrameVerified; i++) {
result = listener.getCaptureResult(CAPTURE_RESULT_TIMEOUT_MS);
if (mStaticInfo.isCapabilitySupported(
CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_READ_SENSOR_SETTINGS)) {
Long exposureTime = getValueNotNull(result, CaptureResult.SENSOR_EXPOSURE_TIME);
Integer sensitivity = getValueNotNull(result, CaptureResult.SENSOR_SENSITIVITY);
mCollector.expectInRange(
String.format(
"Capture for format %d, size %s exposure time is invalid.",
format, size.toString()),
exposureTime,
mStaticInfo.getExposureMinimumOrDefault(),
mStaticInfo.getExposureMaximumOrDefault()
);
mCollector.expectInRange(
String.format("Capture for format %d, size %s sensitivity is invalid.",
format, size.toString()),
sensitivity,
mStaticInfo.getSensitivityMinimumOrDefault(),
mStaticInfo.getSensitivityMaximumOrDefault()
);
}
// TODO: add more key validations.
}
return result;
}
}