Make CameraController fully async, fix shutter sounds, tests

pull/97/head
Mattia Iavarone 8 years ago
parent 2e9715fa89
commit bd0a84a65e
  1. 2
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraCallbacksTest.java
  2. 4
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java
  3. 26
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java
  4. 99
      cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java
  5. 28
      cameraview/src/main/java/com/otaliastudios/cameraview/Camera2.java
  6. 30
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java
  7. 62
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java

@ -225,6 +225,8 @@ public class CameraCallbacksTest extends BaseTest {
verify(listener, times(1)).onOrientationChanged(anyInt());
}
// TODO: test onShutter, here or elsewhere
@Test
public void testProcessJpeg() {

@ -92,10 +92,10 @@ public class CameraViewTest extends BaseTest {
assertEquals(cameraView.getAudio(), Audio.DEFAULT);
assertEquals(cameraView.getVideoQuality(), VideoQuality.DEFAULT);
assertEquals(cameraView.getLocation(), null);
// Self managed
assertEquals(cameraView.getExposureCorrection(), 0f, 0f);
assertEquals(cameraView.getZoom(), 0f, 0f);
// Self managed
assertEquals(cameraView.getPlaySounds(), CameraView.DEFAULT_PLAY_SOUNDS);
assertEquals(cameraView.getCropOutput(), CameraView.DEFAULT_CROP_OUTPUT);
assertEquals(cameraView.getJpegQuality(), CameraView.DEFAULT_JPEG_QUALITY);

@ -40,15 +40,15 @@ public class MockCameraController extends CameraController {
}
@Override
boolean setZoom(float zoom) {
void setZoom(float zoom, PointF[] points, boolean notify) {
mZoomValue = zoom;
mZoomChanged = true;
return true;
}
@Override
boolean setExposureCorrection(float EVvalue) {
void setExposureCorrection(float EVvalue, float[] bounds, PointF[] points, boolean notify) {
mExposureCorrectionValue = EVvalue;
mExposureCorrectionChanged = true;
return true;
}
@Override
@ -92,24 +92,20 @@ public class MockCameraController extends CameraController {
}
@Override
boolean capturePicture() {
void capturePicture() {
mPictureCaptured = true;
return true;
}
@Override
boolean captureSnapshot() {
return true;
void captureSnapshot() {
}
@Override
boolean startVideo(@NonNull File file) {
return true;
void startVideo(@NonNull File file) {
}
@Override
boolean endVideo() {
return true;
void endVideo() {
}
@Override
@ -120,23 +116,19 @@ public class MockCameraController extends CameraController {
@Override
boolean startAutoFocus(@Nullable Gesture gesture, PointF point) {
void startAutoFocus(@Nullable Gesture gesture, PointF point) {
mFocusStarted = true;
return true;
}
@Override
public void onSurfaceChanged() {
}
@Override
public void onSurfaceAvailable() {
}
@Override
public void onBufferAvailable(byte[] buffer) {
}
}

@ -38,9 +38,9 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
private Runnable mPostFocusResetRunnable = new Runnable() {
@Override
public void run() {
synchronized (mLock) {
if (!isCameraAvailable()) return;
mCamera.cancelAutoFocus();
synchronized (mLock) {
Camera.Parameters params = mCamera.getParameters();
params.setFocusAreas(null);
params.setMeteringAreas(null);
@ -54,7 +54,6 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
private boolean mIsSetup = false;
private boolean mIsCapturingImage = false;
private boolean mIsCapturingVideo = false;
private final Object mLock = new Object();
Camera1(CameraView.CameraCallbacks callback) {
@ -90,7 +89,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
@Override
public void onSurfaceChanged() {
LOG.i("onSurfaceChanged, size is", mPreview.getSurfaceSize());
if (mIsSetup) {
if (mIsSetup && isCameraAvailable()) {
// Compute a new camera preview size.
Size newSize = computePreviewSize();
if (!newSize.equals(mPreviewSize)) {
@ -440,11 +439,11 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
}
@Override
boolean capturePicture() {
if (mIsCapturingImage) return false;
if (!isCameraAvailable()) return false;
void capturePicture() {
if (mIsCapturingImage) return;
if (!isCameraAvailable()) return;
if (mSessionType == SessionType.VIDEO && mIsCapturingVideo) {
if (!mOptions.isVideoSnapshotSupported()) return false;
if (!mOptions.isVideoSnapshotSupported()) return;
}
// Set boolean to wait for image callback
@ -461,7 +460,15 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
// 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,
mCamera.takePicture(
new Camera.ShutterCallback() {
@Override
public void onShutter() {
mCameraCallbacks.onShutter(false);
}
},
null,
null,
new Camera.PictureCallback() {
@Override
public void onPictureTaken(byte[] data, final Camera camera) {
@ -476,24 +483,25 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
mCameraCallbacks.processImage(data, consistentWithView, exifFlip);
}
});
return true;
}
@Override
boolean captureSnapshot() {
if (!isCameraAvailable()) return false;
if (mIsCapturingImage) return false;
void captureSnapshot() {
if (!isCameraAvailable()) return;
if (mIsCapturingImage) return;
// This won't work while capturing a video.
// Switch to capturePicture.
if (mIsCapturingVideo) {
capturePicture();
return false;
return;
}
mIsCapturingImage = true;
mCamera.setOneShotPreviewCallback(new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(final byte[] data, Camera camera) {
mCameraCallbacks.onShutter(true);
// 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.
@ -520,11 +528,10 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
// It seems that the buffers are already cleared here, so we need to allocate again.
mCamera.setPreviewCallbackWithBuffer(null); // Release anything left
mCamera.setPreviewCallbackWithBuffer(this); // Add ourselves
mFrameManager.allocate(ImageFormat.getBitsPerPixel(mPreviewFormat), mPreviewSize); mCamera.setPreviewCallbackWithBuffer(Camera1.this);
mCamera.setPreviewCallbackWithBuffer(Camera1.this); // Add ourselves
mFrameManager.allocate(ImageFormat.getBitsPerPixel(mPreviewFormat), mPreviewSize);
}
});
return true;
}
@Override
@ -643,9 +650,9 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
@Override
boolean startVideo(@NonNull File videoFile) {
if (mIsCapturingVideo) return false;
if (!isCameraAvailable()) return false;
void startVideo(@NonNull File videoFile) {
if (mIsCapturingVideo) return;
if (!isCameraAvailable()) return;
if (mSessionType == SessionType.VIDEO) {
mVideoFile = videoFile;
mIsCapturingVideo = true;
@ -653,13 +660,11 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
try {
mMediaRecorder.prepare();
mMediaRecorder.start();
return true;
} catch (Exception e) {
LOG.e("Error while starting MediaRecorder. Swallowing.", e);
mVideoFile = null;
mCamera.lock();
endVideo();
return false;
}
} else {
throw new IllegalStateException("Can't record video while session type is picture");
@ -667,7 +672,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
}
@Override
boolean endVideo() {
void endVideo() {
if (mIsCapturingVideo) {
mIsCapturingVideo = false;
if (mMediaRecorder != null) {
@ -685,9 +690,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
mCameraCallbacks.dispatchOnVideoTaken(mVideoFile);
mVideoFile = null;
}
return true;
}
return false;
}
@ -771,33 +774,42 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
@Override
boolean setZoom(float zoom) {
if (!isCameraAvailable()) return false;
if (!mOptions.isZoomSupported()) return false;
void setZoom(float zoom, PointF[] points, boolean notify) {
if (!isCameraAvailable()) return;
if (!mOptions.isZoomSupported()) return;
mZoomValue = zoom;
synchronized (mLock) {
Camera.Parameters params = mCamera.getParameters();
float max = params.getMaxZoom();
params.setZoom((int) (zoom * max));
mCamera.setParameters(params);
}
return true;
}
if (notify) {
mCameraCallbacks.dispatchOnZoomChanged(zoom, points);
}
}
@Override
boolean setExposureCorrection(float EVvalue) {
if (!isCameraAvailable()) return false;
if (!mOptions.isExposureCorrectionSupported()) return false;
void setExposureCorrection(float EVvalue, float[] bounds, PointF[] points, boolean notify) {
if (!isCameraAvailable()) return;
if (!mOptions.isExposureCorrectionSupported()) return;
float max = mOptions.getExposureCorrectionMaxValue();
float min = mOptions.getExposureCorrectionMinValue();
EVvalue = EVvalue < min ? min : EVvalue > max ? max : EVvalue; // cap
mExposureCorrectionValue = EVvalue;
synchronized (mLock) {
Camera.Parameters params = mCamera.getParameters();
int indexValue = (int) (EVvalue / params.getExposureCompensationStep());
params.setExposureCompensation(indexValue);
mCamera.setParameters(params);
}
return true;
if (notify) {
mCameraCallbacks.dispatchOnExposureCorrectionChanged(EVvalue, bounds, points);
}
}
// -----------------
@ -805,9 +817,9 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
@Override
boolean startAutoFocus(@Nullable final Gesture gesture, PointF point) {
if (!isCameraAvailable()) return false;
if (!mOptions.isAutoFocusSupported()) return false;
void startAutoFocus(@Nullable final Gesture gesture, PointF point) {
if (!isCameraAvailable()) return;
if (!mOptions.isAutoFocusSupported()) return;
final PointF p = new PointF(point.x, point.y); // copy.
List<Camera.Area> meteringAreas2 = computeMeteringAreas(p.x, p.y);
List<Camera.Area> meteringAreas1 = meteringAreas2.subList(0, 1);
@ -832,7 +844,6 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
}
});
}
return true;
}
@ -924,23 +935,15 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
LOG.i("size:", "matchSize:", "found consistent:", consistent.size());
LOG.i("size:", "matchSize:", "found big enough and consistent:", bigEnoughAndConsistent.size());
Size result;
if (biggestPossible) {
if (bigEnoughAndConsistent.size() > 0) {
result = Collections.max(bigEnoughAndConsistent);
result = biggestPossible ?
Collections.max(bigEnoughAndConsistent) :
Collections.min(bigEnoughAndConsistent);
} else if (consistent.size() > 0) {
result = Collections.max(consistent);
} else {
result = Collections.max(sizes);
}
} else {
if (bigEnoughAndConsistent.size() > 0) {
result = Collections.min(bigEnoughAndConsistent);
} else if (consistent.size() > 0) {
result = Collections.max(consistent);
} else {
result = Collections.max(sizes);
}
}
LOG.i("size", "matchSize:", "returning result", result);
return result;
}

@ -57,13 +57,13 @@ class Camera2 extends CameraController {
}
@Override
boolean setZoom(float zoom) {
return false;
void setZoom(float zoom, PointF[] points, boolean notify) {
}
@Override
boolean setExposureCorrection(float EVvalue) {
return false;
void setExposureCorrection(float EVvalue, float[] bounds, PointF[] points, boolean notify) {
}
@Override
@ -97,23 +97,23 @@ class Camera2 extends CameraController {
}
@Override
boolean capturePicture() {
return false;
void capturePicture() {
}
@Override
boolean captureSnapshot() {
return false;
void captureSnapshot() {
}
@Override
boolean startVideo(@NonNull File file) {
return false;
void startVideo(@NonNull File file) {
}
@Override
boolean endVideo() {
return false;
void endVideo() {
}
@Override
@ -122,8 +122,8 @@ class Camera2 extends CameraController {
}
@Override
boolean startAutoFocus(@Nullable Gesture gesture, PointF point) {
return false;
void startAutoFocus(@Nullable Gesture gesture, PointF point) {
}
@Override

@ -30,6 +30,9 @@ abstract class CameraController implements CameraPreview.SurfaceCallback, FrameM
protected Location mLocation;
protected Audio mAudio;
protected float mZoomValue;
protected float mExposureCorrectionValue;
protected Size mCaptureSize;
protected Size mPreviewSize;
protected int mPreviewFormat;
@ -45,6 +48,7 @@ abstract class CameraController implements CameraPreview.SurfaceCallback, FrameM
protected boolean mScheduledForStop = false;
protected boolean mScheduledForRestart = false;
protected int mState = STATE_STOPPED;
protected final Object mLock = new Object();
protected WorkerHandler mHandler;
@ -231,11 +235,11 @@ abstract class CameraController implements CameraPreview.SurfaceCallback, FrameM
// Should restart the session if active.
abstract void setFacing(Facing facing);
// If opened and supported, apply and return true.
abstract boolean setZoom(float zoom);
// If closed, no-op. If opened, check supported and apply.
abstract void setZoom(float zoom, PointF[] points, boolean notify);
// If opened and supported, apply and return true.
abstract boolean setExposureCorrection(float EVvalue);
// If closed, no-op. If opened, check supported and apply.
abstract void setExposureCorrection(float EVvalue, float[] bounds, PointF[] points, boolean notify);
// If closed, keep. If opened, check supported and apply.
abstract void setFlash(Flash flash);
@ -260,17 +264,17 @@ abstract class CameraController implements CameraPreview.SurfaceCallback, FrameM
//region APIs
abstract boolean capturePicture();
abstract void capturePicture();
abstract boolean captureSnapshot();
abstract void captureSnapshot();
abstract boolean startVideo(@NonNull File file);
abstract void startVideo(@NonNull File file);
abstract boolean endVideo();
abstract void endVideo();
abstract boolean shouldFlipSizes(); // Wheter the Sizes should be flipped to match the view orientation.
abstract boolean startAutoFocus(@Nullable Gesture gesture, PointF point);
abstract void startAutoFocus(@Nullable Gesture gesture, PointF point);
//endregion
@ -318,6 +322,14 @@ abstract class CameraController implements CameraPreview.SurfaceCallback, FrameM
return mAudio;
}
final float getZoomValue() {
return mZoomValue;
}
final float getExposureCorrectionValue() {
return mExposureCorrectionValue;
}
final Size getCaptureSize() {
return mCaptureSize;
}

@ -52,8 +52,6 @@ public class CameraView extends FrameLayout {
// Self managed parameters
private int mJpegQuality;
private boolean mCropOutput;
private float mZoomValue;
private float mExposureCorrectionValue;
private boolean mPlaySounds;
private HashMap<Gesture, GestureAction> mGestureMap = new HashMap<>(4);
@ -448,7 +446,7 @@ public class CameraView extends FrameLayout {
// Some gesture layout detected a gesture. It's not known at this moment:
// (1) if it was mapped to some action (we check here)
// (2) if it's supported by the camera (CameraController checks)
private boolean onGesture(GestureLayout source, @NonNull CameraOptions options) {
private void onGesture(GestureLayout source, @NonNull CameraOptions options) {
Gesture gesture = source.getGestureType();
GestureAction action = mGestureMap.get(gesture);
PointF[] points = source.getPoints();
@ -456,36 +454,29 @@ public class CameraView extends FrameLayout {
switch (action) {
case CAPTURE:
return mCameraController.capturePicture();
mCameraController.capturePicture();
break;
case FOCUS:
case FOCUS_WITH_MARKER:
return mCameraController.startAutoFocus(gesture, points[0]);
mCameraController.startAutoFocus(gesture, points[0]);
break;
case ZOOM:
oldValue = mZoomValue;
oldValue = mCameraController.getZoomValue();
newValue = source.scaleValue(oldValue, 0, 1);
if (mCameraController.setZoom(newValue)) {
mZoomValue = newValue;
mCameraCallbacks.dispatchOnZoomChanged(newValue, points);
return true;
}
mCameraController.setZoom(newValue, points, true);
break;
case EXPOSURE_CORRECTION:
oldValue = mExposureCorrectionValue;
oldValue = mCameraController.getExposureCorrectionValue();
float minValue = options.getExposureCorrectionMinValue();
float maxValue = options.getExposureCorrectionMaxValue();
newValue = source.scaleValue(oldValue, minValue, maxValue);
float[] bounds = new float[]{minValue, maxValue};
if (mCameraController.setExposureCorrection(newValue)) {
mExposureCorrectionValue = newValue;
mCameraCallbacks.dispatchOnExposureCorrectionChanged(newValue, bounds, points);
return true;
}
mCameraController.setExposureCorrection(newValue, bounds, points, true);
break;
}
return false;
}
//endregion
@ -640,9 +631,7 @@ public class CameraView extends FrameLayout {
float max = options.getExposureCorrectionMaxValue();
if (EVvalue < min) EVvalue = min;
if (EVvalue > max) EVvalue = max;
if (mCameraController.setExposureCorrection(EVvalue)) {
mExposureCorrectionValue = EVvalue;
}
mCameraController.setExposureCorrection(EVvalue, null, null, false);
}
}
@ -653,7 +642,7 @@ public class CameraView extends FrameLayout {
* @return the current exposure correction value
*/
public float getExposureCorrection() {
return mExposureCorrectionValue;
return mCameraController.getExposureCorrectionValue();
}
@ -670,9 +659,7 @@ public class CameraView extends FrameLayout {
public void setZoom(float zoom) {
if (zoom < 0) zoom = 0;
if (zoom > 1) zoom = 1;
if (mCameraController.setZoom(zoom)) {
mZoomValue = zoom;
}
mCameraController.setZoom(zoom, null, false);
}
@ -681,7 +668,7 @@ public class CameraView extends FrameLayout {
* @return the current zoom value
*/
public float getZoom() {
return mZoomValue;
return mCameraController.getZoomValue();
}
@ -1146,9 +1133,7 @@ public class CameraView extends FrameLayout {
* @see #captureSnapshot()
*/
public void capturePicture() {
if (mCameraController.capturePicture() && mPlaySounds) {
// TODO: playSound on Camera2
}
mCameraController.capturePicture();
}
@ -1163,10 +1148,7 @@ public class CameraView extends FrameLayout {
* @see #capturePicture()
*/
public void captureSnapshot() {
if (mCameraController.captureSnapshot() && mPlaySounds) {
//noinspection all
playSound(MediaActionSound.SHUTTER_CLICK);
}
mCameraController.captureSnapshot();
}
@ -1193,7 +1175,7 @@ public class CameraView extends FrameLayout {
if (file == null) {
file = new File(getContext().getFilesDir(), "video.mp4");
}
if (mCameraController.startVideo(file)) {
mCameraController.startVideo(file);
mUiHandler.post(new Runnable() {
@Override
public void run() {
@ -1202,7 +1184,6 @@ public class CameraView extends FrameLayout {
}
});
}
}
/**
@ -1238,7 +1219,7 @@ public class CameraView extends FrameLayout {
* This will fire {@link CameraListener#onVideoTaken(File)}.
*/
public void stopCapturingVideo() {
if (mCameraController.endVideo()) {
mCameraController.endVideo();
mUiHandler.post(new Runnable() {
@Override
public void run() {
@ -1246,7 +1227,6 @@ public class CameraView extends FrameLayout {
}
});
}
}
/**
@ -1346,6 +1326,7 @@ public class CameraView extends FrameLayout {
void dispatchOnCameraOpened(CameraOptions options);
void dispatchOnCameraClosed();
void onCameraPreviewSizeChanged();
void onShutter(boolean shouldPlaySound);
void processImage(byte[] jpeg, boolean consistentWithView, boolean flipHorizontally);
void processSnapshot(YuvImage image, boolean consistentWithView, boolean flipHorizontally);
void dispatchOnVideoTaken(File file);
@ -1408,6 +1389,13 @@ public class CameraView extends FrameLayout {
});
}
@Override
public void onShutter(boolean shouldPlaySound) {
if (shouldPlaySound && mPlaySounds) {
//noinspection all
playSound(MediaActionSound.SHUTTER_CLICK);
}
}
/**
* What would be great here is to ensure the EXIF tag in the jpeg is consistent with what we expect,

Loading…
Cancel
Save