pull/59/merge
Johnson145 8 years ago committed by GitHub
commit 5781857de7
  1. 74
      README.md
  2. 453
      cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java
  3. 16
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraConfigurationFailedException.java
  4. 16
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java
  5. 15
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraException.java
  6. 13
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java
  7. 17
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraUnavailableException.java
  8. 62
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
  9. 15
      cameraview/src/main/java/com/otaliastudios/cameraview/CapturingFailedException.java
  10. 17
      cameraview/src/main/java/com/otaliastudios/cameraview/CapturingImageFailedException.java
  11. 17
      cameraview/src/main/java/com/otaliastudios/cameraview/CapturingPictureFailedException.java
  12. 17
      cameraview/src/main/java/com/otaliastudios/cameraview/CapturingSnapshotFailedException.java
  13. 16
      cameraview/src/main/java/com/otaliastudios/cameraview/CapturingVideoFailedException.java

@ -148,6 +148,74 @@ camera.postDelayed(new Runnable() {
``` ```
### Error Handling
#### Default Handler
The default implementation will just throw all exceptions to prevent missing error handling.
#### Basic Custom Handler
You can implement custom error handling by overriding the ``onError`` listener.
As soon as this method was overridden at least once (without calling the super method or re-throwing the exception),
the default behavior will be disabled. So pay attention to not swallowing any exceptions.
```java
camera.addCameraListener(new CameraListener() {
@Override
public void onError(CameraException exception) {
throw exception;
}
});
```
#### Advanced Custom Handler
Furthermore, you can distinguish different error types. E.g. some of them imply that the
CameraView should be disabled or restarted while others may be ignored in some use cases.
```java
camera.addCameraListener(new CameraListener() {
@Override
public void onError(CameraException exception) {
if (exception instanceof CameraUnavailableException) {
// the exception prevents the camera from being used. The cause may be
// temporary or permanent. You should restart the camera or deactivate any
// user interaction with the camera.
}
else if (exception instanceof CameraConfigurationFailedException) {
// The previously started setting change failed,
// but the camera should be still available.
}
else if (exception instanceof CapturingFailedException) {
// The previously started capturing (of any kind) failed, but the camera
// should be still available.
if (exception instanceof CapturingImageFailedException) {
// the previously started image capturing failed (snapshot or
// "real picture"), but the camera should be still available.
if (exception instanceof CapturingPictureFailedException) {
// The previously started picture capturing failed, but the camera
// should be still available. This exception does not handle failed
// snapshots.
}
else if (exception instanceof CapturingSnapshotFailedException) {
// The previously started snapshot capturing failed, but the camera
// should be still available. This exception does not handle failed
// "real picture" capturing.
}
}
else if (exception instanceof CapturingVideoFailedException) {
// The previously started video capturing failed, but the camera
// should be still available.
}
}
else {
throw exception;
}
}
});
```
### Other camera events ### Other camera events
Make sure you can react to different camera events by setting up one or more `CameraListener` instances. All these are executed on the UI thread. Make sure you can react to different camera events by setting up one or more `CameraListener` instances. All these are executed on the UI thread.
@ -220,8 +288,7 @@ camera.addCameraListener(new CameraListener() {
* to be changed. This can be used, for example, to draw a seek bar. * to be changed. This can be used, for example, to draw a seek bar.
*/ */
@Override @Override
public void onExposureCorrectionChanged(float newValue, float[] bounds, PointF[] fingers) {} public void onExposureCorrectionChanged(float newValue, float[] bounds, PointF[] fingers) {}
}); });
``` ```
@ -542,6 +609,7 @@ This is what was done since the library was forked. I have kept the original str
- *Tests!* - *Tests!*
- *`CameraLogger` APIs for logging and bug reports* - *`CameraLogger` APIs for logging and bug reports*
- *Better threading, start() in worker thread and callbacks in UI* - *Better threading, start() in worker thread and callbacks in UI*
- *Custom error handling: prevent app crashes (caused by inevitable errors) by simply overriding a listener*
These are still things that need to be done, off the top of my head: These are still things that need to be done, off the top of my head:
@ -550,7 +618,7 @@ These are still things that need to be done, off the top of my head:
- [ ] add a `setPreferredAspectRatio` API to choose the capture size. Preview size will adapt, and then, if let free, the CameraView will adapt as well - [ ] add a `setPreferredAspectRatio` API to choose the capture size. Preview size will adapt, and then, if let free, the CameraView will adapt as well
- [ ] animate grid lines similar to stock camera app - [ ] animate grid lines similar to stock camera app
- [ ] add onRequestPermissionResults for easy permission callback - [ ] add onRequestPermissionResults for easy permission callback
- [ ] better error handling, maybe with a onError(e) method in the public listener, or have each public method return a boolean - [ ] better error handling, maybe extending the current onError(e) method to handle more use cases (e.g. the ones only caught by a boolean return value)
- [ ] decent code coverage - [ ] decent code coverage
## Device-specific issues ## Device-specific issues

