diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java index a0b44053..7466e6df 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java @@ -145,6 +145,7 @@ public class CameraOptions { } } } + // Camera2Engine constructor. @RequiresApi(Build.VERSION_CODES.LOLLIPOP) @SuppressWarnings("deprecation") @@ -215,7 +216,7 @@ public class CameraOptions { // Picture Sizes StreamConfigurationMap streamMap = cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); if (streamMap == null) throw new RuntimeException("StreamConfigurationMap is null. Should not happen."); - android.util.Size[] psizes = streamMap.getOutputSizes(ImageReader.class); + android.util.Size[] psizes = streamMap.getOutputSizes(ImageFormat.JPEG); for (android.util.Size size : psizes) { int width = flipSizes ? size.getHeight() : size.getWidth(); int height = flipSizes ? size.getWidth() : size.getHeight(); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java index fdcbdb35..975dd63f 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java @@ -1902,27 +1902,27 @@ public class CameraView extends FrameLayout implements LifecycleObserver { @Override public void dispatchFrame(final Frame frame) { + mLogger.v("dispatchFrame:", frame.getTime(), "processors:", mFrameProcessors.size()); if (mFrameProcessors.isEmpty()) { // Mark as released. This instance will be reused. frame.release(); - } else { - mLogger.v("dispatchFrame:", frame.getTime(), "processors:", mFrameProcessors.size()); - mFrameProcessorsHandler.run(new Runnable() { - @Override - public void run() { - for (FrameProcessor processor : mFrameProcessors) { - try { - processor.process(frame); - } catch (Exception e) { - mLogger.w("dispatchFrame:", "Error during processor implementation.", - "Can happen when camera is closed while processors are running.", - e); - } + return; + } + mFrameProcessorsHandler.run(new Runnable() { + @Override + public void run() { + for (FrameProcessor processor : mFrameProcessors) { + try { + processor.process(frame); + } catch (Exception e) { + mLogger.w("dispatchFrame:", + "Error during processor implementation.", + "Can happen when camera is closed while processors are running.", e); } - frame.release(); } - }); - } + frame.release(); + } + }); } @Override diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java index 9523ccd2..35936c51 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java @@ -33,7 +33,7 @@ import com.otaliastudios.cameraview.controls.Mode; import com.otaliastudios.cameraview.controls.WhiteBalance; import com.otaliastudios.cameraview.internal.utils.CropHelper; import com.otaliastudios.cameraview.internal.utils.Op; -import com.otaliastudios.cameraview.picture.FullPictureRecorder; +import com.otaliastudios.cameraview.picture.Full1PictureRecorder; import com.otaliastudios.cameraview.picture.Snapshot1PictureRecorder; import com.otaliastudios.cameraview.picture.SnapshotGlPictureRecorder; import com.otaliastudios.cameraview.preview.GlCameraPreview; @@ -467,29 +467,13 @@ public class Camera1Engine extends CameraEngine implements // ----------------- // Picture recording stuff. + @WorkerThread @Override - public void takePicture(final @NonNull PictureResult.Stub stub) { - LOG.v("takePicture: scheduling"); - schedule(null, true, new Runnable() { - @Override - public void run() { - if (mMode == Mode.VIDEO) { - // Could redirect to takePictureSnapshot, but it's better if people know - // what they are doing. - throw new IllegalStateException("Can't take hq pictures while in VIDEO mode"); - } - - LOG.v("takePicture: performing.", isTakingPicture()); - if (isTakingPicture()) return; - stub.isSnapshot = false; - stub.location = mLocation; - stub.rotation = offset(REF_SENSOR, REF_OUTPUT); - stub.size = getPictureSize(REF_OUTPUT); - stub.facing = mFacing; - mPictureRecorder = new FullPictureRecorder(stub, Camera1Engine.this, mCamera); - mPictureRecorder.take(); - } - }); + protected void onTakePicture(@NonNull PictureResult.Stub stub) { + stub.rotation = offset(REF_SENSOR, REF_OUTPUT); + stub.size = getPictureSize(REF_OUTPUT); + mPictureRecorder = new Full1PictureRecorder(stub, Camera1Engine.this, mCamera); + mPictureRecorder.take(); } @WorkerThread 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 4732f802..25ea44d4 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java @@ -2,9 +2,7 @@ package com.otaliastudios.cameraview.engine; import android.annotation.SuppressLint; import android.content.Context; -import android.graphics.Camera; import android.graphics.ImageFormat; -import android.graphics.PixelFormat; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.SurfaceTexture; @@ -14,6 +12,8 @@ import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraDevice; import android.hardware.camera2.CameraManager; import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.CaptureResult; +import android.hardware.camera2.TotalCaptureResult; import android.hardware.camera2.params.StreamConfigurationMap; import android.location.Location; import android.media.Image; @@ -43,6 +43,7 @@ import com.otaliastudios.cameraview.gesture.Gesture; 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.SnapshotGlPictureRecorder; import com.otaliastudios.cameraview.preview.GlCameraPreview; import com.otaliastudios.cameraview.size.AspectRatio; @@ -50,7 +51,6 @@ import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.SizeSelectors; import com.otaliastudios.cameraview.video.Full2VideoRecorder; import com.otaliastudios.cameraview.video.SnapshotVideoRecorder; -import com.otaliastudios.cameraview.video.VideoRecorder; import java.util.ArrayList; import java.util.List; @@ -63,10 +63,12 @@ import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.WorkerThread; +// TODO full2picture fix rotation +// TODO full2picture add shutter +// TODO full2picture see if we don't launch activity // TODO zoom // TODO exposure correction // TODO autofocus -// TODO pictures @RequiresApi(Build.VERSION_CODES.LOLLIPOP) public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAvailableListener { @@ -83,20 +85,23 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv private CameraCaptureSession mSession; private CaptureRequest.Builder mRepeatingRequestBuilder; private CaptureRequest mRepeatingRequest; + private CameraCaptureSession.CaptureCallback mRepeatingRequestCallback; // Frame processing private Size mFrameProcessingSize; private ImageReader mFrameProcessingReader; // need this or the reader surface is collected private final WorkerHandler mFrameConversionHandler; + private Surface mFrameProcessingSurface; + // Preview private Surface mPreviewStreamSurface; - private Surface mFrameProcessingSurface; - // API 23+ for full video recording. The surface is created before. - private Surface mFullVideoPersistentSurface; + // Video recording + private Surface mFullVideoPersistentSurface; // API 23+. The surface is created before. + private VideoResult.Stub mFullVideoPendingStub; // API 21-22. When takeVideo is called, we have to reset the session. - // API 21-22 for full video recording. When takeVideo is called, we have to reset the session. - private VideoResult.Stub mFullVideoPendingStub; + // Picture capturing + private ImageReader mPictureReader; public Camera2Engine(Callback callback) { super(callback); @@ -154,7 +159,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv private CaptureRequest.Builder createRepeatingRequestBuilder(int template) throws CameraAccessException { mRepeatingRequestBuilder = mCamera.createCaptureRequest(template); mRepeatingRequestBuilder.setTag(template); - applyAll(mRepeatingRequestBuilder); + applyAllParameters(mRepeatingRequestBuilder); return mRepeatingRequestBuilder; } @@ -200,7 +205,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv try { mRepeatingRequest = mRepeatingRequestBuilder.build(); final AtomicBoolean firstFrame = new AtomicBoolean(false); - mSession.setRepeatingRequest(mRepeatingRequest, new CameraCaptureSession.CaptureCallback() { + mRepeatingRequestCallback = new CameraCaptureSession.CaptureCallback() { @Override public void onCaptureStarted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, long timestamp, long frameNumber) { super.onCaptureStarted(session, request, timestamp, frameNumber); @@ -208,7 +213,25 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv onFirstFrame.run(); } } - }, null); + + @Override + public void onCaptureProgressed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureResult partialResult) { + super.onCaptureProgressed(session, request, partialResult); + if (mPictureRecorder instanceof Full2PictureRecorder) { + ((Full2PictureRecorder) mPictureRecorder).process(partialResult); + } + } + + @Override + public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { + super.onCaptureCompleted(session, request, result); + if (mPictureRecorder instanceof Full2PictureRecorder) { + ((Full2PictureRecorder) mPictureRecorder).process(result); + } + } + + }; + mSession.setRepeatingRequest(mRepeatingRequest, mRepeatingRequestCallback, null); } catch (CameraAccessException e) { throw new CameraException(e, errorReason); } @@ -387,7 +410,13 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv // 3. PICTURE RECORDING if (mMode == Mode.PICTURE) { - // TODO picture recorder + mPictureReader = ImageReader.newInstance( + mCaptureSize.getWidth(), + mCaptureSize.getHeight(), + ImageFormat.JPEG, + 2 + ); + outputSurfaces.add(mPictureReader.getSurface()); } // 4. FRAME PROCESSING @@ -527,6 +556,10 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv mFrameProcessingReader.close(); mFrameProcessingReader = null; } + if (mPictureReader != null) { + mPictureReader.close(); + mPictureReader = null; + } mSession.close(); mSession = null; return Tasks.forResult(null); @@ -571,8 +604,27 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv } @Override - public void takePicture(@NonNull PictureResult.Stub stub) { - // TODO + protected void onTakePicture(@NonNull PictureResult.Stub stub) { + stub.rotation = offset(REF_SENSOR, REF_OUTPUT); + stub.size = getPictureSize(REF_OUTPUT); + try { + CaptureRequest.Builder builder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); + applyAllParameters(builder); + mPictureRecorder = new Full2PictureRecorder(stub, this, mSession, + mRepeatingRequestBuilder, + mRepeatingRequestCallback, + builder, + mPictureReader); + mPictureRecorder.take(); + } catch (CameraAccessException e) { + throw createCameraException(e); + } + } + + @Override + public void onPictureResult(@Nullable PictureResult.Stub result, @Nullable Exception error) { + super.onPictureResult(result, error); + applyRepeatingRequestBuilder(); } //endregion @@ -668,7 +720,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv //region Parameters - private void applyAll(@NonNull CaptureRequest.Builder builder) { + private void applyAllParameters(@NonNull CaptureRequest.Builder builder) { builder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO); applyDefaultFocus(builder); applyFlash(builder, Flash.OFF); @@ -932,7 +984,15 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv @Override public void startAutoFocus(@Nullable Gesture gesture, @NonNull PointF point) { - // TODO + LOG.i("startAutoFocus", "dispatching. Gesture:", gesture); + mHandler.run(new Runnable() { + @Override + public void run() { + LOG.i("startAutoFocus", "executing. Engine state:", getEngineState()); + if (getEngineState() < STATE_STARTED) return; + // TODO + } + }); } //endregion 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 c550e1e1..52b7ac71 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java @@ -933,7 +933,25 @@ public abstract class CameraEngine implements //region picture and video control - public abstract void takePicture(@NonNull PictureResult.Stub stub); + public final void takePicture(final @NonNull PictureResult.Stub stub) { + LOG.v("takePicture", "scheduling"); + mHandler.run(new Runnable() { + @Override + public void run() { + LOG.v("takePicture", "performing. Engine:", getEngineStateName(), "isTakingPicture:", isTakingPicture()); + if (mMode == Mode.VIDEO) { + // Could redirect to takePictureSnapshot, but it's better if people know what they are doing. + throw new IllegalStateException("Can't take hq pictures while in VIDEO mode"); + } + if (getEngineState() < STATE_STARTED) return; + if (isTakingPicture()) return; + stub.isSnapshot = false; + stub.location = mLocation; + stub.facing = mFacing; + onTakePicture(stub); + } + }); + }; /** * The snapshot size is the {@link #getPreviewStreamSize(int)}, but cropped based on the @@ -967,14 +985,13 @@ public abstract class CameraEngine implements } @Override - public void onPictureResult(@Nullable PictureResult.Stub result) { + public void onPictureResult(@Nullable PictureResult.Stub result, @Nullable Exception error) { mPictureRecorder = null; if (result != null) { mCallback.dispatchOnPictureTaken(result); } else { - // Something went wrong. - LOG.e("onPictureResult", "result is null: something went wrong."); - mCallback.dispatchError(new CameraException(CameraException.REASON_PICTURE_FAILED)); + LOG.e("onPictureResult", "result is null: something went wrong.", error); + mCallback.dispatchError(new CameraException(error, CameraException.REASON_PICTURE_FAILED)); } } @@ -1058,6 +1075,9 @@ public abstract class CameraEngine implements } } + @WorkerThread + protected abstract void onTakePicture(@NonNull PictureResult.Stub stub); + @WorkerThread protected abstract void onTakePictureSnapshot(@NonNull PictureResult.Stub stub, @NonNull AspectRatio viewAspectRatio); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/frame/FrameManager.java b/cameraview/src/main/java/com/otaliastudios/cameraview/frame/FrameManager.java index 2f7d0046..d3f1bcc8 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/frame/FrameManager.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/frame/FrameManager.java @@ -10,19 +10,35 @@ import androidx.annotation.Nullable; import java.util.concurrent.LinkedBlockingQueue; /** - * This class manages the allocation of buffers and Frame objects. - * We are interested in both recycling byte[] buffers so they are not allocated for each - * preview frame, and in recycling Frame instances (so we don't instantiate a lot). + * This class manages the allocation of byte buffers and {@link Frame} objects. + * We are interested in recycling both of them, especially byte[] buffers which can create a lot + * of overhead. * - * For this, we keep a mPoolSize integer that defines the size of instances to keep. - * Whether this does make sense, it depends on how slow the frame processors are. - * If they are very slow, it is possible that some frames will be skipped. + * The FrameManager keeps a {@link #mPoolSize} integer that defines the number of instances to keep. + * The pool size applies to both the {@link Frame} pool and the byte[] pool - it makes sense to use + * the same number since they are consumed at the same time. * - * - byte[] buffer pool: - * this is not kept here, because Camera1 internals already have one that we can't control, but - * it should be OK. The only thing we do is allocate mPoolSize buffers when requested. - * - Frame pool: - * We keep a list of mPoolSize recycled instances, to be reused when a new buffer is available. + * Main methods are: + * - {@link #setUp(int, Size)}: to set up with size and allocate buffers + * - {@link #release()}: to release. After release, a manager can be setUp again. + * - {@link #getFrame(byte[], long, int, Size, int)}: gets a new {@link Frame}. + * + * For both byte buffers and frames to get back to the FrameManager pool, all you have to do + * is call {@link Frame#release()} when done. + * + * Other than this, the FrameManager can work in two modes, depending on whether a {@link BufferCallback} + * is passed to the constructor. The modes changes the buffer behavior. + * + * 1. {@link #BUFFER_MODE_DISPATCH}: in this mode, as soon as we have a buffer, it is dispatched to + * the {@link BufferCallback}. The callback should then fill the buffer, and finally call + * {@link #getFrame(byte[], long, int, Size, int)} to receive a frame. + * This is used for Camera1. + * + * 2. {@link #BUFFER_MODE_ENQUEUE}: in this mode, the manager internally keeps a queue of byte buffers, + * instead of handing them to the callback. The users can ask for buffers through {@link #getBuffer()}. + * This buffer can be filled with data and used to get a frame {@link #getFrame(byte[], long, int, Size, int)}, + * or, in case it was not filled, returned to the queue using {@link #onBufferUnused(byte[])}. + * This is used for Camera2. */ public class FrameManager { diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglBaseSurface.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglBaseSurface.java index 0a869e5c..baff79e6 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglBaseSurface.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglBaseSurface.java @@ -68,10 +68,6 @@ public class EglBaseSurface extends EglElement { // Don't cache width/height here, because the size of the underlying surface can change // out from under us (see e.g. HardwareScalerActivity). - int tempWidth = mEglCore.querySurface(mEGLSurface, EGL14.EGL_WIDTH); - int tempHeight = mEglCore.querySurface(mEGLSurface, EGL14.EGL_HEIGHT); - // TODO remove these logs - LOG.e("ROTBUG", "eglSurfaceWidth:", tempWidth, "eglSurfaceHeight:", tempHeight); //mWidth = mEglCore.querySurface(mEGLSurface, EGL14.EGL_WIDTH); //mHeight = mEglCore.querySurface(mEGLSurface, EGL14.EGL_HEIGHT); } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/FullPictureRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Full1PictureRecorder.java similarity index 88% rename from cameraview/src/main/java/com/otaliastudios/cameraview/picture/FullPictureRecorder.java rename to cameraview/src/main/java/com/otaliastudios/cameraview/picture/Full1PictureRecorder.java index 7708ba45..51bd0a53 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/FullPictureRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Full1PictureRecorder.java @@ -16,15 +16,17 @@ import java.io.IOException; /** * A {@link PictureResult} that uses standard APIs. */ -public class FullPictureRecorder extends PictureRecorder { +public class Full1PictureRecorder extends PictureRecorder { - private static final String TAG = FullPictureRecorder.class.getSimpleName(); + private static final String TAG = Full1PictureRecorder.class.getSimpleName(); @SuppressWarnings("unused") private static final CameraLogger LOG = CameraLogger.create(TAG); private Camera mCamera; - public FullPictureRecorder(@NonNull PictureResult.Stub stub, @Nullable PictureResultListener listener, @NonNull Camera camera) { + public Full1PictureRecorder(@NonNull PictureResult.Stub stub, + @Nullable PictureResultListener listener, + @NonNull Camera camera) { super(stub, listener); mCamera = camera; diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Full2PictureRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Full2PictureRecorder.java new file mode 100644 index 00000000..3ac09316 --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Full2PictureRecorder.java @@ -0,0 +1,195 @@ +package com.otaliastudios.cameraview.picture; + +import android.hardware.Camera; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.CaptureResult; +import android.hardware.camera2.TotalCaptureResult; +import android.media.Image; +import android.media.ImageReader; +import android.os.Build; +import android.view.Surface; + +import com.otaliastudios.cameraview.CameraException; +import com.otaliastudios.cameraview.CameraLogger; +import com.otaliastudios.cameraview.PictureResult; +import com.otaliastudios.cameraview.internal.utils.ExifHelper; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.ByteBuffer; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.exifinterface.media.ExifInterface; + +/** + * A {@link PictureResult} that uses standard APIs. + */ +@RequiresApi(Build.VERSION_CODES.LOLLIPOP) +public class Full2PictureRecorder extends PictureRecorder { + + private static final String TAG = Full2PictureRecorder.class.getSimpleName(); + private static final CameraLogger LOG = CameraLogger.create(TAG); + + private static final int STATE_IDLE = 0; + private static final int STATE_WAITING_FOCUS_LOCK = 1; + private static final int STATE_WAITING_PRECAPTURE_START = 2; + private static final int STATE_WAITING_PRECAPTURE_END = 3; + private static final int STATE_WAITING_CAPTURE = 4; + private static final int STATE_WAITING_IMAGE = 5; + + private CameraCaptureSession mSession; + private CaptureRequest.Builder mBuilder; + private CameraCaptureSession.CaptureCallback mCallback; + private ImageReader mPictureReader; + private CaptureRequest.Builder mPictureBuilder; + private int mState = STATE_IDLE; + + public Full2PictureRecorder(@NonNull PictureResult.Stub stub, + @Nullable PictureResultListener listener, + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest.Builder builder, + @NonNull CameraCaptureSession.CaptureCallback callback, + @NonNull CaptureRequest.Builder pictureBuilder, + @NonNull ImageReader pictureReader) { + super(stub, listener); + mSession = session; + mBuilder = builder; + mCallback = callback; + mPictureBuilder = pictureBuilder; + mPictureReader = pictureReader; + mPictureReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() { + @Override + public void onImageAvailable(ImageReader reader) { + mState = STATE_IDLE; + + // Read the JPEG. + try { + Image image = reader.acquireLatestImage(); + ByteBuffer buffer = image.getPlanes()[0].getBuffer(); + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes); + mResult.data = bytes; + mResult.format = PictureResult.FORMAT_JPEG; + } catch (Exception e) { + mResult = null; + mError = e; + } + + // Before leaving, unlock focus. + try { + mBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, + CaptureRequest.CONTROL_AF_TRIGGER_CANCEL); + mSession.capture(mBuilder.build(), mCallback, null); + } catch (CameraAccessException ignore) { } + + // Leave. + dispatchResult(); + } + }, null); + } + + @Override + public void take() { + runFocusLock(); + } + + private void runFocusLock() { + try { + mState = STATE_WAITING_FOCUS_LOCK; + mBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, + CaptureRequest.CONTROL_AF_TRIGGER_START); + mSession.capture(mBuilder.build(), mCallback, null); + } catch (CameraAccessException e) { + mResult = null; + mError = e; + dispatchResult(); + } + } + + private void runPrecapture() { + try { + mState = STATE_WAITING_PRECAPTURE_START; + mBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); + mSession.capture(mBuilder.build(), mCallback, null); + } catch (CameraAccessException e) { + mResult = null; + mError = e; + dispatchResult(); + } + } + + private void runCapture() { + try { + mPictureBuilder.setTag(CameraDevice.TEMPLATE_STILL_CAPTURE); + mPictureBuilder.addTarget(mPictureReader.getSurface()); + mPictureBuilder.set(CaptureRequest.JPEG_ORIENTATION, mResult.rotation); + // mCaptureSession.stopRepeating(); + mSession.abortCaptures(); + mSession.capture(mPictureBuilder.build(), mCallback, null); + } catch (CameraAccessException e) { + mResult = null; + mError = e; + dispatchResult(); + } + } + + public void process(@NonNull CaptureResult result) { + switch (mState) { + case STATE_IDLE: break; + case STATE_WAITING_FOCUS_LOCK: { + Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); + if (afState == null) { + runCapture(); + } else if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED + || afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { + // CONTROL_AE_STATE can be null on some devices + Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); + if (aeState == null || aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) { + mState = STATE_WAITING_CAPTURE; + runCapture(); + } else { + runPrecapture(); + } + } + break; + } + case STATE_WAITING_PRECAPTURE_START: { + Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); + if (aeState == null || + aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE || + aeState == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED) { + mState = STATE_WAITING_PRECAPTURE_END; + } + break; + } + case STATE_WAITING_PRECAPTURE_END: { + Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); + if (aeState == null + || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) { + mState = STATE_WAITING_CAPTURE; + runCapture(); + } + break; + } + case STATE_WAITING_CAPTURE: { + if (result instanceof TotalCaptureResult + && result.getRequest().getTag() == (Integer) CameraDevice.TEMPLATE_STILL_CAPTURE) { + mState = STATE_WAITING_IMAGE; + } + break; + } + } + } + + @Override + protected void dispatchResult() { + mState = STATE_IDLE; + super.dispatchResult(); + } +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/PictureRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/PictureRecorder.java index e7fec686..96fb4882 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/PictureRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/PictureRecorder.java @@ -29,11 +29,13 @@ public abstract class PictureRecorder { * the result is null. * @param result the result or null if there was some error. */ - void onPictureResult(@Nullable PictureResult.Stub result); + void onPictureResult(@Nullable PictureResult.Stub result, @Nullable Exception error); } @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) PictureResult.Stub mResult; @VisibleForTesting PictureResultListener mListener; + @SuppressWarnings("WeakerAccess") + protected Exception mError; /** * Creates a new picture recorder. @@ -67,7 +69,7 @@ public abstract class PictureRecorder { */ protected void dispatchResult() { if (mListener != null) { - mListener.onPictureResult(mResult); + mListener.onPictureResult(mResult, mError); mListener = null; mResult = null; } diff --git a/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java b/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java index e0bad4c6..d1855572 100644 --- a/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java +++ b/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java @@ -45,7 +45,8 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis camera = findViewById(R.id.camera); camera.setLifecycleOwner(this); camera.addCameraListener(new Listener()); - camera.addFrameProcessor(new FrameProcessor() { + + /* camera.addFrameProcessor(new FrameProcessor() { private long lastTime = System.currentTimeMillis(); @Override public void process(@NonNull Frame frame) { @@ -54,7 +55,7 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis lastTime = newTime; Log.e("Frames", "Delay: " + delay + "millis, FPS: " + 1000 / delay); } - }); + }); */ findViewById(R.id.edit).setOnClickListener(this); findViewById(R.id.capturePicture).setOnClickListener(this); @@ -118,7 +119,7 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis PicturePreviewActivity.setPictureResult(result); Intent intent = new Intent(CameraActivity.this, PicturePreviewActivity.class); intent.putExtra("delay", callbackTime - mCaptureTime); - startActivity(intent); + // TODO temp startActivity(intent); mCaptureTime = 0; } diff --git a/demo/src/main/res/layout/activity_camera.xml b/demo/src/main/res/layout/activity_camera.xml index b0a360ad..03b195b7 100644 --- a/demo/src/main/res/layout/activity_camera.xml +++ b/demo/src/main/res/layout/activity_camera.xml @@ -17,7 +17,7 @@ android:layout_marginBottom="88dp" android:keepScreenOn="true" app:cameraExperimental="true" - app:cameraEngine="camera1" + app:cameraEngine="camera2" app:cameraPreview="glSurface" app:cameraPlaySounds="true" app:cameraGrid="off"