Metering APIs (#580)
	
		
	
				
					
				
			* Add cameraPictureMetering and cameraPictureSnapshotMetering * Adapt Meter and metering package to picture use * Simplify Full2PictureRecorder, we'll use metering package instead * Add doMetering parameter * Implement cameraPictureMetering and cameraPictureSnapshotMetering in engine * Add options in demo app * Add better logs * Add Snapshot2PictureRecorder * Capture the correct frame based on timestamp * Lock AE and AWB. Account for captureBuilder changes * Fix runtime flash changes bug * Small changes * Flash support for metered snapshots * Remove AE and AWB locks * Lock AE/AWB/AF inside the snapshot recorder * Small changes * Fix AutoExposure metering * Create Locker and locking.* parameters * Implement Locker in Camera2Engine * Implement reset delay in Camera2Engine instead of Meter * Simplify Snapshot2PictureRecorder * Fix success value * Unlock inside Camera2Engine * Do not lock for normal gestures * Simplify logic * Improve locking/AutoFocus * Fix TORCH bug * Small changes to locking and metering * Remove AF and AWB for testing * Create action package * Create OneShotAction * Create LogAction * Revisit Full2VideoRecorder using actions * Revisit Full2PictureRecorder using actions * Enable missing functionality in Snapshot2PictureRecorder * Move Snapshot2PictureRecorder using actions, rewrite lock package * Add TimeoutAction * Add comments to the action package * Add meter package * Remove old metering package * Fix various bugs * Add action.abort() * Abort old MeterAction when running new ones * Fix various bugs * Add doc empty page * Add documentation * Fix testspull/608/head
							parent
							
								
									a8fddc482f
								
							
						
					
					
						commit
						4ddd2af731
					
				| @ -0,0 +1,62 @@ | ||||
| package com.otaliastudios.cameraview.engine; | ||||
| 
 | ||||
| import android.hardware.camera2.CaptureRequest; | ||||
| import android.hardware.camera2.CaptureResult; | ||||
| import android.hardware.camera2.TotalCaptureResult; | ||||
| import android.os.Build; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.RequiresApi; | ||||
| 
 | ||||
| import com.otaliastudios.cameraview.CameraLogger; | ||||
| import com.otaliastudios.cameraview.engine.action.ActionHolder; | ||||
| import com.otaliastudios.cameraview.engine.action.BaseAction; | ||||
| 
 | ||||
| @RequiresApi(Build.VERSION_CODES.LOLLIPOP) | ||||
| class LogAction extends BaseAction { | ||||
| 
 | ||||
|     private final static CameraLogger LOG = CameraLogger.create(Camera2Engine.class.getSimpleName()); | ||||
| 
 | ||||
|     private String lastLog; | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCaptureCompleted(@NonNull ActionHolder holder, @NonNull CaptureRequest request, | ||||
|                                    @NonNull TotalCaptureResult result) { | ||||
|         super.onCaptureCompleted(holder, request, result); | ||||
|         Integer aeMode = result.get(CaptureResult.CONTROL_AE_MODE); | ||||
|         Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); | ||||
|         Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); | ||||
|         Boolean aeLock = result.get(CaptureResult.CONTROL_AE_LOCK); | ||||
|         Integer aeTriggerState = result.get(CaptureResult.CONTROL_AE_PRECAPTURE_TRIGGER); | ||||
|         Integer afTriggerState = result.get(CaptureResult.CONTROL_AF_TRIGGER); | ||||
|         String log = "aeMode: " + aeMode + " aeLock: " + aeLock + | ||||
|                 " aeState: " + aeState + " aeTriggerState: " + aeTriggerState + | ||||
|                 " afState: " + afState + " afTriggerState: " + afTriggerState; | ||||
|         if (!log.equals(lastLog)) { | ||||
|             lastLog = log; | ||||
|             LOG.w(log); | ||||
|         } | ||||
| 
 | ||||
|         // START
 | ||||
|         // aeMode: 3 aeLock: false aeState: 4 aeTriggerState: 0 afState: 2 afTriggerState: 0
 | ||||
|         //
 | ||||
|         // DURING metering (focus skips)
 | ||||
|         // aeMode: 3 aeLock: false aeState: 4 aeTriggerState: 0 afState: 0 afTriggerState: 0
 | ||||
|         // aeMode: 3 aeLock: false aeState: 5 aeTriggerState: 1 afState: 0 afTriggerState: 0
 | ||||
|         //
 | ||||
|         // DURING locking (focus skips)
 | ||||
|         // aeMode: 3 aeLock: false aeState: 4 aeTriggerState: 1 afState: 0 afTriggerState: 0
 | ||||
|         // aeMode: 3 aeLock: true aeState: 5 aeTriggerState: 1 afState: 0 afTriggerState: 0
 | ||||
|         //
 | ||||
|         // AFTER locked
 | ||||
|         // aeMode: 3 aeLock: true aeState: 3 aeTriggerState: 1 afState: 0 afTriggerState: 0
 | ||||
|         //
 | ||||
|         // AFTER super.take() called
 | ||||
|         // aeMode: 1 aeLock: true aeState: 5 aeTriggerState: 1 afState: 0 afTriggerState: 0
 | ||||
|         // aeMode: 1 aeLock: true aeState: 3 aeTriggerState: 1 afState: 0 afTriggerState: 0
 | ||||
|         //
 | ||||
|         // Reverting flash changes + reset lock + reset metering
 | ||||
|         // aeMode: 3 aeLock: false aeState: 4 aeTriggerState: 2(1 now) afState: 2 afTriggerState: 0
 | ||||
|         // aeMode: 3 aeLock: false aeState: 1 aeTriggerState: 2(1 now) afState: 2 afTriggerState: 0
 | ||||
|     } | ||||
| } | ||||
| @ -1,380 +0,0 @@ | ||||
| package com.otaliastudios.cameraview.engine; | ||||
| 
 | ||||
| import android.graphics.PointF; | ||||
| import android.graphics.Rect; | ||||
| import android.hardware.camera2.CameraCharacteristics; | ||||
| import android.hardware.camera2.CaptureRequest; | ||||
| import android.hardware.camera2.CaptureResult; | ||||
| import android.hardware.camera2.TotalCaptureResult; | ||||
| import android.hardware.camera2.params.MeteringRectangle; | ||||
| import android.os.Build; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.annotation.RequiresApi; | ||||
| 
 | ||||
| import com.otaliastudios.cameraview.CameraLogger; | ||||
| import com.otaliastudios.cameraview.engine.metering.AutoExposure; | ||||
| import com.otaliastudios.cameraview.engine.metering.AutoFocus; | ||||
| import com.otaliastudios.cameraview.engine.metering.AutoWhiteBalance; | ||||
| import com.otaliastudios.cameraview.engine.metering.MeteringParameter; | ||||
| import com.otaliastudios.cameraview.engine.offset.Axis; | ||||
| import com.otaliastudios.cameraview.engine.offset.Reference; | ||||
| import com.otaliastudios.cameraview.gesture.Gesture; | ||||
| import com.otaliastudios.cameraview.size.AspectRatio; | ||||
| import com.otaliastudios.cameraview.size.Size; | ||||
| 
 | ||||
| import java.util.Arrays; | ||||
| import java.util.List; | ||||
| 
 | ||||