@ -19,6 +19,9 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import static android.hardware.Camera.CAMERA_ERROR_SERVER_DIED;
import static android.hardware.Camera.CAMERA_ERROR_UNKNOWN;
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
class Camera1 extends CameraController { class Camera1 extends CameraController {
@ -37,14 +40,24 @@ class Camera1 extends CameraController {
private Runnable mPostFocusResetRunnable = new Runnable() { private Runnable mPostFocusResetRunnable = new Runnable() {
@Override @Override
public void run() { public void run() {
if (!isCameraAvailable()) return; try {
mCamera.cancelAutoFocus(); if (!isCameraAvailable()) return;
synchronized (mLock) { mCamera.cancelAutoFocus();
Camera.Parameters params = mCamera.getParameters(); synchronized (mLock) {
params.setFocusAreas(null); Camera.Parameters params = mCamera.getParameters();
params.setMeteringAreas(null); params.setFocusAreas(null);
applyDefaultFocus(params); // Revert to internal focus. params.setMeteringAreas(null);
mCamera.setParameters(params); applyDefaultFocus(params); // Revert to internal focus.
mCamera.setParameters(params);
}
}
catch (Exception e) {
// at least setParameters may fail.
// problem may be device-specific to the Samsung Galaxy J5
// TODO why does it fail occasionally and is it possible to prevent such errors?
CameraException cameraException = new CameraConfigurationFailedException("Failed to " +
"reset auto focus.", e);
mCameraCallbacks.onError(cameraException);
} }
} }
}; };
@ -75,8 +88,9 @@ class Camera1 extends CameraController {
try { try {
setup(); setup();
} catch (Exception e) { } catch (Exception e) {
LOG.w("onSurfaceAvailable:", "Exception while binding camera to preview.", e); CameraException cameraException = new CameraUnavailableException(
throw new RuntimeException(e); "onSurfaceAvailable: Exception while binding camera to preview.", e);
mCameraCallbacks.onError(cameraException);
} }
} }
}); });
@ -164,6 +178,38 @@ class Camera1 extends CameraController {
if (collectCameraId()) { if (collectCameraId()) {
mCamera = Camera.open(mCameraId); mCamera = Camera.open(mCameraId);
/**
* attach Android native error listener for the Camera1 API
* TODO it's not yet sure how the caught errors interact with the exceptions caught
* outside of the following handler. Furthermore, for most errors it's not known whether
* they are crucial or not. Therefore, such errors are handled as low-priority
* CameraConfigurationFailedExceptions for now.
*/
mCamera.setErrorCallback(new Camera.ErrorCallback() {
@Override
public void onError(int errorCode, Camera camera) {
// extend error information by known error codes
CameraException cameraException;
if (errorCode == CAMERA_ERROR_SERVER_DIED) {
cameraException = new CameraUnavailableException(
"Media server died. In this case, the application must release the" +
" Camera object and instantiate a new one.");
}
else if (errorCode == CAMERA_ERROR_UNKNOWN) {
cameraException = new CameraConfigurationFailedException(
"Unspecified camera error.");
}
else {
cameraException = new CameraConfigurationFailedException(
"Received camera error code: " + errorCode);
}
// redirect error
mCameraCallbacks.onError(cameraException);
}
});
// Set parameters that might have been set before the camera was opened. // Set parameters that might have been set before the camera was opened.
synchronized (mLock) { synchronized (mLock) {
LOG.i("onStart:", "Applying default parameters."); LOG.i("onStart:", "Applying default parameters.");
@ -250,14 +296,21 @@ class Camera1 extends CameraController {
@Override @Override
void setLocation(Location location) { void setLocation(Location location) {
Location oldLocation = mLocation; try {
mLocation = location; Location oldLocation = mLocation;
if (isCameraAvailable()) { mLocation = location;
synchronized (mLock) { if (isCameraAvailable()) {
Camera.Parameters params = mCamera.getParameters(); synchronized (mLock) {
if (mergeLocation(params, oldLocation)) mCamera.setParameters(params); Camera.Parameters params = mCamera.getParameters();
if (mergeLocation(params, oldLocation)) mCamera.setParameters(params);
}
} }
} }
catch (Exception e) {
CameraException cameraException =
new CameraConfigurationFailedException("Failed to set the location.", e);
mCameraCallbacks.onError(cameraException);
}
} }
private boolean mergeLocation(Camera.Parameters params, Location oldLocation) { private boolean mergeLocation(Camera.Parameters params, Location oldLocation) {
@ -288,14 +341,22 @@ class Camera1 extends CameraController {
@Override @Override
void setWhiteBalance(WhiteBalance whiteBalance) { void setWhiteBalance(WhiteBalance whiteBalance) {
WhiteBalance old = mWhiteBalance; try {
mWhiteBalance = whiteBalance; WhiteBalance old = mWhiteBalance;
if (isCameraAvailable()) { mWhiteBalance = whiteBalance;
synchronized (mLock) { if (isCameraAvailable()) {
Camera.Parameters params = mCamera.getParameters(); synchronized (mLock) {
if (mergeWhiteBalance(params, old)) mCamera.setParameters(params); Camera.Parameters params = mCamera.getParameters();
if (mergeWhiteBalance(params, old)) mCamera.setParameters(params);
}
} }
} }
catch (Exception e) {
// TODO handle, !mergeWhiteBalance, too?
CameraException cameraException =
new CameraConfigurationFailedException("Failed to set the white balance.", e);
mCameraCallbacks.onError(cameraException);
}
} }
private boolean mergeWhiteBalance(Camera.Parameters params, WhiteBalance oldWhiteBalance) { private boolean mergeWhiteBalance(Camera.Parameters params, WhiteBalance oldWhiteBalance) {
@ -309,14 +370,22 @@ class Camera1 extends CameraController {
@Override @Override
void setHdr(Hdr hdr) { void setHdr(Hdr hdr) {
Hdr old = mHdr; try {
mHdr = hdr; Hdr old = mHdr;
if (isCameraAvailable()) { mHdr = hdr;
synchronized (mLock) { if (isCameraAvailable()) {
Camera.Parameters params = mCamera.getParameters(); synchronized (mLock) {
if (mergeHdr(params, old)) mCamera.setParameters(params); Camera.Parameters params = mCamera.getParameters();
if (mergeHdr(params, old)) mCamera.setParameters(params);
}
} }
} }
catch (Exception e) {
// TODO handle, !mergeHdr, too?
CameraException cameraException =
new CameraConfigurationFailedException("Failed to set hdr.", e);
mCameraCallbacks.onError(cameraException);
}
} }
private boolean mergeHdr(Camera.Parameters params, Hdr oldHdr) { private boolean mergeHdr(Camera.Parameters params, Hdr oldHdr) {
@ -341,14 +410,22 @@ class Camera1 extends CameraController {
@Override @Override
void setFlash(Flash flash) { void setFlash(Flash flash) {
Flash old = mFlash; try {
mFlash = flash; Flash old = mFlash;
if (isCameraAvailable()) { mFlash = flash;
synchronized (mLock) { if (isCameraAvailable()) {
Camera.Parameters params = mCamera.getParameters(); synchronized (mLock) {
if (mergeFlash(params, old)) mCamera.setParameters(params); Camera.Parameters params = mCamera.getParameters();
if (mergeFlash(params, old)) mCamera.setParameters(params);
}
} }
} }
catch (Exception e) {
// TODO handle, !mergeFlash, too?
CameraException cameraException =
new CameraConfigurationFailedException("Failed to set flash.", e);
mCameraCallbacks.onError(cameraException);
}
} }
@ -391,29 +468,37 @@ class Camera1 extends CameraController {
@Override @Override
void setVideoQuality(VideoQuality videoQuality) { void setVideoQuality(VideoQuality videoQuality) {
if (mIsCapturingVideo) {
// TODO: actually any call to getParameters() could fail while recording a video. try {
// See. https://stackoverflow.com/questions/14941625/correct-handling-of-exception-getparameters-failed-empty-parameters if (mIsCapturingVideo) {
throw new IllegalStateException("Can't change video quality while recording a video."); // TODO: actually any call to getParameters() could fail while recording a video.
} // See. https://stackoverflow.com/questions/14941625/correct-handling-of-exception-getparameters-failed-empty-parameters
throw new IllegalStateException("Can't change video quality while recording a video.");
mVideoQuality = videoQuality; }
if (isCameraAvailable() && mSessionType == SessionType.VIDEO) {
// Change capture size to a size that fits the video aspect ratio. mVideoQuality = videoQuality;
Size oldSize = mCaptureSize; if (isCameraAvailable() && mSessionType == SessionType.VIDEO) {
mCaptureSize = computeCaptureSize(); // Change capture size to a size that fits the video aspect ratio.
if (!mCaptureSize.equals(oldSize)) { Size oldSize = mCaptureSize;
// New video quality triggers a new aspect ratio. mCaptureSize = computeCaptureSize();
// Go on and see if preview size should change also. if (!mCaptureSize.equals(oldSize)) {
synchronized (mLock) { // New video quality triggers a new aspect ratio.
Camera.Parameters params = mCamera.getParameters(); // Go on and see if preview size should change also.
params.setPictureSize(mCaptureSize.getWidth(), mCaptureSize.getHeight()); synchronized (mLock) {
mCamera.setParameters(params); Camera.Parameters params = mCamera.getParameters();
params.setPictureSize(mCaptureSize.getWidth(), mCaptureSize.getHeight());
mCamera.setParameters(params);
}
onSurfaceChanged();
} }
onSurfaceChanged(); LOG.i("setVideoQuality:", "captureSize:", mCaptureSize);
LOG.i("setVideoQuality:", "previewSize:", mPreviewSize);
} }
LOG.i("setVideoQuality:", "captureSize:", mCaptureSize); }
LOG.i("setVideoQuality:", "previewSize:", mPreviewSize); catch (Exception e) {
CameraException cameraException =
new CameraConfigurationFailedException("Failed to set video quality.", e);
mCameraCallbacks.onError(cameraException);
} }
} }
@ -425,36 +510,43 @@ class Camera1 extends CameraController {
if (!mOptions.isVideoSnapshotSupported()) return false; if (!mOptions.isVideoSnapshotSupported()) return false;
} }
// Set boolean to wait for image callback try {
mIsCapturingImage = true; // Set boolean to wait for image callback
final int exifRotation = computeExifRotation(); mIsCapturingImage = true;
final boolean exifFlip = computeExifFlip(); final int exifRotation = computeExifRotation();
final int sensorToDisplay = computeSensorToDisplayOffset(); final boolean exifFlip = computeExifFlip();
synchronized (mLock) { final int sensorToDisplay = computeSensorToDisplayOffset();
Camera.Parameters params = mCamera.getParameters(); synchronized (mLock) {
params.setRotation(exifRotation); Camera.Parameters params = mCamera.getParameters();
mCamera.setParameters(params); params.setRotation(exifRotation);
mCamera.setParameters(params);
}
// Is the final picture (decoded respecting EXIF) consistent with CameraView orientation?
// We must consider exifOrientation to bring back the picture in the sensor world.
// Then use sensorToDisplay to move to the display world, where CameraView lives.
final boolean consistentWithView = (exifRotation + sensorToDisplay + 180) % 180 == 0;
mCamera.takePicture(null, null, null,
new Camera.PictureCallback() {
@Override
public void onPictureTaken(byte[] data, final Camera camera) {
mIsCapturingImage = false;
mHandler.post(new Runnable() {
@Override
public void run() {
// This is needed, read somewhere in the docs.
camera.startPreview();
}
});
mCameraCallbacks.processImage(data, consistentWithView, exifFlip);
}
});
return true;
}
catch (Exception e) {
CameraException cameraException = new CapturingPictureFailedException("Capturing a picture failed.", e);
mCameraCallbacks.onError(cameraException);
return false;
} }
// Is the final picture (decoded respecting EXIF) consistent with CameraView orientation?
// We must consider exifOrientation to bring back the picture in the sensor world.
// Then use sensorToDisplay to move to the display world, where CameraView lives.
final boolean consistentWithView = (exifRotation + sensorToDisplay + 180) % 180 == 0;
mCamera.takePicture(null, null, null,
new Camera.PictureCallback() {
@Override
public void onPictureTaken(byte[] data, final Camera camera) {
mIsCapturingImage = false;
mHandler.post(new Runnable() {
@Override
public void run() {
// This is needed, read somewhere in the docs.
camera.startPreview();
}
});
mCameraCallbacks.processImage(data, consistentWithView, exifFlip);
}
});
return true;
} }
@ -468,37 +560,45 @@ class Camera1 extends CameraController {
capturePicture(); capturePicture();
return false; return false;
} }
mIsCapturingImage = true;
mCamera.setOneShotPreviewCallback(new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(final byte[] data, Camera camera) {
// Got to rotate the preview frame, since byte[] data here does not include
// EXIF tags automatically set by camera. So either we add EXIF, or we rotate.
// Adding EXIF to a byte array, unfortunately, is hard.
Camera.Parameters params = mCamera.getParameters();
final int sensorToDevice = computeExifRotation();
final int sensorToDisplay = computeSensorToDisplayOffset();
final boolean exifFlip = computeExifFlip();
final boolean flip = sensorToDevice % 180 != 0;
final int preWidth = mPreviewSize.getWidth();
final int preHeight = mPreviewSize.getHeight();
final int postWidth = flip ? preHeight : preWidth;
final int postHeight = flip ? preWidth : preHeight;
final int format = params.getPreviewFormat();
WorkerHandler.run(new Runnable() {
@Override
public void run() {
final boolean consistentWithView = (sensorToDevice + sensorToDisplay + 180) % 180 == 0; try {
byte[] rotatedData = RotationHelper.rotate(data, preWidth, preHeight, sensorToDevice); mIsCapturingImage = true;
YuvImage yuv = new YuvImage(rotatedData, format, postWidth, postHeight, null); mCamera.setOneShotPreviewCallback(new Camera.PreviewCallback() {
mCameraCallbacks.processSnapshot(yuv, consistentWithView, exifFlip); @Override
mIsCapturingImage = false; public void onPreviewFrame(final byte[] data, Camera camera) {
} // Got to rotate the preview frame, since byte[] data here does not include
}); // EXIF tags automatically set by camera. So either we add EXIF, or we rotate.
} // Adding EXIF to a byte array, unfortunately, is hard.
}); Camera.Parameters params = mCamera.getParameters();
return true; final int sensorToDevice = computeExifRotation();
final int sensorToDisplay = computeSensorToDisplayOffset();
final boolean exifFlip = computeExifFlip();
final boolean flip = sensorToDevice % 180 != 0;
final int preWidth = mPreviewSize.getWidth();
final int preHeight = mPreviewSize.getHeight();
final int postWidth = flip ? preHeight : preWidth;
final int postHeight = flip ? preWidth : preHeight;
final int format = params.getPreviewFormat();
WorkerHandler.run(new Runnable() {
@Override
public void run() {
final boolean consistentWithView = (sensorToDevice + sensorToDisplay + 180) % 180 == 0;
byte[] rotatedData = RotationHelper.rotate(data, preWidth, preHeight, sensorToDevice);
YuvImage yuv = new YuvImage(rotatedData, format, postWidth, postHeight, null);
mCameraCallbacks.processSnapshot(yuv, consistentWithView, exifFlip);
mIsCapturingImage = false;
}
});
}
});
return true;
}
catch (Exception e) {
CameraException cameraException = new CapturingSnapshotFailedException("Capturing a snapshot failed.", e);
mCameraCallbacks.onError(cameraException);
return false;
}
} }
@Override @Override
@ -619,14 +719,20 @@ class Camera1 extends CameraController {
mMediaRecorder.start(); mMediaRecorder.start();
return true; return true;
} catch (Exception e) { } catch (Exception e) {
LOG.e("Error while starting MediaRecorder. Swallowing.", e); CameraException cameraException =
new CapturingVideoFailedException("Error while starting MediaRecorder. " +
"Swallowing.", e);
mCameraCallbacks.onError(cameraException);
mVideoFile = null; mVideoFile = null;
mCamera.lock(); mCamera.lock();
endVideo(); endVideo();
return false; return false;
} }
} else { } else {
throw new IllegalStateException("Can't record video while session type is picture"); CameraException cameraException =
new CapturingVideoFailedException("Can't record video while session type is picture");
mCameraCallbacks.onError(cameraException);
return false;
} }
} }
@ -736,32 +842,50 @@ class Camera1 extends CameraController {
@Override @Override
boolean setZoom(float zoom) { boolean setZoom(float zoom) {
if (!isCameraAvailable()) return false; try {
if (!mOptions.isZoomSupported()) return false; if (!isCameraAvailable()) return false;
synchronized (mLock) { if (!mOptions.isZoomSupported()) return false;
Camera.Parameters params = mCamera.getParameters(); synchronized (mLock) {
float max = params.getMaxZoom(); Camera.Parameters params = mCamera.getParameters();
params.setZoom((int) (zoom * max)); float max = params.getMaxZoom();
mCamera.setParameters(params); params.setZoom((int) (zoom * max));
mCamera.setParameters(params);
}
return true;
} }
return true; catch (Exception e) {
CameraException cameraException =
new CameraConfigurationFailedException("Failed to set zoom.", e);
mCameraCallbacks.onError(cameraException);
return false;
}
} }
@Override @Override
boolean setExposureCorrection(float EVvalue) { boolean setExposureCorrection(float EVvalue) {
if (!isCameraAvailable()) return false; try {
if (!mOptions.isExposureCorrectionSupported()) return false; if (!isCameraAvailable()) return false;
float max = mOptions.getExposureCorrectionMaxValue(); if (!mOptions.isExposureCorrectionSupported()) return false;
float min = mOptions.getExposureCorrectionMinValue(); float max = mOptions.getExposureCorrectionMaxValue();
EVvalue = EVvalue < min ? min : EVvalue > max ? max : EVvalue; // cap float min = mOptions.getExposureCorrectionMinValue();
synchronized (mLock) { EVvalue = EVvalue < min ? min : EVvalue > max ? max : EVvalue; // cap
Camera.Parameters params = mCamera.getParameters(); synchronized (mLock) {
int indexValue = (int) (EVvalue / params.getExposureCompensationStep()); Camera.Parameters params = mCamera.getParameters();
params.setExposureCompensation(indexValue); int indexValue = (int) (EVvalue / params.getExposureCompensationStep());
mCamera.setParameters(params); params.setExposureCompensation(indexValue);
mCamera.setParameters(params);
}
return true;
} }
return true; catch (Exception e) {
CameraException cameraException =
new CameraConfigurationFailedException("Failed to set exposure correction.", e);
mCameraCallbacks.onError(cameraException);
return false;
}
} }
// ----------------- // -----------------
@ -772,31 +896,42 @@ class Camera1 extends CameraController {
boolean startAutoFocus(@Nullable final Gesture gesture, PointF point) { boolean startAutoFocus(@Nullable final Gesture gesture, PointF point) {
if (!isCameraAvailable()) return false; if (!isCameraAvailable()) return false;
if (!mOptions.isAutoFocusSupported()) return false; if (!mOptions.isAutoFocusSupported()) return false;
final PointF p = new PointF(point.x, point.y); // copy.
List<Camera.Area> meteringAreas2 = computeMeteringAreas(p.x, p.y); try {
List<Camera.Area> meteringAreas1 = meteringAreas2.subList(0, 1); final PointF p = new PointF(point.x, point.y); // copy.
synchronized (mLock) { List<Camera.Area> meteringAreas2 = computeMeteringAreas(p.x, p.y);
// At this point we are sure that camera supports auto focus... right? Look at CameraView.onTouchEvent(). List<Camera.Area> meteringAreas1 = meteringAreas2.subList(0, 1);
Camera.Parameters params = mCamera.getParameters(); synchronized (mLock) {
int maxAF = params.getMaxNumFocusAreas(); // At this point we are sure that camera supports auto focus... right? Look at CameraView.onTouchEvent().
int maxAE = params.getMaxNumMeteringAreas(); Camera.Parameters params = mCamera.getParameters();
if (maxAF > 0) params.setFocusAreas(maxAF > 1 ? meteringAreas2 : meteringAreas1); int maxAF = params.getMaxNumFocusAreas();
if (maxAE > 0) params.setMeteringAreas(maxAE > 1 ? meteringAreas2 : meteringAreas1); int maxAE = params.getMaxNumMeteringAreas();
params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO); if (maxAF > 0) params.setFocusAreas(maxAF > 1 ? meteringAreas2 : meteringAreas1);
mCamera.setParameters(params); if (maxAE > 0) params.setMeteringAreas(maxAE > 1 ? meteringAreas2 : meteringAreas1);
mCameraCallbacks.dispatchOnFocusStart(gesture, p); params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
// TODO this is not guaranteed to be called... Fix. mCamera.setParameters(params);
mCamera.autoFocus(new Camera.AutoFocusCallback() { mCameraCallbacks.dispatchOnFocusStart(gesture, p);
@Override // TODO this is not guaranteed to be called... Fix.
public void onAutoFocus(boolean success, Camera camera) { mCamera.autoFocus(new Camera.AutoFocusCallback() {
// TODO lock auto exposure and white balance for a while @Override
mCameraCallbacks.dispatchOnFocusEnd(gesture, success, p); public void onAutoFocus(boolean success, Camera camera) {
mHandler.get().removeCallbacks(mPostFocusResetRunnable); // TODO lock auto exposure and white balance for a while
mHandler.get().postDelayed(mPostFocusResetRunnable, mPostFocusResetDelay); mCameraCallbacks.dispatchOnFocusEnd(gesture, success, p);
} mHandler.get().removeCallbacks(mPostFocusResetRunnable);
}); mHandler.get().postDelayed(mPostFocusResetRunnable, mPostFocusResetDelay);
}
});
}
return true;
}
catch (Exception e) {
// at least getParameters and setParameters may fail.
// TODO why do they fail and is it possible to prevent such errors?
CameraException cameraException = new CameraConfigurationFailedException("Failed to " +
"start auto focus.", e);
mCameraCallbacks.onError(cameraException);
return false;
} }
return true;
} }

@ -0,0 +1,16 @@
package com.otaliastudios.cameraview;
/**
* An object of this class describes an error that occurred during the normal runtime of the camera.
* The previously started setting change failed, but the camera should be still available.
*/
public class CameraConfigurationFailedException extends CameraException {
CameraConfigurationFailedException(String message) {
super(message);
}
CameraConfigurationFailedException(String message, Throwable cause) {
super(message, cause);
}
}

@ -104,8 +104,9 @@ abstract class CameraController implements CameraPreview.SurfaceCallback {
mCameraCallbacks.dispatchOnCameraOpened(mOptions); mCameraCallbacks.dispatchOnCameraOpened(mOptions);
} catch (Exception e) { } catch (Exception e) {
LOG.e("Error while starting the camera engine.", e); CameraException cameraException =
throw new RuntimeException(e); new CameraUnavailableException("Error while starting the camera engine.", e);
mCameraCallbacks.onError(cameraException);
} }
} }
}); });
@ -130,8 +131,9 @@ abstract class CameraController implements CameraPreview.SurfaceCallback {
mCameraCallbacks.dispatchOnCameraClosed(); mCameraCallbacks.dispatchOnCameraClosed();
} catch (Exception e) { } catch (Exception e) {
LOG.e("Error while stopping the camera engine.", e); CameraException cameraException =
throw new RuntimeException(e); new CameraUnavailableException("Error while stopping the camera engine.", e);
mCameraCallbacks.onError(cameraException);
} }
} }
}); });
@ -180,9 +182,9 @@ abstract class CameraController implements CameraPreview.SurfaceCallback {
mCameraCallbacks.dispatchOnCameraOpened(mOptions); mCameraCallbacks.dispatchOnCameraOpened(mOptions);
} catch (Exception e) { } catch (Exception e) {
LOG.e("Error while restarting the camera engine.", e); CameraException cameraException =
throw new RuntimeException(e); new CameraUnavailableException("Error while restarting the camera engine.", e);
mCameraCallbacks.onError(cameraException);
} }
} }
}); });

