Camera2 basic support for hq pictures

pull/493/head
Mattia Iavarone 5 years ago
parent e96c9f0846
commit 87796ea2f7
  1. 3
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java
  2. 12
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
  3. 24
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java
  4. 92
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java
  5. 30
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java
  6. 38
      cameraview/src/main/java/com/otaliastudios/cameraview/frame/FrameManager.java
  7. 4
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglBaseSurface.java
  8. 8
      cameraview/src/main/java/com/otaliastudios/cameraview/picture/Full1PictureRecorder.java
  9. 195
      cameraview/src/main/java/com/otaliastudios/cameraview/picture/Full2PictureRecorder.java
  10. 6
      cameraview/src/main/java/com/otaliastudios/cameraview/picture/PictureRecorder.java
  11. 7
      demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java
  12. 2
      demo/src/main/res/layout/activity_camera.xml

@ -145,6 +145,7 @@ public class CameraOptions {
} }
} }
} }
// Camera2Engine constructor. // Camera2Engine constructor.
@RequiresApi(Build.VERSION_CODES.LOLLIPOP) @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
@ -215,7 +216,7 @@ public class CameraOptions {
// Picture Sizes // Picture Sizes
StreamConfigurationMap streamMap = cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); StreamConfigurationMap streamMap = cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
if (streamMap == null) throw new RuntimeException("StreamConfigurationMap is null. Should not happen."); 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) { for (android.util.Size size : psizes) {
int width = flipSizes ? size.getHeight() : size.getWidth(); int width = flipSizes ? size.getHeight() : size.getWidth();
int height = flipSizes ? size.getWidth() : size.getHeight(); int height = flipSizes ? size.getWidth() : size.getHeight();

@ -1902,11 +1902,12 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
@Override @Override
public void dispatchFrame(final Frame frame) { public void dispatchFrame(final Frame frame) {
mLogger.v("dispatchFrame:", frame.getTime(), "processors:", mFrameProcessors.size());
if (mFrameProcessors.isEmpty()) { if (mFrameProcessors.isEmpty()) {
// Mark as released. This instance will be reused. // Mark as released. This instance will be reused.
frame.release(); frame.release();
} else { return;
mLogger.v("dispatchFrame:", frame.getTime(), "processors:", mFrameProcessors.size()); }
mFrameProcessorsHandler.run(new Runnable() { mFrameProcessorsHandler.run(new Runnable() {
@Override @Override
public void run() { public void run() {
@ -1914,16 +1915,15 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
try { try {
processor.process(frame); processor.process(frame);
} catch (Exception e) { } catch (Exception e) {
mLogger.w("dispatchFrame:", "Error during processor implementation.", mLogger.w("dispatchFrame:",
"Can happen when camera is closed while processors are running.", "Error during processor implementation.",
e); "Can happen when camera is closed while processors are running.", e);
} }
} }
frame.release(); frame.release();
} }
}); });
} }
}
@Override @Override
public void dispatchError(final CameraException exception) { public void dispatchError(final CameraException exception) {

@ -33,7 +33,7 @@ import com.otaliastudios.cameraview.controls.Mode;
import com.otaliastudios.cameraview.controls.WhiteBalance; import com.otaliastudios.cameraview.controls.WhiteBalance;
import com.otaliastudios.cameraview.internal.utils.CropHelper; import com.otaliastudios.cameraview.internal.utils.CropHelper;
import com.otaliastudios.cameraview.internal.utils.Op; 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.Snapshot1PictureRecorder;
import com.otaliastudios.cameraview.picture.SnapshotGlPictureRecorder; import com.otaliastudios.cameraview.picture.SnapshotGlPictureRecorder;
import com.otaliastudios.cameraview.preview.GlCameraPreview; import com.otaliastudios.cameraview.preview.GlCameraPreview;
@ -467,30 +467,14 @@ public class Camera1Engine extends CameraEngine implements
// ----------------- // -----------------
// Picture recording stuff. // Picture recording stuff.
@WorkerThread
@Override @Override
public void takePicture(final @NonNull PictureResult.Stub stub) { protected void onTakePicture(@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.rotation = offset(REF_SENSOR, REF_OUTPUT);
stub.size = getPictureSize(REF_OUTPUT); stub.size = getPictureSize(REF_OUTPUT);
stub.facing = mFacing; mPictureRecorder = new Full1PictureRecorder(stub, Camera1Engine.this, mCamera);
mPictureRecorder = new FullPictureRecorder(stub, Camera1Engine.this, mCamera);
mPictureRecorder.take(); mPictureRecorder.take();
} }
});
}
@WorkerThread @WorkerThread
@Override @Override

