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 11eccf4d..469ff557 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java @@ -52,6 +52,7 @@ import com.otaliastudios.cameraview.internal.utils.CropHelper; import com.otaliastudios.cameraview.internal.utils.ImageHelper; import com.otaliastudios.cameraview.internal.utils.WorkerHandler; import com.otaliastudios.cameraview.picture.Full2PictureRecorder; +import com.otaliastudios.cameraview.picture.Snapshot2PictureRecorder; import com.otaliastudios.cameraview.picture.SnapshotGlPictureRecorder; import com.otaliastudios.cameraview.preview.GlCameraPreview; import com.otaliastudios.cameraview.size.AspectRatio; @@ -81,7 +82,6 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv private CameraCharacteristics mCameraCharacteristics; private CameraCaptureSession mSession; private CaptureRequest.Builder mRepeatingRequestBuilder; - private CaptureRequest mRepeatingRequest; private CameraCaptureSession.CaptureCallback mRepeatingRequestCallback; private CaptureResult mLastRepeatingResult; private final Camera2Mapper mMapper = Camera2Mapper.get(); @@ -216,7 +216,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv private void applyRepeatingRequestBuilder(boolean checkStarted, int errorReason, @Nullable final Runnable onFirstFrame) { if (!checkStarted || getPreviewState() == STATE_STARTED) { try { - mRepeatingRequest = mRepeatingRequestBuilder.build(); + CaptureRequest repeatingRequest = mRepeatingRequestBuilder.build(); final AtomicBoolean firstFrame = new AtomicBoolean(false); mRepeatingRequestCallback = new CameraCaptureSession.CaptureCallback() { @Override @@ -245,13 +245,16 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv if (mPictureRecorder instanceof Full2PictureRecorder) { ((Full2PictureRecorder) mPictureRecorder).onCaptureCompleted(result); } + if (mPictureRecorder instanceof Snapshot2PictureRecorder) { + ((Snapshot2PictureRecorder) mPictureRecorder).onCaptureCompleted(result); + } if (mMeter != null && mMeter.isMetering()) { mMeter.onCapture(result); } } }; - mSession.setRepeatingRequest(mRepeatingRequest, mRepeatingRequestCallback, null); + mSession.setRepeatingRequest(repeatingRequest, mRepeatingRequestCallback, null); } catch (CameraAccessException e) { throw new CameraException(e, errorReason); } @@ -543,6 +546,10 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv mVideoRecorder = null; } mPictureRecorder = null; + if (mMeter != null) { + mMeter.resetMetering(); + mMeter = null; + } if (hasFrameProcessors()) { getFrameManager().release(); } @@ -557,8 +564,6 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv throw createCameraException(e); } removeRepeatingRequestBuilderSurfaces(); - mRepeatingRequest = null; - mMeter = null; LOG.i("onStopPreview:", "Returning."); return Tasks.forResult(null); } @@ -617,17 +622,21 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv stub.size = getUncroppedSnapshotSize(Reference.OUTPUT); // Not the real size: it will be cropped to match the view ratio stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR); // Actually it will be rotated and set to 0. if (doMetering) { + LOG.i("onTakePictureSnapshot:", "doMetering is true. Delaying."); mDelayedPictureStub = stub; mDelayedPictureRatio = outputRatio; startMetering(null, null); } else { + LOG.i("onTakePictureSnapshot:", "doMetering is false. Performing."); if (!(mPreview instanceof GlCameraPreview)) { throw new RuntimeException("takePictureSnapshot with Camera2 is only supported with Preview.GL_SURFACE"); } - mPictureRecorder = new SnapshotGlPictureRecorder(stub, this, + mPictureRecorder = new Snapshot2PictureRecorder(stub, this, (GlCameraPreview) mPreview, outputRatio, - getOverlay()); + mSession, + mRepeatingRequestCallback, + mRepeatingRequestBuilder); mPictureRecorder.take(); } } @@ -636,11 +645,12 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv protected void onTakePicture(@NonNull PictureResult.Stub stub, boolean doMetering) { stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR); stub.size = getPictureSize(Reference.OUTPUT); - if (doMetering) { + LOG.i("onTakePicture:", "doMetering is true. Delaying."); mDelayedPictureStub = stub; startMetering(null, null); } else { + LOG.i("onTakePicture:", "doMetering is false. Performing."); try { CaptureRequest.Builder builder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); applyAllParameters(builder); @@ -771,7 +781,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv * the {@link #createRepeatingRequestBuilder(int)} method. */ private void maybeRestorePreviewTemplateAfterVideo() { - int template = (int) mRepeatingRequest.getTag(); + int template = (int) mRepeatingRequestBuilder.build().getTag(); if (template != CameraDevice.TEMPLATE_PREVIEW) { try { createRepeatingRequestBuilder(CameraDevice.TEMPLATE_PREVIEW); @@ -902,8 +912,9 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv // On some devices, switching from TORCH/OFF to AUTO/ON is not immediately // reflected (for example, torch stays active) unless we do as follows. // It's just a way to wake up the AE routine. - builder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); + // TODO this works but seems to cause other issues. + // builder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + // CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); return true; } } @@ -1221,6 +1232,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv if (point != null) { mCallback.dispatchOnFocusEnd(mMeteringGesture, success, point); } else { + LOG.w("onMeteringEnd - restoring the picture capturing. isSnapshot:", mDelayedPictureStub.isSnapshot); if (mDelayedPictureStub.isSnapshot) { onTakePictureSnapshot(mDelayedPictureStub, mDelayedPictureRatio, false); } else { @@ -1231,17 +1243,6 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv } } - /** - * When metering is reset, we're not sure that the engine is still alive. - * We should check this here. - * @param point point - * @return true if metering can be reset - */ - @Override - public boolean canResetMetering(@Nullable PointF point) { - return getEngineState() == STATE_STARTED; - } - /** * Called by {@link Meter} after resetting the metering parameters. * We should apply them, and also go back to default focus. @@ -1250,8 +1251,10 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv */ @Override public void onMeteringReset(@Nullable PointF point) { - applyDefaultFocus(mRepeatingRequestBuilder); - applyRepeatingRequestBuilder(); // only if preview started already + if (getEngineState() == STATE_STARTED) { + applyDefaultFocus(mRepeatingRequestBuilder); + applyRepeatingRequestBuilder(); // only if preview started already + } } //endregion diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Meter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Meter.java index 8211c70f..bd43fefb 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Meter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Meter.java @@ -70,15 +70,6 @@ public class Meter { * */ void onMeteringReset(@Nullable PointF point); - - /** - * 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 - * @return true if can reset - */ - // TODO is this useful? engine could do its checks onMeteringReset() - boolean canResetMetering(@Nullable PointF point); } private static final String TAG = Meter.class.getSimpleName(); @@ -315,7 +306,6 @@ public class Meter { * a reset call, or if {@link #startMetering(CaptureResult, PointF)} was never called. * @return true if metering */ - @SuppressWarnings("WeakerAccess") public boolean isMetering() { return mIsMetering; } @@ -361,20 +351,18 @@ public class Meter { @SuppressWarnings("WeakerAccess") public void resetMetering() { mEngine.mHandler.remove(mResetRunnable); - if (mCallback.canResetMetering(mPoint)) { - LOG.i("Resetting the meter parameters."); - MeteringRectangle whole = null; - if (mPoint != null) { - // If we have a point, we must reset the metering areas. - Rect wholeRect = mCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); - if (wholeRect == null) wholeRect = new Rect(); - whole = new MeteringRectangle(wholeRect, MeteringRectangle.METERING_WEIGHT_DONT_CARE); - } - mAutoFocus.resetMetering(mCharacteristics, mBuilder, whole); - mAutoWhiteBalance.resetMetering(mCharacteristics, mBuilder, whole); - mAutoExposure.resetMetering(mCharacteristics, mBuilder, whole); - mCallback.onMeteringReset(mPoint); + LOG.i("Resetting the meter parameters."); + MeteringRectangle whole = null; + if (mPoint != null) { + // If we have a point, we must reset the metering areas. + Rect wholeRect = mCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); + if (wholeRect == null) wholeRect = new Rect(); + whole = new MeteringRectangle(wholeRect, MeteringRectangle.METERING_WEIGHT_DONT_CARE); } + mAutoFocus.resetMetering(mCharacteristics, mBuilder, whole); + mAutoWhiteBalance.resetMetering(mCharacteristics, mBuilder, whole); + mAutoExposure.resetMetering(mCharacteristics, mBuilder, whole); + mCallback.onMeteringReset(mPoint); } private Runnable mResetRunnable = new Runnable() { diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Full2PictureRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Full2PictureRecorder.java index f94902bf..f6c8b18e 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Full2PictureRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Full2PictureRecorder.java @@ -38,14 +38,13 @@ public class Full2PictureRecorder extends PictureRecorder implements ImageReader private static final int STATE_WAITING_CAPTURE = 1; private static final int STATE_WAITING_IMAGE = 2; - private static final int REQUEST_TAG = CameraDevice.TEMPLATE_STILL_CAPTURE; - private CameraCaptureSession mSession; private CameraCaptureSession.CaptureCallback mCallback; private ImageReader mPictureReader; private CaptureRequest.Builder mPictureBuilder; private boolean mStopPreviewBeforeCapture; private int mState = STATE_IDLE; + private int mSequenceId; public Full2PictureRecorder(@NonNull PictureResult.Stub stub, @Nullable PictureResultListener listener, @@ -67,7 +66,6 @@ public class Full2PictureRecorder extends PictureRecorder implements ImageReader public void take() { try { mState = STATE_WAITING_CAPTURE; - mPictureBuilder.setTag(REQUEST_TAG); mPictureBuilder.addTarget(mPictureReader.getSurface()); mPictureBuilder.set(CaptureRequest.JPEG_ORIENTATION, mResult.rotation); if (mStopPreviewBeforeCapture) { @@ -79,7 +77,7 @@ public class Full2PictureRecorder extends PictureRecorder implements ImageReader mSession.stopRepeating(); mSession.abortCaptures(); } - mSession.capture(mPictureBuilder.build(), mCallback, null); + mSequenceId = mSession.capture(mPictureBuilder.build(), mCallback, null); } catch (CameraAccessException e) { mResult = null; mError = e; @@ -88,14 +86,16 @@ public class Full2PictureRecorder extends PictureRecorder implements ImageReader } public void onCaptureStarted(@NonNull CaptureRequest request) { - if (request.getTag() == (Integer) REQUEST_TAG) { - dispatchOnShutter(false); + if (mState == STATE_WAITING_CAPTURE) { + if (request.getTag() == mPictureBuilder.build().getTag()) { + dispatchOnShutter(false); + } } } public void onCaptureCompleted(@NonNull TotalCaptureResult result) { if (mState == STATE_WAITING_CAPTURE) { - if (result.getRequest().getTag() == (Integer) REQUEST_TAG) { + if (result.getSequenceId() == mSequenceId) { // This has no real use for now other than logging. LOG.i("onCaptureCompleted:", "Got result, moving to STATE_WAITING_IMAGE"); mState = STATE_WAITING_IMAGE; diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Snapshot2PictureRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Snapshot2PictureRecorder.java new file mode 100644 index 00000000..4b6b88cd --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Snapshot2PictureRecorder.java @@ -0,0 +1,104 @@ +package com.otaliastudios.cameraview.picture; + +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCaptureSession; +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.Nullable; +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.CameraEngine; +import com.otaliastudios.cameraview.overlay.Overlay; +import com.otaliastudios.cameraview.preview.GlCameraPreview; +import com.otaliastudios.cameraview.size.AspectRatio; + +@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 int STATE_IDLE = 0; + private final static int STATE_WAITING_CAPTURE = 1; + private final static int STATE_WAITING_IMAGE = 2; + + private final Camera2Engine mEngine; + private final CameraCaptureSession mSession; + private final CameraCaptureSession.CaptureCallback mCallback; + private final CaptureRequest.Builder mBuilder; + private int mState = STATE_IDLE; + private Integer mOldCaptureIntent; + private int mSequenceId; + + public Snapshot2PictureRecorder(@NonNull PictureResult.Stub stub, + @NonNull Camera2Engine engine, + @NonNull GlCameraPreview preview, + @NonNull AspectRatio outputRatio, + @NonNull CameraCaptureSession session, + @NonNull CameraCaptureSession.CaptureCallback callback, + @NonNull CaptureRequest.Builder builder) { + super(stub, engine, preview, outputRatio, engine.getOverlay()); + mEngine = engine; + mSession = session; + mCallback = callback; + mBuilder = builder; + } + + @Override + public void take() { + if (mEngine.getPictureSnapshotMetering()) { + try { + LOG.i("take:", "Engine does metering, adding our CONTROL_CAPTURE_INTENT."); + mState = STATE_WAITING_CAPTURE; + mOldCaptureIntent = mBuilder.get(CaptureRequest.CONTROL_CAPTURE_INTENT); + mBuilder.set(CaptureRequest.CONTROL_CAPTURE_INTENT, CaptureRequest.CONTROL_CAPTURE_INTENT_STILL_CAPTURE); + mSequenceId = mSession.setRepeatingRequest(mBuilder.build(), mCallback, null); + } catch (CameraAccessException e) { + LOG.w("take:", "Camera error while applying our CONTROL_CAPTURE_INTENT", e); + mResult = null; + mError = e; + dispatchResult(); + } + } else { + super.take(); + } + } + + public void onCaptureCompleted(@NonNull TotalCaptureResult result) { + LOG.i("onCaptureCompleted:", + "aeState:", result.get(CaptureResult.CONTROL_AE_STATE), + "flashState:", result.get(CaptureResult.FLASH_STATE)); + if (mState == STATE_WAITING_CAPTURE) { + if (result.getSequenceId() == mSequenceId) { + // This is the request we launched and we're waiting for the right frame. + LOG.w("onCaptureCompleted:", + "aeState:", result.get(CaptureResult.CONTROL_AE_STATE), + "flashState:", result.get(CaptureResult.FLASH_STATE)); + mState = STATE_WAITING_IMAGE; + LOG.i("onCaptureCompleted:", "Got first result! Calling the GL recorder."); + super.take(); + } + } + } + + @Override + protected void dispatchResult() { + if (mState == STATE_WAITING_IMAGE) { + // Revert our changes. + LOG.i("dispatchResult:", "Reverting the capture intent changes."); + try { + mBuilder.set(CaptureRequest.CONTROL_CAPTURE_INTENT, mOldCaptureIntent); + mSession.setRepeatingRequest(mBuilder.build(), mCallback, null); + } catch (CameraAccessException ignore) {} + } + mState = STATE_IDLE; + super.dispatchResult(); + } +} diff --git a/demo/src/main/res/layout/activity_camera.xml b/demo/src/main/res/layout/activity_camera.xml index 2cc1d8a5..72fef775 100644 --- a/demo/src/main/res/layout/activity_camera.xml +++ b/demo/src/main/res/layout/activity_camera.xml @@ -20,7 +20,7 @@ app:cameraPreview="glSurface" app:cameraPlaySounds="true" app:cameraGrid="off" - app:cameraFlash="auto" + app:cameraFlash="on" app:cameraAudio="on" app:cameraFacing="back" app:cameraGestureTap="autoFocus" @@ -29,6 +29,7 @@ app:cameraGestureScrollHorizontal="filterControl1" app:cameraGestureScrollVertical="exposureCorrection" app:cameraMode="picture" + app:cameraPictureSnapshotMetering="true" app:cameraAutoFocusMarker="@string/cameraview_default_autofocus_marker">