@ -0,0 +1,15 @@
package com.otaliastudios.cameraview;
/**
* An object of this class describes an error that occurred during the normal runtime of the camera.
*/
public abstract class CameraException extends RuntimeException {
CameraException(String message) {
super(message);
}
CameraException(String message, Throwable cause) {
super(message, cause);
}
}

@ -130,4 +130,17 @@ public abstract class CameraListener {
} }
/**
* Notifies that an error occurred in any of the previously called methods.
*
* The default implementation will just throw the original exception again to prevent missing
* error handling. Override this method without calling the super method in order to implement
* custom error handling.
*
* @param exception the caught exception
*/
@UiThread
public void onError(CameraException exception) {
throw exception;
}
} }

@ -0,0 +1,17 @@
package com.otaliastudios.cameraview;
/**
* An object of this class describes an error that occurred during the normal runtime of the camera.
* It prevents the camera from being used. The cause may be temporary or permanent. You should
* restart the camera or deactivate any user interaction with the camera.
*/
public class CameraUnavailableException extends CameraException {
CameraUnavailableException(String message) {
super(message);
}
CameraUnavailableException(String message, Throwable cause) {
super(message, cause);
}
}

@ -559,9 +559,10 @@ public class CameraView extends FrameLayout {
return; return;
} }
} }
LOG.e("Permission error:", "When the session type is set to video,", CameraException cameraException = new CameraUnavailableException("Permission " +
"the RECORD_AUDIO permission should be added to the app manifest file."); "error: When the session type is set to video, the RECORD_AUDIO " +
throw new IllegalStateException(CameraLogger.lastMessage); "permission should be added to the app manifest file.");
mCameraCallbacks.onError(cameraException);
} catch (PackageManager.NameNotFoundException e) { } catch (PackageManager.NameNotFoundException e) {
// Not possible. // Not possible.
} }
@ -1004,7 +1005,11 @@ public class CameraView extends FrameLayout {
*/ */
public void setJpegQuality(int jpegQuality) { public void setJpegQuality(int jpegQuality) {
if (jpegQuality <= 0 || jpegQuality > 100) { if (jpegQuality <= 0 || jpegQuality > 100) {
throw new IllegalArgumentException("JPEG quality should be > 0 and <= 100"); IllegalArgumentException illegalArgumentException = new
IllegalArgumentException("JPEG quality should be > 0 and <= 100");
CameraException cameraException = new CameraConfigurationFailedException("Could not" +
" set JpegQuality", illegalArgumentException);
mCameraCallbacks.onError(cameraException);
} }
mJpegQuality = jpegQuality; mJpegQuality = jpegQuality;
} }
@ -1167,15 +1172,16 @@ public class CameraView extends FrameLayout {
* so callers should ensure they have appropriate permissions to write to the file. * so callers should ensure they have appropriate permissions to write to the file.
* Recording will be automatically stopped after durationMillis, unless * Recording will be automatically stopped after durationMillis, unless
* {@link #stopCapturingVideo()} is not called meanwhile. * {@link #stopCapturingVideo()} is not called meanwhile.
* Triggers error handler, if durationMillis is less than 500 milliseconds.
* *
* @param file a file where the video will be saved * @param file a file where the video will be saved
* @param durationMillis video max duration * @param durationMillis video max duration
*
* @throws IllegalArgumentException if durationMillis is less than 500 milliseconds
*/ */
public void startCapturingVideo(File file, long durationMillis) { public void startCapturingVideo(File file, long durationMillis) {
if (durationMillis < 500) { if (durationMillis < 500) {
throw new IllegalArgumentException("Video duration can't be < 500 milliseconds"); CameraException cameraException = new CapturingVideoFailedException("Video duration" +
" can't be < 500 milliseconds");
mCameraCallbacks.onError(cameraException);
} }
startCapturingVideo(file); startCapturingVideo(file);
mUiHandler.postDelayed(new Runnable() { mUiHandler.postDelayed(new Runnable() {
@ -1310,6 +1316,7 @@ public class CameraView extends FrameLayout {
void dispatchOnFocusEnd(@Nullable Gesture trigger, boolean success, PointF where); void dispatchOnFocusEnd(@Nullable Gesture trigger, boolean success, PointF where);
void dispatchOnZoomChanged(final float newValue, final PointF[] fingers); void dispatchOnZoomChanged(final float newValue, final PointF[] fingers);
void dispatchOnExposureCorrectionChanged(float newValue, float[] bounds, PointF[] fingers); void dispatchOnExposureCorrectionChanged(float newValue, float[] bounds, PointF[] fingers);
void onError(CameraException exception);
} }
private class Callbacks implements CameraCallbacks { private class Callbacks implements CameraCallbacks {
@ -1557,6 +1564,47 @@ public class CameraView extends FrameLayout {
} }
}); });
} }
/**
* Log and redirect the given error information to the CameraListeners.
*
* @param exception the error cause
*/
@Override
public void onError(final CameraException exception) {
// log
LOG.e(exception.getMessage(), exception.getCause());
// redirect
mUiHandler.post(new Runnable() {
@Override
public void run() {
// all error listeners will be called, but at most one of them should actually
// throw an exception
int count = 0;
for (CameraListener listener : mListeners) {
try {
listener.onError(exception);
} catch (CameraException ce) {
// if a custom error handler caused a new exception, we throw the new
// one instead of the original one
if (ce == exception) {
count++;
}
else {
throw ce;
}
}
}
// the original exception is only thrown, if all existing listeners threw it
if (count == mListeners.size()) {
throw exception;
}
}
});
}
} }
//endregion //endregion

@ -0,0 +1,15 @@
package com.otaliastudios.cameraview;
/**
* An object of this class describes an error that occurred during the normal runtime of the camera.
* The previously started capturing failed, but the camera should be still available.
*/
public abstract class CapturingFailedException extends CameraException {
CapturingFailedException(String message) {
super(message);
}
CapturingFailedException(String message, Throwable cause) {
super(message, cause);
}
}

@ -0,0 +1,17 @@
package com.otaliastudios.cameraview;
/**
* An object of this class describes an error that occurred during the normal runtime of the camera.
* The previously started image capturing failed (snapshot or "real picture"), but the camera should
* be still available.
*/
public abstract class CapturingImageFailedException extends CapturingFailedException {
CapturingImageFailedException(String message) {
super(message);
}
CapturingImageFailedException(String message, Throwable cause) {
super(message, cause);
}
}

@ -0,0 +1,17 @@
package com.otaliastudios.cameraview;
/**
* An object of this class describes an error that occurred during the normal runtime of the camera.
* The previously started picture capturing failed, but the camera should be still available.
* This exception does not handle failed snapshots.
*/
public class CapturingPictureFailedException extends CapturingImageFailedException {
CapturingPictureFailedException(String message) {
super(message);
}
CapturingPictureFailedException(String message, Throwable cause) {
super(message, cause);
}
}

@ -0,0 +1,17 @@
package com.otaliastudios.cameraview;
/**
* An object of this class describes an error that occurred during the normal runtime of the camera.
* The previously started snapshot capturing failed, but the camera should be still available.
* This exception does not handle failed "real picture" capturing.
*/
public class CapturingSnapshotFailedException extends CapturingImageFailedException {
CapturingSnapshotFailedException(String message) {
super(message);
}
CapturingSnapshotFailedException(String message, Throwable cause) {
super(message, cause);
}
}

@ -0,0 +1,16 @@
package com.otaliastudios.cameraview;
/**
* An object of this class describes an error that occurred during the normal runtime of the camera.
* The previously started video capturing failed, but the camera should be still available.
*/
public class CapturingVideoFailedException extends CapturingFailedException {
CapturingVideoFailedException(String message) {
super(message);
}
CapturingVideoFailedException(String message, Throwable cause) {
super(message, cause);
}
}
Loading…
Cancel
Save