@ -2,9 +2,7 @@ package com.otaliastudios.cameraview.engine;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.graphics.Camera;
import android.graphics.ImageFormat; import android.graphics.ImageFormat;
import android.graphics.PixelFormat;
import android.graphics.PointF; import android.graphics.PointF;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.SurfaceTexture; import android.graphics.SurfaceTexture;
@ -14,6 +12,8 @@ import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice; import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager; import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult;
import android.hardware.camera2.TotalCaptureResult;
import android.hardware.camera2.params.StreamConfigurationMap; import android.hardware.camera2.params.StreamConfigurationMap;
import android.location.Location; import android.location.Location;
import android.media.Image; 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.CropHelper;
import com.otaliastudios.cameraview.internal.utils.ImageHelper; import com.otaliastudios.cameraview.internal.utils.ImageHelper;
import com.otaliastudios.cameraview.internal.utils.WorkerHandler; import com.otaliastudios.cameraview.internal.utils.WorkerHandler;
import com.otaliastudios.cameraview.picture.Full2PictureRecorder;
import com.otaliastudios.cameraview.picture.SnapshotGlPictureRecorder; import com.otaliastudios.cameraview.picture.SnapshotGlPictureRecorder;
import com.otaliastudios.cameraview.preview.GlCameraPreview; import com.otaliastudios.cameraview.preview.GlCameraPreview;
import com.otaliastudios.cameraview.size.AspectRatio; 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.size.SizeSelectors;
import com.otaliastudios.cameraview.video.Full2VideoRecorder; import com.otaliastudios.cameraview.video.Full2VideoRecorder;
import com.otaliastudios.cameraview.video.SnapshotVideoRecorder; import com.otaliastudios.cameraview.video.SnapshotVideoRecorder;
import com.otaliastudios.cameraview.video.VideoRecorder;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -63,10 +63,12 @@ import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
import androidx.annotation.WorkerThread; import androidx.annotation.WorkerThread;
// TODO full2picture fix rotation
// TODO full2picture add shutter
// TODO full2picture see if we don't launch activity
// TODO zoom // TODO zoom
// TODO exposure correction // TODO exposure correction
// TODO autofocus // TODO autofocus
// TODO pictures
@RequiresApi(Build.VERSION_CODES.LOLLIPOP) @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAvailableListener { public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAvailableListener {
@ -83,20 +85,23 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
private CameraCaptureSession mSession; private CameraCaptureSession mSession;
private CaptureRequest.Builder mRepeatingRequestBuilder; private CaptureRequest.Builder mRepeatingRequestBuilder;
private CaptureRequest mRepeatingRequest; private CaptureRequest mRepeatingRequest;
private CameraCaptureSession.CaptureCallback mRepeatingRequestCallback;
// Frame processing // Frame processing
private Size mFrameProcessingSize; private Size mFrameProcessingSize;
private ImageReader mFrameProcessingReader; // need this or the reader surface is collected private ImageReader mFrameProcessingReader; // need this or the reader surface is collected
private final WorkerHandler mFrameConversionHandler; private final WorkerHandler mFrameConversionHandler;
private Surface mFrameProcessingSurface;
// Preview
private Surface mPreviewStreamSurface; private Surface mPreviewStreamSurface;
private Surface mFrameProcessingSurface;
// API 23+ for full video recording. The surface is created before. // Video recording
private Surface mFullVideoPersistentSurface; 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. // Picture capturing
private VideoResult.Stub mFullVideoPendingStub; private ImageReader mPictureReader;
public Camera2Engine(Callback callback) { public Camera2Engine(Callback callback) {
super(callback); super(callback);
@ -154,7 +159,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
private CaptureRequest.Builder createRepeatingRequestBuilder(int template) throws CameraAccessException { private CaptureRequest.Builder createRepeatingRequestBuilder(int template) throws CameraAccessException {
mRepeatingRequestBuilder = mCamera.createCaptureRequest(template); mRepeatingRequestBuilder = mCamera.createCaptureRequest(template);
mRepeatingRequestBuilder.setTag(template); mRepeatingRequestBuilder.setTag(template);
applyAll(mRepeatingRequestBuilder); applyAllParameters(mRepeatingRequestBuilder);
return mRepeatingRequestBuilder; return mRepeatingRequestBuilder;
} }
@ -200,7 +205,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
try { try {
mRepeatingRequest = mRepeatingRequestBuilder.build(); mRepeatingRequest = mRepeatingRequestBuilder.build();
final AtomicBoolean firstFrame = new AtomicBoolean(false); final AtomicBoolean firstFrame = new AtomicBoolean(false);
mSession.setRepeatingRequest(mRepeatingRequest, new CameraCaptureSession.CaptureCallback() { mRepeatingRequestCallback = new CameraCaptureSession.CaptureCallback() {
@Override @Override
public void onCaptureStarted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, long timestamp, long frameNumber) { public void onCaptureStarted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, long timestamp, long frameNumber) {
super.onCaptureStarted(session, request, timestamp, frameNumber); super.onCaptureStarted(session, request, timestamp, frameNumber);
@ -208,7 +213,25 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
onFirstFrame.run(); 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) { } catch (CameraAccessException e) {
throw new CameraException(e, errorReason); throw new CameraException(e, errorReason);
} }
@ -387,7 +410,13 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
// 3. PICTURE RECORDING // 3. PICTURE RECORDING
if (mMode == Mode.PICTURE) { if (mMode == Mode.PICTURE) {
// TODO picture recorder mPictureReader = ImageReader.newInstance(
mCaptureSize.getWidth(),
mCaptureSize.getHeight(),
ImageFormat.JPEG,
2
);
outputSurfaces.add(mPictureReader.getSurface());
} }
// 4. FRAME PROCESSING // 4. FRAME PROCESSING
@ -527,6 +556,10 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
mFrameProcessingReader.close(); mFrameProcessingReader.close();
mFrameProcessingReader = null; mFrameProcessingReader = null;
} }
if (mPictureReader != null) {
mPictureReader.close();
mPictureReader = null;
}
mSession.close(); mSession.close();
mSession = null; mSession = null;
return Tasks.forResult(null); return Tasks.forResult(null);
@ -571,8 +604,27 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
} }
@Override @Override
public void takePicture(@NonNull PictureResult.Stub stub) { protected void onTakePicture(@NonNull PictureResult.Stub stub) {
// TODO 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 //endregion
@ -668,7 +720,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
//region Parameters //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); builder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO);
applyDefaultFocus(builder); applyDefaultFocus(builder);
applyFlash(builder, Flash.OFF); applyFlash(builder, Flash.OFF);
@ -932,8 +984,16 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
@Override @Override
public void startAutoFocus(@Nullable Gesture gesture, @NonNull PointF point) { public void startAutoFocus(@Nullable Gesture gesture, @NonNull PointF point) {
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 // TODO
} }
});
}
//endregion //endregion
} }