| /** | ||||
|  * Helps Camera2-based engines to perform 3A (auto focus, auto exposure and auto white balance) | ||||
|  * metering. Users are required to: | ||||
|  * | ||||
|  * - Call {@link #startMetering(PointF, Gesture)} to start | ||||
|  * - Call {@link #onCapture(CaptureResult)} when they have partial or total results, as long as the | ||||
|  *   meter is still in a metering operation, which can be checked through {@link #isMetering()} | ||||
|  * - Call {@link #resetMetering()} to reset the metering parameters if needed. This is done automatically | ||||
|  *   by the meter based on the reset delay configuration in the engine, but can be called explicitly | ||||
|  *   for example when we have multiple meter requests and want to cancel the old one. | ||||
|  */ | ||||
| @RequiresApi(Build.VERSION_CODES.LOLLIPOP) | ||||
| public class Meter { | ||||
| 
 | ||||
|     /** | ||||
|      * The meter callback. | ||||
|      */ | ||||
|     public interface Callback { | ||||
| 
 | ||||
|         /** | ||||
|          * Notifies that metering has started. At this point implementors should apply | ||||
|          * the builder onto the preview. | ||||
|          * @param point point | ||||
|          * @param gesture gesture | ||||
|          */ | ||||
|         void onMeteringStarted(@NonNull PointF point, @Nullable Gesture gesture); | ||||
| 
 | ||||
|         /** | ||||
|          * Notifies that metering has ended. No action is required for implementors. | ||||
|          * From now on, {@link #isMetering()} will return false so the meter should not | ||||
|          * be passed capture results anymore. | ||||
|          * @param point point | ||||
|          * @param gesture gesture | ||||
|          * @param success success | ||||
|          */ | ||||
|         void onMeteringEnd(@NonNull PointF point, @Nullable Gesture gesture, boolean success); | ||||
| 
 | ||||
|         /** | ||||
|          * Notifies that metering has been reset. From now on, this meter instance | ||||
|          * is done, although in theory it could be reused by calling | ||||
|          * {@link #startMetering(PointF, Gesture)} again. | ||||
|          * @param point point | ||||
|          * @param gesture gesture | ||||
|          */ | ||||
|         void onMeteringReset(@NonNull PointF point, @Nullable Gesture gesture); | ||||
| 
 | ||||
|         /** | ||||
|          * Whether metering can be reset. Since it happens at a future time, this should | ||||
|          * return true if the engine is still in a legit state for this operation. | ||||
|          * @param point point | ||||
|          * @param gesture gesture | ||||
|          * @return true if can reset | ||||
|          */ | ||||
|         // TODO is this useful? engine could do its checks onMeteringReset()
 | ||||
|         boolean canResetMetering(@NonNull PointF point, @Nullable Gesture gesture); | ||||
|     } | ||||
| 
 | ||||
|     private static final String TAG = Meter.class.getSimpleName(); | ||||
|     private static final CameraLogger LOG = CameraLogger.create(TAG); | ||||
|     private static final int FORCED_END_DELAY = 2500; | ||||
| 
 | ||||
|     private final CameraEngine mEngine; | ||||
|     private final CaptureRequest.Builder mBuilder; | ||||
|     private final CameraCharacteristics mCharacteristics; | ||||
|     private final Callback mCallback; | ||||
|     private PointF mPoint; | ||||
|     private Gesture mGesture; | ||||
| 
 | ||||
|     private boolean mIsMetering; | ||||
|     private long mMeteringStartTime; | ||||
|     private MeteringParameter mAutoFocus = new AutoFocus(); | ||||
|     private MeteringParameter mAutoWhiteBalance = new AutoWhiteBalance(); | ||||
|     private MeteringParameter mAutoExposure = new AutoExposure(); | ||||
| 
 | ||||
|     /** | ||||
|      * Creates a new meter. | ||||
|      * @param engine the engine | ||||
|      * @param builder a capture builder | ||||
|      * @param characteristics the camera characteristics | ||||
|      * @param callback the callback | ||||
|      */ | ||||
|     @SuppressWarnings("WeakerAccess") | ||||
|     public Meter(@NonNull CameraEngine engine, | ||||
|                  @NonNull CaptureRequest.Builder builder, | ||||
|                  @NonNull CameraCharacteristics characteristics, | ||||
|                  @NonNull Callback callback) { | ||||
|         mEngine = engine; | ||||
|         mBuilder = builder; | ||||
|         mCharacteristics = characteristics; | ||||
|         mCallback = callback; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Starts a metering sequence. | ||||
|      * @param point point | ||||
|      * @param gesture gesture | ||||
|      */ | ||||
|     @SuppressWarnings("WeakerAccess") | ||||
|     public void startMetering(@NonNull PointF point, @Nullable Gesture gesture) { | ||||
|         mPoint = point; | ||||
|         mGesture = gesture; | ||||
|         mIsMetering = true; | ||||
| 
 | ||||
|         // This is a good Q/A. https://stackoverflow.com/a/33181620/4288782
 | ||||
|         // At first, the point is relative to the View system and does not account our own cropping.
 | ||||
|         // Will keep updating these two below.
 | ||||
|         final PointF referencePoint = new PointF(mPoint.x, mPoint.y); | ||||
|         Size referenceSize = mEngine.mPreview.getSurfaceSize(); | ||||
| 
 | ||||
|         // 1. Account for cropping.
 | ||||
|         // This will enlarge the preview size so that aspect ratio matches.
 | ||||
|         referenceSize = applyPreviewCropping(referenceSize, referencePoint); | ||||
| 
 | ||||
|         // 2. Scale to the preview stream coordinates.
 | ||||
|         // This will move to the preview stream coordinates by scaling.
 | ||||
|         referenceSize = applyPreviewScale(referenceSize, referencePoint); | ||||
| 
 | ||||
|         // 3. Rotate to the stream coordinate system.
 | ||||
|         // This leaves us with sensor stream coordinates.
 | ||||
|         referenceSize = applyPreviewToSensorRotation(referenceSize, referencePoint); | ||||
| 
 | ||||
|         // 4. Move to the crop region coordinate system.
 | ||||
|         // The crop region is the union of all currently active streams.
 | ||||
|         referenceSize = applyCropRegionCoordinates(referenceSize, referencePoint); | ||||
| 
 | ||||
|         // 5. Move to the active array coordinate system.
 | ||||
|         referenceSize = applyActiveArrayCoordinates(referenceSize, referencePoint); | ||||
| 
 | ||||
|         // 6. Now we can compute the metering regions.
 | ||||
|         // We want to define them as a fraction of the visible size which (apart from cropping)
 | ||||
|         // can be obtained through the SENSOR rotated preview stream size.
 | ||||
|         Size visibleSize = mEngine.getPreviewStreamSize(Reference.SENSOR); | ||||
|         //noinspection ConstantConditions
 | ||||
|         MeteringRectangle area1 = createMeteringRectangle(referenceSize, referencePoint, visibleSize, 0.05F, 1000); | ||||
|         MeteringRectangle area2 = createMeteringRectangle(referenceSize, referencePoint, visibleSize, 0.1F, 100); | ||||
|         List<MeteringRectangle> areas = Arrays.asList(area1, area2); | ||||
| 
 | ||||
|         // 7. And finally dispatch everything
 | ||||
|         mAutoFocus.startMetering(mCharacteristics, mBuilder, areas); | ||||
|         mAutoWhiteBalance.startMetering(mCharacteristics, mBuilder, areas); | ||||
|         mAutoExposure.startMetering(mCharacteristics, mBuilder, areas); | ||||
| 
 | ||||
|         // Dispatch to callback
 | ||||
|         mCallback.onMeteringStarted(mPoint, mGesture); | ||||
|         mMeteringStartTime = System.currentTimeMillis(); | ||||
|     } | ||||
| 
 | ||||
|     @SuppressWarnings("UnnecessaryLocalVariable") | ||||
|     @NonNull | ||||
|     private Size applyPreviewCropping(@NonNull Size referenceSize, @NonNull PointF referencePoint) { | ||||
|         Size previewStreamSize = mEngine.getPreviewStreamSize(Reference.VIEW); | ||||
|         Size previewSurfaceSize = referenceSize; | ||||
|         if (previewStreamSize == null) { | ||||
|             throw new IllegalStateException("getPreviewStreamSize should not be null at this point."); | ||||
|         } | ||||
|         int referenceWidth = previewSurfaceSize.getWidth(); | ||||
|         int referenceHeight = previewSurfaceSize.getHeight(); | ||||
|         AspectRatio previewStreamAspectRatio = AspectRatio.of(previewStreamSize); | ||||
|         AspectRatio previewSurfaceAspectRatio = AspectRatio.of(previewSurfaceSize); | ||||
|         if (mEngine.mPreview.isCropping()) { | ||||
|             if (previewStreamAspectRatio.toFloat() > previewSurfaceAspectRatio.toFloat()) { | ||||
|                 // Stream is larger. The x coordinate must be increased: a touch on the left side
 | ||||
|                 // of the surface is not on the left size of stream (it's more to the right).
 | ||||
|                 float scale = previewStreamAspectRatio.toFloat() / previewSurfaceAspectRatio.toFloat(); | ||||
|                 referencePoint.x += previewSurfaceSize.getWidth() * (scale - 1F) / 2F; | ||||
|                 referenceWidth = Math.round(previewSurfaceSize.getWidth() * scale); | ||||
|             } else { | ||||
|                 // Stream is taller. The y coordinate must be increased: a touch on the top side
 | ||||
|                 // of the surface is not on the top size of stream (it's a bit lower).
 | ||||
|                 float scale = previewSurfaceAspectRatio.toFloat() / previewStreamAspectRatio.toFloat(); | ||||
|                 referencePoint.y += previewSurfaceSize.getHeight() * (scale - 1F) / 2F; | ||||
|                 referenceHeight = Math.round(previewSurfaceSize.getHeight() * scale); | ||||
|             } | ||||
|         } | ||||
|         return new Size(referenceWidth, referenceHeight); | ||||
|     } | ||||
| 
 | ||||
|     @SuppressWarnings("ConstantConditions") | ||||
|     @NonNull | ||||
|     private Size applyPreviewScale(@NonNull Size referenceSize, @NonNull PointF referencePoint) { | ||||
|         // The referenceSize how has the same aspect ratio of the previewStreamSize, but they
 | ||||
|         // can still have different size (that is, a scale operation is needed).
 | ||||
|         Size previewStreamSize = mEngine.getPreviewStreamSize(Reference.VIEW); | ||||
|         referencePoint.x *= (float) previewStreamSize.getWidth() / referenceSize.getWidth(); | ||||
|         referencePoint.y *= (float) previewStreamSize.getHeight() / referenceSize.getHeight(); | ||||
|         return previewStreamSize; | ||||
|     } | ||||
| 
 | ||||
|     @SuppressWarnings("SuspiciousNameCombination") | ||||
|     @NonNull | ||||
|     private Size applyPreviewToSensorRotation(@NonNull Size referenceSize, @NonNull PointF referencePoint) { | ||||
|         // Not elegant, but the sin/cos way was failing for some reason.
 | ||||
|         int angle = mEngine.getAngles().offset(Reference.SENSOR, Reference.VIEW, Axis.ABSOLUTE); | ||||
|         boolean flip = angle % 180 != 0; | ||||
|         float tempX = referencePoint.x; | ||||
|         float tempY = referencePoint.y; | ||||
|         if (angle == 0) { | ||||
|             referencePoint.x = tempX; | ||||
|             referencePoint.y = tempY; | ||||
|         } else if (angle == 90) { | ||||
|             referencePoint.x = tempY; | ||||
|             referencePoint.y = referenceSize.getWidth() - tempX; | ||||
|         } else if (angle == 180) { | ||||
|             referencePoint.x = referenceSize.getWidth() - tempX; | ||||
|             referencePoint.y = referenceSize.getHeight() - tempY; | ||||
|         } else if (angle == 270) { | ||||
|             referencePoint.x = referenceSize.getHeight() - tempY; | ||||
|             referencePoint.y = tempX; | ||||
|         } else { | ||||
|             throw new IllegalStateException("Unexpected angle " + angle); | ||||
|         } | ||||
|         return flip ? referenceSize.flip() : referenceSize; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     private Size applyCropRegionCoordinates(@NonNull Size referenceSize, @NonNull PointF referencePoint) { | ||||
|         // The input point and size refer to the stream rect.
 | ||||
|         // The stream rect is part of the 'crop region', as described below.
 | ||||
|         // https://source.android.com/devices/camera/camera3_crop_reprocess.html
 | ||||
|         Rect cropRect = mBuilder.get(CaptureRequest.SCALER_CROP_REGION); | ||||
|         // For now, we don't care about x and y position. Rect should be non-null, but let's be safe.
 | ||||
|         int cropRectWidth = cropRect == null ? referenceSize.getWidth() : cropRect.width(); | ||||
|         int cropRectHeight = cropRect == null ? referenceSize.getHeight() : cropRect.height(); | ||||
|         // The stream is always centered inside the crop region, and one of the dimensions
 | ||||
|         // should always match. We just increase the other one.
 | ||||
|         referencePoint.x += (cropRectWidth - referenceSize.getWidth()) / 2F; | ||||
|         referencePoint.y += (cropRectHeight - referenceSize.getHeight()) / 2F; | ||||
|         return new Size(cropRectWidth, cropRectHeight); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     private Size applyActiveArrayCoordinates(@NonNull Size referenceSize, @NonNull PointF referencePoint) { | ||||
|         // The input point and size refer to the scaler crop region.
 | ||||
|         // We can query for the crop region position inside the active array, so this is easy.
 | ||||
|         Rect cropRect = mBuilder.get(CaptureRequest.SCALER_CROP_REGION); | ||||
|         referencePoint.x += cropRect == null ? 0 : cropRect.left; | ||||
|         referencePoint.y += cropRect == null ? 0 : cropRect.top; | ||||
|         // Finally, get the active rect width and height from characteristics.
 | ||||
|         Rect activeRect = mCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); | ||||
|         if (activeRect == null) { // Should never happen
 | ||||
|             activeRect = new Rect(0, 0, referenceSize.getWidth(), referenceSize.getHeight()); | ||||
|         } | ||||
|         return new Size(activeRect.width(), activeRect.height()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Creates a metering rectangle around the center point. | ||||
|      * The rectangle will have a size that's a factor of the visible width and height. | ||||
|      * The rectangle will also be constrained to be inside the given boundaries, | ||||
|      * so we don't exceed them in case the center point is exactly on one side for example. | ||||
|      * @return a new rectangle | ||||
|      */ | ||||
|     @NonNull | ||||
|     private MeteringRectangle createMeteringRectangle( | ||||
|             @NonNull Size boundaries, | ||||
|             @NonNull PointF center, | ||||
|             @NonNull Size visibleSize, | ||||
|             float factor, | ||||
|             int weight) { | ||||
|         float rectangleWidth = factor * visibleSize.getWidth(); | ||||
|         float rectangleHeight = factor * visibleSize.getHeight(); | ||||
|         float rectangleLeft = center.x - rectangleWidth / 2F; | ||||
|         float rectangleTop = center.y - rectangleHeight / 2F; | ||||
|         // Respect boundaries
 | ||||
|         if (rectangleLeft < 0) rectangleLeft = 0; | ||||
|         if (rectangleTop < 0) rectangleTop = 0; | ||||
|         if (rectangleLeft + rectangleWidth > boundaries.getWidth()) { | ||||
|             rectangleWidth = boundaries.getWidth() - rectangleLeft; | ||||
|         } | ||||
|         if (rectangleTop + rectangleHeight > boundaries.getHeight()) { | ||||
|             rectangleHeight = boundaries.getHeight() - rectangleTop; | ||||
|         } | ||||
|         return new MeteringRectangle( | ||||
|                 (int) rectangleLeft, | ||||
|                 (int) rectangleTop, | ||||
|                 (int) rectangleWidth, | ||||
|                 (int) rectangleHeight, | ||||
|                 weight | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * True if we're metering. False if we're not, for example if we're waiting for | ||||
|      * a reset call, or if {@link #startMetering(PointF, Gesture)} was never called. | ||||
|      * @return true if metering | ||||
|      */ | ||||
|     @SuppressWarnings("WeakerAccess") | ||||
|     public boolean isMetering() { | ||||
|         return mIsMetering; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Should be called when we have partial or total CaptureResults, | ||||
|      * but only while {@link #isMetering()} returns true. | ||||
|      * @param result result | ||||
|      */ | ||||
|     @SuppressWarnings("WeakerAccess") | ||||
|     public void onCapture(@NonNull CaptureResult result) { | ||||
|         if (!mIsMetering) return; // We're not interested in results anymore
 | ||||
|         if (!(result instanceof TotalCaptureResult)) return; // Let's ignore these, contents are missing/wrong
 | ||||
|          | ||||
|         if (!mAutoFocus.isMetered()) mAutoFocus.onCapture(result); | ||||
|         if (!mAutoExposure.isMetered()) mAutoExposure.onCapture(result); | ||||
|         if (!mAutoWhiteBalance.isMetered()) mAutoWhiteBalance.onCapture(result); | ||||
|         if (mAutoFocus.isMetered() && mAutoExposure.isMetered() && mAutoWhiteBalance.isMetered()) { | ||||
|             LOG.i("onCapture:", "all MeteringParameters have converged. Dispatching onMeteringEnd"); | ||||
|             boolean success = mAutoFocus.isSuccessful() | ||||
|                     && mAutoExposure.isSuccessful() | ||||
|                     && mAutoWhiteBalance.isSuccessful(); | ||||
|             onMeteringEnd(success); | ||||
|         } else if (System.currentTimeMillis() - mMeteringStartTime >= FORCED_END_DELAY) { | ||||
|             LOG.i("onCapture:", "FORCED_END_DELAY was reached. Some MeteringParameter is stuck. Forcing end."); | ||||
|             onMeteringEnd(false); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private void onMeteringEnd(boolean success) { | ||||
|         mCallback.onMeteringEnd(mPoint, mGesture, success); | ||||
|         mIsMetering = false; | ||||
|         mEngine.mHandler.remove(mResetRunnable); | ||||
|         if (mEngine.shouldResetAutoFocus()) { | ||||
|             mEngine.mHandler.post(mEngine.getAutoFocusResetDelay(), mResetRunnable); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Can be called to perform the reset at a time different than the one | ||||
|      * specified by the {@link CameraEngine} reset delay. | ||||
|      */ | ||||
|     @SuppressWarnings("WeakerAccess") | ||||
|     public void resetMetering() { | ||||
|         mEngine.mHandler.remove(mResetRunnable); | ||||
|         if (mCallback.canResetMetering(mPoint, mGesture)) { | ||||
|             LOG.i("Resetting the meter parameters."); | ||||
|             Rect whole = mCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); | ||||
|             if (whole == null) whole = new Rect(); | ||||
|             MeteringRectangle rectangle = new MeteringRectangle(whole, MeteringRectangle.METERING_WEIGHT_DONT_CARE); | ||||
|             mAutoFocus.resetMetering(mCharacteristics, mBuilder, rectangle); | ||||
|             mAutoWhiteBalance.resetMetering(mCharacteristics, mBuilder, rectangle); | ||||
|             mAutoExposure.resetMetering(mCharacteristics, mBuilder, rectangle); | ||||
|             mCallback.onMeteringReset(mPoint, mGesture); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private Runnable mResetRunnable = new Runnable() { | ||||
|         @Override | ||||
|         public void run() { | ||||
|             resetMetering(); | ||||
|         } | ||||
|     }; | ||||
| } | ||||
| @ -0,0 +1,85 @@ | ||||
| package com.otaliastudios.cameraview.engine.action; | ||||
| 
 | ||||
| import android.hardware.camera2.CameraCaptureSession; | ||||
| import android.hardware.camera2.CameraCaptureSession.CaptureCallback; | ||||
| import android.hardware.camera2.CaptureRequest; | ||||
| import android.hardware.camera2.CaptureResult; | ||||
| import android.hardware.camera2.TotalCaptureResult; | ||||
| import android.os.Build; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.RequiresApi; | ||||
| 
 | ||||
| /** | ||||
|  * The Action class encapsulates logic for completing an action in a Camera2 environment. | ||||
|  * In this case, we are often interested in constantly receiving the {@link CaptureResult} | ||||
|  * and {@link CaptureRequest} callbacks, as well as applying changes to a {@link CaptureRequest.Builder} | ||||
|  * and having them applied to the sensor. | ||||
|  * | ||||
|  * The Action class receives the given callbacks and can operate over the engine | ||||
|  * through the {@link ActionHolder} object. | ||||
|  * | ||||
|  * Each Action operates on a given state in a given moment. This base class offers the | ||||
|  * {@link #STATE_COMPLETED} state which is common to all actions. | ||||
|  * | ||||
|  * See {@link BaseAction} for a base implementation. | ||||
|  */ | ||||
| @RequiresApi(Build.VERSION_CODES.LOLLIPOP) | ||||
| public interface Action { | ||||
| 
 | ||||
|     int STATE_COMPLETED = Integer.MAX_VALUE; | ||||
| 
 | ||||
|     /** | ||||
|      * Returns the current state. | ||||
|      * @return the state | ||||
|      */ | ||||
|     int getState(); | ||||
| 
 | ||||
|     /** | ||||
|      * Starts this action. | ||||
|      * @param holder the holder | ||||
|      */ | ||||
|     void start(@NonNull ActionHolder holder); | ||||
| 
 | ||||
|     /** | ||||
|      * Aborts this action. | ||||
|      * @param holder the holder | ||||
|      */ | ||||
|     void abort(@NonNull ActionHolder holder); | ||||
| 
 | ||||
|     /** | ||||
|      * Adds an {@link ActionCallback} to receive state | ||||
|      * change events. | ||||
|      * @param callback a callback | ||||
|      */ | ||||
|     void addCallback(@NonNull ActionCallback callback); | ||||
| 
 | ||||
|     /** | ||||
|      * Removes a previously added callback. | ||||
|      * @param callback a callback | ||||
|      */ | ||||
|     void removeCallback(@NonNull ActionCallback callback); | ||||
| 
 | ||||
|     /** | ||||
|      * Called from {@link CaptureCallback#onCaptureStarted(CameraCaptureSession, CaptureRequest, long, long)}. | ||||
|      * @param holder the holder | ||||
|      * @param request the request | ||||
|      */ | ||||
|     void onCaptureStarted(@NonNull ActionHolder holder, @NonNull CaptureRequest request); | ||||
| 
 | ||||
|     /** | ||||
|      * Called from {@link CaptureCallback#onCaptureProgressed(CameraCaptureSession, CaptureRequest, CaptureResult)}. | ||||
|      * @param holder the holder | ||||
|      * @param request the request | ||||
|      * @param result the result | ||||
|      */ | ||||
|     void onCaptureProgressed(@NonNull ActionHolder holder, @NonNull CaptureRequest request, @NonNull CaptureResult result); | ||||
| 
 | ||||
|     /** | ||||
|      * Called from {@link CaptureCallback#onCaptureCompleted(CameraCaptureSession, CaptureRequest, TotalCaptureResult)}. | ||||
|      * @param holder the holder | ||||
|      * @param request the request | ||||
|      * @param result the result | ||||
|      */ | ||||
|     void onCaptureCompleted(@NonNull ActionHolder holder, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result); | ||||
| } | ||||
| @ -0,0 +1,23 @@ | ||||
| package com.otaliastudios.cameraview.engine.action; | ||||
| 
 | ||||
| import android.os.Build; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.RequiresApi; | ||||
| 
 | ||||
| /** | ||||
|  * A callback for {@link Action} state changes. | ||||
|  * See the action class. | ||||
|  * | ||||
|  * See also {@link CompletionCallback}. | ||||
|  */ | ||||
| @RequiresApi(Build.VERSION_CODES.LOLLIPOP) | ||||
| public interface ActionCallback { | ||||
| 
 | ||||
|     /** | ||||
|      * Action state has just changed. | ||||
|      * @param action action | ||||
|      * @param state new state | ||||
|      */ | ||||
|     void onActionStateChanged(@NonNull Action action, int state); | ||||
| } | ||||
| @ -0,0 +1,81 @@ | ||||
| package com.otaliastudios.cameraview.engine.action; | ||||
| 
 | ||||
| import android.hardware.camera2.CameraAccessException; | ||||
| import android.hardware.camera2.CameraCharacteristics; | ||||
| import android.hardware.camera2.CaptureRequest; | ||||
| import android.hardware.camera2.CaptureResult; | ||||
| import android.hardware.camera2.TotalCaptureResult; | ||||
| import android.os.Build; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.RequiresApi; | ||||
| 
 | ||||
| /** | ||||
|  * The holder of {@link Action}. | ||||
|  * | ||||
|  * This class should keep a list or set of currently running actions, and offers | ||||
|  * to them the base Camera2 objects that are needed to apply changes. | ||||
|  * | ||||
|  * This class, or an holder of it, should also forward the capture callbacks | ||||
|  * to all {@link Action}s. See {@link com.otaliastudios.cameraview.engine.Camera2Engine} for | ||||
|  * our implementation. | ||||
|  */ | ||||
| @RequiresApi(Build.VERSION_CODES.LOLLIPOP) | ||||
| public interface ActionHolder { | ||||
| 
 | ||||
|     /** | ||||
|      * Adds a new action | ||||
|      * @param action action | ||||
|      */ | ||||
|     void addAction(@NonNull Action action); | ||||
| 
 | ||||
|     /** | ||||
|      * Removes a previously added action | ||||
|      * @param action action | ||||
|      */ | ||||
|     void removeAction(@NonNull Action action); | ||||
| 
 | ||||
|     /** | ||||
|      * Returns the {@link CameraCharacteristics} of the current | ||||
|      * camera device. | ||||
|      * @param action action | ||||
|      * @return characteristics | ||||
|      */ | ||||
|     @NonNull | ||||
|     CameraCharacteristics getCharacteristics(@NonNull Action action); | ||||
| 
 | ||||
|     /** | ||||
|      * Returns the latest {@link TotalCaptureResult}. Can be used | ||||
|      * by actions to start querying the state before receiving their | ||||
|      * first frame. | ||||
|      * @param action action | ||||
|      * @return last result | ||||
|      */ | ||||
|     @NonNull | ||||
|     TotalCaptureResult getLastResult(@NonNull Action action); | ||||
| 
 | ||||
|     /** | ||||
|      * Returns the current {@link CaptureRequest.Builder} so that | ||||
|      * actions can apply changes to it and later submit them. | ||||
|      * @param action action | ||||
|      * @return the builder | ||||
|      */ | ||||
|     @NonNull | ||||
|     CaptureRequest.Builder getBuilder(@NonNull Action action); | ||||
| 
 | ||||
|     /** | ||||
|      * Applies the current builder (as per {@link #getBuilder(Action)}) | ||||
|      * as a repeating request on the preview. | ||||
|      * @param source action | ||||
|      */ | ||||
|     void applyBuilder(@NonNull Action source); | ||||
| 
 | ||||
|     /** | ||||
|      * Applies the given builder as a single capture request. | ||||
|      * Callers can catch the exception and choose what to do. | ||||
|      * @param source action | ||||
|      * @param builder builder | ||||
|      * @throws CameraAccessException camera exception | ||||
|      */ | ||||
|     void applyBuilder(@NonNull Action source, @NonNull CaptureRequest.Builder builder) throws CameraAccessException; | ||||
| } | ||||
| @ -0,0 +1,63 @@ | ||||
| package com.otaliastudios.cameraview.engine.action; | ||||
| 
 | ||||
| import android.hardware.camera2.CaptureRequest; | ||||
| import android.hardware.camera2.CaptureResult; | ||||
| import android.hardware.camera2.TotalCaptureResult; | ||||
| import android.os.Build; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.RequiresApi; | ||||
| 
 | ||||
| /** | ||||
|  * A simple wrapper around a {@link BaseAction}. | ||||
|  * This can be used to add functionality around a base action. | ||||
|  */ | ||||
| @RequiresApi(Build.VERSION_CODES.LOLLIPOP) | ||||
| public abstract class ActionWrapper extends BaseAction { | ||||
| 
 | ||||
|     /** | ||||
|      * Should return the wrapped action. | ||||
|      * @return the wrapped action | ||||
|      */ | ||||
|     @NonNull | ||||
|     public abstract BaseAction getAction(); | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onStart(@NonNull ActionHolder holder) { | ||||
|         super.onStart(holder); | ||||
|         getAction().addCallback(new ActionCallback() { | ||||
|             @Override | ||||
|             public void onActionStateChanged(@NonNull Action action, int state) { | ||||
|                 setState(state); | ||||
|                 if (state == STATE_COMPLETED) { | ||||
|                     action.removeCallback(this); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|         getAction().onStart(holder); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onAbort(@NonNull ActionHolder holder) { | ||||
|         super.onAbort(holder); | ||||
|         getAction().onAbort(holder); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCaptureStarted(@NonNull ActionHolder holder, @NonNull CaptureRequest request) { | ||||
|         super.onCaptureStarted(holder, request); | ||||
|         getAction().onCaptureStarted(holder, request); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCaptureProgressed(@NonNull ActionHolder holder, @NonNull CaptureRequest request, @NonNull CaptureResult result) { | ||||
|         super.onCaptureProgressed(holder, request, result); | ||||
|         getAction().onCaptureProgressed(holder, request, result); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCaptureCompleted(@NonNull ActionHolder holder, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { | ||||
|         super.onCaptureCompleted(holder, request, result); | ||||
|         getAction().onCaptureCompleted(holder, request, result); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,56 @@ | ||||
| package com.otaliastudios.cameraview.engine.action; | ||||
| 
 | ||||
| import android.os.Build; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.RequiresApi; | ||||
| 
 | ||||
| import java.util.Arrays; | ||||
| 
 | ||||
| /** | ||||
|  * Utilities for creating {@link Action} sequences. | ||||
|  */ | ||||
| @RequiresApi(Build.VERSION_CODES.LOLLIPOP) | ||||
| public class Actions { | ||||
| 
 | ||||
|     /** | ||||
|      * Creates a {@link BaseAction} that executes all the child actions | ||||
|      * together, at the same time, and completes once all of them are | ||||
|      * completed. | ||||
|      * | ||||
|      * @param actions input actions | ||||
|      * @return a new action | ||||
|      */ | ||||
|     @NonNull | ||||
|     public static BaseAction together(@NonNull BaseAction... actions) { | ||||
|         return new TogetherAction(Arrays.asList(actions)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Creates a {@link BaseAction} that executes all the child actions | ||||
|      * in sequence, waiting for the first to complete, then going on with | ||||
|      * the second and so on, finally completing when all are completed. | ||||
|      * | ||||
|      * @param actions input actions | ||||
|      * @return a new action | ||||
|      */ | ||||
|     @NonNull | ||||
|     public static BaseAction sequence(@NonNull BaseAction... actions) { | ||||
|         return new SequenceAction(Arrays.asList(actions)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Creates a {@link BaseAction} that completes as normal, but is also | ||||
|      * forced to complete if the given timeout is reached, by calling | ||||
|      * {@link Action#abort(ActionHolder)}. | ||||
|      * | ||||
|      * @param timeoutMillis timeout in milliseconds | ||||
|      * @param action action | ||||
|      * @return a new action | ||||
|      */ | ||||
|     @NonNull | ||||
|     public static BaseAction timeout(long timeoutMillis, @NonNull BaseAction action) { | ||||
|         return new TimeoutAction(timeoutMillis, action); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -0,0 +1,160 @@ | ||||
| package com.otaliastudios.cameraview.engine.action; | ||||
| 
 | ||||
| import android.hardware.camera2.CameraCharacteristics; | ||||
| import android.hardware.camera2.CaptureRequest; | ||||
| import android.hardware.camera2.CaptureResult; | ||||
| import android.hardware.camera2.TotalCaptureResult; | ||||
| import android.os.Build; | ||||
| 
 | ||||
| import androidx.annotation.CallSuper; | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.RequiresApi; | ||||
| 
 | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
| 
 | ||||
| /** | ||||
|  * The base implementation of {@link Action} that should always be subclassed, | ||||
|  * instead of implementing the root interface itself. | ||||
|  * | ||||
|  * It holds a list of callbacks and dispatches events to them, plus it cares about | ||||
|  * its own lifecycle: | ||||
|  * - when {@link #start(ActionHolder)} is called, we add ourselves to the holder list | ||||
|  * - when {@link #STATE_COMPLETED} is reached, we remove ouverselves from the holder list | ||||
|  * | ||||
|  * This is very important in all cases. | ||||
|  */ | ||||
| @RequiresApi(Build.VERSION_CODES.LOLLIPOP) | ||||
| public abstract class BaseAction implements Action { | ||||
| 
 | ||||
|     private final List<ActionCallback> callbacks = new ArrayList<>(); | ||||
|     private int state; | ||||
|     private ActionHolder holder; | ||||
| 
 | ||||
|     @Override | ||||
|     public final int getState() { | ||||
|         return state; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public final void start(@NonNull ActionHolder holder) { | ||||
|         holder.addAction(this); | ||||
|         onStart(holder); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public final void abort(@NonNull ActionHolder holder) { | ||||
|         holder.removeAction(this); | ||||
|         if (!isCompleted()) { | ||||
|             onAbort(holder); | ||||
|             setState(STATE_COMPLETED); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Action was started and will soon receive events from the | ||||
|      * holder stream. | ||||
|      * @param holder holder | ||||
|      */ | ||||
|     @CallSuper | ||||
|     protected void onStart(@NonNull ActionHolder holder) { | ||||
|         this.holder = holder; // must be here
 | ||||
|         // Overrideable
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Action was aborted and will not receive events from the | ||||
|      * holder stream anymore. It will soon be marked as completed. | ||||
|      * @param holder holder | ||||
|      */ | ||||
|     @SuppressWarnings("unused") | ||||
|     protected void onAbort(@NonNull ActionHolder holder) { | ||||
|         // Overrideable
 | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCaptureStarted(@NonNull ActionHolder holder, @NonNull CaptureRequest request) { | ||||
|         // Overrideable
 | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCaptureProgressed(@NonNull ActionHolder holder, @NonNull CaptureRequest request, @NonNull CaptureResult result) { | ||||
|         // Overrideable
 | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCaptureCompleted(@NonNull ActionHolder holder, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { | ||||
|         // Overrideable
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Called by subclasses to notify of their state. If state is {@link #STATE_COMPLETED}, | ||||
|      * this removes this action from the holder. | ||||
|      * @param newState new state | ||||
|      */ | ||||
|     protected void setState(int newState) { | ||||
|         if (newState != state) { | ||||
|             state = newState; | ||||
|             for (ActionCallback callback : callbacks) { | ||||
|                 callback.onActionStateChanged(this, state); | ||||
|             } | ||||
|             if (state == STATE_COMPLETED) { | ||||
|                 holder.removeAction(this); | ||||
|                 onCompleted(holder); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Whether this action has reached the completed state. | ||||
|      * @return true if completed | ||||
|      */ | ||||
|     public boolean isCompleted() { | ||||
|         return state == STATE_COMPLETED; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Called when this action has completed (possibly aborted). | ||||
|      * @param holder holder | ||||
|      */ | ||||
|     protected void onCompleted(@NonNull ActionHolder holder) { | ||||
|         // Overrideable
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns the holder. | ||||
|      * @return the holder | ||||
|      */ | ||||
|     @NonNull | ||||
|     protected ActionHolder getHolder() { | ||||
|         return holder; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Reads a characteristic with a fallback. | ||||
|      * @param key key | ||||
|      * @param fallback fallback | ||||
|      * @param <T> key type | ||||
|      * @return value or fallback | ||||
|      */ | ||||
|     @NonNull | ||||
|     protected <T> T readCharacteristic(@NonNull CameraCharacteristics.Key<T> key, | ||||
|                                        @NonNull T fallback) { | ||||
|         T value = holder.getCharacteristics(this).get(key); | ||||
|         return value == null ? fallback : value; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void addCallback(@NonNull ActionCallback callback) { | ||||
|         if (!callbacks.contains(callback)) { | ||||
|             callbacks.add(callback); | ||||
|             callback.onActionStateChanged(this, getState()); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void removeCallback(@NonNull ActionCallback callback) { | ||||
|         callbacks.remove(callback); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,27 @@ | ||||
| package com.otaliastudios.cameraview.engine.action; | ||||
| 
 | ||||
| import android.os.Build; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.RequiresApi; | ||||
| 
 | ||||
| /** | ||||
|  * A special {@link ActionCallback} that just checks for the | ||||
|  * completed state. Handy as an inner anonymous class. | ||||
|  */ | ||||
| @RequiresApi(Build.VERSION_CODES.LOLLIPOP) | ||||
| public abstract class CompletionCallback implements ActionCallback { | ||||
| 
 | ||||
|     @Override | ||||
|     public final void onActionStateChanged(@NonNull Action action, int state) { | ||||
|         if (state == Action.STATE_COMPLETED) { | ||||
|             onActionCompleted(action); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * The given action has just reached the completed state. | ||||
|      * @param action action | ||||
|      */ | ||||
|     protected abstract void onActionCompleted(@NonNull Action action); | ||||
| } | ||||
| @ -0,0 +1,94 @@ | ||||
| package com.otaliastudios.cameraview.engine.action; | ||||
| 
 | ||||
| import android.hardware.camera2.CaptureRequest; | ||||
| import android.hardware.camera2.CaptureResult; | ||||
| import android.hardware.camera2.TotalCaptureResult; | ||||
| import android.os.Build; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.RequiresApi; | ||||
| 
 | ||||
| import java.util.List; | ||||
| 
 | ||||
| /** | ||||
|  * Executes a list of actions in sequence, completing once | ||||
|  * the last of them has been completed. | ||||
|  */ | ||||
| @RequiresApi(Build.VERSION_CODES.LOLLIPOP) | ||||
| class SequenceAction extends BaseAction { | ||||
|     // Need to be BaseAction so we can call onStart() instead of start()
 | ||||
|     private final List<BaseAction> actions; | ||||
|     private int runningAction = -1; | ||||
| 
 | ||||
|     SequenceAction(@NonNull List<BaseAction> actions) { | ||||
|         this.actions = actions; | ||||
|         increaseRunningAction(); | ||||
|     } | ||||
| 
 | ||||
|     private void increaseRunningAction() { | ||||
|         boolean first = runningAction == -1; | ||||
|         boolean last = runningAction == actions.size() - 1; | ||||
|         if (last) { | ||||
|             // This was the last action. We're done.
 | ||||
|             setState(STATE_COMPLETED); | ||||
|         } else { | ||||
|             runningAction++; | ||||
|             actions.get(runningAction).addCallback(new ActionCallback() { | ||||
|                 @Override | ||||
|                 public void onActionStateChanged(@NonNull Action action, int state) { | ||||
|                     if (state == STATE_COMPLETED) { | ||||
|                         action.removeCallback(this); | ||||
|                         increaseRunningAction(); | ||||
|                     } | ||||
|                 } | ||||
|             }); | ||||
|             if (!first) { | ||||
|                 actions.get(runningAction).onStart(getHolder()); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onStart(@NonNull ActionHolder holder) { | ||||
|         super.onStart(holder); | ||||
|         if (runningAction >= 0) { | ||||
|             actions.get(runningAction).onStart(holder); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onAbort(@NonNull ActionHolder holder) { | ||||
|         super.onAbort(holder); | ||||
|         if (runningAction >= 0) { | ||||
|             // Previous actions have been completed already.
 | ||||
|             // Future actions will never start. So this is OK.
 | ||||
|             actions.get(runningAction).onAbort(holder); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCaptureStarted(@NonNull ActionHolder holder, @NonNull CaptureRequest request) { | ||||
|         super.onCaptureStarted(holder, request); | ||||
|         if (runningAction >= 0) { | ||||
|             actions.get(runningAction).onCaptureStarted(holder, request); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCaptureProgressed(@NonNull ActionHolder holder, @NonNull CaptureRequest request, | ||||
|                                     @NonNull CaptureResult result) { | ||||
|         super.onCaptureProgressed(holder, request, result); | ||||
|         if (runningAction >= 0) { | ||||
|             actions.get(runningAction).onCaptureProgressed(holder, request, result); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCaptureCompleted(@NonNull ActionHolder holder, @NonNull CaptureRequest request, | ||||
|                                    @NonNull TotalCaptureResult result) { | ||||
|         super.onCaptureCompleted(holder, request, result); | ||||
|         if (runningAction >= 0) { | ||||
|             actions.get(runningAction).onCaptureCompleted(holder, request, result); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,50 @@ | ||||
| package com.otaliastudios.cameraview.engine.action; | ||||
| 
 | ||||
| import android.hardware.camera2.CaptureRequest; | ||||
| import android.hardware.camera2.TotalCaptureResult; | ||||
| import android.os.Build; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.RequiresApi; | ||||
| 
 | ||||
| /** | ||||
|  * An {@link Action} that wraps another, and forces the completion | ||||
|  * after the given timeout in milliseconds is reached. | ||||
|  */ | ||||
| @RequiresApi(Build.VERSION_CODES.LOLLIPOP) | ||||
| class TimeoutAction extends ActionWrapper { | ||||
| 
 | ||||
|     private long startMillis; | ||||
|     private long timeoutMillis; | ||||
|     private BaseAction action; | ||||
| 
 | ||||
|     TimeoutAction(long timeoutMillis, @NonNull BaseAction action) { | ||||
|         this.timeoutMillis = timeoutMillis; | ||||
|         this.action = action; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public BaseAction getAction() { | ||||
|         return action; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onStart(@NonNull ActionHolder holder) { | ||||
|         startMillis = System.currentTimeMillis(); | ||||
|         super.onStart(holder); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCaptureCompleted(@NonNull ActionHolder holder, | ||||
|                                    @NonNull CaptureRequest request, | ||||
|                                    @NonNull TotalCaptureResult result) { | ||||
|         super.onCaptureCompleted(holder, request, result); | ||||
|         if (!isCompleted()) { | ||||
|             if (System.currentTimeMillis() > startMillis + timeoutMillis) { | ||||
|                 // This will set our state to COMPLETED and stop requests.
 | ||||
|                 getAction().abort(holder); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,84 @@ | ||||
| package com.otaliastudios.cameraview.engine.action; | ||||
| 
 | ||||
| import android.hardware.camera2.CaptureRequest; | ||||
| import android.hardware.camera2.CaptureResult; | ||||
| import android.hardware.camera2.TotalCaptureResult; | ||||
| import android.os.Build; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.RequiresApi; | ||||
| 
 | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
| 
 | ||||
| /** | ||||
|  * Performs a list of actions together, completing | ||||
|  * once all of them have completed. | ||||
|  */ | ||||
| @RequiresApi(Build.VERSION_CODES.LOLLIPOP) | ||||
| class TogetherAction extends BaseAction { | ||||
|     // Need to be BaseAction so we can call onStart() instead of start()
 | ||||
|     private final List<BaseAction> actions; | ||||
|     private final List<BaseAction> runningActions; | ||||
| 
 | ||||
|     TogetherAction(@NonNull final List<BaseAction> actions) { | ||||
|         this.actions = new ArrayList<>(actions); | ||||
|         this.runningActions = new ArrayList<>(actions); | ||||
|         for (BaseAction action : actions) { | ||||
|             action.addCallback(new ActionCallback() { | ||||
|                 @Override | ||||
|                 public void onActionStateChanged(@NonNull Action action, int state) { | ||||
|                     if (state == STATE_COMPLETED) { | ||||
|                         //noinspection SuspiciousMethodCalls
 | ||||
|                         runningActions.remove(action); | ||||
|                     } | ||||
|                     if (runningActions.isEmpty()) { | ||||
|                         setState(STATE_COMPLETED); | ||||
|                     } | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onStart(@NonNull ActionHolder holder) { | ||||
|         super.onStart(holder); | ||||
|         for (BaseAction action : actions) { | ||||
|             if (!action.isCompleted()) action.onStart(holder); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onAbort(@NonNull ActionHolder holder) { | ||||
|         super.onAbort(holder); | ||||
|         for (BaseAction action : actions) { | ||||
|             if (!action.isCompleted()) action.onAbort(holder); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCaptureStarted(@NonNull ActionHolder holder, @NonNull CaptureRequest request) { | ||||
|         super.onCaptureStarted(holder, request); | ||||
|         for (BaseAction action : actions) { | ||||
|             if (!action.isCompleted()) action.onCaptureStarted(holder, request); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCaptureProgressed(@NonNull ActionHolder holder, @NonNull CaptureRequest request, | ||||
|                                     @NonNull CaptureResult result) { | ||||
|         super.onCaptureProgressed(holder, request, result); | ||||
|         for (BaseAction action : actions) { | ||||
|             if (!action.isCompleted()) action.onCaptureProgressed(holder, request, result); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCaptureCompleted(@NonNull ActionHolder holder, @NonNull CaptureRequest request, | ||||
|                                    @NonNull TotalCaptureResult result) { | ||||
|         super.onCaptureCompleted(holder, request, result); | ||||
|         for (BaseAction action : actions) { | ||||
|             if (!action.isCompleted()) action.onCaptureCompleted(holder, request, result); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,32 @@ | ||||
| package com.otaliastudios.cameraview.engine.lock; | ||||
| 
 | ||||
| import android.hardware.camera2.CameraCharacteristics; | ||||
| import android.os.Build; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.RequiresApi; | ||||
| 
 | ||||
| import com.otaliastudios.cameraview.engine.action.ActionHolder; | ||||
| import com.otaliastudios.cameraview.engine.action.BaseAction; | ||||
| 
 | ||||
| @RequiresApi(Build.VERSION_CODES.LOLLIPOP) | ||||
| public abstract class BaseLock extends BaseAction { | ||||
| 
 | ||||
|     @Override | ||||
|     protected final void onStart(@NonNull ActionHolder holder) { | ||||
|         super.onStart(holder); | ||||
|         boolean isSkipped = checkShouldSkip(holder); | ||||
|         boolean isSupported = checkIsSupported(holder); | ||||
|         if (isSupported && !isSkipped) { | ||||
|             onStarted(holder); | ||||
|         } else { | ||||
|             setState(STATE_COMPLETED); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     protected abstract void onStarted(@NonNull ActionHolder holder); | ||||
| 
 | ||||
|     protected abstract boolean checkShouldSkip(@NonNull ActionHolder holder); | ||||
| 
 | ||||
|     protected abstract boolean checkIsSupported(@NonNull ActionHolder holder); | ||||
| } | ||||
| @ -0,0 +1,78 @@ | ||||
| package com.otaliastudios.cameraview.engine.lock; | ||||
| 
 | ||||
| import android.hardware.camera2.CameraCharacteristics; | ||||
| import android.hardware.camera2.CaptureRequest; | ||||
| import android.hardware.camera2.CaptureResult; | ||||
| import android.hardware.camera2.TotalCaptureResult; | ||||
| import android.os.Build; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.RequiresApi; | ||||
| 
 | ||||
| import com.otaliastudios.cameraview.CameraLogger; | ||||
| import com.otaliastudios.cameraview.engine.action.ActionHolder; | ||||
| 
 | ||||
| @RequiresApi(Build.VERSION_CODES.LOLLIPOP) | ||||
| public class ExposureLock extends BaseLock { | ||||
| 
 | ||||
|     private final static String TAG = ExposureLock.class.getSimpleName(); | ||||
|     private final static CameraLogger LOG = CameraLogger.create(TAG); | ||||
| 
 | ||||
|     @Override | ||||
|     protected boolean checkIsSupported(@NonNull ActionHolder holder) { | ||||
|         boolean isNotLegacy = readCharacteristic(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL, -1) | ||||
|                 != CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY; | ||||
|         // Not sure we should check aeMode as well, probably all aeModes support locking,
 | ||||
|         // but this should not be a big issue since we're not even using different AE modes.
 | ||||
|         Integer aeMode = holder.getBuilder(this).get(CaptureRequest.CONTROL_AE_MODE); | ||||
|         boolean isAEOn = aeMode != null && | ||||
|                 (aeMode == CameraCharacteristics.CONTROL_AE_MODE_ON | ||||
|                         || aeMode == CameraCharacteristics.CONTROL_AE_MODE_ON_ALWAYS_FLASH | ||||
|                         || aeMode == CameraCharacteristics.CONTROL_AE_MODE_ON_AUTO_FLASH | ||||
|                         || aeMode == CameraCharacteristics.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE | ||||
|                         || aeMode == 5 /* CameraCharacteristics.CONTROL_AE_MODE_ON_EXTERNAL_FLASH, API 28 */); | ||||
|         boolean result = isNotLegacy && isAEOn; | ||||
|         LOG.i("checkIsSupported:", result); | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected boolean checkShouldSkip(@NonNull ActionHolder holder) { | ||||
|         Integer aeState = holder.getLastResult(this).get(CaptureResult.CONTROL_AE_STATE); | ||||
|         boolean result = aeState != null && aeState == CaptureResult.CONTROL_AE_STATE_LOCKED; | ||||
|         LOG.i("checkShouldSkip:", result); | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onStarted(@NonNull ActionHolder holder) { | ||||
|         int cancelTrigger = Build.VERSION.SDK_INT >= 23 | ||||
|                 ? CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL | ||||
|                 : CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE; | ||||
|         holder.getBuilder(this).set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, cancelTrigger); | ||||
|         holder.getBuilder(this).set(CaptureRequest.CONTROL_AE_LOCK, true); | ||||
|         holder.applyBuilder(this); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCaptureCompleted(@NonNull ActionHolder holder, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { | ||||
|         super.onCaptureCompleted(holder, request, result); | ||||
|         Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); | ||||
|         LOG.i("processCapture:", "aeState:", aeState); | ||||
|         if (aeState == null) return; | ||||
|         switch (aeState) { | ||||
|             case CaptureRequest.CONTROL_AE_STATE_LOCKED: { | ||||
|                 setState(STATE_COMPLETED); | ||||
|                 break; | ||||
|             } | ||||
|             case CaptureRequest.CONTROL_AE_STATE_PRECAPTURE: | ||||
|             case CaptureRequest.CONTROL_AE_STATE_CONVERGED: | ||||
|             case CaptureRequest.CONTROL_AE_STATE_INACTIVE: | ||||
|             case CaptureRequest.CONTROL_AE_STATE_SEARCHING: | ||||
|             case CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED: { | ||||
|                 // Wait...
 | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,82 @@ | ||||
| package com.otaliastudios.cameraview.engine.lock; | ||||
| 
 | ||||
| import android.hardware.camera2.CameraCharacteristics; | ||||
| import android.hardware.camera2.CaptureRequest; | ||||
| import android.hardware.camera2.CaptureResult; | ||||
| import android.hardware.camera2.TotalCaptureResult; | ||||
| import android.os.Build; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.RequiresApi; | ||||
| 
 | ||||
| import com.otaliastudios.cameraview.CameraLogger; | ||||
| import com.otaliastudios.cameraview.engine.action.ActionHolder; | ||||
| 
 | ||||
| @RequiresApi(Build.VERSION_CODES.LOLLIPOP) | ||||
| public class FocusLock extends BaseLock { | ||||
| 
 | ||||
|     private final static String TAG = FocusLock.class.getSimpleName(); | ||||
|     private final static CameraLogger LOG = CameraLogger.create(TAG); | ||||
| 
 | ||||
|     @Override | ||||
|     protected boolean checkIsSupported(@NonNull ActionHolder holder) { | ||||
|         // We'll lock by changing the AF mode to AUTO.
 | ||||
|         // In that mode, AF won't change unless someone starts a trigger operation.
 | ||||
|         int[] modes = readCharacteristic(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES, new int[]{}); | ||||
|         for (int mode : modes) { | ||||
|             if (mode == CameraCharacteristics.CONTROL_AF_MODE_AUTO) { | ||||
|                 return true; | ||||
|             } | ||||
|         } | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected boolean checkShouldSkip(@NonNull ActionHolder holder) { | ||||
|         CaptureResult lastResult = holder.getLastResult(this); | ||||
|         Integer afState = lastResult.get(CaptureResult.CONTROL_AF_STATE); | ||||
|         boolean afStateOk = afState != null && | ||||
|                 (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED | ||||
|                         || afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED | ||||
|                         || afState == CaptureResult.CONTROL_AF_STATE_INACTIVE | ||||
|                         || afState == CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED | ||||
|                         || afState == CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED); | ||||
|         Integer afMode = lastResult.get(CaptureResult.CONTROL_AF_MODE); | ||||
|         boolean afModeOk = afMode != null && afMode == CaptureResult.CONTROL_AF_MODE_AUTO; | ||||
|         boolean result = afStateOk && afModeOk; | ||||
|         LOG.i("checkShouldSkip:", result); | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onStarted(@NonNull ActionHolder holder) { | ||||
|         holder.getBuilder(this).set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); | ||||
|         holder.getBuilder(this).set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_CANCEL); | ||||
|         holder.applyBuilder(this); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCaptureCompleted(@NonNull ActionHolder holder, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { | ||||
|         super.onCaptureCompleted(holder, request, result); | ||||
|         Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); | ||||
|         Integer afMode = result.get(CaptureResult.CONTROL_AF_MODE); | ||||
|         LOG.i("onCapture:", "afState:", afState, "afMode:", afMode); | ||||
|         if (afState == null || afMode == null) return; | ||||
|         if (afMode != CaptureResult.CONTROL_AF_MODE_AUTO) return; | ||||
|         switch (afState) { | ||||
|             case CaptureRequest.CONTROL_AF_STATE_FOCUSED_LOCKED: | ||||
|             case CaptureRequest.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED: | ||||
|             case CaptureRequest.CONTROL_AF_STATE_INACTIVE: | ||||
|             case CaptureRequest.CONTROL_AF_STATE_PASSIVE_FOCUSED: | ||||
|             case CaptureRequest.CONTROL_AF_STATE_PASSIVE_UNFOCUSED: { | ||||
|                 setState(STATE_COMPLETED); | ||||
|                 break; | ||||
|             } | ||||
|             case CaptureRequest.CONTROL_AF_STATE_ACTIVE_SCAN: | ||||
|             case CaptureRequest.CONTROL_AF_STATE_PASSIVE_SCAN: { | ||||
|                 // Wait...
 | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,26 @@ | ||||
| package com.otaliastudios.cameraview.engine.lock; | ||||
| 
 | ||||
| import android.os.Build; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.RequiresApi; | ||||
| 
 | ||||
| import com.otaliastudios.cameraview.engine.action.ActionWrapper; | ||||
| import com.otaliastudios.cameraview.engine.action.Actions; | ||||
| import com.otaliastudios.cameraview.engine.action.BaseAction; | ||||
| 
 | ||||
| @RequiresApi(Build.VERSION_CODES.LOLLIPOP) | ||||
| public class LockAction extends ActionWrapper { | ||||
| 
 | ||||
|     private final BaseAction action = Actions.together( | ||||
|             new ExposureLock(), | ||||
|             new FocusLock(), | ||||
|             new WhiteBalanceLock() | ||||
|     ); | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public BaseAction getAction() { | ||||
|         return action; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,64 @@ | ||||
| package com.otaliastudios.cameraview.engine.lock; | ||||
| 
 | ||||
| import android.hardware.camera2.CameraCharacteristics; | ||||
| import android.hardware.camera2.CaptureRequest; | ||||
| import android.hardware.camera2.CaptureResult; | ||||
| import android.hardware.camera2.TotalCaptureResult; | ||||
| import android.os.Build; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.RequiresApi; | ||||
| 
 | ||||
| import com.otaliastudios.cameraview.CameraLogger; | ||||
| import com.otaliastudios.cameraview.engine.action.ActionHolder; | ||||
| 
 | ||||
| @RequiresApi(Build.VERSION_CODES.LOLLIPOP) | ||||
| public class WhiteBalanceLock extends BaseLock { | ||||
| 
 | ||||
|     private final static String TAG = WhiteBalanceLock.class.getSimpleName(); | ||||
|     private final static CameraLogger LOG = CameraLogger.create(TAG); | ||||
| 
 | ||||
|     @Override | ||||
|     protected boolean checkIsSupported(@NonNull ActionHolder holder) { | ||||
|         boolean isNotLegacy = readCharacteristic(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL, -1) | ||||
|                 != CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY; | ||||
|         Integer awbMode = holder.getBuilder(this).get(CaptureRequest.CONTROL_AWB_MODE); | ||||
|         boolean result = isNotLegacy && awbMode != null && awbMode == CaptureRequest.CONTROL_AWB_MODE_AUTO; | ||||
|         LOG.i("checkIsSupported:", result); | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected boolean checkShouldSkip(@NonNull ActionHolder holder) { | ||||
|         Integer awbState = holder.getLastResult(this).get(CaptureResult.CONTROL_AWB_STATE); | ||||
|         boolean result = awbState != null && awbState == CaptureRequest.CONTROL_AWB_STATE_LOCKED; | ||||
|         LOG.i("checkShouldSkip:", result); | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onStarted(@NonNull ActionHolder holder) { | ||||
|         holder.getBuilder(this).set(CaptureRequest.CONTROL_AWB_LOCK, true); | ||||
|         holder.applyBuilder(this); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCaptureCompleted(@NonNull ActionHolder holder, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { | ||||
|         super.onCaptureCompleted(holder, request, result); | ||||
|         Integer awbState = result.get(CaptureResult.CONTROL_AWB_STATE); | ||||
|         LOG.i("processCapture:", "awbState:", awbState); | ||||
|         if (awbState == null) return; | ||||
|         switch (awbState) { | ||||
|             case CaptureRequest.CONTROL_AWB_STATE_LOCKED: { | ||||
|                 setState(STATE_COMPLETED); | ||||
|                 break; | ||||
|             } | ||||
|             case CaptureRequest.CONTROL_AWB_STATE_CONVERGED: | ||||
|             case CaptureRequest.CONTROL_AWB_STATE_INACTIVE: | ||||
|             case CaptureRequest.CONTROL_AWB_STATE_SEARCHING: { | ||||
|                 // Wait...
 | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,62 @@ | ||||
| package com.otaliastudios.cameraview.engine.meter; | ||||
| 
 | ||||
| import android.hardware.camera2.params.MeteringRectangle; | ||||
| import android.os.Build; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.RequiresApi; | ||||
| 
 | ||||
| import com.otaliastudios.cameraview.CameraLogger; | ||||
| import com.otaliastudios.cameraview.engine.action.ActionHolder; | ||||
| import com.otaliastudios.cameraview.engine.action.BaseAction; | ||||
| 
 | ||||
| import java.util.List; | ||||
| 
 | ||||
| @RequiresApi(Build.VERSION_CODES.LOLLIPOP) | ||||
| public abstract class BaseMeter extends BaseAction { | ||||
| 
 | ||||
|     private final static String TAG = BaseMeter.class.getSimpleName(); | ||||
|     private final static CameraLogger LOG = CameraLogger.create(TAG); | ||||
| 
 | ||||
|     private final List<MeteringRectangle> areas; | ||||
|     private boolean isSuccessful; | ||||
|     private boolean skipIfPossible; | ||||
| 
 | ||||
|     @SuppressWarnings("WeakerAccess") | ||||
|     protected BaseMeter(@NonNull List<MeteringRectangle> areas, boolean skipIfPossible) { | ||||
|         this.areas = areas; | ||||
|         this.skipIfPossible = skipIfPossible; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected final void onStart(@NonNull ActionHolder holder) { | ||||
|         super.onStart(holder); | ||||
|         boolean isSkipped = skipIfPossible && checkShouldSkip(holder); | ||||
|         boolean isSupported = checkIsSupported(holder); | ||||
|         if (isSupported && !isSkipped) { | ||||
|             LOG.i("onStart:", "supported and not skipped. Dispatching onStarted."); | ||||
|             onStarted(holder, areas); | ||||
|         } else { | ||||
|             LOG.i("onStart:", "not supported or skipped. Dispatching COMPLETED state."); | ||||
|             setSuccessful(true); | ||||
|             setState(STATE_COMPLETED); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     protected abstract void onStarted(@NonNull ActionHolder holder, | ||||
|                                       @NonNull List<MeteringRectangle> areas); | ||||
| 
 | ||||
|     protected abstract boolean checkShouldSkip(@NonNull ActionHolder holder); | ||||
| 
 | ||||
|     protected abstract boolean checkIsSupported(@NonNull ActionHolder holder); | ||||
| 
 | ||||
|     @SuppressWarnings("WeakerAccess") | ||||
|     protected void setSuccessful(boolean successful) { | ||||
|         isSuccessful = successful; | ||||
|     } | ||||
| 
 | ||||
|     @SuppressWarnings("WeakerAccess") | ||||
|     public boolean isSuccessful() { | ||||
|         return isSuccessful; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,40 @@ | ||||
| package com.otaliastudios.cameraview.engine.meter; | ||||
| 
 | ||||
| import android.graphics.Rect; | ||||
| import android.hardware.camera2.CameraCharacteristics; | ||||
| import android.hardware.camera2.params.MeteringRectangle; | ||||
| import android.os.Build; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.annotation.RequiresApi; | ||||
| 
 | ||||
| import com.otaliastudios.cameraview.engine.action.ActionHolder; | ||||
| import com.otaliastudios.cameraview.engine.action.BaseAction; | ||||
| 
 | ||||
| import java.util.List; | ||||
| 
 | ||||
| @RequiresApi(Build.VERSION_CODES.LOLLIPOP) | ||||
| public abstract class BaseReset extends BaseAction { | ||||
| 
 | ||||
|     private boolean resetArea; | ||||
| 
 | ||||
|     @SuppressWarnings("WeakerAccess") | ||||
|     protected BaseReset(boolean resetArea) { | ||||
|         this.resetArea = resetArea; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected final void onStart(@NonNull ActionHolder holder) { | ||||
|         super.onStart(holder); | ||||
|         MeteringRectangle area = null; | ||||
|         if (resetArea) { | ||||
|             Rect rect = readCharacteristic(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE, new Rect()); | ||||
|             area = new MeteringRectangle(rect, MeteringRectangle.METERING_WEIGHT_DONT_CARE); | ||||
|         } | ||||
|         onStarted(holder, area); | ||||
|     } | ||||
| 
 | ||||
|     protected abstract void onStarted(@NonNull ActionHolder holder, | ||||
|                                       @Nullable MeteringRectangle area); | ||||
| } | ||||
| @ -0,0 +1,150 @@ | ||||
| package com.otaliastudios.cameraview.engine.meter; | ||||
| 
 | ||||
| import android.hardware.camera2.CameraCharacteristics; | ||||
| import android.hardware.camera2.CaptureRequest; | ||||
| import android.hardware.camera2.CaptureResult; | ||||
| import android.hardware.camera2.TotalCaptureResult; | ||||
| import android.hardware.camera2.params.MeteringRectangle; | ||||
| import android.os.Build; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.RequiresApi; | ||||
| 
 | ||||
| import com.otaliastudios.cameraview.CameraLogger; | ||||
| import com.otaliastudios.cameraview.engine.action.ActionHolder; | ||||
| 
 | ||||
| import java.util.List; | ||||
| 
 | ||||
| @RequiresApi(Build.VERSION_CODES.LOLLIPOP) | ||||
| public class ExposureMeter extends BaseMeter { | ||||
| 
 | ||||
|     private static final String TAG = ExposureMeter.class.getSimpleName(); | ||||
|     private static final CameraLogger LOG = CameraLogger.create(TAG); | ||||
| 
 | ||||
|     private static final int STATE_WAITING_PRECAPTURE = 0; | ||||
|     private static final int STATE_WAITING_PRECAPTURE_END = 1; | ||||
| 
 | ||||
|     @SuppressWarnings("WeakerAccess") | ||||
|     public ExposureMeter(@NonNull List<MeteringRectangle> areas, boolean skipIfPossible) { | ||||
|         super(areas, skipIfPossible); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected boolean checkIsSupported(@NonNull ActionHolder holder) { | ||||
|         // In our case, this means checking if we support the AE precapture trigger.
 | ||||
|         boolean isNotLegacy = readCharacteristic(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL, -1) | ||||
|                 != CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY; | ||||
|         Integer aeMode = holder.getBuilder(this).get(CaptureRequest.CONTROL_AE_MODE); | ||||
|         boolean isAEOn = aeMode != null && | ||||
|                 (aeMode == CameraCharacteristics.CONTROL_AE_MODE_ON | ||||
|                         || aeMode == CameraCharacteristics.CONTROL_AE_MODE_ON_ALWAYS_FLASH | ||||
|                         || aeMode == CameraCharacteristics.CONTROL_AE_MODE_ON_AUTO_FLASH | ||||
|                         || aeMode == CameraCharacteristics.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE | ||||
|                         || aeMode == 5 /* CameraCharacteristics.CONTROL_AE_MODE_ON_EXTERNAL_FLASH, API 28 */); | ||||
|         boolean result = isNotLegacy && isAEOn; | ||||
|         LOG.i("checkIsSupported:", result); | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected boolean checkShouldSkip(@NonNull ActionHolder holder) { | ||||
|         Integer aeState = holder.getLastResult(this).get(CaptureResult.CONTROL_AE_STATE); | ||||
|         boolean result = aeState != null && aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED; | ||||
|         LOG.i("checkShouldSkip:", result); | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onStarted(@NonNull ActionHolder holder, @NonNull List<MeteringRectangle> areas) { | ||||
|         LOG.i("onStarted:", "with areas:", areas); | ||||
| 
 | ||||
|         // Launch the precapture trigger.
 | ||||
|         holder.getBuilder(this).set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, | ||||
|                 CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); | ||||
| 
 | ||||
|         // Check the regions.
 | ||||
|         int maxRegions = readCharacteristic(CameraCharacteristics.CONTROL_MAX_REGIONS_AE, 0); | ||||
|         if (!areas.isEmpty() && maxRegions > 0) { | ||||
|             int max = Math.min(maxRegions, areas.size()); | ||||
|             holder.getBuilder(this).set(CaptureRequest.CONTROL_AE_REGIONS, | ||||
|                     areas.subList(0, max).toArray(new MeteringRectangle[]{})); | ||||
|         } | ||||
| 
 | ||||
|         // Apply
 | ||||
|         holder.applyBuilder(this); | ||||
|         setState(STATE_WAITING_PRECAPTURE); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onCompleted(@NonNull ActionHolder holder) { | ||||
|         super.onCompleted(holder); | ||||
|         // Remove (but not apply) the risky parameter so it is not included in new requests.
 | ||||
|         // Documentation about this key says that this should be allowed.
 | ||||
|         holder.getBuilder(this).set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, null); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCaptureCompleted(@NonNull ActionHolder holder, @NonNull CaptureRequest request, | ||||
|                                    @NonNull TotalCaptureResult result) { | ||||
|         super.onCaptureCompleted(holder, request, result); | ||||
|         Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); | ||||
|         Integer aeTriggerState = result.get(CaptureResult.CONTROL_AE_PRECAPTURE_TRIGGER); | ||||
|         LOG.i("onCaptureCompleted:", "aeState:", aeState, "aeTriggerState:", aeTriggerState); | ||||
|         if (aeState == null) return; | ||||
| 
 | ||||
|         if (getState() == STATE_WAITING_PRECAPTURE) { | ||||
|             switch (aeState) { | ||||
|                 case CaptureResult.CONTROL_AE_STATE_PRECAPTURE: { | ||||
|                     setState(STATE_WAITING_PRECAPTURE_END); | ||||
|                     break; | ||||
|                 } | ||||
|                 case CaptureResult.CONTROL_AE_STATE_CONVERGED: | ||||
|                 case CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED: { | ||||
|                     // PRECAPTURE is a transient state. Being here might mean that precapture run
 | ||||
|                     // and was successful, OR that the trigger was not even received yet. To
 | ||||
|                     // distinguish, check the trigger state.
 | ||||
|                     if (aeTriggerState != null | ||||
|                             && aeTriggerState == CaptureResult.CONTROL_AE_PRECAPTURE_TRIGGER_START) { | ||||
|                         setSuccessful(true); | ||||
|                         setState(STATE_COMPLETED); | ||||
|                     } | ||||
|                     break; | ||||
|                 } | ||||
|                 case CaptureResult.CONTROL_AE_STATE_LOCKED: { | ||||
|                     // There's nothing we can do, AE was locked, triggers are ignored.
 | ||||
|                     setSuccessful(false); | ||||
|                     setState(STATE_COMPLETED); | ||||
|                     break; | ||||
|                 } | ||||
|                 case CaptureResult.CONTROL_AE_STATE_INACTIVE: | ||||
|                 case CaptureResult.CONTROL_AE_STATE_SEARCHING: { | ||||
|                     // Wait...
 | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (getState() == STATE_WAITING_PRECAPTURE_END) { | ||||
|             switch (aeState) { | ||||
|                 case CaptureResult.CONTROL_AE_STATE_CONVERGED: | ||||
|                 case CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED: { | ||||
|                     setSuccessful(true); | ||||
|                     setState(STATE_COMPLETED); | ||||
|                     break; | ||||
|                 } | ||||
|                 case CaptureResult.CONTROL_AE_STATE_LOCKED: { | ||||
|                     // There's nothing we can do, AE was locked, triggers are ignored.
 | ||||
|                     setSuccessful(false); | ||||
|                     setState(STATE_COMPLETED); | ||||
|                     break; | ||||
|                 } | ||||
|                 case CaptureResult.CONTROL_AE_STATE_PRECAPTURE: | ||||
|                 case CaptureResult.CONTROL_AE_STATE_INACTIVE: | ||||
|                 case CaptureResult.CONTROL_AE_STATE_SEARCHING: { | ||||
|                     // Wait...
 | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,72 @@ | ||||
| package com.otaliastudios.cameraview.engine.meter; | ||||
| 
 | ||||
| import android.hardware.camera2.CameraCharacteristics; | ||||
| import android.hardware.camera2.CaptureRequest; | ||||
| import android.hardware.camera2.CaptureResult; | ||||
| import android.hardware.camera2.TotalCaptureResult; | ||||
| import android.hardware.camera2.params.MeteringRectangle; | ||||
| import android.os.Build; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.annotation.RequiresApi; | ||||
| 
 | ||||
| import com.otaliastudios.cameraview.CameraLogger; | ||||
| import com.otaliastudios.cameraview.engine.action.ActionHolder; | ||||
| 
 | ||||
| @RequiresApi(Build.VERSION_CODES.LOLLIPOP) | ||||
| public class ExposureReset extends BaseReset { | ||||
| 
 | ||||
|     private static final String TAG = ExposureReset.class.getSimpleName(); | ||||
|     private static final CameraLogger LOG = CameraLogger.create(TAG); | ||||
| 
 | ||||
|     private static final int STATE_WAITING_LOCK = 0; | ||||
| 
 | ||||
|     @SuppressWarnings("WeakerAccess") | ||||
|     public ExposureReset() { | ||||
|         super(true); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onStarted(@NonNull ActionHolder holder, @Nullable MeteringRectangle area) { | ||||
|         int maxRegions = readCharacteristic(CameraCharacteristics.CONTROL_MAX_REGIONS_AE, 0); | ||||
|         if (area != null && maxRegions > 0) { | ||||
|             holder.getBuilder(this).set(CaptureRequest.CONTROL_AE_REGIONS, | ||||
|                     new MeteringRectangle[]{area}); | ||||
|         } | ||||
| 
 | ||||
|         // NOTE: precapture might not be supported, in which case I think it will be ignored.
 | ||||
|         Integer trigger = holder.getLastResult(this).get(CaptureResult.CONTROL_AE_PRECAPTURE_TRIGGER); | ||||
|         LOG.i("onStarted:", "last precapture trigger is", trigger); | ||||
|         if (trigger != null && trigger == CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START) { | ||||
|             LOG.i("onStarted:", "canceling precapture."); | ||||
|             int newTrigger = Build.VERSION.SDK_INT >= 23 | ||||
|                     ? CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL | ||||
|                     : CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE; | ||||
|             holder.getBuilder(this).set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, newTrigger); | ||||
|         } | ||||
| 
 | ||||
|         // Documentation about CONTROL_AE_PRECAPTURE_TRIGGER says that, if it was started but not
 | ||||
|         // followed by a CAPTURE_INTENT_STILL_PICTURE request, the internal AE routine might remain
 | ||||
|         // locked unless we unlock manually.
 | ||||
|         // This is often the case for us, since the snapshot picture recorder does not use the intent
 | ||||
|         // and anyway we use the precapture sequence for touch metering as well.
 | ||||
|         // To reset, docs suggest the use of CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL, which we do above,
 | ||||
|         // or the technique used below: locking then unlocking. This proved to be the ONLY method
 | ||||
|         // to unlock reliably, unlike the cancel trigger (which we'll run anyway).
 | ||||
|         holder.getBuilder(this).set(CaptureRequest.CONTROL_AE_LOCK, true); | ||||
|         holder.applyBuilder(this); | ||||
|         setState(STATE_WAITING_LOCK); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCaptureCompleted(@NonNull ActionHolder holder, @NonNull CaptureRequest request, | ||||
|                                    @NonNull TotalCaptureResult result) { | ||||
|         super.onCaptureCompleted(holder, request, result); | ||||
|         if (getState() == STATE_WAITING_LOCK) { | ||||
|             holder.getBuilder(this).set(CaptureRequest.CONTROL_AE_LOCK, false); | ||||
|             holder.applyBuilder(this); | ||||
|             setState(STATE_COMPLETED); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,96 @@ | ||||
| package com.otaliastudios.cameraview.engine.meter; | ||||
| 
 | ||||
| import android.hardware.camera2.CameraCharacteristics; | ||||
| import android.hardware.camera2.CaptureRequest; | ||||
| import android.hardware.camera2.CaptureResult; | ||||
| import android.hardware.camera2.TotalCaptureResult; | ||||
| import android.hardware.camera2.params.MeteringRectangle; | ||||
| import android.os.Build; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.RequiresApi; | ||||
| 
 | ||||
| import com.otaliastudios.cameraview.CameraLogger; | ||||
| import com.otaliastudios.cameraview.engine.action.ActionHolder; | ||||
| 
 | ||||
| import java.util.List; | ||||
| 
 | ||||
| @RequiresApi(Build.VERSION_CODES.LOLLIPOP) | ||||
| public class FocusMeter extends BaseMeter { | ||||
| 
 | ||||
|     private static final String TAG = FocusMeter.class.getSimpleName(); | ||||
|     private static final CameraLogger LOG = CameraLogger.create(TAG); | ||||
| 
 | ||||
|     public FocusMeter(@NonNull List<MeteringRectangle> areas, boolean skipIfPossible) { | ||||
|         super(areas, skipIfPossible); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected boolean checkIsSupported(@NonNull ActionHolder holder) { | ||||
|         // Exclude OFF and EDOF as per docs. These do no support the trigger.
 | ||||
|         Integer afMode = holder.getBuilder(this).get(CaptureRequest.CONTROL_AF_MODE); | ||||
|         boolean result = afMode != null && | ||||
|                 (afMode == CameraCharacteristics.CONTROL_AF_MODE_AUTO | ||||
|                         || afMode == CameraCharacteristics.CONTROL_AF_MODE_CONTINUOUS_PICTURE | ||||
|                         || afMode == CameraCharacteristics.CONTROL_AF_MODE_CONTINUOUS_VIDEO | ||||
|                         || afMode == CameraCharacteristics.CONTROL_AF_MODE_MACRO); | ||||
|         LOG.i("checkIsSupported:", result); | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected boolean checkShouldSkip(@NonNull ActionHolder holder) { | ||||
|         Integer afState = holder.getLastResult(this).get(CaptureResult.CONTROL_AF_STATE); | ||||
|         boolean result = afState != null && | ||||
|                 (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED || | ||||
|                         afState == CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED); | ||||
|         LOG.i("checkShouldSkip:", result); | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onStarted(@NonNull ActionHolder holder, @NonNull List<MeteringRectangle> areas) { | ||||
|         LOG.i("onStarted:", "with areas:", areas); | ||||
|         holder.getBuilder(this).set(CaptureRequest.CONTROL_AF_TRIGGER, | ||||
|                 CaptureRequest.CONTROL_AF_TRIGGER_START); | ||||
|         int maxRegions = readCharacteristic(CameraCharacteristics.CONTROL_MAX_REGIONS_AF, 0); | ||||
|         if (!areas.isEmpty() && maxRegions > 0) { | ||||
|             int max = Math.min(maxRegions, areas.size()); | ||||
|             holder.getBuilder(this).set(CaptureRequest.CONTROL_AF_REGIONS, | ||||
|                     areas.subList(0, max).toArray(new MeteringRectangle[]{})); | ||||
|         } | ||||
|         holder.applyBuilder(this); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onCompleted(@NonNull ActionHolder holder) { | ||||
|         super.onCompleted(holder); | ||||
|         // Remove (but not apply) the risky parameter so it is not included in new requests.
 | ||||
|         // Documentation about this key says that this should be allowed.
 | ||||
|         holder.getBuilder(this).set(CaptureRequest.CONTROL_AF_TRIGGER, null); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCaptureCompleted(@NonNull ActionHolder holder, @NonNull CaptureRequest request, | ||||
|                                    @NonNull TotalCaptureResult result) { | ||||
|         super.onCaptureCompleted(holder, request, result); | ||||
|         Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); | ||||
|         LOG.i("onCaptureCompleted:", "afState:", afState); | ||||
|         if (afState == null) return; | ||||
|         switch (afState) { | ||||
|             case CaptureRequest.CONTROL_AF_STATE_FOCUSED_LOCKED: { | ||||
|                 setSuccessful(true); | ||||
|                 setState(STATE_COMPLETED); | ||||
|                 break; | ||||
|             } | ||||
|             case CaptureRequest.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED: { | ||||
|                 setSuccessful(false); | ||||
|                 setState(STATE_COMPLETED); | ||||
|                 break; | ||||
|             } | ||||
|             case CaptureRequest.CONTROL_AF_STATE_INACTIVE: break; | ||||
|             case CaptureRequest.CONTROL_AF_STATE_ACTIVE_SCAN: break; | ||||
|             default: break; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,49 @@ | ||||
| package com.otaliastudios.cameraview.engine.meter; | ||||
| 
 | ||||
| import android.hardware.camera2.CameraCharacteristics; | ||||
| import android.hardware.camera2.CaptureRequest; | ||||
| import android.hardware.camera2.CaptureResult; | ||||
| import android.hardware.camera2.params.MeteringRectangle; | ||||
| import android.os.Build; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.annotation.RequiresApi; | ||||
| 
 | ||||
| import com.otaliastudios.cameraview.CameraLogger; | ||||
| import com.otaliastudios.cameraview.engine.action.ActionHolder; | ||||
| 
 | ||||
| @RequiresApi(Build.VERSION_CODES.LOLLIPOP) | ||||
| public class FocusReset extends BaseReset { | ||||
| 
 | ||||
|     private static final String TAG = FocusReset.class.getSimpleName(); | ||||
|     private static final CameraLogger LOG = CameraLogger.create(TAG); | ||||
| 
 | ||||
|     @SuppressWarnings("WeakerAccess") | ||||
|     public FocusReset() { | ||||
|         super(true); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onStarted(@NonNull ActionHolder holder, @Nullable MeteringRectangle area) { | ||||
|         boolean changed = false; | ||||
|         int maxRegions = readCharacteristic(CameraCharacteristics.CONTROL_MAX_REGIONS_AF, 0); | ||||
|         if (area != null && maxRegions > 0) { | ||||
|             holder.getBuilder(this).set(CaptureRequest.CONTROL_AF_REGIONS, | ||||
|                     new MeteringRectangle[]{area}); | ||||
|             changed = true; | ||||
|         } | ||||
| 
 | ||||
|         // NOTE: trigger might not be supported, in which case I think it will be ignored.
 | ||||
|         Integer trigger = holder.getLastResult(this).get(CaptureResult.CONTROL_AF_TRIGGER); | ||||
|         LOG.w("onStarted:", "last focus trigger is", trigger); | ||||
|         if (trigger != null && trigger == CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START) { | ||||
|             holder.getBuilder(this).set(CaptureRequest.CONTROL_AF_TRIGGER, | ||||
|                     CaptureRequest.CONTROL_AF_TRIGGER_CANCEL); | ||||
|             changed = true; | ||||
|         } | ||||
| 
 | ||||
|         if (changed) holder.applyBuilder(this); | ||||
|         setState(STATE_COMPLETED); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,262 @@ | ||||
| package com.otaliastudios.cameraview.engine.meter; | ||||
| 
 | ||||
| import android.graphics.PointF; | ||||
| import android.graphics.Rect; | ||||
| import android.hardware.camera2.CameraCharacteristics; | ||||
| import android.hardware.camera2.CaptureRequest; | ||||
| import android.hardware.camera2.TotalCaptureResult; | ||||
| import android.hardware.camera2.params.MeteringRectangle; | ||||
| import android.os.Build; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.annotation.RequiresApi; | ||||
| 
 | ||||
| import com.otaliastudios.cameraview.CameraLogger; | ||||
| import com.otaliastudios.cameraview.engine.CameraEngine; | ||||
| import com.otaliastudios.cameraview.engine.action.ActionHolder; | ||||
| import com.otaliastudios.cameraview.engine.action.ActionWrapper; | ||||
| import com.otaliastudios.cameraview.engine.action.Actions; | ||||
| import com.otaliastudios.cameraview.engine.action.BaseAction; | ||||
| import com.otaliastudios.cameraview.engine.offset.Axis; | ||||
| import com.otaliastudios.cameraview.engine.offset.Reference; | ||||
| import com.otaliastudios.cameraview.size.AspectRatio; | ||||
| import com.otaliastudios.cameraview.size.Size; | ||||
| 
 | ||||
| import java.util.ArrayList; | ||||
| import java.util.Arrays; | ||||
| import java.util.List; | ||||
| 
 | ||||
| @RequiresApi(Build.VERSION_CODES.LOLLIPOP) | ||||
| public class MeterAction extends ActionWrapper { | ||||
| 
 | ||||
|     private final static String TAG = MeterAction.class.getSimpleName(); | ||||
|     private final static CameraLogger LOG = CameraLogger.create(TAG); | ||||
| 
 | ||||
|     private List<BaseMeter> meters; | ||||
|     private BaseAction action; | ||||
|     private ActionHolder holder; | ||||
|     private final PointF point; | ||||
|     private final CameraEngine engine; | ||||
|     private final boolean skipIfPossible; | ||||
| 
 | ||||
|     public MeterAction(@NonNull CameraEngine engine, @Nullable PointF point, | ||||
|                        boolean skipIfPossible) { | ||||
|         this.point = point; | ||||
|         this.engine = engine; | ||||
|         this.skipIfPossible = skipIfPossible; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public BaseAction getAction() { | ||||
|         return action; | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     public PointF getPoint() { | ||||
|         return point; | ||||
|     } | ||||
| 
 | ||||
|     public boolean isSuccessful() { | ||||
|         for (BaseMeter meter : meters) { | ||||
|             if (!meter.isSuccessful()) { | ||||
|                 LOG.i("isSuccessful:", "returning false."); | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
|         LOG.i("isSuccessful:", "returning true."); | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onStart(@NonNull ActionHolder holder) { | ||||
|         LOG.w("onStart:", "initializing."); | ||||
|         initialize(holder); | ||||
|         LOG.w("onStart:", "initialized."); | ||||
|         super.onStart(holder); | ||||
|     } | ||||
| 
 | ||||
|     private void initialize(@NonNull ActionHolder holder) { | ||||
|         this.holder = holder; | ||||
|         List<MeteringRectangle> areas = new ArrayList<>(); | ||||
|         if (point != null) { | ||||
|             // This is a good Q/A. https://stackoverflow.com/a/33181620/4288782
 | ||||
|             // At first, the point is relative to the View system and does not account our own cropping.
 | ||||
|             // Will keep updating these two below.
 | ||||
|             final PointF referencePoint = new PointF(point.x, point.y); | ||||
|             Size referenceSize = engine.getPreview().getSurfaceSize(); | ||||
| 
 | ||||
|             // 1. Account for cropping.
 | ||||
|             // This will enlarge the preview size so that aspect ratio matches.
 | ||||
|             referenceSize = applyPreviewCropping(referenceSize, referencePoint); | ||||
| 
 | ||||
|             // 2. Scale to the preview stream coordinates.
 | ||||
|             // This will move to the preview stream coordinates by scaling.
 | ||||
|             referenceSize = applyPreviewScale(referenceSize, referencePoint); | ||||
| 
 | ||||
|             // 3. Rotate to the stream coordinate system.
 | ||||
|             // This leaves us with sensor stream coordinates.
 | ||||
|             referenceSize = applyPreviewToSensorRotation(referenceSize, referencePoint); | ||||
| 
 | ||||
|             // 4. Move to the crop region coordinate system.
 | ||||
|             // The crop region is the union of all currently active streams.
 | ||||
|             referenceSize = applyCropRegionCoordinates(referenceSize, referencePoint); | ||||
| 
 | ||||
|             // 5. Move to the active array coordinate system.
 | ||||
|             referenceSize = applyActiveArrayCoordinates(referenceSize, referencePoint); | ||||
| 
 | ||||
|             // 6. Now we can compute the metering regions.
 | ||||
|             // We want to define them as a fraction of the visible size which (apart from cropping)
 | ||||
|             // can be obtained through the SENSOR rotated preview stream size.
 | ||||
|             Size visibleSize = engine.getPreviewStreamSize(Reference.SENSOR); | ||||
|             //noinspection ConstantConditions
 | ||||
|             MeteringRectangle area1 = createMeteringRectangle(referenceSize, referencePoint, | ||||
|                     visibleSize, 0.05F, 1000); | ||||
|             MeteringRectangle area2 = createMeteringRectangle(referenceSize, referencePoint, | ||||
|                     visibleSize, 0.1F, 100); | ||||
|             areas.add(area1); | ||||
|             areas.add(area2); | ||||
|         } | ||||
| 
 | ||||
|         BaseMeter ae = new ExposureMeter(areas, skipIfPossible); | ||||
|         BaseMeter af = new FocusMeter(areas, skipIfPossible); | ||||
|         BaseMeter awb = new WhiteBalanceMeter(areas, skipIfPossible); | ||||
|         meters = Arrays.asList(ae, af, awb); | ||||
|         action = Actions.together(ae, af, awb); | ||||
|     } | ||||
| 
 | ||||
|     @SuppressWarnings("UnnecessaryLocalVariable") | ||||
|     @NonNull | ||||
|     private Size applyPreviewCropping(@NonNull Size referenceSize, @NonNull PointF referencePoint) { | ||||
|         Size previewStreamSize = engine.getPreviewStreamSize(Reference.VIEW); | ||||
|         Size previewSurfaceSize = referenceSize; | ||||
|         if (previewStreamSize == null) { | ||||
|             throw new IllegalStateException("getPreviewStreamSize should not be null at this point."); | ||||
|         } | ||||
|         int referenceWidth = previewSurfaceSize.getWidth(); | ||||
|         int referenceHeight = previewSurfaceSize.getHeight(); | ||||
|         AspectRatio previewStreamAspectRatio = AspectRatio.of(previewStreamSize); | ||||
|         AspectRatio previewSurfaceAspectRatio = AspectRatio.of(previewSurfaceSize); | ||||
|         if (engine.getPreview().isCropping()) { | ||||
|             if (previewStreamAspectRatio.toFloat() > previewSurfaceAspectRatio.toFloat()) { | ||||
|                 // Stream is larger. The x coordinate must be increased: a touch on the left side
 | ||||
|                 // of the surface is not on the left size of stream (it's more to the right).
 | ||||
|                 float scale = previewStreamAspectRatio.toFloat() / previewSurfaceAspectRatio.toFloat(); | ||||
|                 referencePoint.x += previewSurfaceSize.getWidth() * (scale - 1F) / 2F; | ||||
|                 referenceWidth = Math.round(previewSurfaceSize.getWidth() * scale); | ||||
|             } else { | ||||
|                 // Stream is taller. The y coordinate must be increased: a touch on the top side
 | ||||
|                 // of the surface is not on the top size of stream (it's a bit lower).
 | ||||
|                 float scale = previewSurfaceAspectRatio.toFloat() / previewStreamAspectRatio.toFloat(); | ||||
|                 referencePoint.y += previewSurfaceSize.getHeight() * (scale - 1F) / 2F; | ||||
|                 referenceHeight = Math.round(previewSurfaceSize.getHeight() * scale); | ||||
|             } | ||||
|         } | ||||
|         return new Size(referenceWidth, referenceHeight); | ||||
|     } | ||||
| 
 | ||||
|     @SuppressWarnings("ConstantConditions") | ||||
|     @NonNull | ||||
|     private Size applyPreviewScale(@NonNull Size referenceSize, @NonNull PointF referencePoint) { | ||||
|         // The referenceSize how has the same aspect ratio of the previewStreamSize, but they
 | ||||
|         // can still have different size (that is, a scale operation is needed).
 | ||||
|         Size previewStreamSize = engine.getPreviewStreamSize(Reference.VIEW); | ||||
|         referencePoint.x *= (float) previewStreamSize.getWidth() / referenceSize.getWidth(); | ||||
|         referencePoint.y *= (float) previewStreamSize.getHeight() / referenceSize.getHeight(); | ||||
|         return previewStreamSize; | ||||
|     } | ||||
| 
 | ||||
|     @SuppressWarnings("SuspiciousNameCombination") | ||||
|     @NonNull | ||||
|     private Size applyPreviewToSensorRotation(@NonNull Size referenceSize, @NonNull PointF referencePoint) { | ||||
|         // Not elegant, but the sin/cos way was failing for some reason.
 | ||||
|         int angle = engine.getAngles().offset(Reference.SENSOR, Reference.VIEW, Axis.ABSOLUTE); | ||||
|         boolean flip = angle % 180 != 0; | ||||
|         float tempX = referencePoint.x; | ||||
|         float tempY = referencePoint.y; | ||||
|         if (angle == 0) { | ||||
|             referencePoint.x = tempX; | ||||
|             referencePoint.y = tempY; | ||||
|         } else if (angle == 90) { | ||||
|             referencePoint.x = tempY; | ||||
|             referencePoint.y = referenceSize.getWidth() - tempX; | ||||
|         } else if (angle == 180) { | ||||
|             referencePoint.x = referenceSize.getWidth() - tempX; | ||||
|             referencePoint.y = referenceSize.getHeight() - tempY; | ||||
|         } else if (angle == 270) { | ||||
|             referencePoint.x = referenceSize.getHeight() - tempY; | ||||
|             referencePoint.y = tempX; | ||||
|         } else { | ||||
|             throw new IllegalStateException("Unexpected angle " + angle); | ||||
|         } | ||||
|         return flip ? referenceSize.flip() : referenceSize; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     private Size applyCropRegionCoordinates(@NonNull Size referenceSize, @NonNull PointF referencePoint) { | ||||
|         // The input point and size refer to the stream rect.
 | ||||
|         // The stream rect is part of the 'crop region', as described below.
 | ||||
|         // https://source.android.com/devices/camera/camera3_crop_reprocess.html
 | ||||
|         Rect cropRect = holder.getBuilder(this).get(CaptureRequest.SCALER_CROP_REGION); | ||||
|         // For now, we don't care about x and y position. Rect should be non-null, but let's be safe.
 | ||||
|         int cropRectWidth = cropRect == null ? referenceSize.getWidth() : cropRect.width(); | ||||
|         int cropRectHeight = cropRect == null ? referenceSize.getHeight() : cropRect.height(); | ||||
|         // The stream is always centered inside the crop region, and one of the dimensions
 | ||||
|         // should always match. We just increase the other one.
 | ||||
|         referencePoint.x += (cropRectWidth - referenceSize.getWidth()) / 2F; | ||||
|         referencePoint.y += (cropRectHeight - referenceSize.getHeight()) / 2F; | ||||
|         return new Size(cropRectWidth, cropRectHeight); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     private Size applyActiveArrayCoordinates(@NonNull Size referenceSize, @NonNull PointF referencePoint) { | ||||
|         // The input point and size refer to the scaler crop region.
 | ||||
|         // We can query for the crop region position inside the active array, so this is easy.
 | ||||
|         Rect cropRect = holder.getBuilder(this).get(CaptureRequest.SCALER_CROP_REGION); | ||||
|         referencePoint.x += cropRect == null ? 0 : cropRect.left; | ||||
|         referencePoint.y += cropRect == null ? 0 : cropRect.top; | ||||
|         // Finally, get the active rect width and height from characteristics.
 | ||||
|         Rect activeRect = holder.getCharacteristics(this).get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); | ||||
|         if (activeRect == null) { // Should never happen
 | ||||
|             activeRect = new Rect(0, 0, referenceSize.getWidth(), referenceSize.getHeight()); | ||||
|         } | ||||
|         return new Size(activeRect.width(), activeRect.height()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Creates a metering rectangle around the center point. | ||||
|      * The rectangle will have a size that's a factor of the visible width and height. | ||||
|      * The rectangle will also be constrained to be inside the given boundaries, | ||||
|      * so we don't exceed them in case the center point is exactly on one side for example. | ||||
|      * @return a new rectangle | ||||
|      */ | ||||
|     @NonNull | ||||
|     private MeteringRectangle createMeteringRectangle( | ||||
|             @NonNull Size boundaries, | ||||
|             @NonNull PointF center, | ||||
|             @NonNull Size visibleSize, | ||||
|             float factor, | ||||
|             int weight) { | ||||
|         float rectangleWidth = factor * visibleSize.getWidth(); | ||||
|         float rectangleHeight = factor * visibleSize.getHeight(); | ||||
|         float rectangleLeft = center.x - rectangleWidth / 2F; | ||||
|         float rectangleTop = center.y - rectangleHeight / 2F; | ||||
|         // Respect boundaries
 | ||||
|         if (rectangleLeft < 0) rectangleLeft = 0; | ||||
|         if (rectangleTop < 0) rectangleTop = 0; | ||||
|         if (rectangleLeft + rectangleWidth > boundaries.getWidth()) { | ||||
|             rectangleWidth = boundaries.getWidth() - rectangleLeft; | ||||
|         } | ||||
|         if (rectangleTop + rectangleHeight > boundaries.getHeight()) { | ||||
|             rectangleHeight = boundaries.getHeight() - rectangleTop; | ||||
|         } | ||||
|         return new MeteringRectangle( | ||||
|                 (int) rectangleLeft, | ||||
|                 (int) rectangleTop, | ||||
|                 (int) rectangleWidth, | ||||
|                 (int) rectangleHeight, | ||||
|                 weight | ||||
|         ); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,34 @@ | ||||
| package com.otaliastudios.cameraview.engine.meter; | ||||
| 
 | ||||
| import android.os.Build; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.RequiresApi; | ||||
| 
 | ||||
| import com.otaliastudios.cameraview.engine.action.ActionHolder; | ||||
| import com.otaliastudios.cameraview.engine.action.ActionWrapper; | ||||
| import com.otaliastudios.cameraview.engine.action.Actions; | ||||
| import com.otaliastudios.cameraview.engine.action.BaseAction; | ||||
| import com.otaliastudios.cameraview.engine.lock.ExposureLock; | ||||
| import com.otaliastudios.cameraview.engine.lock.FocusLock; | ||||
| import com.otaliastudios.cameraview.engine.lock.WhiteBalanceLock; | ||||
| 
 | ||||
| @RequiresApi(Build.VERSION_CODES.LOLLIPOP) | ||||
| public class MeterResetAction extends ActionWrapper { | ||||
| 
 | ||||
|     private final BaseAction action; | ||||
| 
 | ||||
|     public MeterResetAction() { | ||||
|         this.action = Actions.together( | ||||
|                 new ExposureReset(), | ||||
|                 new FocusReset(), | ||||
|                 new WhiteBalanceReset() | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public BaseAction getAction() { | ||||
|         return action; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,85 @@ | ||||
| package com.otaliastudios.cameraview.engine.meter; | ||||
| 
 | ||||
| import android.hardware.camera2.CameraCharacteristics; | ||||
| import android.hardware.camera2.CaptureRequest; | ||||
| import android.hardware.camera2.CaptureResult; | ||||
| import android.hardware.camera2.TotalCaptureResult; | ||||
| import android.hardware.camera2.params.MeteringRectangle; | ||||
| import android.os.Build; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.RequiresApi; | ||||
| 
 | ||||
| import com.otaliastudios.cameraview.CameraLogger; | ||||
| import com.otaliastudios.cameraview.engine.action.ActionHolder; | ||||
| 
 | ||||
| import java.util.List; | ||||
| 
 | ||||
| @RequiresApi(Build.VERSION_CODES.LOLLIPOP) | ||||
| public class WhiteBalanceMeter extends BaseMeter { | ||||
| 
 | ||||
|     private static final String TAG = WhiteBalanceMeter.class.getSimpleName(); | ||||
|     private static final CameraLogger LOG = CameraLogger.create(TAG); | ||||
| 
 | ||||
|     public WhiteBalanceMeter(@NonNull List<MeteringRectangle> areas, boolean skipIfPossible) { | ||||
|         super(areas, skipIfPossible); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected boolean checkIsSupported(@NonNull ActionHolder holder) { | ||||
|         boolean isNotLegacy = readCharacteristic(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL, -1) | ||||
|                 != CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY; | ||||
|         Integer awbMode = holder.getBuilder(this).get(CaptureRequest.CONTROL_AWB_MODE); | ||||
|         boolean result = isNotLegacy && awbMode != null && awbMode == CaptureRequest.CONTROL_AWB_MODE_AUTO; | ||||
|         LOG.i("checkIsSupported:", result); | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected boolean checkShouldSkip(@NonNull ActionHolder holder) { | ||||
|         Integer awbState = holder.getLastResult(this).get(CaptureResult.CONTROL_AWB_STATE); | ||||
|         boolean result = awbState != null && awbState == CaptureRequest.CONTROL_AWB_STATE_CONVERGED; | ||||
|         LOG.i("checkShouldSkip:", result); | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onStarted(@NonNull ActionHolder holder, @NonNull List<MeteringRectangle> areas) { | ||||
|         LOG.i("onStarted:", "with areas:", areas); | ||||
|         int maxRegions = readCharacteristic(CameraCharacteristics.CONTROL_MAX_REGIONS_AWB, 0); | ||||
|         if (!areas.isEmpty() && maxRegions > 0) { | ||||
|             int max = Math.min(maxRegions, areas.size()); | ||||
|             holder.getBuilder(this).set(CaptureRequest.CONTROL_AWB_REGIONS, | ||||
|                     areas.subList(0, max).toArray(new MeteringRectangle[]{})); | ||||
|             holder.applyBuilder(this); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCaptureCompleted(@NonNull ActionHolder holder, @NonNull CaptureRequest request, | ||||
|                                    @NonNull TotalCaptureResult result) { | ||||
|         super.onCaptureCompleted(holder, request, result); | ||||
|         Integer awbState = result.get(CaptureResult.CONTROL_AWB_STATE); | ||||
|         LOG.i("onCaptureCompleted:", "awbState:", awbState); | ||||
|         if (awbState == null) return; | ||||
| 
 | ||||
|         switch (awbState) { | ||||
|             case CaptureRequest.CONTROL_AWB_STATE_CONVERGED: { | ||||
|                 setSuccessful(true); | ||||
|                 setState(STATE_COMPLETED); | ||||
|                 break; | ||||
|             } | ||||
|             case CaptureRequest.CONTROL_AWB_STATE_LOCKED: { | ||||
|                 // Nothing we can do if AWB was locked.
 | ||||
|                 setSuccessful(false); | ||||
|                 setState(STATE_COMPLETED); | ||||
|                 break; | ||||
|             } | ||||
|             case CaptureRequest.CONTROL_AWB_STATE_INACTIVE: | ||||
|             case CaptureRequest.CONTROL_AWB_STATE_SEARCHING: { | ||||
|                 // Wait...
 | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,40 @@ | ||||
| package com.otaliastudios.cameraview.engine.meter; | ||||
| 
 | ||||
| import android.hardware.camera2.CameraCharacteristics; | ||||
| import android.hardware.camera2.CaptureRequest; | ||||
| import android.hardware.camera2.CaptureResult; | ||||
| import android.hardware.camera2.TotalCaptureResult; | ||||
| import android.hardware.camera2.params.MeteringRectangle; | ||||
| import android.os.Build; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.annotation.RequiresApi; | ||||
| 
 | ||||
| import com.otaliastudios.cameraview.CameraLogger; | ||||
| import com.otaliastudios.cameraview.engine.action.ActionHolder; | ||||
| 
 | ||||
| import java.util.List; | ||||
| 
 | ||||
| @RequiresApi(Build.VERSION_CODES.LOLLIPOP) | ||||
| public class WhiteBalanceReset extends BaseReset { | ||||
| 
 | ||||
|     private static final String TAG = WhiteBalanceReset.class.getSimpleName(); | ||||
|     private static final CameraLogger LOG = CameraLogger.create(TAG); | ||||
| 
 | ||||
|     @SuppressWarnings("WeakerAccess") | ||||
|     public WhiteBalanceReset() { | ||||
|         super(true); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onStarted(@NonNull ActionHolder holder, @Nullable MeteringRectangle area) { | ||||
|         LOG.w("onStarted:", "with area:", area); | ||||
|         int maxRegions = readCharacteristic(CameraCharacteristics.CONTROL_MAX_REGIONS_AWB, 0); | ||||
|         if (area != null && maxRegions > 0) { | ||||
|             holder.getBuilder(this).set(CaptureRequest.CONTROL_AWB_REGIONS, new MeteringRectangle[]{area}); | ||||
|             holder.applyBuilder(this); | ||||
|         } | ||||
|         setState(STATE_COMPLETED); | ||||
|     } | ||||
| } | ||||
| @ -1,101 +0,0 @@ | ||||
| package com.otaliastudios.cameraview.engine.metering; | ||||
| 
 | ||||
| import android.hardware.camera2.CameraCharacteristics; | ||||
| import android.hardware.camera2.CaptureRequest; | ||||
| import android.hardware.camera2.CaptureResult; | ||||
| import android.hardware.camera2.params.MeteringRectangle; | ||||
| import android.os.Build; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.RequiresApi; | ||||
| 
 | ||||
| import com.otaliastudios.cameraview.CameraLogger; | ||||
| 
 | ||||
| import java.util.List; | ||||
| 
 | ||||
| @RequiresApi(Build.VERSION_CODES.LOLLIPOP) | ||||
| public class AutoExposure extends MeteringParameter { | ||||
| 
 | ||||
|     private static final String TAG = AutoExposure.class.getSimpleName(); | ||||
|     private static final CameraLogger LOG = CameraLogger.create(TAG); | ||||
| 
 | ||||
|     private boolean isStarted; | ||||
| 
 | ||||
|     @Override | ||||
|     public void startMetering(@NonNull CameraCharacteristics characteristics, | ||||
|                               @NonNull CaptureRequest.Builder builder, | ||||
|                               @NonNull List<MeteringRectangle> areas) { | ||||
|         isSuccessful = false; | ||||
|         isMetered = false; | ||||
|         isStarted = false; | ||||
| 
 | ||||
|         boolean isNotLegacy = readCharacteristic(characteristics, | ||||
|                 CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL, -1) != | ||||
|                 CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY; | ||||
|         Integer aeMode = builder.get(CaptureRequest.CONTROL_AE_MODE); | ||||
|         boolean isAEOn = aeMode != null && | ||||
|                 (aeMode == CameraCharacteristics.CONTROL_AE_MODE_ON | ||||
|                         || aeMode == CameraCharacteristics.CONTROL_AE_MODE_ON_ALWAYS_FLASH | ||||
|                         || aeMode == CameraCharacteristics.CONTROL_AE_MODE_ON_AUTO_FLASH | ||||
|                         || aeMode == CameraCharacteristics.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE | ||||
|                         || aeMode == 5 /* CameraCharacteristics.CONTROL_AE_MODE_ON_EXTERNAL_FLASH, API 28 */); | ||||
|         isSupported = isNotLegacy && isAEOn; | ||||
| 
 | ||||
|         if (isSupported) { | ||||
|             builder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, | ||||
|                     CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); | ||||
|         } | ||||
| 
 | ||||
|         // Even if precapture is not supported, check the regions anyway.
 | ||||
|         int maxRegions = readCharacteristic(characteristics, | ||||
|                 CameraCharacteristics.CONTROL_MAX_REGIONS_AE, 0); | ||||
|         if (maxRegions > 0) { | ||||
|             int max = Math.min(maxRegions, areas.size()); | ||||
|             builder.set(CaptureRequest.CONTROL_AE_REGIONS, | ||||
|                     areas.subList(0, max).toArray(new MeteringRectangle[]{})); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCapture(@NonNull CaptureResult result) { | ||||
|         if (isMetered || !isSupported) return; | ||||
|         Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); | ||||
|         LOG.i("onCapture:", "aeState:", aeState); | ||||
|         if (aeState == null) return; | ||||
| 
 | ||||
|         if (!isStarted) { | ||||
|             if (aeState == CaptureRequest.CONTROL_AE_STATE_PRECAPTURE) { | ||||
|                 isStarted = true; | ||||
|             } else if (aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED | ||||
|                     || aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED) { | ||||
|                 // PRECAPTURE is a transient state, so also check for the final states.
 | ||||
|                 isMetered = true; | ||||
|                 isSuccessful = true; | ||||
|             } | ||||
|         } else { | ||||
|             if (aeState == CaptureRequest.CONTROL_AE_STATE_CONVERGED | ||||
|                     || aeState == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED) { | ||||
|                 isMetered = true; | ||||
|                 isSuccessful = true; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void resetMetering(@NonNull CameraCharacteristics characteristics, | ||||
|                               @NonNull CaptureRequest.Builder builder, | ||||
|                               @NonNull MeteringRectangle area) { | ||||
|         int maxRegions = readCharacteristic(characteristics, | ||||
|                 CameraCharacteristics.CONTROL_MAX_REGIONS_AE, 0); | ||||
|         if (maxRegions > 0) { | ||||
|             builder.set(CaptureRequest.CONTROL_AE_REGIONS, new MeteringRectangle[]{area}); | ||||
|         } | ||||
|         if (isSupported) { | ||||
|             // Cleanup any precapture sequence.
 | ||||
|             if (Build.VERSION.SDK_INT >= 23) { | ||||
|                 builder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, | ||||
|                         CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -1,87 +0,0 @@ | ||||
| package com.otaliastudios.cameraview.engine.metering; | ||||
| 
 | ||||
| import android.hardware.camera2.CameraCharacteristics; | ||||
| import android.hardware.camera2.CaptureRequest; | ||||
| import android.hardware.camera2.CaptureResult; | ||||
| import android.hardware.camera2.params.MeteringRectangle; | ||||
| import android.os.Build; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.RequiresApi; | ||||
| 
 | ||||
| import com.otaliastudios.cameraview.CameraLogger; | ||||
| 
 | ||||
| import java.util.List; | ||||
| 
 | ||||
| @RequiresApi(Build.VERSION_CODES.LOLLIPOP) | ||||
| public class AutoFocus extends MeteringParameter { | ||||
| 
 | ||||
|     private static final String TAG = AutoFocus.class.getSimpleName(); | ||||
|     private static final CameraLogger LOG = CameraLogger.create(TAG); | ||||
| 
 | ||||
|     @Override | ||||
|     public void startMetering(@NonNull CameraCharacteristics characteristics, | ||||
|                               @NonNull CaptureRequest.Builder builder, | ||||
|                               @NonNull List<MeteringRectangle> areas) { | ||||
|         isSuccessful = false; | ||||
|         isMetered = false; | ||||
| 
 | ||||
|         Integer afMode = builder.get(CaptureRequest.CONTROL_AF_MODE); | ||||
|         // Exclude OFF and EDOF as per docs.
 | ||||
|         isSupported = afMode != null && | ||||
|                 (afMode == CameraCharacteristics.CONTROL_AF_MODE_AUTO | ||||
|                 || afMode == CameraCharacteristics.CONTROL_AF_MODE_CONTINUOUS_PICTURE | ||||
|                 || afMode == CameraCharacteristics.CONTROL_AF_MODE_CONTINUOUS_VIDEO | ||||
|                 || afMode == CameraCharacteristics.CONTROL_AF_MODE_MACRO); | ||||
|         if (isSupported) { | ||||
|             builder.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); | ||||
|         } | ||||
| 
 | ||||
|         // Even if auto is not supported, change the regions anyway.
 | ||||
|         int maxRegions = readCharacteristic(characteristics, CameraCharacteristics.CONTROL_MAX_REGIONS_AF, 0); | ||||
|         if (maxRegions > 0) { | ||||
|             int max = Math.min(maxRegions, areas.size()); | ||||
|             builder.set(CaptureRequest.CONTROL_AF_REGIONS, | ||||
|                     areas.subList(0, max).toArray(new MeteringRectangle[]{})); | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCapture(@NonNull CaptureResult result) { | ||||
|         if (isMetered || !isSupported) return; | ||||
|         Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); | ||||
|         LOG.i("onCapture:", "afState:", afState); | ||||
|         if (afState == null) return; | ||||
|         switch (afState) { | ||||
|             case CaptureRequest.CONTROL_AF_STATE_FOCUSED_LOCKED: { | ||||
|                 isMetered = true; | ||||
|                 isSuccessful = true; | ||||
|                 break; | ||||
|             } | ||||
|             case CaptureRequest.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED: { | ||||
|                 isMetered = true; | ||||
|                 isSuccessful = false; | ||||
|                 break; | ||||
|             } | ||||
|             case CaptureRequest.CONTROL_AF_STATE_INACTIVE: break; | ||||
|             case CaptureRequest.CONTROL_AF_STATE_ACTIVE_SCAN: break; | ||||
|             default: break; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void resetMetering(@NonNull CameraCharacteristics characteristics, | ||||
|                               @NonNull CaptureRequest.Builder builder, | ||||
|                               @NonNull MeteringRectangle area) { | ||||
|         int maxRegions = readCharacteristic(characteristics, | ||||
|                 CameraCharacteristics.CONTROL_MAX_REGIONS_AF, 0); | ||||
|         if (maxRegions > 0) { | ||||
|             builder.set(CaptureRequest.CONTROL_AF_REGIONS, new MeteringRectangle[]{area}); | ||||
|         } | ||||
| 
 | ||||
|         if (isSupported) { // Cleanup any trigger.
 | ||||
|             builder.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_CANCEL); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -1,81 +0,0 @@ | ||||
| package com.otaliastudios.cameraview.engine.metering; | ||||
| 
 | ||||
| import android.hardware.camera2.CameraCharacteristics; | ||||
| import android.hardware.camera2.CaptureRequest; | ||||
| import android.hardware.camera2.CaptureResult; | ||||
| import android.hardware.camera2.params.MeteringRectangle; | ||||
| import android.os.Build; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.RequiresApi; | ||||
| 
 | ||||
| import com.otaliastudios.cameraview.CameraLogger; | ||||
| 
 | ||||
| import java.util.List; | ||||
| 
 | ||||
| @RequiresApi(Build.VERSION_CODES.LOLLIPOP) | ||||
| public class AutoWhiteBalance extends MeteringParameter { | ||||
| 
 | ||||
|     private static final String TAG = AutoWhiteBalance.class.getSimpleName(); | ||||
|     private static final CameraLogger LOG = CameraLogger.create(TAG); | ||||
| 
 | ||||
|     @Override | ||||
|     public void startMetering(@NonNull CameraCharacteristics characteristics, | ||||
|                               @NonNull CaptureRequest.Builder builder, | ||||
|                               @NonNull List<MeteringRectangle> areas) { | ||||
|         isSuccessful = false; | ||||
|         isMetered = false; | ||||
| 
 | ||||
|         boolean isNotLegacy = readCharacteristic(characteristics, | ||||
|                 CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL, -1) != | ||||
|                 CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY; | ||||
|         Integer awbMode = builder.get(CaptureRequest.CONTROL_AWB_MODE); | ||||
|         isSupported = isNotLegacy && awbMode != null && awbMode == CaptureRequest.CONTROL_AWB_MODE_AUTO; | ||||
| 
 | ||||
|         if (isSupported) { | ||||
|             // Remove any lock. We're not setting any, but just in case.
 | ||||
|             builder.set(CaptureRequest.CONTROL_AWB_LOCK, false); | ||||
|         } | ||||
| 
 | ||||
|         // Even if auto is not supported, change the regions anyway.
 | ||||
|         int maxRegions = readCharacteristic(characteristics, | ||||
|                 CameraCharacteristics.CONTROL_MAX_REGIONS_AWB, 0); | ||||
|         if (maxRegions > 0) { | ||||
|             int max = Math.min(maxRegions, areas.size()); | ||||
|             builder.set(CaptureRequest.CONTROL_AWB_REGIONS, | ||||
|                     areas.subList(0, max).toArray(new MeteringRectangle[]{})); | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCapture(@NonNull CaptureResult result) { | ||||
|         if (isMetered || !isSupported) return; | ||||
|         Integer awbState = result.get(CaptureResult.CONTROL_AWB_STATE); | ||||
|         LOG.i("onCapture:", "awbState:", awbState); | ||||
|         if (awbState == null) return; | ||||
| 
 | ||||
|         switch (awbState) { | ||||
|             case CaptureRequest.CONTROL_AWB_STATE_CONVERGED: { | ||||
|                 isMetered = true; | ||||
|                 isSuccessful = true; | ||||
|                 break; | ||||
|             } | ||||
|             case CaptureRequest.CONTROL_AWB_STATE_LOCKED: break; | ||||
|             case CaptureRequest.CONTROL_AWB_STATE_INACTIVE: break; | ||||
|             case CaptureRequest.CONTROL_AWB_STATE_SEARCHING: break; | ||||
|             default: break; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void resetMetering(@NonNull CameraCharacteristics characteristics, | ||||
|                               @NonNull CaptureRequest.Builder builder, | ||||
|                               @NonNull MeteringRectangle area) { | ||||
|         int maxRegions = readCharacteristic(characteristics, | ||||
|                 CameraCharacteristics.CONTROL_MAX_REGIONS_AWB, 0); | ||||
|         if (maxRegions > 0) { | ||||
|             builder.set(CaptureRequest.CONTROL_AWB_REGIONS, new MeteringRectangle[]{area}); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -1,54 +0,0 @@ | ||||
| package com.otaliastudios.cameraview.engine.metering; | ||||
| 
 | ||||
| import android.hardware.camera2.CameraCharacteristics; | ||||
| import android.hardware.camera2.CaptureRequest; | ||||
| import android.hardware.camera2.CaptureResult; | ||||
| import android.hardware.camera2.params.MeteringRectangle; | ||||
| import android.os.Build; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.RequiresApi; | ||||
| 
 | ||||
| import java.util.List; | ||||
| 
 | ||||
| @RequiresApi(Build.VERSION_CODES.LOLLIPOP) | ||||
| public abstract class MeteringParameter { | ||||
| 
 | ||||
|     @SuppressWarnings("WeakerAccess") | ||||
|     protected boolean isSupported; | ||||
| 
 | ||||
|     @SuppressWarnings("WeakerAccess") | ||||
|     protected boolean isSuccessful; | ||||
| 
 | ||||
|     @SuppressWarnings("WeakerAccess") | ||||
|     protected boolean isMetered; | ||||
| 
 | ||||
|     @SuppressWarnings("WeakerAccess") | ||||
|     @NonNull | ||||
|     protected <T> T readCharacteristic(@NonNull CameraCharacteristics characteristics, | ||||
|                                        @NonNull CameraCharacteristics.Key<T> key, | ||||
|                                        @NonNull T fallback) { | ||||
|         T value = characteristics.get(key); | ||||
|         return value == null ? fallback : value; | ||||
|     } | ||||
| 
 | ||||
|     public final boolean isMetered() { | ||||
|         // A non supported parameter should always appear as metered
 | ||||
|         return isMetered || !isSupported; | ||||
|     } | ||||
| 
 | ||||
|     public final boolean isSuccessful() { | ||||
|         // A non supported parameter should always appear as successful
 | ||||
|         return isSuccessful || !isSupported; | ||||
|     } | ||||
| 
 | ||||
|     public abstract void startMetering(@NonNull CameraCharacteristics characteristics, | ||||
|                        @NonNull CaptureRequest.Builder builder, | ||||
|                        @NonNull List<MeteringRectangle> areas); | ||||
| 
 | ||||
|     public abstract void resetMetering(@NonNull CameraCharacteristics characteristics, | ||||
|                        @NonNull CaptureRequest.Builder builder, | ||||
|                        @NonNull MeteringRectangle area); | ||||
| 
 | ||||
|     public abstract void onCapture(@NonNull CaptureResult result); | ||||
| } | ||||
| @ -0,0 +1,139 @@ | ||||
| package com.otaliastudios.cameraview.picture; | ||||
| 
 | ||||
| import android.graphics.SurfaceTexture; | ||||
| import android.hardware.camera2.CameraAccessException; | ||||
| import android.hardware.camera2.CaptureRequest; | ||||
| import android.hardware.camera2.CaptureResult; | ||||
| import android.hardware.camera2.TotalCaptureResult; | ||||
| import android.os.Build; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.RequiresApi; | ||||
| 
 | ||||
| import com.otaliastudios.cameraview.CameraLogger; | ||||
| import com.otaliastudios.cameraview.PictureResult; | ||||
| import com.otaliastudios.cameraview.engine.Camera2Engine; | ||||
| import com.otaliastudios.cameraview.engine.action.Action; | ||||
| import com.otaliastudios.cameraview.engine.action.ActionHolder; | ||||
| import com.otaliastudios.cameraview.engine.action.Actions; | ||||
| import com.otaliastudios.cameraview.engine.action.BaseAction; | ||||
| import com.otaliastudios.cameraview.engine.action.CompletionCallback; | ||||
| import com.otaliastudios.cameraview.engine.lock.LockAction; | ||||
| import com.otaliastudios.cameraview.preview.GlCameraPreview; | ||||
| import com.otaliastudios.cameraview.size.AspectRatio; | ||||
| 
 | ||||
| /** | ||||
|  * Wraps {@link SnapshotGlPictureRecorder} for Camera2. | ||||
|  * | ||||
|  * Camera2 engine supports metering for snapshots and we expect for them to correctly fire flash as well. | ||||
|  * The first idea, and in theory, the most correct one, was to set {@link CaptureRequest#CONTROL_CAPTURE_INTENT} | ||||
|  * to {@link CaptureRequest#CONTROL_CAPTURE_INTENT_STILL_CAPTURE}. | ||||
|  * | ||||
|  * According to documentation, this will automatically trigger the flash if parameters says so. | ||||
|  * In fact this is what happens, but it is a very fast flash that only lasts for 1 or 2 frames. | ||||
|  * It's not easy to call super.take() at the exact time so that we capture the frame that was lit. | ||||
|  * I have tried by comparing {@link SurfaceTexture#getTimestamp()} and {@link CaptureResult#SENSOR_TIMESTAMP} | ||||
|  * to identify the correct frame. These timestamps match, but the frame is not the correct one. | ||||
|  * | ||||
|  * So what we do here is ignore the {@link CaptureRequest#CONTROL_CAPTURE_INTENT} and instead open the | ||||
|  * torch, if requested to do so. Then wait for exposure to settle again and finally take a snapshot. | ||||
|  * I'd still love to use the capture intent instead of this, but was not able yet. | ||||
|  */ | ||||
| @RequiresApi(Build.VERSION_CODES.LOLLIPOP) | ||||
| public class Snapshot2PictureRecorder extends SnapshotGlPictureRecorder { | ||||
| 
 | ||||
|     private final static String TAG = Snapshot2PictureRecorder.class.getSimpleName(); | ||||
|     private final static CameraLogger LOG = CameraLogger.create(TAG); | ||||
|     private final static long LOCK_TIMEOUT = 2500; | ||||
| 
 | ||||
|     private static class FlashAction extends BaseAction { | ||||
| 
 | ||||
|         @Override | ||||
|         protected void onStart(@NonNull ActionHolder holder) { | ||||
|             super.onStart(holder); | ||||
|             LOG.i("FlashAction:", "Parameters locked, opening torch."); | ||||
|             holder.getBuilder(this).set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH); | ||||
|             holder.getBuilder(this).set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); | ||||
|             holder.applyBuilder(this); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void onCaptureCompleted(@NonNull ActionHolder holder, | ||||
|                                        @NonNull CaptureRequest request, | ||||
|                                        @NonNull TotalCaptureResult result) { | ||||
|             super.onCaptureCompleted(holder, request, result); | ||||
|             Integer flashState = result.get(CaptureResult.FLASH_STATE); | ||||
|             if (flashState == null) { | ||||
|                 LOG.w("FlashAction:", "Waiting flash, but flashState is null! Taking snapshot."); | ||||
|                 setState(STATE_COMPLETED); | ||||
|             } else if (flashState == CaptureResult.FLASH_STATE_FIRED) { | ||||
|                 LOG.i("FlashAction:", "Waiting flash and we have FIRED state! Taking snapshot."); | ||||
|                 setState(STATE_COMPLETED); | ||||
|             } else { | ||||
|                 LOG.i("FlashAction:", "Waiting flash but flashState is", | ||||
|                         flashState, ". Waiting..."); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private final Action mAction; | ||||
|     private final ActionHolder mHolder; | ||||
|     private final boolean mActionNeeded; | ||||
|     private Integer mOriginalAeMode; | ||||
|     private Integer mOriginalFlashMode; | ||||
| 
 | ||||
|     public Snapshot2PictureRecorder(@NonNull PictureResult.Stub stub, | ||||
|                                     @NonNull Camera2Engine engine, | ||||
|                                     @NonNull GlCameraPreview preview, | ||||
|                                     @NonNull AspectRatio outputRatio) { | ||||
|         super(stub, engine, preview, outputRatio); | ||||
|         mHolder = engine; | ||||
| 
 | ||||
|         mAction = Actions.sequence( | ||||
|                 Actions.timeout(LOCK_TIMEOUT, new LockAction()), | ||||
|                 new FlashAction()); | ||||
|         mAction.addCallback(new CompletionCallback() { | ||||
|             @Override | ||||
|             protected void onActionCompleted(@NonNull Action action) { | ||||
|                 LOG.i("Taking picture with super.take()."); | ||||
|                 Snapshot2PictureRecorder.super.take(); | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         Integer aeState = mHolder.getLastResult(mAction).get(CaptureResult.CONTROL_AE_STATE); | ||||
|         mActionNeeded = engine.getPictureSnapshotMetering() | ||||
|                 && aeState != null | ||||
|                 && aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED; | ||||
|         mOriginalAeMode = mHolder.getBuilder(mAction).get(CaptureRequest.CONTROL_AE_MODE); | ||||
|         mOriginalFlashMode = mHolder.getBuilder(mAction).get(CaptureRequest.FLASH_MODE); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void take() { | ||||
|         if (!mActionNeeded) { | ||||
|             LOG.i("take:", "Engine does no metering or needs no flash, taking fast snapshot."); | ||||
|             super.take(); | ||||
|         } else { | ||||
|             LOG.i("take:", "Engine needs flash. Starting action"); | ||||
|             mAction.start(mHolder); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void dispatchResult() { | ||||
|         // Revert our changes.
 | ||||
|         LOG.i("dispatchResult:", "Reverting the flash changes."); | ||||
|         try { | ||||
|             // See Camera2Engine.setFlash() comments: turning TORCH off has bugs and we must do
 | ||||
|             // as follows.
 | ||||
|             CaptureRequest.Builder builder = mHolder.getBuilder(mAction); | ||||
|             builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); | ||||
|             builder.set(CaptureRequest.FLASH_MODE, CaptureResult.FLASH_MODE_OFF); | ||||
|             mHolder.applyBuilder(mAction, builder); | ||||
|             builder.set(CaptureRequest.CONTROL_AE_MODE, mOriginalAeMode); | ||||
|             builder.set(CaptureRequest.FLASH_MODE, mOriginalFlashMode); | ||||
|             mHolder.applyBuilder(mAction); | ||||
|         } catch (CameraAccessException ignore) {} | ||||
|         super.dispatchResult(); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,158 @@ | ||||
| --- | ||||
| layout: page | ||||
| title: "Metering" | ||||
| subtitle: "Exposure and metering controls" | ||||
| description: "Exposure and metering controls" | ||||
| category: docs | ||||
| order: 4 | ||||
| date: 2019-09-04 19:39:03 | ||||
| disqus: 1 | ||||
| --- | ||||
| 
 | ||||
| In CameraView grammar, metering is the act of measuring the scene brightness, colors and focus | ||||
| distance in order to automatically adapt the camera exposure, focus and white balance (AE, AF and AWB, | ||||
| often referred as 3A). | ||||
| 
 | ||||
| We treat three different types on metering: [continuous metering](#continuous-metering),  | ||||
| [picture metering](#picture-metering) and [touch metering](#touch-metering). | ||||
| 
 | ||||
| You can also apply adjustment to the metered exposure through the [exposure correction](#exposure-correction) control. | ||||
| 
 | ||||
| ### Continuous Metering | ||||
| 
 | ||||
| By default, and if the device supports it, all three routines (AE, AF, AWB) are continuously metered | ||||
| as the device moves or the scene changes. | ||||
| 
 | ||||
| - For AE, this is always enabled if supported | ||||
| - For AF, this is always enabled if supported | ||||
| - For AWB, this is enabled if the `WhiteBalance` parameter is set to `AUTO` [[docs]](#controls.html#camerawhitebalance) | ||||
| 
 | ||||
| ### Picture Metering | ||||
| 
 | ||||
| *In Camera1, picture metering is always enabled for pictures, and always disabled for picture snapshots.  | ||||
| The following applies to Camera2 only.* | ||||
| 
 | ||||
| The camera engine will try to trigger metering when a picture is requested, either with `takePicture()` | ||||
| or `takePictureSnapshot()`. This has two obvious consequences: | ||||
| 
 | ||||
| - improves the picture quality | ||||
| - increases the latency, because metering takes time | ||||
| 
 | ||||
| For these reasons, picture metering is **enabled** by default for HQ pictures and **disabled** by | ||||
| default for picture snapshots. However, the behavior can be changed with two flags and their | ||||
| respective XML attributes: | ||||
| 
 | ||||
| ```java | ||||
| cameraView.setPictureMetering(true); // Meter before takePicture() | ||||
| cameraView.setPictureMetering(false); // Don't | ||||
| cameraView.setPictureSnapshotMetering(true); // Meter before takePictureSnapshot() | ||||
| cameraView.setPictureSnapshotMetering(false); // Don't | ||||
| ``` | ||||
| 
 | ||||
| ### Touch Metering | ||||
| 
 | ||||
| Touch metering is triggered by either a [Gesture](gestures.html) or by the developer itself, which | ||||
| can start touch metering on a specific point with the `startAutoFocus(float, float)` API. | ||||
| This action needs the coordinates of a point computed with respect to the view width and height. | ||||
| 
 | ||||
| In both cases, the metering callbacks will be triggered: | ||||
| 
 | ||||
| ```java | ||||
| cameraView.addCameraListener(new CameraListener() { | ||||
|      | ||||
|     @Override | ||||
|     public void onAutoFocusStart(@NonNull PointF point) { | ||||
|         // Touch metering was started by a gesture or by startAutoFocus(float, float). | ||||
|         // The camera is currently trying to meter around that area. | ||||
|         // This can be used to draw things on screen. | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onAutoFocusEnd(boolean successful, @NonNull PointF point) { | ||||
|         // Touch metering operation just ended. If successful, the camera will have converged | ||||
|         // to a new focus point, and possibly new exposure and white balance as well. | ||||
|         // The point is the same that was passed to onAutoFocusStart. | ||||
|     } | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| Touch metering is not guaranteed to be supported: check the `CameraOptions` to be sure. | ||||
| 
 | ||||
| ##### Touch Metering Markers | ||||
| 
 | ||||
| You can set a marker for drawing on screen in response to touch metering events. | ||||
| In XML, you should pass the qualified class name of your marker. | ||||
| 
 | ||||
| ```java | ||||
| cameraView.setAutoFocusMarker(null); | ||||
| cameraView.setAutoFocusMarker(marker); | ||||
| ``` | ||||
| 
 | ||||
| We offer a default marker (similar to the old `focusWithMarker` attribute in v1), | ||||
| which you can set in XML using the `@string/cameraview_default_autofocus_marker` resource, | ||||
| or programmatically: | ||||
| 
 | ||||
| ```java | ||||
| cameraView.setAutoFocusMarker(new DefaultAutoFocusMarker()); | ||||
| ``` | ||||
| 
 | ||||
| ##### Touch Metering Reset Delay | ||||
| 
 | ||||
| You control control how a touch metering operation is reset after completed. | ||||
| Setting a value <= 0 or == Long.MAX_VALUE will not reset the metering values. | ||||
| This is useful for low end devices that have slow auto-focus capabilities. | ||||
| Defaults to 3 seconds. | ||||
| 
 | ||||
| ```java | ||||
| cameraView.setCameraAutoFocusResetDelay(1000);  // 1 second | ||||
| cameraView.setCameraAutoFocusResetDelay(0);  // NO reset | ||||
| cameraView.setCameraAutoFocusResetDelay(-1);  // NO reset | ||||
| cameraView.setCameraAutoFocusResetDelay(Long.MAX_VALUE);  // NO reset | ||||
| ``` | ||||
| 
 | ||||
| ### Exposure correction | ||||
| 
 | ||||
| There are two ways to control the exposure correction value: | ||||
| 
 | ||||
| - User can change the exposure correction with a [Gesture](gestures.html) | ||||
| - The developer can change this value with the `setExposureCorrection(float)` API, passing in the EV | ||||
|   value, in camera stops. This value should be contained in the minimum and maximum supported values, | ||||
|   as returned by `CameraOptions`. | ||||
| 
 | ||||
| Both actions will trigger the exposure correction callback, which can be used, for example, to draw a seek bar: | ||||
| 
 | ||||
| ```java | ||||
| cameraView.addCameraListener(new CameraListener() { | ||||
|      | ||||
|     @UiThread | ||||
|     public void onExposureCorrectionChanged(float newValue, @NonNull float[] bounds, @Nullable PointF[] fingers) { | ||||
|         // newValue: the new correction value | ||||
|         // bounds: min and max bounds for newValue, as returned by CameraOptions | ||||
|         // fingers: finger positions that caused the event, null if not caused by touch | ||||
|     } | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| EV correction is not guaranteed to be supported: check the `CameraOptions` to be sure. | ||||
| 
 | ||||
| ### Related XML Attributes | ||||
| 
 | ||||
| ```xml | ||||
| <com.otaliastudios.cameraview.CameraView | ||||
|     app:cameraPictureMetering="true|false" | ||||
|     app:cameraPictureSnapshotMetering="false|true" | ||||
|     app:cameraAutoFocusMarker="@string/cameraview_default_autofocus_marker" | ||||
|     app:cameraAutoFocusResetDelay="3000"/> | ||||
| ``` | ||||
| 
 | ||||
| ### Related APIs | ||||
| 
 | ||||
| |Method|Description| | ||||
| |------|-----------| | ||||
| |`setPictureMetering(boolean)`|Whether the engine should trigger 3A metering when a picture is requested. Defaults to true.| | ||||
| |`setPictureSnapshotMetering(boolean)`|Whether the engine should trigger 3A metering when a picture snapshot is requested. Defaults to false.| | ||||
| |`startAutoFocus(float, float)`|Starts the 3A touch metering routine at the given coordinates, with respect to the view system.| | ||||
| |`CameraOptions.isAutoFocusSupported()`|Whether touch metering (metering with respect to a specific region of the screen) is supported.| | ||||
| |`setExposureCorrection(float)`|Changes the exposure adjustment, in EV stops. A positive value means a brighter picture.| | ||||
| |`CameraOptions.getExposureCorrectionMinValue()`|The minimum value of negative exposure correction, in EV stops.| | ||||
| |`CameraOptions.getExposureCorrectionMaxValue()`|The maximum value of positive exposure correction, in EV stops.| | ||||
					Loading…
					
					
				
		Reference in new issue