Basic Camera2 open-close integration

pull/493/head
Mattia Iavarone 5 years ago
parent 3c345826cc
commit 4166031ce2
  1. 3
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/IntegrationTest.java
  2. 45
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java
  3. 6
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
  4. 29
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java
  5. 409
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java
  6. 153
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java
  7. 19
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Mapper.java
  8. 36
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/WorkerHandler.java
  9. 4
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/CameraPreview.java
  10. 1
      demo/src/main/res/layout/activity_camera.xml

@ -109,7 +109,7 @@ public class IntegrationTest extends BaseTest {
uiExceptionTask.end(e); uiExceptionTask.end(e);
} }
}); });
controller.mCrashHandler = crashThread.get(); controller.mCrashHandler = crashThread.getHandler();
} }
@After @After
@ -393,6 +393,7 @@ public class IntegrationTest extends BaseTest {
controller.mPlaySoundsTask.await(300); controller.mPlaySoundsTask.await(300);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
Camera.CameraInfo info = new Camera.CameraInfo(); Camera.CameraInfo info = new Camera.CameraInfo();
Camera.getCameraInfo(controller.mCameraId, info); Camera.getCameraInfo(controller.mCameraId, info);
if (info.canDisableShutterSound) { if (info.canDisableShutterSound) {

@ -1,7 +1,15 @@
package com.otaliastudios.cameraview; package com.otaliastudios.cameraview;
import android.graphics.ImageFormat;
import android.hardware.Camera; import android.hardware.Camera;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.ImageReader;
import android.media.MediaRecorder;
import android.os.Build;
import com.otaliastudios.cameraview.controls.Audio; import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.controls.Control; import com.otaliastudios.cameraview.controls.Control;
@ -19,6 +27,7 @@ import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
@ -130,6 +139,42 @@ public class CameraOptions {
} }
} }
} }
// Camera2Engine constructor.
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
@SuppressWarnings("deprecation")
public CameraOptions(@NonNull CameraManager manager, @NonNull CameraCharacteristics characteristics, boolean flipSizes) throws CameraAccessException {
Mapper mapper = Mapper.get(Engine.CAMERA2);
// Facing
for (String cameraId : manager.getCameraIdList()) {
CameraCharacteristics cameraCharacteristics = manager.getCameraCharacteristics(cameraId);
Integer cameraFacing = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING);
if (cameraFacing != null) {
Facing value = mapper.unmapFacing(cameraFacing);
if (value != null) supportedFacing.add(value);
}
}
// Picture Sizes
StreamConfigurationMap streamMap = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
if (streamMap == null) throw new RuntimeException("StreamConfigurationMap is null. Should not happen.");
android.util.Size[] psizes = streamMap.getOutputSizes(ImageReader.class);
for (android.util.Size size : psizes) {
int width = flipSizes ? size.getHeight() : size.getWidth();
int height = flipSizes ? size.getWidth() : size.getHeight();
supportedPictureSizes.add(new Size(width, height));
supportedPictureAspectRatio.add(AspectRatio.of(width, height));
}
// Video Sizes
android.util.Size[] vsizes = streamMap.getOutputSizes(MediaRecorder.class);
for (android.util.Size size : vsizes) {
int width = flipSizes ? size.getHeight() : size.getWidth();
int height = flipSizes ? size.getWidth() : size.getHeight();
supportedVideoSizes.add(new Size(width, height));
supportedVideoAspectRatio.add(AspectRatio.of(width, height));
}
}
/** /**
* Shorthand for getSupported*().contains(value). * Shorthand for getSupported*().contains(value).

@ -1718,6 +1718,12 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
private CameraLogger mLogger = CameraLogger.create(CameraCallbacks.class.getSimpleName()); private CameraLogger mLogger = CameraLogger.create(CameraCallbacks.class.getSimpleName());
@NonNull
@Override
public Context getContext() {
return CameraView.this.getContext();
}
@Override @Override
public void dispatchOnCameraOpened(final CameraOptions options) { public void dispatchOnCameraOpened(final CameraOptions options) {
mLogger.i("dispatchOnCameraOpened", options); mLogger.i("dispatchOnCameraOpened", options);

@ -11,6 +11,7 @@ import android.location.Location;
import android.os.Build; import android.os.Build;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread; import androidx.annotation.WorkerThread;
import android.view.SurfaceHolder; import android.view.SurfaceHolder;
@ -56,6 +57,7 @@ public class Camera1Engine extends CameraEngine implements Camera.PreviewCallbac
private static final int AUTOFOCUS_END_DELAY_MILLIS = 2500; private static final int AUTOFOCUS_END_DELAY_MILLIS = 2500;
private Camera mCamera; private Camera mCamera;
@VisibleForTesting int mCameraId;
private boolean mIsBound = false; private boolean mIsBound = false;
private Runnable mFocusEndRunnable; private Runnable mFocusEndRunnable;
@ -310,9 +312,9 @@ public class Camera1Engine extends CameraEngine implements Camera.PreviewCallbac
@Override @Override
protected void onStop() { protected void onStop() {
LOG.i("onStop:", "About to clean up."); LOG.i("onStop:", "About to clean up.");
mHandler.get().removeCallbacks(mFocusResetRunnable); mHandler.remove(mFocusResetRunnable);
if (mFocusEndRunnable != null) { if (mFocusEndRunnable != null) {
mHandler.get().removeCallbacks(mFocusEndRunnable); mHandler.remove(mFocusEndRunnable);
} }
if (mVideoRecorder != null) { if (mVideoRecorder != null) {
mVideoRecorder.stop(); mVideoRecorder.stop();
@ -329,11 +331,6 @@ public class Camera1Engine extends CameraEngine implements Camera.PreviewCallbac
mCaptureSize = null; mCaptureSize = null;
mIsBound = false; mIsBound = false;
LOG.w("onStop:", "Clean up.", "Returning."); LOG.w("onStop:", "Clean up.", "Returning.");
// We were saving a reference to the exception here and throwing to the user.
// I don't think it's correct. We are closing and have already done our best
// to clean up resources. No need to throw.
// if (error != null) throw new CameraException(error);
} }
private boolean collectCameraId() { private boolean collectCameraId() {
@ -364,7 +361,7 @@ public class Camera1Engine extends CameraEngine implements Camera.PreviewCallbac
if (error == Camera.CAMERA_ERROR_SERVER_DIED) { if (error == Camera.CAMERA_ERROR_SERVER_DIED) {
// Looks like this is recoverable. // Looks like this is recoverable.
LOG.w("Recoverable error inside the onError callback.", "CAMERA_ERROR_SERVER_DIED"); LOG.w("Recoverable error inside the onError callback.", "CAMERA_ERROR_SERVER_DIED");
stopImmediately(); stopNow();
start(); start();
return; return;
} }
@ -630,10 +627,6 @@ public class Camera1Engine extends CameraEngine implements Camera.PreviewCallbac
stub.size = getUncroppedSnapshotSize(REF_OUTPUT); // Not the real size: it will be cropped to match the view ratio stub.size = getUncroppedSnapshotSize(REF_OUTPUT); // Not the real size: it will be cropped to match the view ratio
stub.rotation = offset(REF_SENSOR, REF_OUTPUT); // Actually it will be rotated and set to 0. stub.rotation = offset(REF_SENSOR, REF_OUTPUT); // Actually it will be rotated and set to 0.
AspectRatio outputRatio = flip(REF_OUTPUT, REF_VIEW) ? viewAspectRatio.flip() : viewAspectRatio; AspectRatio outputRatio = flip(REF_OUTPUT, REF_VIEW) ? viewAspectRatio.flip() : viewAspectRatio;
// LOG.e("ROTBUG_pic", "aspectRatio (REF_VIEW):", viewAspectRatio);
// LOG.e("ROTBUG_pic", "aspectRatio (REF_OUTPUT):", outputRatio);
// LOG.e("ROTBUG_pic", "sizeUncropped (REF_OUTPUT):", result.size);
// LOG.e("ROTBUG_pic", "rotation:", result.rotation);
LOG.v("Rotations", "SV", offset(REF_SENSOR, REF_VIEW), "VS", offset(REF_VIEW, REF_SENSOR)); LOG.v("Rotations", "SV", offset(REF_SENSOR, REF_VIEW), "VS", offset(REF_VIEW, REF_SENSOR));
LOG.v("Rotations", "SO", offset(REF_SENSOR, REF_OUTPUT), "OS", offset(REF_OUTPUT, REF_SENSOR)); LOG.v("Rotations", "SO", offset(REF_SENSOR, REF_OUTPUT), "OS", offset(REF_OUTPUT, REF_SENSOR));
@ -656,7 +649,7 @@ public class Camera1Engine extends CameraEngine implements Camera.PreviewCallbac
} }
private boolean isCameraAvailable() { private boolean isCameraAvailable() {
switch (mState) { switch (getState()) {
// If we are stopped, don't. // If we are stopped, don't.
case STATE_STOPPED: case STATE_STOPPED:
return false; return false;
@ -920,7 +913,7 @@ public class Camera1Engine extends CameraEngine implements Camera.PreviewCallbac
// The auto focus callback is not guaranteed to be called, but we really want it to be. // The auto focus callback is not guaranteed to be called, but we really want it to be.
// So we remove the old runnable if still present and post a new one. // So we remove the old runnable if still present and post a new one.
if (mFocusEndRunnable != null) mHandler.get().removeCallbacks(mFocusEndRunnable); if (mFocusEndRunnable != null) mHandler.remove(mFocusEndRunnable);
mFocusEndRunnable = new Runnable() { mFocusEndRunnable = new Runnable() {
@Override @Override
public void run() { public void run() {
@ -929,7 +922,7 @@ public class Camera1Engine extends CameraEngine implements Camera.PreviewCallbac
} }
} }
}; };
mHandler.get().postDelayed(mFocusEndRunnable, AUTOFOCUS_END_DELAY_MILLIS); mHandler.post(AUTOFOCUS_END_DELAY_MILLIS, mFocusEndRunnable);
// Wrapping autoFocus in a try catch to handle some device specific exceptions, // Wrapping autoFocus in a try catch to handle some device specific exceptions,
// see See https://github.com/natario1/CameraView/issues/181. // see See https://github.com/natario1/CameraView/issues/181.
@ -938,13 +931,13 @@ public class Camera1Engine extends CameraEngine implements Camera.PreviewCallbac
@Override @Override
public void onAutoFocus(boolean success, Camera camera) { public void onAutoFocus(boolean success, Camera camera) {
if (mFocusEndRunnable != null) { if (mFocusEndRunnable != null) {
mHandler.get().removeCallbacks(mFocusEndRunnable); mHandler.remove(mFocusEndRunnable);
mFocusEndRunnable = null; mFocusEndRunnable = null;
} }
mCallback.dispatchOnFocusEnd(gesture, success, p); mCallback.dispatchOnFocusEnd(gesture, success, p);
mHandler.get().removeCallbacks(mFocusResetRunnable); mHandler.remove(mFocusResetRunnable);
if (shouldResetAutoFocus()) { if (shouldResetAutoFocus()) {
mHandler.get().postDelayed(mFocusResetRunnable, getAutoFocusResetDelay()); mHandler.post(getAutoFocusResetDelay(), mFocusResetRunnable);
} }
} }
}); });

@ -1,8 +1,20 @@
package com.otaliastudios.cameraview.engine; package com.otaliastudios.cameraview.engine;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.PointF; import android.graphics.PointF;
import android.graphics.SurfaceTexture;
import android.hardware.Camera;
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.params.StreamConfigurationMap;
import android.location.Location; import android.location.Location;
import android.os.Build; import android.os.Build;
import android.view.Surface;
import android.view.SurfaceHolder; import android.view.SurfaceHolder;
import com.otaliastudios.cameraview.CameraException; import com.otaliastudios.cameraview.CameraException;
@ -18,13 +30,20 @@ import com.otaliastudios.cameraview.controls.Hdr;
import com.otaliastudios.cameraview.controls.Mode; import com.otaliastudios.cameraview.controls.Mode;
import com.otaliastudios.cameraview.controls.WhiteBalance; import com.otaliastudios.cameraview.controls.WhiteBalance;
import com.otaliastudios.cameraview.gesture.Gesture; import com.otaliastudios.cameraview.gesture.Gesture;
import com.otaliastudios.cameraview.internal.utils.Task;
import com.otaliastudios.cameraview.size.AspectRatio; import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.size.Size;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
import androidx.annotation.WorkerThread;
@RequiresApi(Build.VERSION_CODES.LOLLIPOP) @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
@ -33,36 +52,391 @@ public class Camera2Engine extends CameraEngine {
private static final String TAG = Camera2Engine.class.getSimpleName(); private static final String TAG = Camera2Engine.class.getSimpleName();
private static final CameraLogger LOG = CameraLogger.create(TAG); private static final CameraLogger LOG = CameraLogger.create(TAG);
private final CameraManager mManager;
private String mCameraId;
private CameraDevice mCamera;
private CameraCaptureSession mSession; // TODO must be released and nulled
private CaptureRequest.Builder mPreviewStreamRequestBuilder; // TODO must be nulled
private CaptureRequest mPreviewStreamRequest; // TODO must be nulled
private boolean mIsBound = false;
public Camera2Engine(Callback callback) { public Camera2Engine(Callback callback) {
super(callback); super(callback);
mMapper = Mapper.get(Engine.CAMERA2); mMapper = Mapper.get(Engine.CAMERA2);
mManager = (CameraManager) mCallback.getContext().getSystemService(Context.CAMERA_SERVICE);
} }
@Override private void schedule(@Nullable final Task<Void> task, final boolean ensureAvailable, final Runnable action) {
public void onSurfaceAvailable() { mHandler.post(new Runnable() {
@Override
public void run() {
if (ensureAvailable && !isCameraAvailable()) {
if (task != null) task.end(null);
} else {
action.run();
if (task != null) task.end(null);
}
}
});
} }
@Override @NonNull
public void onSurfaceChanged() { 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 Size computePreviewStreamSize() throws CameraAccessException {
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 computePreviewStreamSize(candidates);
} }
@Override private boolean collectCameraId() throws CameraAccessException {
public void onSurfaceDestroyed() { int internalFacing = mMapper.map(mFacing);
int cameras = mManager.getCameraIdList().length;
LOG.i("collectCameraId", "Facing:", mFacing, "Internal:", internalFacing, "Cameras:", cameras);
for (String cameraId : mManager.getCameraIdList()) {
CameraCharacteristics characteristics = mManager.getCameraCharacteristics(cameraId);
if (internalFacing == readCharacteristic(characteristics, CameraCharacteristics.LENS_FACING, -99)) {
mCameraId = cameraId;
mSensorOffset = readCharacteristic(characteristics, CameraCharacteristics.SENSOR_ORIENTATION, 0);
return true;
}
}
return false;
}
private boolean isCameraAvailable() {
switch (getState()) {
// If we are stopped, don't.
case STATE_STOPPED:
return false;
// If we are going to be closed, don't act on camera.
// Even if mCamera != null, it might have been released.
case STATE_STOPPING:
return false;
// If we are started, mCamera should never be null.
case STATE_STARTED:
return true;
// If we are starting, theoretically we could act.
// Just check that camera is available.
case STATE_STARTING:
return mCamera != null;
}
return false;
} }
@Override @Override
protected void onStart() { protected void onStart() {
if (isCameraAvailable()) {
LOG.w("onStart:", "Camera not available. Should not happen.");
onStop(); // Should not happen.
}
try {
if (collectCameraId()) {
createCamera();
LOG.i("onStart:", "Ended");
} else {
LOG.e("onStart:", "No camera available for facing", mFacing);
throw new CameraException(CameraException.REASON_NO_CAMERA);
}
} catch (CameraAccessException e) {
// TODO
}
}
@SuppressLint("MissingPermission")
private void createCamera() throws CameraAccessException {
mManager.openCamera(mCameraId, new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice camera) {
mCamera = camera;
// TODO Set parameters that might have been set before the camera was opened.
try {
LOG.i("createCamera:", "Applying default parameters.");
CameraCharacteristics characteristics = mManager.getCameraCharacteristics(mCameraId);
mCameraOptions = new CameraOptions(mManager, characteristics, flip(REF_SENSOR, REF_VIEW));
// applyDefaultFocus(params);
// applyFlash(params, Flash.OFF);
// applyLocation(params, null);
// applyWhiteBalance(params, WhiteBalance.AUTO);
// applyHdr(params, Hdr.OFF);
// applyPlaySounds(mPlaySounds);
// params.setRecordingHint(mMode == Mode.VIDEO);
// mCamera.setParameters(params);
} catch (CameraAccessException e) {
// TODO
throw new RuntimeException(e);
}
// Set display orientation, not allowed during preview
// TODO not needed anymore? mCamera.setDisplayOrientation(offset(REF_SENSOR, REF_VIEW));
try {
if (shouldBindToSurface()) bindToSurface("onStart");
} catch (CameraAccessException e) {
// TODO
throw new RuntimeException(e);
}
}
@Override
public void onDisconnected(@NonNull CameraDevice camera) {
// TODO not sure what to do here. maybe stop(). Read docs.
}
@Override
public void onError(@NonNull CameraDevice camera, int error) {
// TODO
}
}, null);
}
private boolean shouldBindToSurface() {
return isCameraAvailable() && mPreview != null && mPreview.hasSurface() && !mIsBound;
}
/**
* The act of binding an "open" camera to a "ready" preview.
* These can happen at different times but we want to end up here.
* At this point we are sure that mPreview is not null.
*/
@SuppressLint("Recycle")
@WorkerThread
private void bindToSurface(final @NonNull String trigger) throws CameraAccessException {
LOG.i("bindToSurface:", "Started");
Object output = mPreview.getOutput();
Surface previewSurface;
if (output instanceof SurfaceHolder) {
previewSurface = ((SurfaceHolder) output).getSurface();
} else if (output instanceof SurfaceTexture) {
previewSurface = new Surface((SurfaceTexture) output);
} else {
throw new RuntimeException("Unknown CameraPreview output class.");
}
//noinspection ArraysAsListWithZeroOrOneArgument
List<Surface> outputSurfaces = Arrays.asList(previewSurface);
mPreviewStreamRequestBuilder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
mPreviewStreamRequestBuilder.addTarget(previewSurface);
// null handler means using the current looper which is totally ok.
mCamera.createCaptureSession(outputSurfaces, new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession session) {
try {
mCaptureSize = computeCaptureSize();
mPreviewStreamSize = computePreviewStreamSize();
mSession = session;
mIsBound = true;
if (shouldStartPreview()) startPreview(trigger);
} catch (CameraAccessException e) {
// TODO
throw new RuntimeException(e);
}
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession session) {
// TODO
}
}, null);
}
private boolean shouldStartPreview() {
return isCameraAvailable() && mIsBound;
}
/**
* To be called when the preview size is setup or changed.
* @param trigger a log helper
*/
private void startPreview(@NonNull String trigger) {
LOG.i(trigger, "Dispatching onCameraPreviewStreamSizeChanged.");
mCallback.onCameraPreviewStreamSizeChanged();
Size previewSize = getPreviewStreamSize(REF_VIEW);
if (previewSize == null) {
throw new IllegalStateException("previewStreamSize should not be null at this point.");
}
mPreview.setStreamSize(previewSize.getWidth(), previewSize.getHeight());
// TODO mPreviewStreamFormat = params.getPreviewFormat();
// TODO: previewSize and captureSize
/* params.setPreviewSize(mPreviewStreamSize.getWidth(), mPreviewStreamSize.getHeight()); // <- not allowed during preview
if (mMode == Mode.PICTURE) {
params.setPictureSize(mCaptureSize.getWidth(), mCaptureSize.getHeight()); // <- allowed
} else {
// mCaptureSize in this case is a video size. The available video sizes are not necessarily
// a subset of the picture sizes, so we can't use the mCaptureSize value: it might crash.
// However, the setPictureSize() passed here is useless : we don't allow HQ pictures in video mode.
// While this might be lifted in the future, for now, just use a picture capture size.
Size pictureSize = computeCaptureSize(Mode.PICTURE);
params.setPictureSize(pictureSize.getWidth(), pictureSize.getHeight());
} */
// TODO mCamera.setPreviewCallbackWithBuffer(null); // Release anything left
// TODO mCamera.setPreviewCallbackWithBuffer(this); // Add ourselves
// TODO mFrameManager.allocateBuffers(ImageFormat.getBitsPerPixel(mPreviewStreamFormat), mPreviewStreamSize);
LOG.i(trigger, "Starting preview with startPreview().");
try {
mPreviewStreamRequest = mPreviewStreamRequestBuilder.build();
mSession.setRepeatingRequest(mPreviewStreamRequest, null, null);
} catch (Exception e) {
LOG.e(trigger, "Failed to start preview.", e);
throw new CameraException(e, CameraException.REASON_FAILED_TO_START_PREVIEW);
}
LOG.i(trigger, "Started preview.");
} }
@Override @Override
protected void onStop() { protected void onStop() {
LOG.i("onStop:", "About to clean up.");
if (mVideoRecorder != null) {
mVideoRecorder.stop();
mVideoRecorder = null;
}
if (mCamera != null) {
stopPreview();
if (mIsBound) unbindFromSurface();
destroyCamera();
}
mCameraOptions = null;
mCamera = null;
mPreviewStreamSize = null;
mCaptureSize = null;
mIsBound = false;
LOG.w("onStop:", "Clean up.", "Returning.");
}
private void stopPreview() {
mPreviewStreamFormat = 0;
mFrameManager.release();
// TODO mCamera.setPreviewCallbackWithBuffer(null); // Release anything left
try {
mSession.stopRepeating();
// TODO should wait for onReady?
} catch (CameraAccessException e) {
LOG.w("stopRepeating failed!", e);
}
mPreviewStreamRequest = null;
} }
private void unbindFromSurface() {
mIsBound = false;
mPreviewStreamRequestBuilder = null;
mPreviewStreamSize = null;
mCaptureSize = null;
mSession.close();
mSession = null;
}
private void destroyCamera() {
try {
LOG.i("destroyCamera:", "Clean up.", "Releasing camera.");
mCamera.close();
LOG.i("destroyCamera:", "Clean up.", "Released camera.");
} catch (Exception e) {
LOG.w("destroyCamera:", "Clean up.", "Exception while releasing camera.", e);
}
mCamera = null;
mCameraOptions = null;
}
/**
* Preview surface is now available. If camera is open, set up.
* At this point we are sure that mPreview is not null.
*/
@Override
public void onSurfaceAvailable() {
LOG.i("onSurfaceAvailable:", "Size is", getPreviewSurfaceSize(REF_VIEW));
schedule(null, false, new Runnable() {
@Override
public void run() {
LOG.i("onSurfaceAvailable:", "Inside handler. About to bind.");
try {
if (shouldBindToSurface()) bindToSurface("onSurfaceAvailable");
} catch (CameraAccessException e) {
// TODO
throw new RuntimeException(e);
}
}
});
}
/**
* Preview surface did change its size. Compute a new preview size.
* This requires stopping and restarting the preview.
* At this point we are sure that mPreview is not null.
*/
@Override
public void onSurfaceChanged() {
LOG.i("onSurfaceChanged, size is", getPreviewSurfaceSize(REF_VIEW));
schedule(null, true, new Runnable() {
@Override
public void run() {
if (!mIsBound) return;
// Compute a new camera preview size and apply.
try {
Size newSize = computePreviewStreamSize();
if (newSize.equals(mPreviewStreamSize)) return;
LOG.i("onSurfaceChanged:", "Computed a new preview size. Going on.");
mPreviewStreamSize = newSize;
} catch (CameraAccessException e) {
// TODO
throw new RuntimeException(e);
}
stopPreview();
startPreview("onSurfaceChanged:");
}
});
}
@Override
public void onSurfaceDestroyed() {
LOG.i("onSurfaceDestroyed");
schedule(null, true, new Runnable() {
@Override
public void run() {
stopPreview();
if (mIsBound) unbindFromSurface();
}
});
}
@Override @Override
public void onBufferAvailable(@NonNull byte[] buffer) { public void onBufferAvailable(@NonNull byte[] buffer) {
@ -75,7 +449,26 @@ public class Camera2Engine extends CameraEngine {
@Override @Override
public void setFacing(@NonNull Facing facing) { public void setFacing(@NonNull Facing facing) {
final Facing old = mFacing;
if (facing != old) {
mFacing = facing;
schedule(null, true, new Runnable() {
@Override
public void run() {
boolean success;
try {
success = collectCameraId();
} catch (CameraAccessException e) {
success = false;
}
if (success) {
restart();
} else {
mFacing = old;
}
}
});
}
} }
@Override @Override

@ -1,5 +1,6 @@
package com.otaliastudios.cameraview.engine; package com.otaliastudios.cameraview.engine;
import android.content.Context;
import android.graphics.PointF; import android.graphics.PointF;
import android.location.Location; import android.location.Location;
@ -42,12 +43,47 @@ import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
/**
* PROCESS
* Setting up the Camera is usually a 4 steps process:
* 1. Setting up the Surface (done by {@link CameraPreview}
* 2. Opening the camera (done by us)
* 3. Binding the camera to the surface (done by us)
* 4. Starting the camera preview (done by us)
*
* The first two steps can actually happen at the same time, anyway
* the order is not guaranteed.
* So at the end of both step 1 and 2, the engine should check if both have
* been performed and trigger the steps 3 and 4.
*
*
* STATE
* In the {@link CameraEngine} notation,
* - START [Async] means doing step 2 (which will eventually trigger 3 and 4)
* - STOP [Async] means undoing 4 (if needed), undoing 3 (if needed), then undoing 2.
* - RESTART [Async] means completing a STOP then a START
* - DESTROY [Sync] means performing a silent and synchronous STOP, ignoring all exceptions. This can make the engine unusable.
*
* So the engine state will be:
* - {@link #STATE_STARTING} if we're into step 2
* - {@link #STATE_STARTED} if we've completed step 2. No clue about 3 or 4.
* - {@link #STATE_STOPPING} if we're undoing steps 4, 3 and 2.
* - {@link #STATE_STOPPED} if we have undone steps 4, 3 and 2 (or they never started at all).
*
*
* THREADING
* Subclasses should always execute code on the thread given by {@link #mHandler}.
* This thread has a special {@link Thread.UncaughtExceptionHandler} that handles exceptions
* and dispatches error to the callback (instead of crashing the app).
* This lets subclasses run code safely and directly throw {@link CameraException}s when needed.
*/
public abstract class CameraEngine implements public abstract class CameraEngine implements
CameraPreview.SurfaceCallback, CameraPreview.SurfaceCallback,
FrameManager.BufferCallback, FrameManager.BufferCallback {
Thread.UncaughtExceptionHandler {
public interface Callback { public interface Callback {
@NonNull Context getContext();
void dispatchOnCameraOpened(CameraOptions options); void dispatchOnCameraOpened(CameraOptions options);
void dispatchOnCameraClosed(); void dispatchOnCameraClosed();
void onCameraPreviewStreamSizeChanged(); void onCameraPreviewStreamSizeChanged();
@ -99,7 +135,6 @@ public abstract class CameraEngine implements
@VisibleForTesting int mSnapshotMaxWidth = Integer.MAX_VALUE; // in REF_VIEW for consistency with SizeSelectors @VisibleForTesting int mSnapshotMaxWidth = Integer.MAX_VALUE; // in REF_VIEW for consistency with SizeSelectors
@VisibleForTesting int mSnapshotMaxHeight = Integer.MAX_VALUE; // in REF_VIEW for consistency with SizeSelectors @VisibleForTesting int mSnapshotMaxHeight = Integer.MAX_VALUE; // in REF_VIEW for consistency with SizeSelectors
protected int mCameraId;
protected CameraOptions mCameraOptions; protected CameraOptions mCameraOptions;
protected Mapper mMapper; protected Mapper mMapper;
protected PictureRecorder mPictureRecorder; protected PictureRecorder mPictureRecorder;
@ -117,7 +152,8 @@ public abstract class CameraEngine implements
private int mDisplayOffset; private int mDisplayOffset;
private int mDeviceOrientation; private int mDeviceOrientation;
protected int mState = STATE_STOPPED; // Subclasses should not change this. Use getState() instead.
@VisibleForTesting int mState = STATE_STOPPED;
// Used for testing. // Used for testing.
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mZoomTask = new Task<>(); @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mZoomTask = new Task<>();
@ -132,73 +168,86 @@ public abstract class CameraEngine implements
protected CameraEngine(Callback callback) { protected CameraEngine(Callback callback) {
mCallback = callback; mCallback = callback;
mCrashHandler = new Handler(Looper.getMainLooper()); mCrashHandler = new Handler(Looper.getMainLooper());
mHandler = WorkerHandler.get("CameraViewController"); mHandler = WorkerHandler.get("CameraViewEngine");
mHandler.getThread().setUncaughtExceptionHandler(this); mHandler.getThread().setUncaughtExceptionHandler(new CrashExceptionHandler());
mFrameManager = new FrameManager(2, this); mFrameManager = new FrameManager(2, this);
} }
public void setPreview(@NonNull CameraPreview cameraPreview) { public void setPreview(@NonNull CameraPreview cameraPreview) {
if (mPreview != null) mPreview.setSurfaceCallback(null);
mPreview = cameraPreview; mPreview = cameraPreview;
mPreview.setSurfaceCallback(this); mPreview.setSurfaceCallback(this);
} }
//region Error handling //region Error handling
private static class NoOpExceptionHandler implements Thread.UncaughtExceptionHandler { /**
* The base exception handler, which inspects the exception and
* decides what to do.
*/
private class CrashExceptionHandler implements Thread.UncaughtExceptionHandler {
@Override @Override
public void uncaughtException(Thread t, Throwable e) { public void uncaughtException(final Thread thread, final Throwable throwable) {
// No-op. // Something went wrong. Thread is terminated (about to?).
// Move to other thread and release resources.
if (!(throwable instanceof CameraException)) {
// This is unexpected, either a bug or something the developer should know.
// Release and crash the UI thread so we get bug reports.
LOG.e("uncaughtException:", "Unexpected exception:", throwable);
destroy();
mCrashHandler.post(new Runnable() {
@Override
public void run() {
RuntimeException exception;
if (throwable instanceof RuntimeException) {
exception = (RuntimeException) throwable;
} else {
exception = new RuntimeException(throwable);
}
throw exception;
}
});
} else {
final CameraException error = (CameraException) throwable;
LOG.e("uncaughtException:", "Interrupting thread with state:", ss(), "due to CameraException:", error);
final boolean unrecoverable = error.isUnrecoverable();
thread.interrupt();
// Restart handler.
mHandler = WorkerHandler.get("CameraViewEngine");
mHandler.getThread().setUncaughtExceptionHandler(new CrashExceptionHandler());
mHandler.post(new Runnable() {
@Override
public void run() {
if (unrecoverable) stopNow();
mCallback.dispatchError(error);
}
});
}
} }
} }
@Override /**
public void uncaughtException(final Thread thread, final Throwable throwable) { * A static exception handler used during destruction to avoid leaks,
// Something went wrong. Thread is terminated (about to?). * since the default handler is not static and the thread might survive the engine.
// Move to other thread and release resources. */
if (!(throwable instanceof CameraException)) { private static class NoOpExceptionHandler implements Thread.UncaughtExceptionHandler {
// This is unexpected, either a bug or something the developer should know. @Override
// Release and crash the UI thread so we get bug reports. public void uncaughtException(Thread t, Throwable e) {
LOG.e("uncaughtException:", "Unexpected exception:", throwable); // No-op.
destroy();
mCrashHandler.post(new Runnable() {
@Override
public void run() {
RuntimeException exception;
if (throwable instanceof RuntimeException) {
exception = (RuntimeException) throwable;
} else {
exception = new RuntimeException(throwable);
}
throw exception;
}
});
} else {
// At the moment all CameraExceptions are unrecoverable, there was something
// wrong when starting, stopping, or binding the camera to the preview.
final CameraException error = (CameraException) throwable;
LOG.e("uncaughtException:", "Interrupting thread with state:", ss(), "due to CameraException:", error);
thread.interrupt();
mHandler = WorkerHandler.get("CameraViewController");
mHandler.getThread().setUncaughtExceptionHandler(this);
LOG.i("uncaughtException:", "Calling stopImmediately and notifying.");
mHandler.post(new Runnable() {
@Override
public void run() {
stopImmediately();
mCallback.dispatchError(error);
}
});
} }
} }
// Public & not final so we can verify with mockito in CameraViewTest /**
* Not final due to mockito requirements, but this is basically
* it, nothing more to do.
*/
public void destroy() { public void destroy() {
LOG.i("destroy:", "state:", ss()); LOG.i("destroy:", "state:", ss());
// Prevent CameraEngine leaks. Don't set to null, or exceptions // Prevent CameraEngine leaks. Don't set to null, or exceptions
// inside the standard stop() method might crash the main thread. // inside the standard stop() method might crash the main thread.
mHandler.getThread().setUncaughtExceptionHandler(new NoOpExceptionHandler()); mHandler.getThread().setUncaughtExceptionHandler(new NoOpExceptionHandler());
// Stop if needed. // Stop if needed.
stopImmediately(); stopNow();
} }
//endregion //endregion
@ -254,18 +303,18 @@ public abstract class CameraEngine implements
} }
// Stops the preview synchronously, ensuring no exceptions are thrown. // Stops the preview synchronously, ensuring no exceptions are thrown.
final void stopImmediately() { final void stopNow() {
try { try {
// Don't check, try stop again. // Don't check, try stop again.
LOG.i("stopImmediately:", "State was:", ss()); LOG.i("stopNow:", "State was:", ss());
if (mState == STATE_STOPPED) return; if (mState == STATE_STOPPED) return;
mState = STATE_STOPPING; mState = STATE_STOPPING;
onStop(); onStop();
mState = STATE_STOPPED; mState = STATE_STOPPED;
LOG.i("stopImmediately:", "Stopped. State is:", ss()); LOG.i("stopNow:", "Stopped. State is:", ss());
} catch (Exception e) { } catch (Exception e) {
// Do nothing. // Do nothing.
LOG.i("stopImmediately:", "Swallowing exception while stopping.", e); LOG.i("stopNow:", "Swallowing exception while stopping.", e);
mState = STATE_STOPPED; mState = STATE_STOPPED;
} }
} }

@ -1,6 +1,7 @@
package com.otaliastudios.cameraview.engine; package com.otaliastudios.cameraview.engine;
import android.hardware.Camera; import android.hardware.Camera;
import android.hardware.camera2.CameraCharacteristics;
import android.os.Build; import android.os.Build;
import com.otaliastudios.cameraview.controls.Engine; import com.otaliastudios.cameraview.controls.Engine;
@ -12,6 +13,7 @@ import com.otaliastudios.cameraview.controls.WhiteBalance;
import java.util.HashMap; import java.util.HashMap;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
/** /**
* A Mapper maps camera engine constants to CameraView constants. * A Mapper maps camera engine constants to CameraView constants.
@ -25,11 +27,11 @@ public abstract class Mapper {
if (engine == Engine.CAMERA1) { if (engine == Engine.CAMERA1) {
if (CAMERA1 == null) CAMERA1 = new Camera1Mapper(); if (CAMERA1 == null) CAMERA1 = new Camera1Mapper();
return CAMERA1; return CAMERA1;
} else if (engine == Engine.CAMERA2) { } else if (engine == Engine.CAMERA2 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (CAMERA2 == null) CAMERA2 = new Camera2Mapper(); if (CAMERA2 == null) CAMERA2 = new Camera2Mapper();
return CAMERA2; return CAMERA2;
} else { } else {
throw new IllegalArgumentException("Unknown engine."); throw new IllegalArgumentException("Unknown engine or unsupported API level.");
} }
} }
@ -130,8 +132,17 @@ public abstract class Mapper {
} }
} }
@SuppressWarnings("unchecked")
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private static class Camera2Mapper extends Mapper { private static class Camera2Mapper extends Mapper {
private static final HashMap<Facing, Integer> FACING = new HashMap<>();
static {
FACING.put(Facing.BACK, CameraCharacteristics.LENS_FACING_BACK);
FACING.put(Facing.FRONT, CameraCharacteristics.LENS_FACING_FRONT);
}
@Override @Override
public <T> T map(Flash flash) { public <T> T map(Flash flash) {
return null; return null;
@ -139,7 +150,7 @@ public abstract class Mapper {
@Override @Override
public <T> T map(Facing facing) { public <T> T map(Facing facing) {
return null; return (T) FACING.get(facing);
} }
@Override @Override
@ -159,7 +170,7 @@ public abstract class Mapper {
@Override @Override
public <T> Facing unmapFacing(T cameraConstant) { public <T> Facing unmapFacing(T cameraConstant) {
return null; return reverseLookup(FACING, cameraConstant);
} }
@Override @Override

@ -10,6 +10,7 @@ import androidx.annotation.NonNull;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
/** /**
* Class holding a background handler. * Class holding a background handler.
@ -60,12 +61,19 @@ public class WorkerHandler {
private HandlerThread mThread; private HandlerThread mThread;
private Handler mHandler; private Handler mHandler;
private Executor mExecutor;
private WorkerHandler(@NonNull String name) { private WorkerHandler(@NonNull String name) {
mThread = new HandlerThread(name); mThread = new HandlerThread(name);
mThread.setDaemon(true); mThread.setDaemon(true);
mThread.start(); mThread.start();
mHandler = new Handler(mThread.getLooper()); mHandler = new Handler(mThread.getLooper());
mExecutor = new Executor() {
@Override
public void execute(Runnable command) {
post(command);
}
};
} }
/** /**
@ -76,11 +84,28 @@ public class WorkerHandler {
mHandler.post(runnable); mHandler.post(runnable);
} }
/**
* Post an action on this handler.
* @param delay the delay in millis
* @param runnable the action
*/
public void post(long delay, @NonNull Runnable runnable) {
mHandler.postDelayed(runnable, delay);
}
/**
* Removes a previously added action from this handler.
* @param runnable the action
*/
public void remove(@NonNull Runnable runnable) {
mHandler.removeCallbacks(runnable);
}
/** /**
* Returns the android backing {@link Handler}. * Returns the android backing {@link Handler}.
* @return the handler * @return the handler
*/ */
public Handler get() { public Handler getHandler() {
return mHandler; return mHandler;
} }
@ -102,6 +127,15 @@ public class WorkerHandler {
return mThread.getLooper(); return mThread.getLooper();
} }
/**
* Returns an {@link Executor}.
* @return the executor
*/
@NonNull
public Executor getExecutor() {
return mExecutor;
}
/** /**
* Destroys all handlers, interrupting their work and * Destroys all handlers, interrupting their work and
* removing them from our cache. * removing them from our cache.

@ -74,11 +74,11 @@ public abstract class CameraPreview<T extends View, Output> {
* Sets a callback to be notified of surface events (creation, change, destruction) * Sets a callback to be notified of surface events (creation, change, destruction)
* @param callback a callback * @param callback a callback
*/ */
public final void setSurfaceCallback(@NonNull SurfaceCallback callback) { public final void setSurfaceCallback(@Nullable SurfaceCallback callback) {
mSurfaceCallback = callback; mSurfaceCallback = callback;
// If surface already available, dispatch. // If surface already available, dispatch.
if (mOutputSurfaceWidth != 0 || mOutputSurfaceHeight != 0) { if (mOutputSurfaceWidth != 0 || mOutputSurfaceHeight != 0) {
mSurfaceCallback.onSurfaceAvailable(); if (callback != null) callback.onSurfaceAvailable();
} }
} }

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

Loading…
Cancel
Save