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