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