@ -933,7 +933,25 @@ public abstract class CameraEngine implements
//region picture and video control //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 * The snapshot size is the {@link #getPreviewStreamSize(int)}, but cropped based on the
@ -967,14 +985,13 @@ public abstract class CameraEngine implements
} }
@Override @Override
public void onPictureResult(@Nullable PictureResult.Stub result) { public void onPictureResult(@Nullable PictureResult.Stub result, @Nullable Exception error) {
mPictureRecorder = null; mPictureRecorder = null;
if (result != null) { if (result != null) {
mCallback.dispatchOnPictureTaken(result); mCallback.dispatchOnPictureTaken(result);
} else { } else {
// Something went wrong. LOG.e("onPictureResult", "result is null: something went wrong.", error);
LOG.e("onPictureResult", "result is null: something went wrong."); mCallback.dispatchError(new CameraException(error, CameraException.REASON_PICTURE_FAILED));
mCallback.dispatchError(new CameraException(CameraException.REASON_PICTURE_FAILED));
} }
} }
@ -1058,6 +1075,9 @@ public abstract class CameraEngine implements
} }
} }
@WorkerThread
protected abstract void onTakePicture(@NonNull PictureResult.Stub stub);
@WorkerThread @WorkerThread
protected abstract void onTakePictureSnapshot(@NonNull PictureResult.Stub stub, @NonNull AspectRatio viewAspectRatio); protected abstract void onTakePictureSnapshot(@NonNull PictureResult.Stub stub, @NonNull AspectRatio viewAspectRatio);

