From bd0a84a65ea338a1f73e7ef522d22cbd8031b77e Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Mon, 13 Nov 2017 13:43:20 +0100 Subject: [PATCH] Make CameraController fully async, fix shutter sounds, tests --- .../cameraview/CameraCallbacksTest.java | 2 + .../cameraview/CameraViewTest.java | 4 +- .../cameraview/MockCameraController.java | 26 ++--- .../com/otaliastudios/cameraview/Camera1.java | 109 +++++++++--------- .../com/otaliastudios/cameraview/Camera2.java | 28 ++--- .../cameraview/CameraController.java | 30 +++-- .../otaliastudios/cameraview/CameraView.java | 88 ++++++-------- 7 files changed, 142 insertions(+), 145 deletions(-) diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraCallbacksTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraCallbacksTest.java index e8d9f83b..eb1ff0ca 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraCallbacksTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraCallbacksTest.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() { diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java index 8afa085c..8eb5ddeb 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java @@ -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); diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java index daae144a..ccbc9eee 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java @@ -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) { - } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java index 64a3afed..9f727797 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java @@ -38,9 +38,9 @@ class Camera1 extends CameraController implements Camera.PreviewCallback { private Runnable mPostFocusResetRunnable = new Runnable() { @Override public void run() { - if (!isCameraAvailable()) return; - mCamera.cancelAutoFocus(); synchronized (mLock) { + if (!isCameraAvailable()) return; + mCamera.cancelAutoFocus(); 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 meteringAreas2 = computeMeteringAreas(p.x, p.y); List meteringAreas1 = meteringAreas2.subList(0, 1); @@ -832,7 +844,6 @@ class Camera1 extends CameraController implements Camera.PreviewCallback { } }); } - return true; } @@ -924,22 +935,14 @@ 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); - } else if (consistent.size() > 0) { - result = Collections.max(consistent); - } else { - result = Collections.max(sizes); - } + if (bigEnoughAndConsistent.size() > 0) { + result = biggestPossible ? + Collections.max(bigEnoughAndConsistent) : + Collections.min(bigEnoughAndConsistent); + } else if (consistent.size() > 0) { + result = Collections.max(consistent); } else { - if (bigEnoughAndConsistent.size() > 0) { - result = Collections.min(bigEnoughAndConsistent); - } else if (consistent.size() > 0) { - result = Collections.max(consistent); - } else { - result = Collections.max(sizes); - } + result = Collections.max(sizes); } LOG.i("size", "matchSize:", "returning result", result); return result; diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera2.java b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera2.java index c03e1423..4d047ea7 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera2.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera2.java @@ -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 diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java index 3a519552..979c8b58 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java @@ -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; } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java index 863fbed9..f7e2ed95 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java @@ -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 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,15 +1175,14 @@ public class CameraView extends FrameLayout { if (file == null) { file = new File(getContext().getFilesDir(), "video.mp4"); } - if (mCameraController.startVideo(file)) { - mUiHandler.post(new Runnable() { - @Override - public void run() { - mKeepScreenOn = getKeepScreenOn(); - if (!mKeepScreenOn) setKeepScreenOn(true); - } - }); - } + mCameraController.startVideo(file); + mUiHandler.post(new Runnable() { + @Override + public void run() { + mKeepScreenOn = getKeepScreenOn(); + if (!mKeepScreenOn) setKeepScreenOn(true); + } + }); } @@ -1238,14 +1219,13 @@ public class CameraView extends FrameLayout { * This will fire {@link CameraListener#onVideoTaken(File)}. */ public void stopCapturingVideo() { - if (mCameraController.endVideo()) { - mUiHandler.post(new Runnable() { - @Override - public void run() { - if (getKeepScreenOn() != mKeepScreenOn) setKeepScreenOn(mKeepScreenOn); - } - }); - } + mCameraController.endVideo(); + mUiHandler.post(new Runnable() { + @Override + public void run() { + if (getKeepScreenOn() != mKeepScreenOn) setKeepScreenOn(mKeepScreenOn); + } + }); } @@ -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,