/* * Copyright 2016 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 com.android.mediaframeworktest.stress; import com.android.mediaframeworktest.Camera2SurfaceViewTestCase; import com.android.mediaframeworktest.helpers.CameraTestUtils.SimpleCaptureCallback; import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraDevice; import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.CaptureResult; import android.util.Log; import android.util.Size; import java.util.Arrays; import static android.hardware.camera2.CameraCharacteristics.CONTROL_AE_MODE_OFF; import static android.hardware.camera2.CameraCharacteristics.CONTROL_AE_MODE_ON; import static android.hardware.camera2.CameraCharacteristics.CONTROL_AE_MODE_ON_ALWAYS_FLASH; import static android.hardware.camera2.CameraCharacteristics.CONTROL_AE_MODE_ON_AUTO_FLASH; import static android.hardware.camera2.CameraCharacteristics.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE; import static com.android.mediaframeworktest.helpers.CameraTestUtils.getValueNotNull; /** * <p> * Basic test for camera CaptureRequest key controls. * </p> * <p> * Several test categories are covered: manual sensor control, 3A control, * manual ISP control and other per-frame control and synchronization. * </p> * * adb shell am instrument \ * -e class com.android.mediaframeworktest.stress.Camera2CaptureRequestTest#testAeModeAndLock \ * -e iterations 10 \ * -e waitIntervalMs 1000 \ * -e resultToFile false \ * -r -w com.android.mediaframeworktest/.Camera2InstrumentationTestRunner */ public class Camera2CaptureRequestTest extends Camera2SurfaceViewTestCase { private static final String TAG = "CaptureRequestTest"; private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE); /** 30ms exposure time must be supported by full capability devices. */ private static final long DEFAULT_EXP_TIME_NS = 30000000L; private static final int DEFAULT_SENSITIVITY = 100; private static final long EXPOSURE_TIME_ERROR_MARGIN_NS = 100000L; // 100us, Approximation. private static final float EXPOSURE_TIME_ERROR_MARGIN_RATE = 0.03f; // 3%, Approximation. private static final float SENSITIVITY_ERROR_MARGIN_RATE = 0.03f; // 3%, Approximation. private static final int DEFAULT_NUM_EXPOSURE_TIME_STEPS = 3; private static final int DEFAULT_NUM_SENSITIVITY_STEPS = 16; private static final int DEFAULT_SENSITIVITY_STEP_SIZE = 100; @Override protected void setUp() throws Exception { super.setUp(); } @Override protected void tearDown() throws Exception { super.tearDown(); } /** * Test AE mode and lock. * * <p> * For AE lock, when it is locked, exposure parameters shouldn't be changed. * For AE modes, each mode should satisfy the per frame controls defined in * API specifications. * </p> */ public void testAeModeAndLock() throws Exception { for (int i = 0; i < mCameraIds.length; i++) { try { openDevice(mCameraIds[i]); if (!mStaticInfo.isColorOutputSupported()) { Log.i(TAG, "Camera " + mCameraIds[i] + " does not support color outputs, skipping"); continue; } Size maxPreviewSz = mOrderedPreviewSizes.get(0); // Max preview size. // Update preview surface with given size for all sub-tests. updatePreviewSurface(maxPreviewSz); // Test iteration starts... for (int iteration = 0; iteration < getIterationCount(); ++iteration) { Log.v(TAG, String.format("AE mode and lock: %d/%d", iteration + 1, getIterationCount())); // Test aeMode and lock int[] aeModes = mStaticInfo.getAeAvailableModesChecked(); for (int mode : aeModes) { aeModeAndLockTestByMode(mode); } getResultPrinter().printStatus(getIterationCount(), iteration + 1, mCameraIds[i]); Thread.sleep(getTestWaitIntervalMs()); } } finally { closeDevice(); } } } /** * Test the all available AE modes and AE lock. * <p> * For manual AE mode, test iterates through different sensitivities and * exposure times, validate the result exposure time correctness. For * CONTROL_AE_MODE_ON_ALWAYS_FLASH mode, the AE lock and flash are tested. * For the rest of the AUTO mode, AE lock is tested. * </p> * * @param mode */ private void aeModeAndLockTestByMode(int mode) throws Exception { switch (mode) { case CONTROL_AE_MODE_OFF: if (mStaticInfo.isCapabilitySupported( CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_MANUAL_SENSOR)) { // Test manual exposure control. aeManualControlTest(); } else { Log.w(TAG, "aeModeAndLockTestByMode - can't test AE mode OFF without " + "manual sensor control"); } break; case CONTROL_AE_MODE_ON: case CONTROL_AE_MODE_ON_AUTO_FLASH: case CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE: case CONTROL_AE_MODE_ON_ALWAYS_FLASH: // Test AE lock for above AUTO modes. aeAutoModeTestLock(mode); break; default: throw new UnsupportedOperationException("Unhandled AE mode " + mode); } } /** * Test AE auto modes. * <p> * Use single request rather than repeating request to test AE lock per frame control. * </p> */ private void aeAutoModeTestLock(int mode) throws Exception { CaptureRequest.Builder requestBuilder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); if (mStaticInfo.isAeLockSupported()) { requestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false); } requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, mode); configurePreviewOutput(requestBuilder); final int MAX_NUM_CAPTURES_DURING_LOCK = 5; for (int i = 1; i <= MAX_NUM_CAPTURES_DURING_LOCK; i++) { autoAeMultipleCapturesThenTestLock(requestBuilder, mode, i); } } /** * Issue multiple auto AE captures, then lock AE, validate the AE lock vs. * the first capture result after the AE lock. The right AE lock behavior is: * When it is locked, it locks to the current exposure value, and all subsequent * request with lock ON will have the same exposure value locked. */ private void autoAeMultipleCapturesThenTestLock( CaptureRequest.Builder requestBuilder, int aeMode, int numCapturesDuringLock) throws Exception { if (numCapturesDuringLock < 1) { throw new IllegalArgumentException("numCapturesBeforeLock must be no less than 1"); } if (VERBOSE) { Log.v(TAG, "Camera " + mCamera.getId() + ": Testing auto AE mode and lock for mode " + aeMode + " with " + numCapturesDuringLock + " captures before lock"); } final int NUM_CAPTURES_BEFORE_LOCK = 2; SimpleCaptureCallback listener = new SimpleCaptureCallback(); CaptureResult[] resultsDuringLock = new CaptureResult[numCapturesDuringLock]; boolean canSetAeLock = mStaticInfo.isAeLockSupported(); // Reset the AE lock to OFF, since we are reusing this builder many times if (canSetAeLock) { requestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false); } // Just send several captures with auto AE, lock off. CaptureRequest request = requestBuilder.build(); for (int i = 0; i < NUM_CAPTURES_BEFORE_LOCK; i++) { mSession.capture(request, listener, mHandler); } waitForNumResults(listener, NUM_CAPTURES_BEFORE_LOCK); if (!canSetAeLock) { // Without AE lock, the remaining tests items won't work return; } // Then fire several capture to lock the AE. requestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true); int requestCount = captureRequestsSynchronized( requestBuilder.build(), numCapturesDuringLock, listener, mHandler); int[] sensitivities = new int[numCapturesDuringLock]; long[] expTimes = new long[numCapturesDuringLock]; Arrays.fill(sensitivities, -1); Arrays.fill(expTimes, -1L); // Get the AE lock on result and validate the exposure values. waitForNumResults(listener, requestCount - numCapturesDuringLock); for (int i = 0; i < resultsDuringLock.length; i++) { resultsDuringLock[i] = listener.getCaptureResult(WAIT_FOR_RESULT_TIMEOUT_MS); } for (int i = 0; i < numCapturesDuringLock; i++) { mCollector.expectKeyValueEquals( resultsDuringLock[i], CaptureResult.CONTROL_AE_LOCK, true); } // Can't read manual sensor/exposure settings without manual sensor if (mStaticInfo.isCapabilitySupported( CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_READ_SENSOR_SETTINGS)) { int sensitivityLocked = getValueNotNull(resultsDuringLock[0], CaptureResult.SENSOR_SENSITIVITY); long expTimeLocked = getValueNotNull(resultsDuringLock[0], CaptureResult.SENSOR_EXPOSURE_TIME); for (int i = 1; i < resultsDuringLock.length; i++) { mCollector.expectKeyValueEquals( resultsDuringLock[i], CaptureResult.SENSOR_EXPOSURE_TIME, expTimeLocked); mCollector.expectKeyValueEquals( resultsDuringLock[i], CaptureResult.SENSOR_SENSITIVITY, sensitivityLocked); } } } /** * Iterate through exposure times and sensitivities for manual AE control. * <p> * Use single request rather than repeating request to test manual exposure * value change per frame control. * </p> */ private void aeManualControlTest() throws Exception { CaptureRequest.Builder requestBuilder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CONTROL_AE_MODE_OFF); configurePreviewOutput(requestBuilder); SimpleCaptureCallback listener = new SimpleCaptureCallback(); long[] expTimes = getExposureTimeTestValues(); int[] sensitivities = getSensitivityTestValues(); // Submit single request at a time, then verify the result. for (int i = 0; i < expTimes.length; i++) { for (int j = 0; j < sensitivities.length; j++) { if (VERBOSE) { Log.v(TAG, "Camera " + mCamera.getId() + ": Testing sensitivity " + sensitivities[j] + ", exposure time " + expTimes[i] + "ns"); } changeExposure(requestBuilder, expTimes[i], sensitivities[j]); mSession.capture(requestBuilder.build(), listener, mHandler); // make sure timeout is long enough for long exposure time long timeout = WAIT_FOR_RESULT_TIMEOUT_MS + expTimes[i]; CaptureResult result = listener.getCaptureResult(timeout); long resultExpTime = getValueNotNull(result, CaptureResult.SENSOR_EXPOSURE_TIME); int resultSensitivity = getValueNotNull(result, CaptureResult.SENSOR_SENSITIVITY); validateExposureTime(expTimes[i], resultExpTime); validateSensitivity(sensitivities[j], resultSensitivity); validateFrameDurationForCapture(result); } } // TODO: Add another case to test where we can submit all requests, then wait for // results, which will hide the pipeline latency. this is not only faster, but also // test high speed per frame control and synchronization. } //---------------------------------------------------------------- //---------Below are common functions for all tests.-------------- //---------------------------------------------------------------- /** * Enable exposure manual control and change exposure and sensitivity and * clamp the value into the supported range. */ private void changeExposure(CaptureRequest.Builder requestBuilder, long expTime, int sensitivity) { // Check if the max analog sensitivity is available and no larger than max sensitivity. // The max analog sensitivity is not actually used here. This is only an extra sanity check. mStaticInfo.getMaxAnalogSensitivityChecked(); expTime = mStaticInfo.getExposureClampToRange(expTime); sensitivity = mStaticInfo.getSensitivityClampToRange(sensitivity); requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CONTROL_AE_MODE_OFF); requestBuilder.set(CaptureRequest.SENSOR_EXPOSURE_TIME, expTime); requestBuilder.set(CaptureRequest.SENSOR_SENSITIVITY, sensitivity); } /** * Get the exposure time array that contains multiple exposure time steps in * the exposure time range. */ private long[] getExposureTimeTestValues() { long[] testValues = new long[DEFAULT_NUM_EXPOSURE_TIME_STEPS + 1]; long maxExpTime = mStaticInfo.getExposureMaximumOrDefault(DEFAULT_EXP_TIME_NS); long minExpTime = mStaticInfo.getExposureMinimumOrDefault(DEFAULT_EXP_TIME_NS); long range = maxExpTime - minExpTime; double stepSize = range / (double)DEFAULT_NUM_EXPOSURE_TIME_STEPS; for (int i = 0; i < testValues.length; i++) { testValues[i] = maxExpTime - (long)(stepSize * i); testValues[i] = mStaticInfo.getExposureClampToRange(testValues[i]); } return testValues; } /** * Get the sensitivity array that contains multiple sensitivity steps in the * sensitivity range. * <p> * Sensitivity number of test values is determined by * {@value #DEFAULT_SENSITIVITY_STEP_SIZE} and sensitivity range, and * bounded by {@value #DEFAULT_NUM_SENSITIVITY_STEPS}. * </p> */ private int[] getSensitivityTestValues() { int maxSensitivity = mStaticInfo.getSensitivityMaximumOrDefault( DEFAULT_SENSITIVITY); int minSensitivity = mStaticInfo.getSensitivityMinimumOrDefault( DEFAULT_SENSITIVITY); int range = maxSensitivity - minSensitivity; int stepSize = DEFAULT_SENSITIVITY_STEP_SIZE; int numSteps = range / stepSize; // Bound the test steps to avoid supper long test. if (numSteps > DEFAULT_NUM_SENSITIVITY_STEPS) { numSteps = DEFAULT_NUM_SENSITIVITY_STEPS; stepSize = range / numSteps; } int[] testValues = new int[numSteps + 1]; for (int i = 0; i < testValues.length; i++) { testValues[i] = maxSensitivity - stepSize * i; testValues[i] = mStaticInfo.getSensitivityClampToRange(testValues[i]); } return testValues; } /** * Validate the AE manual control exposure time. * * <p>Exposure should be close enough, and only round down if they are not equal.</p> * * @param request Request exposure time * @param result Result exposure time */ private void validateExposureTime(long request, long result) { long expTimeDelta = request - result; long expTimeErrorMargin = (long)(Math.max(EXPOSURE_TIME_ERROR_MARGIN_NS, request * EXPOSURE_TIME_ERROR_MARGIN_RATE)); // First, round down not up, second, need close enough. mCollector.expectTrue("Exposture time is invalid for AE manaul control test, request: " + request + " result: " + result, expTimeDelta < expTimeErrorMargin && expTimeDelta >= 0); } /** * Validate AE manual control sensitivity. * * @param request Request sensitivity * @param result Result sensitivity */ private void validateSensitivity(int request, int result) { float sensitivityDelta = request - result; float sensitivityErrorMargin = request * SENSITIVITY_ERROR_MARGIN_RATE; // First, round down not up, second, need close enough. mCollector.expectTrue("Sensitivity is invalid for AE manaul control test, request: " + request + " result: " + result, sensitivityDelta < sensitivityErrorMargin && sensitivityDelta >= 0); } /** * Validate frame duration for a given capture. * * <p>Frame duration should be longer than exposure time.</p> * * @param result The capture result for a given capture */ private void validateFrameDurationForCapture(CaptureResult result) { long expTime = getValueNotNull(result, CaptureResult.SENSOR_EXPOSURE_TIME); long frameDuration = getValueNotNull(result, CaptureResult.SENSOR_FRAME_DURATION); if (VERBOSE) { Log.v(TAG, "frame duration: " + frameDuration + " Exposure time: " + expTime); } mCollector.expectTrue(String.format("Frame duration (%d) should be longer than exposure" + " time (%d) for a given capture", frameDuration, expTime), frameDuration >= expTime); validatePipelineDepth(result); } /** * Validate the pipeline depth result. * * @param result The capture result to get pipeline depth data */ private void validatePipelineDepth(CaptureResult result) { final byte MIN_PIPELINE_DEPTH = 1; byte maxPipelineDepth = mStaticInfo.getPipelineMaxDepthChecked(); Byte pipelineDepth = getValueNotNull(result, CaptureResult.REQUEST_PIPELINE_DEPTH); mCollector.expectInRange(String.format("Pipeline depth must be in the range of [%d, %d]", MIN_PIPELINE_DEPTH, maxPipelineDepth), pipelineDepth, MIN_PIPELINE_DEPTH, maxPipelineDepth); } }