diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java index dab96e3b..adc6b6b1 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java @@ -565,7 +565,6 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv @Override protected Task onStopPreview() { LOG.i("onStopPreview:", "About to clean up."); - // TODO clear actions? if (mVideoRecorder != null) { // This should synchronously call onVideoResult that will reset the repeating builder // to the PREVIEW template. This is very important. diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java index 0af40837..f27f4731 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java @@ -227,6 +227,11 @@ public abstract class CameraEngine implements mPreview.setSurfaceCallback(this); } + @NonNull + public CameraPreview getPreview() { + return mPreview; + } + //region Error handling /** diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/Actions.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/Actions.java index f85a0973..f6ba9838 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/Actions.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/Actions.java @@ -109,7 +109,9 @@ public class Actions { } private void increaseRunningAction() { - if (runningAction == actions.size() - 1) { + boolean first = runningAction == -1; + boolean last = runningAction == actions.size() - 1; + if (last) { // This was the last action. We're done. setState(STATE_COMPLETED); } else { @@ -123,6 +125,9 @@ public class Actions { } } }); + if (!first) { + actions.get(runningAction).onStart(getHolder()); + } } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/BaseAction.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/BaseAction.java index 814abcd0..7c13e60d 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/BaseAction.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/BaseAction.java @@ -1,5 +1,6 @@ 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; @@ -100,6 +101,22 @@ public abstract class BaseAction implements Action { return holder; } + + /** + * Reads a characteristic with a fallback. + * @param key key + * @param fallback fallback + * @param key type + * @return value or fallback + */ + @SuppressWarnings("WeakerAccess") + @NonNull + protected T readCharacteristic(@NonNull CameraCharacteristics.Key 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)) { diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/lock/BaseLock.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/lock/BaseLock.java index 5790f11e..d3806443 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/lock/BaseLock.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/lock/BaseLock.java @@ -12,14 +12,6 @@ import com.otaliastudios.cameraview.engine.action.BaseAction; @RequiresApi(Build.VERSION_CODES.LOLLIPOP) public abstract class BaseLock extends BaseAction { - @SuppressWarnings("WeakerAccess") - @NonNull - protected T readCharacteristic(@NonNull CameraCharacteristics.Key key, - @NonNull T fallback) { - T value = getHolder().getCharacteristics(this).get(key); - return value == null ? fallback : value; - } - @Override protected final void onStart(@NonNull ActionHolder holder) { super.onStart(holder); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/BaseMeter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/BaseMeter.java new file mode 100644 index 00000000..f6814191 --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/BaseMeter.java @@ -0,0 +1,55 @@ +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.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 List areas; + private boolean isSuccessful; + private boolean skipIfPossible; + + @SuppressWarnings("WeakerAccess") + protected BaseMeter(@NonNull List 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) { + onStarted(holder, areas); + } else { + setSuccessful(true); + setState(STATE_COMPLETED); + } + } + + protected abstract void onStarted(@NonNull ActionHolder holder, + @NonNull List areas); + + protected abstract boolean checkShouldSkip(@NonNull ActionHolder holder); + + protected abstract boolean checkIsSupported(@NonNull ActionHolder holder); + + @SuppressWarnings("WeakerAccess") + protected void setSuccessful(boolean successful) { + isSuccessful = successful; + } + + public boolean isSuccessful() { + return isSuccessful; + } +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/BaseReset.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/BaseReset.java new file mode 100644 index 00000000..b4ea622d --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/BaseReset.java @@ -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); +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/ExposureMeter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/ExposureMeter.java new file mode 100644 index 00000000..df1da34b --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/ExposureMeter.java @@ -0,0 +1,141 @@ +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; + + public ExposureMeter(@NonNull List areas, boolean skipIfPossible) { + super(areas, skipIfPossible); + } + + // TODO set trigger to null? + + @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 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 + 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; + } + } + } + } +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/ExposureReset.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/ExposureReset.java new file mode 100644 index 00000000..4997e01d --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/ExposureReset.java @@ -0,0 +1,50 @@ +package com.otaliastudios.cameraview.engine.meter; + +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CaptureRequest; +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); + + public ExposureReset(boolean resetArea) { + super(resetArea); + } + + @Override + protected void onStarted(@NonNull ActionHolder holder, @Nullable MeteringRectangle area) { + boolean changed = false; + 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}); + changed = true; + } + + // NOTE: precapture might not be supported, in which case I think it will be ignored. + Integer trigger = holder.getBuilder(this).get(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER); + LOG.w("onStarted:", "current precapture trigger is", trigger); + if (trigger == null || trigger == CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START) { + LOG.w("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); + changed = true; + } + + if (changed) holder.applyBuilder(this); + setState(STATE_COMPLETED); + } +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/FocusMeter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/FocusMeter.java new file mode 100644 index 00000000..7dfc7974 --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/FocusMeter.java @@ -0,0 +1,89 @@ +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 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 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); + } + + // TODO Set trigger to 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; + } + } +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/FocusReset.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/FocusReset.java new file mode 100644 index 00000000..318dee05 --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/FocusReset.java @@ -0,0 +1,47 @@ +package com.otaliastudios.cameraview.engine.meter; + +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CaptureRequest; +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); + + public FocusReset(boolean resetArea) { + super(resetArea); + } + + @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.getBuilder(this).get(CaptureRequest.CONTROL_AF_TRIGGER); + LOG.w("onStarted:", "current 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); + } +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/MeterAction.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/MeterAction.java new file mode 100644 index 00000000..d06a0014 --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/MeterAction.java @@ -0,0 +1,251 @@ +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.params.MeteringRectangle; +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; + +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 List 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()) { + return false; + } + } + return true; + } + + @Override + protected void onStart(@NonNull ActionHolder holder) { + initialize(holder); + super.onStart(holder); + } + + private void initialize(@NonNull ActionHolder holder) { + this.holder = holder; + List 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 = readCharacteristic(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE, + 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 + ); + } +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/MeterResetAction.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/MeterResetAction.java new file mode 100644 index 00000000..91dae114 --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/MeterResetAction.java @@ -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(boolean resetAreas) { + this.action = Actions.together( + new ExposureReset(resetAreas), + new FocusReset(resetAreas), + new WhiteBalanceReset(resetAreas) + ); + } + + @NonNull + @Override + public BaseAction getAction() { + return action; + } +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/WhiteBalanceMeter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/WhiteBalanceMeter.java new file mode 100644 index 00000000..420cd2bf --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/WhiteBalanceMeter.java @@ -0,0 +1,86 @@ +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 com.otaliastudios.cameraview.engine.metering.Parameter; + +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 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 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; + } + } + } +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/WhiteBalanceReset.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/WhiteBalanceReset.java new file mode 100644 index 00000000..6bbfa7e0 --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/WhiteBalanceReset.java @@ -0,0 +1,38 @@ +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); + + public WhiteBalanceReset(boolean resetArea) { + super(resetArea); + } + + @Override + protected void onStarted(@NonNull ActionHolder holder, @Nullable MeteringRectangle 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); + } +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/metering/AutoExposure.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/metering/AutoExposure.java index 8130e416..2bf2c235 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/metering/AutoExposure.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/metering/AutoExposure.java @@ -80,10 +80,6 @@ public class AutoExposure extends Parameter { if (changed) { notifyBuilderChanged(false); - // Remove any problematic control for future requests - // NOTE: activating this invalidates the logic for early exit in processCapture - /* builder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); */ } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/metering/AutoFocus.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/metering/AutoFocus.java index cc378955..720a8b52 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/metering/AutoFocus.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/metering/AutoFocus.java @@ -71,9 +71,6 @@ public class AutoFocus extends Parameter { if (changed) { notifyBuilderChanged(false); - // Remove any problematic control for future requests - /* builder.set(CaptureRequest.CONTROL_AF_TRIGGER, - CaptureRequest.CONTROL_AF_TRIGGER_IDLE); */ } }