|
|
package com.otaliastudios.cameraview.engine;
|
|
|
|
|
|
import android.annotation.SuppressLint;
|
|
|
import android.content.Context;
|
|
|
import android.graphics.ImageFormat;
|
|
|
import android.graphics.PointF;
|
|
|
import android.graphics.Rect;
|
|
|
import android.graphics.SurfaceTexture;
|
|
|
import android.hardware.camera2.CameraAccessException;
|
|
|
import android.hardware.camera2.CameraCaptureSession;
|
|
|
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;
|
|
|
import android.media.ImageReader;
|
|
|
import android.os.Build;
|
|
|
import android.util.Pair;
|
|
|
import android.util.Range;
|
|
|
import android.util.Rational;
|
|
|
import android.view.Surface;
|
|
|
import android.view.SurfaceHolder;
|
|
|
|
|
|
import androidx.annotation.NonNull;
|
|
|
import androidx.annotation.Nullable;
|
|
|
import androidx.annotation.RequiresApi;
|
|
|
import androidx.annotation.VisibleForTesting;
|
|
|
|
|
|
import com.google.android.gms.tasks.Task;
|
|
|
import com.google.android.gms.tasks.TaskCompletionSource;
|
|
|
import com.google.android.gms.tasks.Tasks;
|
|
|
import com.otaliastudios.cameraview.CameraException;
|
|
|
import com.otaliastudios.cameraview.CameraOptions;
|
|
|
import com.otaliastudios.cameraview.PictureResult;
|
|
|
import com.otaliastudios.cameraview.VideoResult;
|
|
|
import com.otaliastudios.cameraview.controls.Facing;
|
|
|
import com.otaliastudios.cameraview.controls.Flash;
|
|
|
import com.otaliastudios.cameraview.controls.Hdr;
|
|
|
import com.otaliastudios.cameraview.controls.Mode;
|
|
|
import com.otaliastudios.cameraview.controls.PictureFormat;
|
|
|
import com.otaliastudios.cameraview.controls.WhiteBalance;
|
|
|
import com.otaliastudios.cameraview.engine.action.Action;
|
|
|
import com.otaliastudios.cameraview.engine.action.ActionHolder;
|
|
|
import com.otaliastudios.cameraview.engine.action.Actions;
|
|
|
import com.otaliastudios.cameraview.engine.action.BaseAction;
|
|
|
import com.otaliastudios.cameraview.engine.action.CompletionCallback;
|
|
|
import com.otaliastudios.cameraview.engine.action.LogAction;
|
|
|
import com.otaliastudios.cameraview.engine.mappers.Camera2Mapper;
|
|
|
import com.otaliastudios.cameraview.engine.meter.MeterAction;
|
|
|
import com.otaliastudios.cameraview.engine.meter.MeterResetAction;
|
|
|
import com.otaliastudios.cameraview.engine.offset.Axis;
|
|
|
import com.otaliastudios.cameraview.engine.offset.Reference;
|
|
|
import com.otaliastudios.cameraview.engine.options.Camera2Options;
|
|
|
import com.otaliastudios.cameraview.engine.orchestrator.CameraState;
|
|
|
import com.otaliastudios.cameraview.frame.Frame;
|
|
|
import com.otaliastudios.cameraview.frame.FrameManager;
|
|
|
import com.otaliastudios.cameraview.frame.ImageFrameManager;
|
|
|
import com.otaliastudios.cameraview.gesture.Gesture;
|
|
|
import com.otaliastudios.cameraview.internal.utils.CropHelper;
|
|
|
import com.otaliastudios.cameraview.internal.utils.WorkerHandler;
|
|
|
import com.otaliastudios.cameraview.picture.Full2PictureRecorder;
|
|
|
import com.otaliastudios.cameraview.picture.Snapshot2PictureRecorder;
|
|
|
import com.otaliastudios.cameraview.preview.GlCameraPreview;
|
|
|
import com.otaliastudios.cameraview.size.AspectRatio;
|
|
|
import com.otaliastudios.cameraview.size.Size;
|
|
|
import com.otaliastudios.cameraview.video.Full2VideoRecorder;
|
|
|
import com.otaliastudios.cameraview.video.SnapshotVideoRecorder;
|
|
|
|
|
|
import java.util.ArrayList;
|
|
|
import java.util.List;
|
|
|
import java.util.concurrent.Callable;
|
|
|
import java.util.concurrent.CopyOnWriteArrayList;
|
|
|
import java.util.concurrent.ExecutionException;
|
|
|
|
|
|
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
|
|
public class Camera2Engine extends CameraBaseEngine implements
|
|
|
ImageReader.OnImageAvailableListener,
|
|
|
ActionHolder {
|
|
|
|
|
|
private static final int FRAME_PROCESSING_POOL_SIZE = 2;
|
|
|
private static final int FRAME_PROCESSING_FORMAT = ImageFormat.YUV_420_888;
|
|
|
@VisibleForTesting static final long METER_TIMEOUT = 2500;
|
|
|
|
|
|
private final CameraManager mManager;
|
|
|
private String mCameraId;
|
|
|
private CameraDevice mCamera;
|
|
|
private CameraCharacteristics mCameraCharacteristics;
|
|
|
private CameraCaptureSession mSession;
|
|
|
private CaptureRequest.Builder mRepeatingRequestBuilder;
|
|
|
private TotalCaptureResult mLastRepeatingResult;
|
|
|
private final Camera2Mapper mMapper = Camera2Mapper.get();
|
|
|
|
|
|
// Frame processing
|
|
|
private ImageReader mFrameProcessingReader; // need this or the reader surface is collected
|
|
|
private Surface mFrameProcessingSurface;
|
|
|
private final WorkerHandler mFrameProcessingHandler = WorkerHandler.get("FrameProcessing");
|
|
|
private final Object mFrameProcessingImageLock = new Object();
|
|
|
|
|
|
// Preview
|
|
|
private Surface mPreviewStreamSurface;
|
|
|
|
|
|
// Video recording
|
|
|
// When takeVideo is called, we restart the session.
|
|
|
private VideoResult.Stub mFullVideoPendingStub;
|
|
|
|
|
|
// Picture capturing
|
|
|
private ImageReader mPictureReader;
|
|
|
private final boolean mPictureCaptureStopsPreview = false; // can be configurable at some point
|
|
|
|
|
|
// Actions
|
|
|
// Use COW to properly synchronize the list. We'll iterate much more than mutate
|
|
|
private final List<Action> mActions = new CopyOnWriteArrayList<>();
|
|
|
private MeterAction mMeterAction;
|
|
|
|
|
|
public Camera2Engine(Callback callback) {
|
|
|
super(callback);
|
|
|
mManager = (CameraManager) getCallback().getContext()
|
|
|
.getSystemService(Context.CAMERA_SERVICE);
|
|
|
new LogAction().start(this);
|
|
|
}
|
|
|
|
|
|
//region Utilities
|
|
|
|
|
|
@VisibleForTesting
|
|
|
@NonNull
|
|
|
<T> T readCharacteristic(@NonNull CameraCharacteristics.Key<T> key,
|
|
|
@NonNull T fallback) {
|
|
|
return readCharacteristic(mCameraCharacteristics, key, fallback);
|
|
|
}
|
|
|
|
|
|
@NonNull
|
|
|
private <T> T readCharacteristic(@NonNull CameraCharacteristics characteristics,
|
|
|
@NonNull CameraCharacteristics.Key<T> key,
|
|
|
@NonNull T fallback) {
|
|
|
T value = characteristics.get(key);
|
|
|
return value == null ? fallback : value;
|
|
|
}
|
|
|
|
|
|
@NonNull
|
|
|
private CameraException createCameraException(@NonNull CameraAccessException exception) {
|
|
|
int reason;
|
|
|
switch (exception.getReason()) {
|
|
|
case CameraAccessException.CAMERA_DISABLED:
|
|
|
case CameraAccessException.CAMERA_IN_USE:
|
|
|
case CameraAccessException.MAX_CAMERAS_IN_USE: {
|
|
|
reason = CameraException.REASON_FAILED_TO_CONNECT;
|
|
|
break;
|
|
|
}
|
|
|
case CameraAccessException.CAMERA_ERROR:
|
|
|
case CameraAccessException.CAMERA_DISCONNECTED: {
|
|
|
reason = CameraException.REASON_DISCONNECTED;
|
|
|
break;
|
|
|
}
|
|
|
default: {
|
|
|
reason = CameraException.REASON_UNKNOWN;
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
return new CameraException(exception, reason);
|
|
|
}
|
|
|
|
|
|
@NonNull
|
|
|
private CameraException createCameraException(int stateCallbackError) {
|
|
|
int reason;
|
|
|
switch (stateCallbackError) {
|
|
|
case CameraDevice.StateCallback.ERROR_CAMERA_DISABLED: // Device policy
|
|
|
case CameraDevice.StateCallback.ERROR_CAMERA_DEVICE: // Fatal error
|
|
|
case CameraDevice.StateCallback.ERROR_CAMERA_SERVICE: // Fatal error, might have to
|
|
|
// restart the device
|
|
|
case CameraDevice.StateCallback.ERROR_CAMERA_IN_USE:
|
|
|
case CameraDevice.StateCallback.ERROR_MAX_CAMERAS_IN_USE: {
|
|
|
reason = CameraException.REASON_FAILED_TO_CONNECT;
|
|
|
break;
|
|
|
}
|
|
|
default: {
|
|
|
reason = CameraException.REASON_UNKNOWN;
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
return new CameraException(reason);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* When creating a new builder, we want to
|
|
|
* - set it to {@link #mRepeatingRequestBuilder}, the current one
|
|
|
* - add a tag for the template just in case
|
|
|
* - apply all the current parameters
|
|
|
*/
|
|
|
@SuppressWarnings("UnusedReturnValue")
|
|
|
@NonNull
|
|
|
private CaptureRequest.Builder createRepeatingRequestBuilder(int template)
|
|
|
throws CameraAccessException {
|
|
|
CaptureRequest.Builder oldBuilder = mRepeatingRequestBuilder;
|
|
|
mRepeatingRequestBuilder = mCamera.createCaptureRequest(template);
|
|
|
mRepeatingRequestBuilder.setTag(template);
|
|
|
applyAllParameters(mRepeatingRequestBuilder, oldBuilder);
|
|
|
return mRepeatingRequestBuilder;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Sets up the repeating request builder with default surfaces and extra ones
|
|
|
* if needed (like a video recording surface).
|
|
|
*/
|
|
|
private void addRepeatingRequestBuilderSurfaces(@NonNull Surface... extraSurfaces) {
|
|
|
mRepeatingRequestBuilder.addTarget(mPreviewStreamSurface);
|
|
|
if (mFrameProcessingSurface != null) {
|
|
|
mRepeatingRequestBuilder.addTarget(mFrameProcessingSurface);
|
|
|
}
|
|
|
for (Surface extraSurface : extraSurfaces) {
|
|
|
if (extraSurface == null) {
|
|
|
throw new IllegalArgumentException("Should not add a null surface.");
|
|
|
}
|
|
|
mRepeatingRequestBuilder.addTarget(extraSurface);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Removes default surfaces from the repeating request builder.
|
|
|
*/
|
|
|
private void removeRepeatingRequestBuilderSurfaces() {
|
|
|
mRepeatingRequestBuilder.removeTarget(mPreviewStreamSurface);
|
|
|
if (mFrameProcessingSurface != null) {
|
|
|
mRepeatingRequestBuilder.removeTarget(mFrameProcessingSurface);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Applies the repeating request builder to the preview, assuming we actually have a preview
|
|
|
* running. Can be called after changing parameters to the builder.
|
|
|
*
|
|
|
* To apply a new builder (for example switch between TEMPLATE_PREVIEW and TEMPLATE_RECORD)
|
|
|
* it should be set before calling this method, for example by calling
|
|
|
* {@link #createRepeatingRequestBuilder(int)}.
|
|
|
*/
|
|
|
@EngineThread
|
|
|
@SuppressWarnings("WeakerAccess")
|
|
|
protected void applyRepeatingRequestBuilder() {
|
|
|
applyRepeatingRequestBuilder(true, CameraException.REASON_DISCONNECTED);
|
|
|
}
|
|
|
|
|
|
@EngineThread
|
|
|
private void applyRepeatingRequestBuilder(boolean checkStarted, int errorReason) {
|
|
|
if ((getState() == CameraState.PREVIEW && !isChangingState()) || !checkStarted) {
|
|
|
try {
|
|
|
mSession.setRepeatingRequest(mRepeatingRequestBuilder.build(),
|
|
|
mRepeatingRequestCallback, null);
|
|
|
} catch (CameraAccessException e) {
|
|
|
throw new CameraException(e, errorReason);
|
|
|
} catch (IllegalStateException e) {
|
|
|
// mSession is invalid - has been closed. This is extremely worrying because
|
|
|
// it means that the session state and getPreviewState() are not synced.
|
|
|
// This probably signals an error in the setup/teardown synchronization.
|
|
|
LOG.e("applyRepeatingRequestBuilder: session is invalid!", e,
|
|
|
"checkStarted:", checkStarted,
|
|
|
"currentThread:", Thread.currentThread().getName(),
|
|
|
"state:", getState(),
|
|
|
"targetState:", getTargetState());
|
|
|
throw new CameraException(CameraException.REASON_DISCONNECTED);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private final CameraCaptureSession.CaptureCallback mRepeatingRequestCallback
|
|
|
= new CameraCaptureSession.CaptureCallback() {
|
|
|
@Override
|
|
|
public void onCaptureStarted(@NonNull CameraCaptureSession session,
|
|
|
@NonNull CaptureRequest request,
|
|
|
long timestamp,
|
|
|
long frameNumber) {
|
|
|
for (Action action : mActions) {
|
|
|
action.onCaptureStarted(Camera2Engine.this, request);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public void onCaptureProgressed(@NonNull CameraCaptureSession session,
|
|
|
@NonNull CaptureRequest request,
|
|
|
@NonNull CaptureResult partialResult) {
|
|
|
for (Action action : mActions) {
|
|
|
action.onCaptureProgressed(Camera2Engine.this, request, partialResult);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public void onCaptureCompleted(@NonNull CameraCaptureSession session,
|
|
|
@NonNull CaptureRequest request,
|
|
|
@NonNull TotalCaptureResult result) {
|
|
|
mLastRepeatingResult = result;
|
|
|
for (Action action : mActions) {
|
|
|
action.onCaptureCompleted(Camera2Engine.this, request, result);
|
|
|
}
|
|
|
}
|
|
|
};
|
|
|
|
|
|
//endregion
|
|
|
|
|
|
//region Protected APIs
|
|
|
|
|
|
@EngineThread
|
|
|
@NonNull
|
|
|
@Override
|
|
|
protected List<Size> getPreviewStreamAvailableSizes() {
|
|
|
try {
|
|
|
CameraCharacteristics characteristics = mManager.getCameraCharacteristics(mCameraId);
|
|
|
StreamConfigurationMap streamMap =
|
|
|
characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
|
|
|
if (streamMap == null) {
|
|
|
throw new RuntimeException("StreamConfigurationMap is null. Should not happen.");
|
|
|
}
|
|
|
// This works because our previews return either a SurfaceTexture or a SurfaceHolder,
|
|
|
// which are accepted class types by the getOutputSizes method.
|
|
|
android.util.Size[] sizes = streamMap.getOutputSizes(mPreview.getOutputClass());
|
|
|
List<Size> candidates = new ArrayList<>(sizes.length);
|
|
|
for (android.util.Size size : sizes) {
|
|
|
Size add = new Size(size.getWidth(), size.getHeight());
|
|
|
if (!candidates.contains(add)) candidates.add(add);
|
|
|
}
|
|
|
return candidates;
|
|
|
} catch (CameraAccessException e) {
|
|
|
throw createCameraException(e);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@EngineThread
|
|
|
@NonNull
|
|
|
@Override
|
|
|
protected List<Size> getFrameProcessingAvailableSizes() {
|
|
|
try {
|
|
|
CameraCharacteristics characteristics = mManager.getCameraCharacteristics(mCameraId);
|
|
|
StreamConfigurationMap streamMap =
|
|
|
characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
|
|
|
if (streamMap == null) {
|
|
|
throw new RuntimeException("StreamConfigurationMap is null. Should not happen.");
|
|
|
}
|
|
|
android.util.Size[] sizes = streamMap.getOutputSizes(mFrameProcessingFormat);
|
|
|
List<Size> candidates = new ArrayList<>(sizes.length);
|
|
|
for (android.util.Size size : sizes) {
|
|
|
Size add = new Size(size.getWidth(), size.getHeight());
|
|
|
if (!candidates.contains(add)) candidates.add(add);
|
|
|
}
|
|
|
return candidates;
|
|
|
} catch (CameraAccessException e) {
|
|
|
throw createCameraException(e);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@EngineThread
|
|
|
@Override
|
|
|
protected void onPreviewStreamSizeChanged() {
|
|
|
LOG.i("onPreviewStreamSizeChanged:", "Calling restartBind().");
|
|
|
restartBind();
|
|
|
}
|
|
|
|
|
|
@EngineThread
|
|
|
@Override
|
|
|
protected final boolean collectCameraInfo(@NonNull Facing facing) {
|
|
|
int internalFacing = mMapper.mapFacing(facing);
|
|
|
String[] cameraIds = null;
|
|
|
try {
|
|
|
cameraIds = mManager.getCameraIdList();
|
|
|
} catch (CameraAccessException e) {
|
|
|
// This should never happen, I don't see how it could crash here.
|
|
|
// However, let's launch an unrecoverable exception.
|
|
|
throw createCameraException(e);
|
|
|
}
|
|
|
LOG.i("collectCameraInfo", "Facing:", facing,
|
|
|
"Internal:", internalFacing,
|
|
|
"Cameras:", cameraIds.length);
|
|
|
for (String cameraId : cameraIds) {
|
|
|
try {
|
|
|
CameraCharacteristics characteristics = mManager.getCameraCharacteristics(cameraId);
|
|
|
if (internalFacing == readCharacteristic(characteristics,
|
|
|
CameraCharacteristics.LENS_FACING, -99)) {
|
|
|
mCameraId = cameraId;
|
|
|
int sensorOffset = readCharacteristic(characteristics,
|
|
|
CameraCharacteristics.SENSOR_ORIENTATION, 0);
|
|
|
getAngles().setSensorOffset(facing, sensorOffset);
|
|
|
return true;
|
|
|
}
|
|
|
} catch (CameraAccessException ignore) {
|
|
|
// This specific camera has been disconnected.
|
|
|
// Keep searching in other camerIds.
|
|
|
}
|
|
|
}
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
//endregion
|
|
|
|
|
|
//region Start
|
|
|
|
|
|
@EngineThread
|
|
|
@SuppressLint("MissingPermission")
|
|
|
@NonNull
|
|
|
@Override
|
|
|
protected Task<CameraOptions> onStartEngine() {
|
|
|
final TaskCompletionSource<CameraOptions> task = new TaskCompletionSource<>();
|
|
|
try {
|
|
|
// We have a valid camera for this Facing. Go on.
|
|
|
mManager.openCamera(mCameraId, new CameraDevice.StateCallback() {
|
|
|
@Override
|
|
|
public void onOpened(@NonNull CameraDevice camera) {
|
|
|
mCamera = camera;
|
|
|
|
|
|
// Set parameters that might have been set before the camera was opened.
|
|
|
try {
|
|
|
LOG.i("onStartEngine:", "Opened camera device.");
|
|
|
mCameraCharacteristics = mManager.getCameraCharacteristics(mCameraId);
|
|
|
boolean flip = getAngles().flip(Reference.SENSOR, Reference.VIEW);
|
|
|
int format;
|
|
|
switch (mPictureFormat) {
|
|
|
case JPEG: format = ImageFormat.JPEG; break;
|
|
|
case DNG: format = ImageFormat.RAW_SENSOR; break;
|
|
|
default: throw new IllegalArgumentException("Unknown format:"
|
|
|
+ mPictureFormat);
|
|
|
}
|
|
|
mCameraOptions = new Camera2Options(mManager, mCameraId, flip, format);
|
|
|
createRepeatingRequestBuilder(CameraDevice.TEMPLATE_PREVIEW);
|
|
|
} catch (CameraAccessException e) {
|
|
|
task.trySetException(createCameraException(e));
|
|
|
return;
|
|
|
}
|
|
|
task.trySetResult(mCameraOptions);
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public void onDisconnected(@NonNull CameraDevice camera) {
|
|
|
// Not sure if this is called INSTEAD of onOpened() or can be called after
|
|
|
// as well. Cover both cases with an unrecoverable exception so that the
|
|
|
// engine is properly destroyed.
|
|
|
CameraException exception
|
|
|
= new CameraException(CameraException.REASON_DISCONNECTED);
|
|
|
if (!task.getTask().isComplete()) {
|
|
|
task.trySetException(exception);
|
|
|
} else {
|
|
|
LOG.i("CameraDevice.StateCallback reported disconnection.");
|
|
|
throw exception;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public void onError(@NonNull CameraDevice camera, int error) {
|
|
|
if (!task.getTask().isComplete()) {
|
|
|
task.trySetException(createCameraException(error));
|
|
|
} else {
|
|
|
// This happened while the engine is running. Throw unrecoverable exception
|
|
|
// so that engine is properly destroyed.
|
|
|
LOG.e("CameraDevice.StateCallback reported an error:", error);
|
|
|
throw new CameraException(CameraException.REASON_DISCONNECTED);
|
|
|
}
|
|
|
}
|
|
|
}, null);
|
|
|
} catch (CameraAccessException e) {
|
|
|
throw createCameraException(e);
|
|
|
}
|
|
|
return task.getTask();
|
|
|
}
|
|
|
|
|
|
@EngineThread
|
|
|
@NonNull
|
|
|
@Override
|
|
|
protected Task<Void> onStartBind() {
|
|
|
LOG.i("onStartBind:", "Started");
|
|
|
final TaskCompletionSource<Void> task = new TaskCompletionSource<>();
|
|
|
|
|
|
// Compute sizes.
|
|
|
// TODO preview stream should never be bigger than 1920x1080 as per
|
|
|
// CameraDevice.createCaptureSession. This should be probably be applied
|
|
|
// before all the other external selectors, to treat it as a hard limit.
|
|
|
// OR: pass an int into these functions to be able to take smaller dims
|
|
|
// when session configuration fails
|
|
|
// OR: both.
|
|
|
mCaptureSize = computeCaptureSize();
|
|
|
mPreviewStreamSize = computePreviewStreamSize();
|
|
|
|
|
|
// Deal with surfaces.
|
|
|
// In Camera2, instead of applying the size to the camera params object,
|
|
|
// we must resize our own surfaces and configure them before opening the session.
|
|
|
List<Surface> outputSurfaces = new ArrayList<>();
|
|
|
|
|
|
// 1. PREVIEW
|
|
|
// Create a preview surface with the correct size.
|
|
|
final Object output = mPreview.getOutput();
|
|
|
if (output instanceof SurfaceHolder) {
|
|
|
try {
|
|
|
// This must be called from the UI thread...
|
|
|
Tasks.await(Tasks.call(new Callable<Void>() {
|
|
|
@Override
|
|
|
public Void call() {
|
|
|
((SurfaceHolder) output).setFixedSize(
|
|
|
mPreviewStreamSize.getWidth(),
|
|
|
mPreviewStreamSize.getHeight());
|
|
|
return null;
|
|
|
}
|
|
|
}));
|
|
|
} catch (ExecutionException | InterruptedException e) {
|
|
|
throw new CameraException(e, CameraException.REASON_FAILED_TO_CONNECT);
|
|
|
}
|
|
|
mPreviewStreamSurface = ((SurfaceHolder) output).getSurface();
|
|
|
} else if (output instanceof SurfaceTexture) {
|
|
|
((SurfaceTexture) output).setDefaultBufferSize(
|
|
|
mPreviewStreamSize.getWidth(),
|
|
|
mPreviewStreamSize.getHeight());
|
|
|
mPreviewStreamSurface = new Surface((SurfaceTexture) output);
|
|
|
} else {
|
|
|
throw new RuntimeException("Unknown CameraPreview output class.");
|
|
|
}
|
|
|
outputSurfaces.add(mPreviewStreamSurface);
|
|
|
|
|
|
// 2. VIDEO RECORDING
|
|
|
if (getMode() == Mode.VIDEO) {
|
|
|
if (mFullVideoPendingStub != null) {
|
|
|
Full2VideoRecorder recorder = new Full2VideoRecorder(this, mCameraId);
|
|
|
try {
|
|
|
outputSurfaces.add(recorder.createInputSurface(mFullVideoPendingStub));
|
|
|
} catch (Full2VideoRecorder.PrepareException e) {
|
|
|
throw new CameraException(e, CameraException.REASON_FAILED_TO_CONNECT);
|
|
|
}
|
|
|
mVideoRecorder = recorder;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 3. PICTURE RECORDING
|
|
|
// Format is supported, or it would have thrown in Camera2Options constructor.
|
|
|
if (getMode() == Mode.PICTURE) {
|
|
|
int format;
|
|
|
switch (mPictureFormat) {
|
|
|
case JPEG: format = ImageFormat.JPEG; break;
|
|
|
case DNG: format = ImageFormat.RAW_SENSOR; break;
|
|
|
default: throw new IllegalArgumentException("Unknown format:" + mPictureFormat);
|
|
|
}
|
|
|
mPictureReader = ImageReader.newInstance(
|
|
|
mCaptureSize.getWidth(),
|
|
|
mCaptureSize.getHeight(),
|
|
|
format, 2);
|
|
|
outputSurfaces.add(mPictureReader.getSurface());
|
|
|
}
|
|
|
|
|
|
// 4. FRAME PROCESSING
|
|
|
if (hasFrameProcessors()) {
|
|
|
mFrameProcessingSize = computeFrameProcessingSize();
|
|
|
mFrameProcessingReader = ImageReader.newInstance(
|
|
|
mFrameProcessingSize.getWidth(),
|
|
|
mFrameProcessingSize.getHeight(),
|
|
|
mFrameProcessingFormat,
|
|
|
getFrameProcessingPoolSize());
|
|
|
mFrameProcessingReader.setOnImageAvailableListener(this,
|
|
|
mFrameProcessingHandler.getHandler());
|
|
|
mFrameProcessingSurface = mFrameProcessingReader.getSurface();
|
|
|
outputSurfaces.add(mFrameProcessingSurface);
|
|
|
} else {
|
|
|
mFrameProcessingReader = null;
|
|
|
mFrameProcessingSize = null;
|
|
|
mFrameProcessingSurface = null;
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
// null handler means using the current looper which is totally ok.
|
|
|
mCamera.createCaptureSession(outputSurfaces, new CameraCaptureSession.StateCallback() {
|
|
|
@Override
|
|
|
public void onConfigured(@NonNull CameraCaptureSession session) {
|
|
|
mSession = session;
|
|
|
LOG.i("onStartBind:", "Completed");
|
|
|
task.trySetResult(null);
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public void onConfigureFailed(@NonNull CameraCaptureSession session) {
|
|
|
// This SHOULD be a library error so we throw a RuntimeException.
|
|
|
String message = LOG.e("onConfigureFailed! Session", session);
|
|
|
throw new RuntimeException(message);
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public void onReady(@NonNull CameraCaptureSession session) {
|
|
|
super.onReady(session);
|
|
|
LOG.i("CameraCaptureSession.StateCallback reported onReady.");
|
|
|
}
|
|
|
}, null);
|
|
|
} catch (CameraAccessException e) {
|
|
|
throw createCameraException(e);
|
|
|
}
|
|
|
return task.getTask();
|
|
|
}
|
|
|
|
|
|
@EngineThread
|
|
|
@NonNull
|
|
|
@Override
|
|
|
protected Task<Void> onStartPreview() {
|
|
|
LOG.i("onStartPreview:", "Dispatching onCameraPreviewStreamSizeChanged.");
|
|
|
getCallback().onCameraPreviewStreamSizeChanged();
|
|
|
|
|
|
Size previewSizeForView = getPreviewStreamSize(Reference.VIEW);
|
|
|
if (previewSizeForView == null) {
|
|
|
throw new IllegalStateException("previewStreamSize should not be null at this point.");
|
|
|
}
|
|
|
mPreview.setStreamSize(previewSizeForView.getWidth(), previewSizeForView.getHeight());
|
|
|
mPreview.setDrawRotation(getAngles().offset(Reference.BASE, Reference.VIEW, Axis.ABSOLUTE));
|
|
|
if (hasFrameProcessors()) {
|
|
|
getFrameManager().setUp(mFrameProcessingFormat, mFrameProcessingSize);
|
|
|
}
|
|
|
|
|
|
LOG.i("onStartPreview:", "Starting preview.");
|
|
|
addRepeatingRequestBuilderSurfaces();
|
|
|
applyRepeatingRequestBuilder(false,
|
|
|
CameraException.REASON_FAILED_TO_START_PREVIEW);
|
|
|
LOG.i("onStartPreview:", "Started preview.");
|
|
|
|
|
|
// Start delayed video if needed.
|
|
|
if (mFullVideoPendingStub != null) {
|
|
|
// Do not call takeVideo/onTakeVideo. It will reset some stub parameters that
|
|
|
// the recorder sets. Also we are posting so that doTakeVideo sees a started preview.
|
|
|
final VideoResult.Stub stub = mFullVideoPendingStub;
|
|
|
mFullVideoPendingStub = null;
|
|
|
getOrchestrator().scheduleStateful("do take video", CameraState.PREVIEW,
|
|
|
new Runnable() {
|
|
|
@Override
|
|
|
public void run() {
|
|
|
doTakeVideo(stub);
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
return Tasks.forResult(null);
|
|
|
}
|
|
|
|
|
|
//endregion
|
|
|
|
|
|
//region Stop
|
|
|
|
|
|
@EngineThread
|
|
|
@NonNull
|
|
|
@Override
|
|
|
protected Task<Void> onStopPreview() {
|
|
|
LOG.i("onStopPreview:", "Started.");
|
|
|
if (mVideoRecorder != null) {
|
|
|
// This should synchronously call onVideoResult that will reset the repeating builder
|
|
|
// to the PREVIEW template. This is very important.
|
|
|
mVideoRecorder.stop(true);
|
|
|
mVideoRecorder = null;
|
|
|
}
|
|
|
mPictureRecorder = null;
|
|
|
if (hasFrameProcessors()) {
|
|
|
getFrameManager().release();
|
|
|
}
|
|
|
// Removing the part below for now. It hangs on emulators and can take a lot of time
|
|
|
// in real devices, for benefits that I'm not 100% sure about.
|
|
|
if (false) {
|
|
|
try {
|
|
|
// Preferring abortCaptures() over stopRepeating(): it makes sure that all
|
|
|
// in-flight operations are discarded as fast as possible, which is what we want.
|
|
|
// NOTE: this call is asynchronous. Should find a good way to wait for the outcome.
|
|
|
LOG.i("onStopPreview:", "calling abortCaptures().");
|
|
|
mSession.abortCaptures();
|
|
|
LOG.i("onStopPreview:", "called abortCaptures().");
|
|
|
} catch (CameraAccessException e) {
|
|
|
// This tells us that we should stop everything. It's better to throw an
|
|
|
// unrecoverable exception rather than just swallow, so everything gets stopped.
|
|
|
LOG.w("onStopPreview:", "abortCaptures failed!", e);
|
|
|
throw createCameraException(e);
|
|
|
} catch (IllegalStateException e) {
|
|
|
// This tells us that the session was already closed.
|
|
|
// Not sure if this can happen, but we can swallow it.
|
|
|
}
|
|
|
}
|
|
|
removeRepeatingRequestBuilderSurfaces();
|
|
|
mLastRepeatingResult = null;
|
|
|
LOG.i("onStopPreview:", "Returning.");
|
|
|
return Tasks.forResult(null);
|
|
|
}
|
|
|
|
|
|
@EngineThread
|
|
|
@NonNull
|
|
|
@Override
|
|
|
protected Task<Void> onStopBind() {
|
|
|
LOG.i("onStopBind:", "About to clean up.");
|
|
|
mFrameProcessingSurface = null;
|
|
|
mPreviewStreamSurface = null;
|
|
|
mPreviewStreamSize = null;
|
|
|
mCaptureSize = null;
|
|
|
mFrameProcessingSize = null;
|
|
|
if (mFrameProcessingReader != null) {
|
|
|
// WARNING: This call synchronously releases all Images and their underlying
|
|
|
// properties. This can cause issues if the Image is being used.
|
|
|
mFrameProcessingReader.close();
|
|
|
mFrameProcessingReader = null;
|
|
|
}
|
|
|
if (mPictureReader != null) {
|
|
|
mPictureReader.close();
|
|
|
mPictureReader = null;
|
|
|
}
|
|
|
mSession.close();
|
|
|
mSession = null;
|
|
|
LOG.i("onStopBind:", "Returning.");
|
|
|
return Tasks.forResult(null);
|
|
|
}
|
|
|
|
|
|
@EngineThread
|
|
|
@NonNull
|
|
|
@Override
|
|
|
protected Task<Void> onStopEngine() {
|
|
|
try {
|
|
|
LOG.i("onStopEngine:", "Clean up.", "Releasing camera.");
|
|
|
// Just like Camera1Engine, this call can hang (at least on emulators) and if
|
|
|
// we don't find a way around the lock, it leaves the camera in a bad state.
|
|
|
//
|
|
|
// 12:33:28.152 2888 5470 I CameraEngine: onStopEngine: Clean up. Releasing camera.[0m
|
|
|
// 12:33:29.476 1384 1555 E audio_hw_generic: pcm_write failed cannot write stream data: I/O error[0m
|
|
|
// 12:33:33.206 1512 3616 E Camera3-Device: Camera 0: waitUntilDrainedLocked: Error waiting for HAL to drain: Connection timed out (-110)[0m
|
|
|
// 12:33:33.242 1512 3616 E CameraDeviceClient: detachDevice: waitUntilDrained failed with code 0xffffff92[0m
|
|
|
// 12:33:33.243 1512 3616 E Camera3-Device: Camera 0: disconnect: Shutting down in an error state[0m
|
|
|
//
|
|
|
// I believe there is a thread deadlock due to this call internally waiting to
|
|
|
// dispatch some callback to us (pending captures, ...), but the callback thread
|
|
|
// is blocked here. We try to workaround this in CameraEngine.destroy().
|
|
|
mCamera.close();
|
|
|
LOG.i("onStopEngine:", "Clean up.", "Released camera.");
|
|
|
} catch (Exception e) {
|
|
|
LOG.w("onStopEngine:", "Clean up.", "Exception while releasing camera.", e);
|
|
|
}
|
|
|
mCamera = null;
|
|
|
|
|
|
// After engine is stopping, the repeating request builder will be null,
|
|
|
// so the ActionHolder.getBuilder() contract would be broken. Same for characteristics.
|
|
|
// This can cause crashes if some ongoing Action queries the holder. So we abort them.
|
|
|
LOG.i("onStopEngine:", "Aborting actions.");
|
|
|
for (Action action : mActions) {
|
|
|
action.abort(this);
|
|
|
}
|
|
|
|
|
|
mCameraCharacteristics = null;
|
|
|
mCameraOptions = null;
|
|
|
mVideoRecorder = null;
|
|
|
mRepeatingRequestBuilder = null;
|
|
|
LOG.w("onStopEngine:", "Returning.");
|
|
|
return Tasks.forResult(null);
|
|
|
}
|
|
|
|
|
|
//endregion
|
|
|
|
|
|
//region Pictures
|
|
|
|
|
|
@EngineThread
|
|
|
@Override
|
|
|
protected void onTakePictureSnapshot(@NonNull final PictureResult.Stub stub,
|
|
|
@NonNull final AspectRatio outputRatio,
|
|
|
boolean doMetering) {
|
|
|
if (doMetering) {
|
|
|
LOG.i("onTakePictureSnapshot:", "doMetering is true. Delaying.");
|
|
|
Action action = Actions.timeout(METER_TIMEOUT, createMeterAction(null));
|
|
|
action.addCallback(new CompletionCallback() {
|
|
|
@Override
|
|
|
protected void onActionCompleted(@NonNull Action action) {
|
|
|
// This is called on any thread, so be careful.
|
|
|
setPictureSnapshotMetering(false);
|
|
|
takePictureSnapshot(stub);
|
|
|
setPictureSnapshotMetering(true);
|
|
|
}
|
|
|
});
|
|
|
action.start(this);
|
|
|
} 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");
|
|
|
}
|
|
|
// stub.size is not the real size: it will be cropped to the given ratio
|
|
|
// stub.rotation will be set to 0 - we rotate the texture instead.
|
|
|
stub.size = getUncroppedSnapshotSize(Reference.OUTPUT);
|
|
|
stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT,
|
|
|
Axis.RELATIVE_TO_SENSOR);
|
|
|
mPictureRecorder = new Snapshot2PictureRecorder(stub, this,
|
|
|
(GlCameraPreview) mPreview, outputRatio);
|
|
|
mPictureRecorder.take();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@EngineThread
|
|
|
@Override
|
|
|
protected void onTakePicture(@NonNull final PictureResult.Stub stub, boolean doMetering) {
|
|
|
if (doMetering) {
|
|
|
LOG.i("onTakePicture:", "doMetering is true. Delaying.");
|
|
|
Action action = Actions.timeout(METER_TIMEOUT, createMeterAction(null));
|
|
|
action.addCallback(new CompletionCallback() {
|
|
|
@Override
|
|
|
protected void onActionCompleted(@NonNull Action action) {
|
|
|
// This is called on any thread, so be careful.
|
|
|
setPictureMetering(false);
|
|
|
takePicture(stub);
|
|
|
setPictureMetering(true);
|
|
|
}
|
|
|
});
|
|
|
action.start(this);
|
|
|
} else {
|
|
|
LOG.i("onTakePicture:", "doMetering is false. Performing.");
|
|
|
stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT,
|
|
|
Axis.RELATIVE_TO_SENSOR);
|
|
|
stub.size = getPictureSize(Reference.OUTPUT);
|
|
|
try {
|
|
|
if (mPictureCaptureStopsPreview) {
|
|
|
// These two are present in official samples and are probably meant to
|
|
|
// speed things up? But from my tests, they actually make everything slower.
|
|
|
// So this is disabled by default with a boolean flag. Maybe in the future
|
|
|
// we can make this configurable as some people might want to stop the preview
|
|
|
// while picture is being taken even if it increases the latency.
|
|
|
mSession.stopRepeating();
|
|
|
mSession.abortCaptures();
|
|
|
}
|
|
|
CaptureRequest.Builder builder
|
|
|
= mCamera.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
|
|
|
applyAllParameters(builder, mRepeatingRequestBuilder);
|
|
|
mPictureRecorder = new Full2PictureRecorder(stub, this, builder,
|
|
|
mPictureReader);
|
|
|
mPictureRecorder.take();
|
|
|
} catch (CameraAccessException e) {
|
|
|
throw createCameraException(e);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public void onPictureResult(@Nullable PictureResult.Stub result, @Nullable Exception error) {
|
|
|
boolean fullPicture = mPictureRecorder instanceof Full2PictureRecorder;
|
|
|
super.onPictureResult(result, error);
|
|
|
if (fullPicture && mPictureCaptureStopsPreview) {
|
|
|
applyRepeatingRequestBuilder();
|
|
|
}
|
|
|
|
|
|
// Some picture recorders might lock metering, and we usually run a metering sequence
|
|
|
// before running the recorders. So, run an unlock/reset sequence if needed.
|
|
|
boolean unlock = (fullPicture && getPictureMetering())
|
|
|
|| (!fullPicture && getPictureSnapshotMetering());
|
|
|
if (unlock) {
|
|
|
getOrchestrator().scheduleStateful("reset metering after picture",
|
|
|
CameraState.PREVIEW,
|
|
|
new Runnable() {
|
|
|
@Override
|
|
|
public void run() {
|
|
|
unlockAndResetMetering();
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
|
|
|
//endregion
|
|
|
|
|
|
//region Videos
|
|
|
|
|
|
@EngineThread
|
|
|
@Override
|
|
|
protected void onTakeVideo(@NonNull VideoResult.Stub stub) {
|
|
|
LOG.i("onTakeVideo", "called.");
|
|
|
stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT,
|
|
|
Axis.RELATIVE_TO_SENSOR);
|
|
|
stub.size = getAngles().flip(Reference.SENSOR, Reference.OUTPUT) ?
|
|
|
mCaptureSize.flip() : mCaptureSize;
|
|
|
// We must restart the session at each time.
|
|
|
// Save the pending data and restart the session.
|
|
|
LOG.w("onTakeVideo", "calling restartBind.");
|
|
|
mFullVideoPendingStub = stub;
|
|
|
restartBind();
|
|
|
}
|
|
|
|
|
|
private void doTakeVideo(@NonNull final VideoResult.Stub stub) {
|
|
|
if (!(mVideoRecorder instanceof Full2VideoRecorder)) {
|
|
|
throw new IllegalStateException("doTakeVideo called, but video recorder " +
|
|
|
"is not a Full2VideoRecorder! " + mVideoRecorder);
|
|
|
}
|
|
|
Full2VideoRecorder recorder = (Full2VideoRecorder) mVideoRecorder;
|
|
|
try {
|
|
|
createRepeatingRequestBuilder(CameraDevice.TEMPLATE_RECORD);
|
|
|
addRepeatingRequestBuilderSurfaces(recorder.getInputSurface());
|
|
|
applyRepeatingRequestBuilder(true, CameraException.REASON_DISCONNECTED);
|
|
|
mVideoRecorder.start(stub);
|
|
|
} catch (CameraAccessException e) {
|
|
|
onVideoResult(null, e);
|
|
|
throw createCameraException(e);
|
|
|
} catch (CameraException e) {
|
|
|
onVideoResult(null, e);
|
|
|
throw e;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@EngineThread
|
|
|
@Override
|
|
|
protected void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub,
|
|
|
@NonNull AspectRatio outputRatio) {
|
|
|
if (!(mPreview instanceof GlCameraPreview)) {
|
|
|
throw new IllegalStateException("Video snapshots are only supported with GL_SURFACE.");
|
|
|
}
|
|
|
GlCameraPreview glPreview = (GlCameraPreview) mPreview;
|
|
|
Size outputSize = getUncroppedSnapshotSize(Reference.OUTPUT);
|
|
|
if (outputSize == null) {
|
|
|
throw new IllegalStateException("outputSize should not be null.");
|
|
|
}
|
|
|
Rect outputCrop = CropHelper.computeCrop(outputSize, outputRatio);
|
|
|
outputSize = new Size(outputCrop.width(), outputCrop.height());
|
|
|
stub.size = outputSize;
|
|
|
// Vertical: 0 (270-0-0)
|
|
|
// Left (unlocked): 270 (270-90-270)
|
|
|
// Right (unlocked): 90 (270-270-90)
|
|
|
// Upside down (unlocked): 180 (270-180-180)
|
|
|
// Left (locked): 270 (270-0-270)
|
|
|
// Right (locked): 90 (270-0-90)
|
|
|
// Upside down (locked): 180 (270-0-180)
|
|
|
// Unlike Camera1, the correct formula seems to be deviceOrientation,
|
|
|
// which means offset(Reference.BASE, Reference.OUTPUT, Axis.ABSOLUTE).
|
|
|
stub.rotation = getAngles().offset(Reference.BASE, Reference.OUTPUT, Axis.ABSOLUTE);
|
|
|
stub.videoFrameRate = Math.round(mPreviewFrameRate);
|
|
|
LOG.i("onTakeVideoSnapshot", "rotation:", stub.rotation, "size:", stub.size);
|
|
|
|
|
|
// Start.
|
|
|
// The overlay rotation should alway be VIEW-OUTPUT, just liek Camera1Engine.
|
|
|
int overlayRotation = getAngles().offset(Reference.VIEW, Reference.OUTPUT, Axis.ABSOLUTE);
|
|
|
mVideoRecorder = new SnapshotVideoRecorder(this, glPreview, getOverlay(),
|
|
|
overlayRotation);
|
|
|
mVideoRecorder.start(stub);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* When video ends we must stop the recorder and remove the recorder surface from
|
|
|
* camera outputs. This is done in onVideoResult. However, on some devices, order matters.
|
|
|
* If we stop the recorder and AFTER send camera frames to it, the camera will try to fill
|
|
|
* the recorder "abandoned" Surface and on some devices with a poor internal implementation
|
|
|
* (HW_LEVEL_LEGACY) this crashes. So if the conditions are met, we restore here. Issue #549.
|
|
|
*/
|
|
|
@Override
|
|
|
public void onVideoRecordingEnd() {
|
|
|
super.onVideoRecordingEnd();
|
|
|
// SnapshotRecorder will invoke this on its own thread which is risky, but if it was a
|
|
|
// snapshot, this function does nothing so it's safe.
|
|
|
boolean needsIssue549Workaround = (mVideoRecorder instanceof Full2VideoRecorder) &&
|
|
|
(readCharacteristic(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL, -1)
|
|
|
== CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY);
|
|
|
if (needsIssue549Workaround) {
|
|
|
LOG.w("Applying the Issue549 workaround.", Thread.currentThread());
|
|
|
maybeRestorePreviewTemplateAfterVideo();
|
|
|
LOG.w("Applied the Issue549 workaround. Sleeping...");
|
|
|
try { Thread.sleep(600); } catch (InterruptedException ignore) {}
|
|
|
LOG.w("Applied the Issue549 workaround. Slept!");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public void onVideoResult(@Nullable VideoResult.Stub result, @Nullable Exception exception) {
|
|
|
super.onVideoResult(result, exception);
|
|
|
// SnapshotRecorder will invoke this on its own thread, so let's post in our own thread
|
|
|
// and check camera state before trying to restore the preview. Engine might have been
|
|
|
// torn down in the engine thread while this was still being called.
|
|
|
getOrchestrator().scheduleStateful("restore preview template", CameraState.BIND,
|
|
|
new Runnable() {
|
|
|
@Override
|
|
|
public void run() {
|
|
|
maybeRestorePreviewTemplateAfterVideo();
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Video recorders might change the camera template to {@link CameraDevice#TEMPLATE_RECORD}.
|
|
|
* After the video is taken, we should restore the template preview, which also means that
|
|
|
* we'll remove any extra surface target that was added by the video recorder.
|
|
|
*
|
|
|
* This method avoids doing this twice by checking the request tag, as set by
|
|
|
* the {@link #createRepeatingRequestBuilder(int)} method.
|
|
|
*/
|
|
|
@EngineThread
|
|
|
private void maybeRestorePreviewTemplateAfterVideo() {
|
|
|
int template = (int) mRepeatingRequestBuilder.build().getTag();
|
|
|
if (template != CameraDevice.TEMPLATE_PREVIEW) {
|
|
|
try {
|
|
|
createRepeatingRequestBuilder(CameraDevice.TEMPLATE_PREVIEW);
|
|
|
addRepeatingRequestBuilderSurfaces();
|
|
|
applyRepeatingRequestBuilder();
|
|
|
} catch (CameraAccessException e) {
|
|
|
throw createCameraException(e);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
//endregion
|
|
|
|
|
|
//region Parameters
|
|
|
|
|
|
private void applyAllParameters(@NonNull CaptureRequest.Builder builder,
|
|
|
@Nullable CaptureRequest.Builder oldBuilder) {
|
|
|
LOG.i("applyAllParameters:", "called for tag", builder.build().getTag());
|
|
|
builder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO);
|
|
|
applyDefaultFocus(builder);
|
|
|
applyFlash(builder, Flash.OFF);
|
|
|
applyLocation(builder, null);
|
|
|
applyWhiteBalance(builder, WhiteBalance.AUTO);
|
|
|
applyHdr(builder, Hdr.OFF);
|
|
|
applyZoom(builder, 0F);
|
|
|
applyExposureCorrection(builder, 0F);
|
|
|
applyPreviewFrameRate(builder, 0F);
|
|
|
|
|
|
if (oldBuilder != null) {
|
|
|
// We might be in a metering operation, or the old builder might have some special
|
|
|
// metering parameters. Copy these special keys over to the new builder.
|
|
|
// These are the keys changed by metering.Parameters, or by us in applyFocusForMetering.
|
|
|
builder.set(CaptureRequest.CONTROL_AF_REGIONS,
|
|
|
oldBuilder.get(CaptureRequest.CONTROL_AF_REGIONS));
|
|
|
builder.set(CaptureRequest.CONTROL_AE_REGIONS,
|
|
|
oldBuilder.get(CaptureRequest.CONTROL_AE_REGIONS));
|
|
|
builder.set(CaptureRequest.CONTROL_AWB_REGIONS,
|
|
|
oldBuilder.get(CaptureRequest.CONTROL_AWB_REGIONS));
|
|
|
builder.set(CaptureRequest.CONTROL_AF_MODE,
|
|
|
oldBuilder.get(CaptureRequest.CONTROL_AF_MODE));
|
|
|
// Do NOT copy exposure or focus triggers!
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@SuppressWarnings("WeakerAccess")
|
|
|
protected void applyDefaultFocus(@NonNull CaptureRequest.Builder builder) {
|
|
|
int[] modesArray = readCharacteristic(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES,
|
|
|
new int[]{});
|
|
|
List<Integer> modes = new ArrayList<>();
|
|
|
for (int mode : modesArray) { modes.add(mode); }
|
|
|
if (getMode() == Mode.VIDEO &&
|
|
|
modes.contains(CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO)) {
|
|
|
builder.set(CaptureRequest.CONTROL_AF_MODE,
|
|
|
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (modes.contains(CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)) {
|
|
|
builder.set(CaptureRequest.CONTROL_AF_MODE,
|
|
|
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (modes.contains(CaptureRequest.CONTROL_AF_MODE_AUTO)) {
|
|
|
builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (modes.contains(CaptureRequest.CONTROL_AF_MODE_OFF)) {
|
|
|
builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF);
|
|
|
builder.set(CaptureRequest.LENS_FOCUS_DISTANCE, 0F);
|
|
|
//noinspection UnnecessaryReturnStatement
|
|
|
return;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* All focus modes support the AF trigger, except OFF and EDOF.
|
|
|
* However, unlike the preview, we'd prefer AUTO to any CONTINUOUS value.
|
|
|
* An AUTO value means that focus is locked unless we run the focus trigger,
|
|
|
* which is what metering does.
|
|
|
*
|
|
|
* @param builder builder
|
|
|
*/
|
|
|
@SuppressWarnings("WeakerAccess")
|
|
|
protected void applyFocusForMetering(@NonNull CaptureRequest.Builder builder) {
|
|
|
int[] modesArray = readCharacteristic(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES,
|
|
|
new int[]{});
|
|
|
List<Integer> modes = new ArrayList<>();
|
|
|
for (int mode : modesArray) { modes.add(mode); }
|
|
|
if (modes.contains(CaptureRequest.CONTROL_AF_MODE_AUTO)) {
|
|
|
builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO);
|
|
|
return;
|
|
|
}
|
|
|
if (getMode() == Mode.VIDEO &&
|
|
|
modes.contains(CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO)) {
|
|
|
builder.set(CaptureRequest.CONTROL_AF_MODE,
|
|
|
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (modes.contains(CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)) {
|
|
|
builder.set(CaptureRequest.CONTROL_AF_MODE,
|
|
|
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
|
|
|
//noinspection UnnecessaryReturnStatement
|
|
|
return;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public void setFlash(@NonNull final Flash flash) {
|
|
|
final Flash old = mFlash;
|
|
|
mFlash = flash;
|
|
|
mFlashTask = getOrchestrator().scheduleStateful("flash (" + flash + ")",
|
|
|
CameraState.ENGINE,
|
|
|
new Runnable() {
|
|
|
@Override
|
|
|
public void run() {
|
|
|
boolean shouldApply = applyFlash(mRepeatingRequestBuilder, old);
|
|
|
boolean needsWorkaround = getState() == CameraState.PREVIEW;
|
|
|
if (needsWorkaround) {
|
|
|
// Runtime changes to the flash value are not correctly handled by the
|
|
|
// driver. See https://stackoverflow.com/q/53003383/4288782 for example.
|
|
|
// For this reason, we go back to OFF, capture once, then go to the new one.
|
|
|
mFlash = Flash.OFF;
|
|
|
applyFlash(mRepeatingRequestBuilder, old);
|
|
|
try {
|
|
|
mSession.capture(mRepeatingRequestBuilder.build(), null,
|
|
|
null);
|
|
|
} catch (CameraAccessException e) {
|
|
|
throw createCameraException(e);
|
|
|
}
|
|
|
mFlash = flash;
|
|
|
applyFlash(mRepeatingRequestBuilder, old);
|
|
|
applyRepeatingRequestBuilder();
|
|
|
|
|
|
} else if (shouldApply) {
|
|
|
applyRepeatingRequestBuilder();
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* This sets the CONTROL_AE_MODE to either:
|
|
|
* - {@link CaptureRequest#CONTROL_AE_MODE_ON}
|
|
|
* - {@link CaptureRequest#CONTROL_AE_MODE_ON_AUTO_FLASH}
|
|
|
* - {@link CaptureRequest#CONTROL_AE_MODE_ON_ALWAYS_FLASH}
|
|
|
*
|
|
|
* The API offers a high level control through {@link CaptureRequest#CONTROL_AE_MODE},
|
|
|
* which is what the mapper looks at. It will trigger (if specified) flash only for
|
|
|
* still captures which is exactly what we want.
|
|
|
*
|
|
|
* However, we set CONTROL_AE_MODE to ON/OFF (depending
|
|
|
* on which is available) with both {@link Flash#OFF} and {@link Flash#TORCH}.
|
|
|
*
|
|
|
* When CONTROL_AE_MODE is ON or OFF, the low level control, called
|
|
|
* {@link CaptureRequest#FLASH_MODE}, becomes effective, and that's where we can actually
|
|
|
* distinguish between a turned off flash and a torch flash.
|
|
|
*/
|
|
|
@SuppressWarnings("WeakerAccess")
|
|
|
protected boolean applyFlash(@NonNull CaptureRequest.Builder builder,
|
|
|
@NonNull Flash oldFlash) {
|
|
|
if (mCameraOptions.supports(mFlash)) {
|
|
|
int[] availableAeModesArray = readCharacteristic(
|
|
|
CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES, new int[]{});
|
|
|
List<Integer> availableAeModes = new ArrayList<>();
|
|
|
for (int mode : availableAeModesArray) { availableAeModes.add(mode); }
|
|
|
|
|
|
List<Pair<Integer, Integer>> pairs = mMapper.mapFlash(mFlash);
|
|
|
for (Pair<Integer, Integer> pair : pairs) {
|
|
|
if (availableAeModes.contains(pair.first)) {
|
|
|
LOG.i("applyFlash: setting CONTROL_AE_MODE to", pair.first);
|
|
|
LOG.i("applyFlash: setting FLASH_MODE to", pair.second);
|
|
|
builder.set(CaptureRequest.CONTROL_AE_MODE, pair.first);
|
|
|
builder.set(CaptureRequest.FLASH_MODE, pair.second);
|
|
|
return true;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
mFlash = oldFlash;
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public void setLocation(@Nullable Location location) {
|
|
|
final Location old = mLocation;
|
|
|
mLocation = location;
|
|
|
mLocationTask = getOrchestrator().scheduleStateful("location",
|
|
|
CameraState.ENGINE,
|
|
|
new Runnable() {
|
|
|
@Override
|
|
|
public void run() {
|
|
|
if (applyLocation(mRepeatingRequestBuilder, old)) {
|
|
|
applyRepeatingRequestBuilder();
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
@SuppressWarnings("WeakerAccess")
|
|
|
protected boolean applyLocation(@NonNull CaptureRequest.Builder builder,
|
|
|
@SuppressWarnings("unused") @Nullable Location oldLocation) {
|
|
|
if (mLocation != null) {
|
|
|
builder.set(CaptureRequest.JPEG_GPS_LOCATION, mLocation);
|
|
|
}
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public void setWhiteBalance(@NonNull WhiteBalance whiteBalance) {
|
|
|
final WhiteBalance old = mWhiteBalance;
|
|
|
mWhiteBalance = whiteBalance;
|
|
|
mWhiteBalanceTask = getOrchestrator().scheduleStateful(
|
|
|
"white balance (" + whiteBalance + ")",
|
|
|
CameraState.ENGINE,
|
|
|
new Runnable() {
|
|
|
@Override
|
|
|
public void run() {
|
|
|
if (applyWhiteBalance(mRepeatingRequestBuilder, old)) {
|
|
|
applyRepeatingRequestBuilder();
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
@SuppressWarnings("WeakerAccess")
|
|
|
protected boolean applyWhiteBalance(@NonNull CaptureRequest.Builder builder,
|
|
|
@NonNull WhiteBalance oldWhiteBalance) {
|
|
|
if (mCameraOptions.supports(mWhiteBalance)) {
|
|
|
int whiteBalance = mMapper.mapWhiteBalance(mWhiteBalance);
|
|
|
builder.set(CaptureRequest.CONTROL_AWB_MODE, whiteBalance);
|
|
|
return true;
|
|
|
}
|
|
|
mWhiteBalance = oldWhiteBalance;
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public void setHdr(@NonNull Hdr hdr) {
|
|
|
final Hdr old = mHdr;
|
|
|
mHdr = hdr;
|
|
|
mHdrTask = getOrchestrator().scheduleStateful("hdr (" + hdr + ")",
|
|
|
CameraState.ENGINE,
|
|
|
new Runnable() {
|
|
|
@Override
|
|
|
public void run() {
|
|
|
if (applyHdr(mRepeatingRequestBuilder, old)) {
|
|
|
applyRepeatingRequestBuilder();
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
@SuppressWarnings("WeakerAccess")
|
|
|
protected boolean applyHdr(@NonNull CaptureRequest.Builder builder, @NonNull Hdr oldHdr) {
|
|
|
if (mCameraOptions.supports(mHdr)) {
|
|
|
int hdr = mMapper.mapHdr(mHdr);
|
|
|
builder.set(CaptureRequest.CONTROL_SCENE_MODE, hdr);
|
|
|
return true;
|
|
|
}
|
|
|
mHdr = oldHdr;
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public void setZoom(final float zoom, final @Nullable PointF[] points, final boolean notify) {
|
|
|
final float old = mZoomValue;
|
|
|
mZoomValue = zoom;
|
|
|
mZoomTask = getOrchestrator().scheduleStateful(
|
|
|
"zoom (" + zoom + ")",
|
|
|
CameraState.ENGINE,
|
|
|
new Runnable() {
|
|
|
@Override
|
|
|
public void run() {
|
|
|
if (applyZoom(mRepeatingRequestBuilder, old)) {
|
|
|
applyRepeatingRequestBuilder();
|
|
|
if (notify) {
|
|
|
getCallback().dispatchOnZoomChanged(zoom, points);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
@SuppressWarnings("WeakerAccess")
|
|
|
protected boolean applyZoom(@NonNull CaptureRequest.Builder builder, float oldZoom) {
|
|
|
if (mCameraOptions.isZoomSupported()) {
|
|
|
float maxZoom = readCharacteristic(
|
|
|
CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM, 1F);
|
|
|
// converting 0.0f-1.0f zoom scale to the actual camera digital zoom scale
|
|
|
// (which will be for example, 1.0-10.0)
|
|
|
float calculatedZoom = (mZoomValue * (maxZoom - 1.0f)) + 1.0f;
|
|
|
Rect newRect = getZoomRect(calculatedZoom, maxZoom);
|
|
|
builder.set(CaptureRequest.SCALER_CROP_REGION, newRect);
|
|
|
return true;
|
|
|
}
|
|
|
mZoomValue = oldZoom;
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
@NonNull
|
|
|
private Rect getZoomRect(float zoomLevel, float maxDigitalZoom) {
|
|
|
Rect activeRect = readCharacteristic(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE,
|
|
|
new Rect());
|
|
|
int minW = (int) (activeRect.width() / maxDigitalZoom);
|
|
|
int minH = (int) (activeRect.height() / maxDigitalZoom);
|
|
|
int difW = activeRect.width() - minW;
|
|
|
int difH = activeRect.height() - minH;
|
|
|
|
|
|
// When zoom is 1, we want to return new Rect(0, 0, width, height).
|
|
|
// When zoom is maxZoom, we want to return a centered rect with minW and minH
|
|
|
int cropW = (int) (difW * (zoomLevel - 1) / (maxDigitalZoom - 1) / 2F);
|
|
|
int cropH = (int) (difH * (zoomLevel - 1) / (maxDigitalZoom - 1) / 2F);
|
|
|
return new Rect(cropW, cropH, activeRect.width() - cropW,
|
|
|
activeRect.height() - cropH);
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public void setExposureCorrection(final float EVvalue,
|
|
|
@NonNull final float[] bounds,
|
|
|
@Nullable final PointF[] points,
|
|
|
final boolean notify) {
|
|
|
final float old = mExposureCorrectionValue;
|
|
|
mExposureCorrectionValue = EVvalue;
|
|
|
mExposureCorrectionTask = getOrchestrator().scheduleStateful(
|
|
|
"exposure correction (" + EVvalue + ")",
|
|
|
CameraState.ENGINE,
|
|
|
new Runnable() {
|
|
|
@Override
|
|
|
public void run() {
|
|
|
if (applyExposureCorrection(mRepeatingRequestBuilder, old)) {
|
|
|
applyRepeatingRequestBuilder();
|
|
|
if (notify) {
|
|
|
getCallback().dispatchOnExposureCorrectionChanged(EVvalue, bounds, points);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
@SuppressWarnings("WeakerAccess")
|
|
|
protected boolean applyExposureCorrection(@NonNull CaptureRequest.Builder builder,
|
|
|
float oldEVvalue) {
|
|
|
if (mCameraOptions.isExposureCorrectionSupported()) {
|
|
|
Rational exposureCorrectionStep = readCharacteristic(
|
|
|
CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP,
|
|
|
new Rational(1, 1));
|
|
|
int exposureCorrectionSteps = Math.round(mExposureCorrectionValue
|
|
|
* exposureCorrectionStep.floatValue());
|
|
|
builder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, exposureCorrectionSteps);
|
|
|
return true;
|
|
|
}
|
|
|
mExposureCorrectionValue = oldEVvalue;
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public void setPlaySounds(boolean playSounds) {
|
|
|
mPlaySounds = playSounds;
|
|
|
mPlaySoundsTask = Tasks.forResult(null);
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public void setPreviewFrameRate(float previewFrameRate) {
|
|
|
final float oldPreviewFrameRate = mPreviewFrameRate;
|
|
|
mPreviewFrameRate = previewFrameRate;
|
|
|
mPreviewFrameRateTask = getOrchestrator().scheduleStateful(
|
|
|
"preview fps (" + previewFrameRate + ")",
|
|
|
CameraState.ENGINE,
|
|
|
new Runnable() {
|
|
|
@Override
|
|
|
public void run() {
|
|
|
if (applyPreviewFrameRate(mRepeatingRequestBuilder, oldPreviewFrameRate)) {
|
|
|
applyRepeatingRequestBuilder();
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
@SuppressWarnings("WeakerAccess")
|
|
|
protected boolean applyPreviewFrameRate(@NonNull CaptureRequest.Builder builder,
|
|
|
float oldPreviewFrameRate) {
|
|
|
//noinspection unchecked
|
|
|
Range<Integer>[] fallback = new Range[]{};
|
|
|
Range<Integer>[] fpsRanges = readCharacteristic(
|
|
|
CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES,
|
|
|
fallback);
|
|
|
if (mPreviewFrameRate == 0F) {
|
|
|
// 0F is a special value. Fallback to a reasonable default.
|
|
|
for (Range<Integer> fpsRange : fpsRanges) {
|
|
|
if (fpsRange.contains(30) || fpsRange.contains(24)) {
|
|
|
builder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange);
|
|
|
return true;
|
|
|
}
|
|
|
}
|
|
|
} else {
|
|
|
// If out of boundaries, adjust it.
|
|
|
mPreviewFrameRate = Math.min(mPreviewFrameRate,
|
|
|
mCameraOptions.getPreviewFrameRateMaxValue());
|
|
|
mPreviewFrameRate = Math.max(mPreviewFrameRate,
|
|
|
mCameraOptions.getPreviewFrameRateMinValue());
|
|
|
for (Range<Integer> fpsRange : fpsRanges) {
|
|
|
if (fpsRange.contains(Math.round(mPreviewFrameRate))) {
|
|
|
builder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange);
|
|
|
return true;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
mPreviewFrameRate = oldPreviewFrameRate;
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public void setPictureFormat(final @NonNull PictureFormat pictureFormat) {
|
|
|
if (pictureFormat != mPictureFormat) {
|
|
|
mPictureFormat = pictureFormat;
|
|
|
getOrchestrator().scheduleStateful("picture format (" + pictureFormat + ")",
|
|
|
CameraState.ENGINE,
|
|
|
new Runnable() {
|
|
|
@Override
|
|
|
public void run() {
|
|
|
restart();
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
|
|
|
//endregion
|
|
|
|
|
|
//region Frame Processing
|
|
|
|
|
|
protected int getFrameProcessingPoolSize() {
|
|
|
return FRAME_PROCESSING_POOL_SIZE;
|
|
|
}
|
|
|
|
|
|
@NonNull
|
|
|
@Override
|
|
|
protected FrameManager instantiateFrameManager() {
|
|
|
return new ImageFrameManager(getFrameProcessingPoolSize());
|
|
|
}
|
|
|
|
|
|
// Frame processing thread
|
|
|
@Override
|
|
|
public void onImageAvailable(ImageReader reader) {
|
|
|
LOG.v("onImageAvailable", "trying to acquire Image.");
|
|
|
Image image = null;
|
|
|
try {
|
|
|
image = reader.acquireLatestImage();
|
|
|
} catch (Exception ignore) { }
|
|
|
if (image == null) {
|
|
|
LOG.w("onImageAvailable", "failed to acquire Image!");
|
|
|
} else if (getState() == CameraState.PREVIEW && !isChangingState()) {
|
|
|
// After preview, the frame manager is correctly set up
|
|
|
//noinspection unchecked
|
|
|
Frame frame = getFrameManager().getFrame(image,
|
|
|
System.currentTimeMillis(),
|
|
|
getAngles().offset(Reference.SENSOR,
|
|
|
Reference.OUTPUT,
|
|
|
Axis.RELATIVE_TO_SENSOR));
|
|
|
getCallback().dispatchFrame(frame);
|
|
|
} else {
|
|
|
image.close();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public void setHasFrameProcessors(final boolean hasFrameProcessors) {
|
|
|
// Frame processing is set up partially when binding and partially when starting
|
|
|
// the preview. If the value is changed between the two, the preview step can crash.
|
|
|
getOrchestrator().schedule("has frame processors (" + hasFrameProcessors + ")",
|
|
|
true, new Runnable() {
|
|
|
@Override
|
|
|
public void run() {
|
|
|
if (getState().isAtLeast(CameraState.BIND) && isChangingState()) {
|
|
|
// Extremely rare case in which this was called in between startBind and
|
|
|
// startPreview. This can cause issues. Try later.
|
|
|
setHasFrameProcessors(hasFrameProcessors);
|
|
|
return;
|
|
|
}
|
|
|
// Apply and restart.
|
|
|
mHasFrameProcessors = hasFrameProcessors;
|
|
|
if (getState().isAtLeast(CameraState.BIND)) {
|
|
|
restartBind();
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public void setFrameProcessingFormat(final int format) {
|
|
|
// This is called during initialization. Set our default first.
|
|
|
if (mFrameProcessingFormat == 0) mFrameProcessingFormat = FRAME_PROCESSING_FORMAT;
|
|
|
// Frame processing format is used both when binding and when starting the preview.
|
|
|
// If the value is changed between the two, the preview step can crash.
|
|
|
getOrchestrator().schedule("frame processing format (" + format + ")",
|
|
|
true, new Runnable() {
|
|
|
@Override
|
|
|
public void run() {
|
|
|
if (getState().isAtLeast(CameraState.BIND) && isChangingState()) {
|
|
|
// Extremely rare case in which this was called in between startBind and
|
|
|
// startPreview. This can cause issues. Try later.
|
|
|
setFrameProcessingFormat(format);
|
|
|
return;
|
|
|
}
|
|
|
mFrameProcessingFormat = format > 0 ? format : FRAME_PROCESSING_FORMAT;
|
|
|
if (getState().isAtLeast(CameraState.BIND)) {
|
|
|
restartBind();
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
//endregion
|
|
|
|
|
|
//region 3A Metering
|
|
|
|
|
|
@Override
|
|
|
public void startAutoFocus(@Nullable final Gesture gesture, @NonNull final PointF point) {
|
|
|
// This will only work when we have a preview, since it launches the preview
|
|
|
// in the end. Even without this it would need the bind state at least,
|
|
|
// since we need the preview size.
|
|
|
getOrchestrator().scheduleStateful("autofocus (" + gesture + ")",
|
|
|
CameraState.PREVIEW,
|
|
|
new Runnable() {
|
|
|
@Override
|
|
|
public void run() {
|
|
|
// The camera options API still has the auto focus API but it really
|
|
|
// refers to "3A metering to a specific point". Since we have a point, check.
|
|
|
if (!mCameraOptions.isAutoFocusSupported()) return;
|
|
|
|
|
|
// Create the meter and start.
|
|
|
getCallback().dispatchOnFocusStart(gesture, point);
|
|
|
final MeterAction action = createMeterAction(point);
|
|
|
Action wrapper = Actions.timeout(METER_TIMEOUT, action);
|
|
|
wrapper.start(Camera2Engine.this);
|
|
|
wrapper.addCallback(new CompletionCallback() {
|
|
|
@Override
|
|
|
protected void onActionCompleted(@NonNull Action a) {
|
|
|
getCallback().dispatchOnFocusEnd(gesture, action.isSuccessful(), point);
|
|
|
getOrchestrator().remove("reset metering");
|
|
|
if (shouldResetAutoFocus()) {
|
|
|
getOrchestrator().scheduleStatefulDelayed("reset metering",
|
|
|
CameraState.PREVIEW,
|
|
|
getAutoFocusResetDelay(),
|
|
|
new Runnable() {
|
|
|
@Override
|
|
|
public void run() {
|
|
|
unlockAndResetMetering();
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
@NonNull
|
|
|
private MeterAction createMeterAction(@Nullable PointF point) {
|
|
|
// Before creating any new meter action, abort the old one.
|
|
|
if (mMeterAction != null) mMeterAction.abort(this);
|
|
|
// The meter will check the current configuration to see if AF/AE/AWB should run.
|
|
|
// - AE should be on CONTROL_AE_MODE_ON* (this depends on setFlash())
|
|
|
// - AWB should be on CONTROL_AWB_MODE_AUTO (this depends on setWhiteBalance())
|
|
|
// - AF should be on CONTROL_AF_MODE_AUTO or others
|
|
|
// The last one is under our control because the library has no focus API.
|
|
|
// So let's set a good af mode here. This operation is reverted during onMeteringReset().
|
|
|
applyFocusForMetering(mRepeatingRequestBuilder);
|
|
|
mMeterAction = new MeterAction(Camera2Engine.this, point,
|
|
|
point == null);
|
|
|
return mMeterAction;
|
|
|
}
|
|
|
|
|
|
@EngineThread
|
|
|
private void unlockAndResetMetering() {
|
|
|
// Needs the PREVIEW state!
|
|
|
Actions.sequence(
|
|
|
new BaseAction() {
|
|
|
@Override
|
|
|
protected void onStart(@NonNull ActionHolder holder) {
|
|
|
super.onStart(holder);
|
|
|
applyDefaultFocus(holder.getBuilder(this));
|
|
|
holder.getBuilder(this)
|
|
|
.set(CaptureRequest.CONTROL_AE_LOCK, false);
|
|
|
holder.getBuilder(this)
|
|
|
.set(CaptureRequest.CONTROL_AWB_LOCK, false);
|
|
|
holder.applyBuilder(this);
|
|
|
setState(STATE_COMPLETED);
|
|
|
// TODO should wait results?
|
|
|
}
|
|
|
},
|
|
|
new MeterResetAction()
|
|
|
).start(Camera2Engine.this);
|
|
|
}
|
|
|
|
|
|
//endregion
|
|
|
|
|
|
//region Actions
|
|
|
|
|
|
@Override
|
|
|
public void addAction(final @NonNull Action action) {
|
|
|
if (!mActions.contains(action)) {
|
|
|
mActions.add(action);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public void removeAction(final @NonNull Action action) {
|
|
|
mActions.remove(action);
|
|
|
}
|
|
|
|
|
|
@NonNull
|
|
|
@Override
|
|
|
public CameraCharacteristics getCharacteristics(@NonNull Action action) {
|
|
|
return mCameraCharacteristics;
|
|
|
}
|
|
|
|
|
|
@Nullable
|
|
|
@Override
|
|
|
public TotalCaptureResult getLastResult(@NonNull Action action) {
|
|
|
return mLastRepeatingResult;
|
|
|
}
|
|
|
|
|
|
@NonNull
|
|
|
@Override
|
|
|
public CaptureRequest.Builder getBuilder(@NonNull Action action) {
|
|
|
return mRepeatingRequestBuilder;
|
|
|
}
|
|
|
|
|
|
@EngineThread
|
|
|
@Override
|
|
|
public void applyBuilder(@NonNull Action source) {
|
|
|
// NOTE: Should never be called on a non-engine thread!
|
|
|
// Non-engine threads are not protected by the uncaught exception handler
|
|
|
// and can make the process crash.
|
|
|
applyRepeatingRequestBuilder();
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public void applyBuilder(@NonNull Action source, @NonNull CaptureRequest.Builder builder)
|
|
|
throws CameraAccessException {
|
|
|
// Risky - would be better to ensure that thread is the engine one.
|
|
|
if (getState() == CameraState.PREVIEW && !isChangingState()) {
|
|
|
mSession.capture(builder.build(), mRepeatingRequestCallback, null);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
//endregion
|
|
|
} |