@ -10,19 +10,35 @@ import androidx.annotation.Nullable;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
/** /**
* This class manages the allocation of buffers and Frame objects. * This class manages the allocation of byte buffers and {@link Frame} objects.
* We are interested in both recycling byte[] buffers so they are not allocated for each * We are interested in recycling both of them, especially byte[] buffers which can create a lot
* preview frame, and in recycling Frame instances (so we don't instantiate a lot). * of overhead.
* *
* For this, we keep a mPoolSize integer that defines the size of instances to keep. * The FrameManager keeps a {@link #mPoolSize} integer that defines the number of instances to keep.
* Whether this does make sense, it depends on how slow the frame processors are. * The pool size applies to both the {@link Frame} pool and the byte[] pool - it makes sense to use
* If they are very slow, it is possible that some frames will be skipped. * the same number since they are consumed at the same time.
* *
* - byte[] buffer pool: * Main methods are:
* this is not kept here, because Camera1 internals already have one that we can't control, but * - {@link #setUp(int, Size)}: to set up with size and allocate buffers
* it should be OK. The only thing we do is allocate mPoolSize buffers when requested. * - {@link #release()}: to release. After release, a manager can be setUp again.
* - Frame pool: * - {@link #getFrame(byte[], long, int, Size, int)}: gets a new {@link Frame}.
* We keep a list of mPoolSize recycled instances, to be reused when a new buffer is available. *
* 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 { public class FrameManager {

@ -68,10 +68,6 @@ public class EglBaseSurface extends EglElement {
// Don't cache width/height here, because the size of the underlying surface can change // Don't cache width/height here, because the size of the underlying surface can change
// out from under us (see e.g. HardwareScalerActivity). // 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); //mWidth = mEglCore.querySurface(mEGLSurface, EGL14.EGL_WIDTH);
//mHeight = mEglCore.querySurface(mEGLSurface, EGL14.EGL_HEIGHT); //mHeight = mEglCore.querySurface(mEGLSurface, EGL14.EGL_HEIGHT);
} }

@ -16,15 +16,17 @@ import java.io.IOException;
/** /**
* A {@link PictureResult} that uses standard APIs. * 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") @SuppressWarnings("unused")
private static final CameraLogger LOG = CameraLogger.create(TAG); private static final CameraLogger LOG = CameraLogger.create(TAG);
private Camera mCamera; 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); super(stub, listener);
mCamera = camera; mCamera = camera;

@ -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();
}
}

@ -29,11 +29,13 @@ public abstract class PictureRecorder {
* the result is null. * the result is null.
* @param result the result or null if there was some error. * @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(otherwise = VisibleForTesting.PROTECTED) PictureResult.Stub mResult;
@VisibleForTesting PictureResultListener mListener; @VisibleForTesting PictureResultListener mListener;
@SuppressWarnings("WeakerAccess")
protected Exception mError;
/** /**
* Creates a new picture recorder. * Creates a new picture recorder.
@ -67,7 +69,7 @@ public abstract class PictureRecorder {
*/ */
protected void dispatchResult() { protected void dispatchResult() {
if (mListener != null) { if (mListener != null) {
mListener.onPictureResult(mResult); mListener.onPictureResult(mResult, mError);
mListener = null; mListener = null;
mResult = null; mResult = null;
} }

@ -45,7 +45,8 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis
camera = findViewById(R.id.camera); camera = findViewById(R.id.camera);
camera.setLifecycleOwner(this); camera.setLifecycleOwner(this);
camera.addCameraListener(new Listener()); camera.addCameraListener(new Listener());
camera.addFrameProcessor(new FrameProcessor() {
/* camera.addFrameProcessor(new FrameProcessor() {
private long lastTime = System.currentTimeMillis(); private long lastTime = System.currentTimeMillis();
@Override @Override
public void process(@NonNull Frame frame) { public void process(@NonNull Frame frame) {
@ -54,7 +55,7 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis
lastTime = newTime; lastTime = newTime;
Log.e("Frames", "Delay: " + delay + "millis, FPS: " + 1000 / delay); Log.e("Frames", "Delay: " + delay + "millis, FPS: " + 1000 / delay);
} }
}); }); */
findViewById(R.id.edit).setOnClickListener(this); findViewById(R.id.edit).setOnClickListener(this);
findViewById(R.id.capturePicture).setOnClickListener(this); findViewById(R.id.capturePicture).setOnClickListener(this);
@ -118,7 +119,7 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis
PicturePreviewActivity.setPictureResult(result); PicturePreviewActivity.setPictureResult(result);
Intent intent = new Intent(CameraActivity.this, PicturePreviewActivity.class); Intent intent = new Intent(CameraActivity.this, PicturePreviewActivity.class);
intent.putExtra("delay", callbackTime - mCaptureTime); intent.putExtra("delay", callbackTime - mCaptureTime);
startActivity(intent); // TODO temp startActivity(intent);
mCaptureTime = 0; mCaptureTime = 0;
} }

@ -17,7 +17,7 @@
android:layout_marginBottom="88dp" android:layout_marginBottom="88dp"
android:keepScreenOn="true" android:keepScreenOn="true"
app:cameraExperimental="true" app:cameraExperimental="true"
app:cameraEngine="camera1" app:cameraEngine="camera2"
app:cameraPreview="glSurface" app:cameraPreview="glSurface"
app:cameraPlaySounds="true" app:cameraPlaySounds="true"
app:cameraGrid="off" app:cameraGrid="off"

Loading…
Cancel
Save