From 33162f0e3143544227b918edc6690d143fef2469 Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Wed, 15 Nov 2017 17:47:35 +0100 Subject: [PATCH] Improve CameraOptions APIs, cleanup (#101) * Improve CameraOptions APIs, cleanup * Test flip * Remove unwanted API --- .../cameraview/CameraOptions1Test.java | 96 +++++++++++-- .../cameraview/CameraViewCallbacksTest.java | 25 +--- .../cameraview/MockCameraController.java | 9 +- .../cameraview/OrientationHelperTest.java | 48 +++---- .../com/otaliastudios/cameraview/Camera1.java | 122 +++++------------ .../com/otaliastudios/cameraview/Camera2.java | 5 - .../cameraview/CameraController.java | 126 +++++++++++------- .../cameraview/CameraOptions.java | 32 ++++- .../otaliastudios/cameraview/CameraView.java | 34 +---- .../com/otaliastudios/cameraview/Size.java | 13 +- .../cameraview/OrientationHelper.java | 62 ++++----- .../otaliastudios/cameraview/SizeTest.java | 6 +- 12 files changed, 288 insertions(+), 290 deletions(-) diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraOptions1Test.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraOptions1Test.java index 02f8816b..402a47da 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraOptions1Test.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraOptions1Test.java @@ -4,8 +4,6 @@ package com.otaliastudios.cameraview; import android.hardware.Camera; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; -import android.test.ActivityInstrumentationTestCase2; -import android.test.InstrumentationTestCase; import org.junit.Test; import org.junit.runner.RunWith; @@ -16,8 +14,11 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -import static org.mockito.Mockito.*; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; @RunWith(AndroidJUnit4.class) @SmallTest @@ -25,7 +26,9 @@ public class CameraOptions1Test extends BaseTest { @Test public void testEmpty() { - CameraOptions o = new CameraOptions(mock(Camera.Parameters.class)); + CameraOptions o = new CameraOptions(mock(Camera.Parameters.class), false); + assertTrue(o.getSupportedPictureAspectRatios().isEmpty()); + assertTrue(o.getSupportedPictureSizes().isEmpty()); assertTrue(o.getSupportedWhiteBalance().isEmpty()); assertTrue(o.getSupportedFlash().isEmpty()); assertTrue(o.getSupportedHdr().isEmpty()); @@ -37,6 +40,75 @@ public class CameraOptions1Test extends BaseTest { assertEquals(o.getExposureCorrectionMinValue(), 0f, 0); } + private Camera.Size mockCameraSize(int width, int height) { + Camera.Size cs = mock(Camera.Size.class); + cs.width = width; + cs.height = height; + return cs; + } + + @Test + public void testPictureSizes() { + List sizes = Arrays.asList( + mockCameraSize(100, 200), + mockCameraSize(50, 50), + mockCameraSize(1600, 900), + mockCameraSize(1000, 2000) + ); + Camera.Parameters params = mock(Camera.Parameters.class); + when(params.getSupportedPictureSizes()).thenReturn(sizes); + CameraOptions o = new CameraOptions(params, false); + Set supportedSizes = o.getSupportedPictureSizes(); + assertEquals(supportedSizes.size(), sizes.size()); + for (Camera.Size size : sizes) { + Size internalSize = new Size(size.width, size.height); + assertTrue(supportedSizes.contains(internalSize)); + } + } + + @Test + public void testPictureSizesFlip() { + List sizes = Arrays.asList( + mockCameraSize(100, 200), + mockCameraSize(50, 50), + mockCameraSize(1600, 900), + mockCameraSize(1000, 2000) + ); + Camera.Parameters params = mock(Camera.Parameters.class); + when(params.getSupportedPictureSizes()).thenReturn(sizes); + CameraOptions o = new CameraOptions(params, true); + Set supportedSizes = o.getSupportedPictureSizes(); + assertEquals(supportedSizes.size(), sizes.size()); + for (Camera.Size size : sizes) { + Size internalSize = new Size(size.width, size.height).flip(); + assertTrue(supportedSizes.contains(internalSize)); + } + } + + @Test + public void testPictureAspectRatio() { + List sizes = Arrays.asList( + mockCameraSize(100, 200), + mockCameraSize(50, 50), + mockCameraSize(1600, 900), + mockCameraSize(1000, 2000) + ); + + Set expected = new HashSet<>(); + expected.add(AspectRatio.of(1, 2)); + expected.add(AspectRatio.of(1, 1)); + expected.add(AspectRatio.of(16, 9)); + + Camera.Parameters params = mock(Camera.Parameters.class); + when(params.getSupportedPictureSizes()).thenReturn(sizes); + CameraOptions o = new CameraOptions(params, false); + Set supportedRatios = o.getSupportedPictureAspectRatios(); + assertEquals(supportedRatios.size(), expected.size()); + for (AspectRatio ratio : expected) { + assertTrue(supportedRatios.contains(ratio)); + } + } + @Test public void testFacing() { Set supported = new HashSet<>(); @@ -46,7 +118,7 @@ public class CameraOptions1Test extends BaseTest { supported.add(cameraInfo.facing); } - CameraOptions o = new CameraOptions(mock(Camera.Parameters.class)); + CameraOptions o = new CameraOptions(mock(Camera.Parameters.class), false); Mapper m = new Mapper.Mapper1(); Set s = o.getSupportedFacing(); assertEquals(o.getSupportedFacing().size(), supported.size()); @@ -64,7 +136,7 @@ public class CameraOptions1Test extends BaseTest { when(params.getMaxExposureCompensation()).thenReturn(0); when(params.getMinExposureCompensation()).thenReturn(0); - CameraOptions o = new CameraOptions(params); + CameraOptions o = new CameraOptions(params, false); assertFalse(o.supports(GestureAction.FOCUS)); assertFalse(o.supports(GestureAction.FOCUS_WITH_MARKER)); assertTrue(o.supports(GestureAction.CAPTURE)); @@ -82,7 +154,7 @@ public class CameraOptions1Test extends BaseTest { Camera.Parameters.WHITE_BALANCE_SHADE // Not supported )); - CameraOptions o = new CameraOptions(params); + CameraOptions o = new CameraOptions(params, false); assertEquals(o.getSupportedWhiteBalance().size(), 2); assertTrue(o.getSupportedWhiteBalance().contains(WhiteBalance.AUTO)); assertTrue(o.getSupportedWhiteBalance().contains(WhiteBalance.CLOUDY)); @@ -99,7 +171,7 @@ public class CameraOptions1Test extends BaseTest { Camera.Parameters.FLASH_MODE_RED_EYE // Not supported )); - CameraOptions o = new CameraOptions(params); + CameraOptions o = new CameraOptions(params, false); assertEquals(o.getSupportedFlash().size(), 2); assertTrue(o.getSupportedFlash().contains(Flash.AUTO)); assertTrue(o.getSupportedFlash().contains(Flash.TORCH)); @@ -116,7 +188,7 @@ public class CameraOptions1Test extends BaseTest { Camera.Parameters.SCENE_MODE_FIREWORKS // Not supported )); - CameraOptions o = new CameraOptions(params); + CameraOptions o = new CameraOptions(params, false); assertEquals(o.getSupportedHdr().size(), 2); assertTrue(o.getSupportedHdr().contains(Hdr.OFF)); assertTrue(o.getSupportedHdr().contains(Hdr.ON)); @@ -130,7 +202,7 @@ public class CameraOptions1Test extends BaseTest { when(params.isVideoSnapshotSupported()).thenReturn(true); when(params.isZoomSupported()).thenReturn(true); when(params.getSupportedFocusModes()).thenReturn(Arrays.asList(Camera.Parameters.FOCUS_MODE_AUTO)); - CameraOptions o = new CameraOptions(params); + CameraOptions o = new CameraOptions(params, false); assertTrue(o.isVideoSnapshotSupported()); assertTrue(o.isZoomSupported()); assertTrue(o.isAutoFocusSupported()); @@ -142,7 +214,7 @@ public class CameraOptions1Test extends BaseTest { when(params.getMaxExposureCompensation()).thenReturn(10); when(params.getMinExposureCompensation()).thenReturn(-10); when(params.getExposureCompensationStep()).thenReturn(0.5f); - CameraOptions o = new CameraOptions(params); + CameraOptions o = new CameraOptions(params, false); assertTrue(o.isExposureCorrectionSupported()); assertEquals(o.getExposureCorrectionMinValue(), -10f * 0.5f, 0f); assertEquals(o.getExposureCorrectionMaxValue(), 10f * 0.5f, 0f); diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewCallbacksTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewCallbacksTest.java index 894a6501..ddf91bf8 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewCallbacksTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewCallbacksTest.java @@ -194,31 +194,8 @@ public class CameraViewCallbacksTest extends BaseTest { } @Test - public void testOrientationCallbacks_deviceOnly() { + public void testOrientationCallbacks() { completeTask().when(listener).onOrientationChanged(anyInt()); - - // Assert not called. Both methods must be called. - camera.mCameraCallbacks.onDeviceOrientationChanged(0); - assertNull(task.await(200)); - verify(listener, never()).onOrientationChanged(anyInt()); - } - - @Test - public void testOrientationCallbacks_displayOnly() { - completeTask().when(listener).onOrientationChanged(anyInt()); - - // Assert not called. Both methods must be called. - camera.mCameraCallbacks.onDisplayOffsetChanged(0); - assertNull(task.await(200)); - verify(listener, never()).onOrientationChanged(anyInt()); - } - - @Test - public void testOrientationCallbacks_both() { - completeTask().when(listener).onOrientationChanged(anyInt()); - - // Assert called. - camera.mCameraCallbacks.onDisplayOffsetChanged(0); camera.mCameraCallbacks.onDeviceOrientationChanged(90); assertNotNull(task.await(200)); verify(listener, times(1)).onOrientationChanged(anyInt()); diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java index d777e45a..cc3026dd 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java @@ -20,7 +20,7 @@ public class MockCameraController extends CameraController { } void setMockCameraOptions(CameraOptions options) { - mOptions = options; + mCameraOptions = options; } void setMockPreviewSize(Size size) { @@ -108,13 +108,6 @@ public class MockCameraController extends CameraController { void endVideo() { } - @Override - boolean shouldFlipSizes() { - return false; - } - - - @Override void startAutoFocus(@Nullable Gesture gesture, PointF point) { mFocusStarted = true; diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/OrientationHelperTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/OrientationHelperTest.java index 36db45a0..d2e0cea4 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/OrientationHelperTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/OrientationHelperTest.java @@ -1,10 +1,6 @@ package com.otaliastudios.cameraview; -import android.annotation.TargetApi; -import android.app.Instrumentation; -import android.app.UiAutomation; -import android.support.test.InstrumentationRegistry; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; import android.view.OrientationEventListener; @@ -22,44 +18,44 @@ import static org.mockito.Mockito.*; public class OrientationHelperTest extends BaseTest { private OrientationHelper helper; - private OrientationHelper.Callbacks callbacks; + private OrientationHelper.Callback callback; @Before public void setUp() { ui(new Runnable() { @Override public void run() { - callbacks = mock(OrientationHelper.Callbacks.class); - helper = new OrientationHelper(context(), callbacks); + callback = mock(OrientationHelper.Callback.class); + helper = new OrientationHelper(context(), callback); } }); } @After public void tearDown() { - callbacks = null; + callback = null; helper = null; } @Test public void testEnable() { assertNotNull(helper.mListener); - assertNull(helper.mDisplay); + assertEquals(helper.getDisplayOffset(), -1); + assertEquals(helper.getDeviceOrientation(), -1); helper.enable(context()); assertNotNull(helper.mListener); - assertNotNull(helper.mDisplay); + assertNotEquals(helper.getDisplayOffset(), -1); // Don't know about device orientation. // Ensure nothing bad if called twice. helper.enable(context()); assertNotNull(helper.mListener); - assertNotNull(helper.mDisplay); + assertNotEquals(helper.getDisplayOffset(), -1); helper.disable(); assertNotNull(helper.mListener); - assertNull(helper.mDisplay); - - verify(callbacks, atLeastOnce()).onDisplayOffsetChanged(anyInt()); + assertEquals(helper.getDisplayOffset(), -1); + assertEquals(helper.getDeviceOrientation(), -1); } @Test @@ -69,30 +65,30 @@ public class OrientationHelperTest extends BaseTest { // right after enabling. But that's fine for us, times(1) will be OK either way. helper.enable(context()); helper.mListener.onOrientationChanged(OrientationEventListener.ORIENTATION_UNKNOWN); - assertEquals(helper.mLastOrientation, 0); + assertEquals(helper.getDeviceOrientation(), 0); helper.mListener.onOrientationChanged(10); - assertEquals(helper.mLastOrientation, 0); + assertEquals(helper.getDeviceOrientation(), 0); helper.mListener.onOrientationChanged(-10); - assertEquals(helper.mLastOrientation, 0); + assertEquals(helper.getDeviceOrientation(), 0); helper.mListener.onOrientationChanged(44); - assertEquals(helper.mLastOrientation, 0); + assertEquals(helper.getDeviceOrientation(), 0); helper.mListener.onOrientationChanged(360); - assertEquals(helper.mLastOrientation, 0); + assertEquals(helper.getDeviceOrientation(), 0); // Callback called just once. - verify(callbacks, times(1)).onDeviceOrientationChanged(0); + verify(callback, times(1)).onDeviceOrientationChanged(0); helper.mListener.onOrientationChanged(90); helper.mListener.onOrientationChanged(91); - assertEquals(helper.mLastOrientation, 90); - verify(callbacks, times(1)).onDeviceOrientationChanged(90); + assertEquals(helper.getDeviceOrientation(), 90); + verify(callback, times(1)).onDeviceOrientationChanged(90); helper.mListener.onOrientationChanged(180); - assertEquals(helper.mLastOrientation, 180); - verify(callbacks, times(1)).onDeviceOrientationChanged(180); + assertEquals(helper.getDeviceOrientation(), 180); + verify(callback, times(1)).onDeviceOrientationChanged(180); helper.mListener.onOrientationChanged(270); - assertEquals(helper.mLastOrientation, 270); - verify(callbacks, times(1)).onDeviceOrientationChanged(270); + assertEquals(helper.getDeviceOrientation(), 270); + verify(callback, times(1)).onDeviceOrientationChanged(270); } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java index e3199eee..2c4e1a17 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java @@ -9,7 +9,6 @@ import android.hardware.Camera; import android.location.Location; import android.media.CamcorderProfile; import android.media.MediaRecorder; -import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.WorkerThread; @@ -18,7 +17,6 @@ import android.view.SurfaceHolder; import java.io.File; import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; import java.util.List; @@ -29,10 +27,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera private static final CameraLogger LOG = CameraLogger.create(TAG); private Camera mCamera; - private MediaRecorder mMediaRecorder; - private File mVideoFile; - - private int mSensorOffset; + private boolean mIsBound = false; private final int mPostFocusResetDelay = 3000; private Runnable mPostFocusResetRunnable = new Runnable() { @@ -48,11 +43,6 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera } }; - private Mapper mMapper; - private boolean mIsBound = false; - private boolean mIsCapturingImage = false; - private boolean mIsCapturingVideo = false; - Camera1(CameraView.CameraCallbacks callback) { super(callback); mMapper = new Mapper.Mapper1(); @@ -135,7 +125,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera throw new CameraException(e); } - mPictureSize = computePictureSize(sizesFromList(mCamera.getParameters().getSupportedPictureSizes())); + mPictureSize = computePictureSize(); mPreviewSize = computePreviewSize(sizesFromList(mCamera.getParameters().getSupportedPreviewSizes())); applySizesAndStartPreview("bindToSurface:"); mIsBound = true; @@ -182,7 +172,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera LOG.i("onStart:", "Applying default parameters."); Camera.Parameters params = mCamera.getParameters(); mExtraProperties = new ExtraProperties(params); - mOptions = new CameraOptions(params); + mCameraOptions = new CameraOptions(params, shouldFlipSizes()); applyDefaultFocus(params); mergeFlash(params, Flash.DEFAULT); mergeLocation(params, null); @@ -192,7 +182,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera mCamera.setParameters(params); // Try starting preview. - mCamera.setDisplayOrientation(computeSensorToDisplayOffset()); // <- not allowed during preview + mCamera.setDisplayOrientation(computeSensorToViewOffset()); // <- not allowed during preview if (shouldBindToSurface()) bindToSurface(); LOG.i("onStart:", "Ended"); } @@ -230,7 +220,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera } } mExtraProperties = null; - mOptions = null; + mCameraOptions = null; mCamera = null; mPreviewSize = null; mPictureSize = null; @@ -346,7 +336,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera } private boolean mergeWhiteBalance(Camera.Parameters params, WhiteBalance oldWhiteBalance) { - if (mOptions.supports(mWhiteBalance)) { + if (mCameraOptions.supports(mWhiteBalance)) { params.setWhiteBalance((String) mMapper.map(mWhiteBalance)); return true; } @@ -368,7 +358,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera } private boolean mergeHdr(Camera.Parameters params, Hdr oldHdr) { - if (mOptions.supports(mHdr)) { + if (mCameraOptions.supports(mHdr)) { params.setSceneMode((String) mMapper.map(mHdr)); return true; } @@ -403,7 +393,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera private boolean mergeFlash(Camera.Parameters params, Flash oldFlash) { - if (mOptions.supports(mFlash)) { + if (mCameraOptions.supports(mFlash)) { params.setFlashMode((String) mMapper.map(mFlash)); return true; } @@ -456,7 +446,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera if (mSessionType == SessionType.VIDEO) { // Change capture size to a size that fits the video aspect ratio. Size oldSize = mPictureSize; - mPictureSize = computePictureSize(sizesFromList(mCamera.getParameters().getSupportedPictureSizes())); + mPictureSize = computePictureSize(); if (!mPictureSize.equals(oldSize)) { // New video quality triggers a new aspect ratio. // Go on and see if preview size should change also. @@ -480,20 +470,16 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera public void run() { LOG.v("capturePicture: performing.", mIsCapturingImage); if (mIsCapturingImage) return; - if (mIsCapturingVideo && !mOptions.isVideoSnapshotSupported()) return; + if (mIsCapturingVideo && !mCameraOptions.isVideoSnapshotSupported()) return; mIsCapturingImage = true; - final int exifRotation = computeExifRotation(); - final boolean exifFlip = computeExifFlip(); - final int sensorToDisplay = computeSensorToDisplayOffset(); + final int sensorToOutput = computeSensorToOutputOffset(); + final int sensorToView = computeSensorToViewOffset(); + final boolean outputMatchesView = (sensorToOutput + sensorToView + 180) % 180 == 0; + final boolean outputFlip = mFacing == Facing.FRONT; Camera.Parameters params = mCamera.getParameters(); - params.setRotation(exifRotation); + params.setRotation(sensorToOutput); 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( new Camera.ShutterCallback() { @Override @@ -507,7 +493,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera @Override public void onPictureTaken(byte[] data, final Camera camera) { mIsCapturingImage = false; - mCameraCallbacks.processImage(data, consistentWithView, exifFlip); + mCameraCallbacks.processImage(data, outputMatchesView, outputFlip); camera.startPreview(); // This is needed, read somewhere in the docs. } } @@ -540,10 +526,11 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, 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. - final int sensorToDevice = computeExifRotation(); - final int sensorToDisplay = computeSensorToDisplayOffset(); - final boolean exifFlip = computeExifFlip(); - final boolean flip = sensorToDevice % 180 != 0; + final int sensorToOutput = computeSensorToOutputOffset(); + final int sensorToView = computeSensorToViewOffset(); + final boolean outputMatchesView = (sensorToOutput + sensorToView + 180) % 180 == 0; + final boolean outputFlip = mFacing == Facing.FRONT; + final boolean flip = sensorToOutput % 180 != 0; final int preWidth = mPreviewSize.getWidth(); final int preHeight = mPreviewSize.getHeight(); final int postWidth = flip ? preHeight : preWidth; @@ -554,11 +541,10 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera public void run() { LOG.v("captureSnapshot: rotating."); - final boolean consistentWithView = (sensorToDevice + sensorToDisplay + 180) % 180 == 0; - byte[] rotatedData = RotationHelper.rotate(data, preWidth, preHeight, sensorToDevice); + byte[] rotatedData = RotationHelper.rotate(data, preWidth, preHeight, sensorToOutput); LOG.v("captureSnapshot: rotated."); YuvImage yuv = new YuvImage(rotatedData, format, postWidth, postHeight, null); - mCameraCallbacks.processSnapshot(yuv, consistentWithView, exifFlip); + mCameraCallbacks.processSnapshot(yuv, outputMatchesView, outputFlip); mIsCapturingImage = false; } }); @@ -577,20 +563,12 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera public void onPreviewFrame(byte[] data, Camera camera) { Frame frame = mFrameManager.getFrame(data, System.currentTimeMillis(), - computeExifRotation(), + computeSensorToOutputOffset(), mPreviewSize, mPreviewFormat); mCameraCallbacks.dispatchFrame(frame); } - @Override - boolean shouldFlipSizes() { - int offset = computeSensorToDisplayOffset(); - LOG.i("shouldFlipSizes:", "mDeviceOrientation=", mDeviceOrientation, "mSensorOffset=", mSensorOffset); - LOG.i("shouldFlipSizes:", "sensorToDisplay=", offset); - return offset % 180 != 0; - } - private boolean isCameraAvailable() { switch (mState) { // If we are stopped, don't. @@ -607,44 +585,6 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera return false; } - - // Internal: - - - /** - * Returns how much should the sensor image be rotated before being shown. - * It is meant to be fed to Camera.setDisplayOrientation(). - */ - private int computeSensorToDisplayOffset() { - if (mFacing == Facing.FRONT) { - // or: (360 - ((mSensorOffset + mDisplayOffset) % 360)) % 360; - return ((mSensorOffset - mDisplayOffset) + 360 + 180) % 360; - } else { - return (mSensorOffset - mDisplayOffset + 360) % 360; - } - } - - /** - * Returns the orientation to be set as a exif tag. This is already managed by - * the camera APIs as long as you call {@link Camera.Parameters#setRotation(int)}. - * This ignores flipping for facing camera. - */ - private int computeExifRotation() { - if (mFacing == Facing.FRONT) { - return (mSensorOffset - mDeviceOrientation + 360) % 360; - } else { - return (mSensorOffset + mDeviceOrientation) % 360; - } - } - - /** - * Whether the exif tag should include a 'flip' operation. - */ - private boolean computeExifFlip() { - return mFacing == Facing.FRONT; - } - - // ----------------- // Video recording stuff. @@ -735,7 +675,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera } mMediaRecorder.setOutputFile(mVideoFile.getAbsolutePath()); - mMediaRecorder.setOrientationHint(computeExifRotation()); + mMediaRecorder.setOrientationHint(computeSensorToOutputOffset()); // Not needed. mMediaRecorder.setPreviewDisplay(mPreview.getSurface()); } @@ -748,7 +688,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera schedule(mZoomTask, true, new Runnable() { @Override public void run() { - if (!mOptions.isZoomSupported()) return; + if (!mCameraOptions.isZoomSupported()) return; mZoomValue = zoom; Camera.Parameters params = mCamera.getParameters(); @@ -769,11 +709,11 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera schedule(mExposureCorrectionTask, true, new Runnable() { @Override public void run() { - if (!mOptions.isExposureCorrectionSupported()) return; + if (!mCameraOptions.isExposureCorrectionSupported()) return; float value = EVvalue; - float max = mOptions.getExposureCorrectionMaxValue(); - float min = mOptions.getExposureCorrectionMinValue(); + float max = mCameraOptions.getExposureCorrectionMaxValue(); + float min = mCameraOptions.getExposureCorrectionMinValue(); value = value < min ? min : value > max ? max : value; // cap mExposureCorrectionValue = value; Camera.Parameters params = mCamera.getParameters(); @@ -806,10 +746,10 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera schedule(null, true, new Runnable() { @Override public void run() { - if (!mOptions.isAutoFocusSupported()) return; + if (!mCameraOptions.isAutoFocusSupported()) return; final PointF p = new PointF(point.x, point.y); // copy. List meteringAreas2 = computeMeteringAreas(p.x, p.y, - viewWidthF, viewHeightF, computeSensorToDisplayOffset()); + viewWidthF, viewHeightF, computeSensorToViewOffset()); List meteringAreas1 = meteringAreas2.subList(0, 1); // At this point we are sure that camera supports auto focus... right? Look at CameraView.onTouchEvent(). diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera2.java b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera2.java index 67f4637b..48bed418 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera2.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera2.java @@ -116,11 +116,6 @@ class Camera2 extends CameraController { } - @Override - boolean shouldFlipSizes() { - return false; - } - @Override void startAutoFocus(@Nullable Gesture gesture, PointF point) { diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java index 4fc29f8b..a5d80006 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java @@ -5,6 +5,7 @@ import android.location.Location; import android.media.CamcorderProfile; +import android.media.MediaRecorder; import android.os.Build; import android.os.Handler; import android.os.Looper; @@ -13,6 +14,7 @@ import android.support.annotation.Nullable; import android.support.annotation.WorkerThread; import java.io.File; +import java.util.ArrayList; import java.util.List; abstract class CameraController implements @@ -32,7 +34,6 @@ abstract class CameraController implements protected CameraPreview mPreview; protected WorkerHandler mHandler; /* for tests */ Handler mCrashHandler; - protected int mCameraId; protected Facing mFacing; protected Flash mFlash; @@ -42,21 +43,28 @@ abstract class CameraController implements protected Hdr mHdr; protected Location mLocation; protected Audio mAudio; - protected float mZoomValue; protected float mExposureCorrectionValue; + protected int mCameraId; + protected ExtraProperties mExtraProperties; + protected CameraOptions mCameraOptions; + protected Mapper mMapper; + protected FrameManager mFrameManager; + protected SizeSelector mPictureSizeSelector; + protected MediaRecorder mMediaRecorder; + protected File mVideoFile; protected Size mPictureSize; protected Size mPreviewSize; protected int mPreviewFormat; - protected ExtraProperties mExtraProperties; - protected CameraOptions mOptions; - protected FrameManager mFrameManager; - protected SizeSelector mPictureSizeSelector; + protected int mSensorOffset; + private int mDisplayOffset; + private int mDeviceOrientation; + + protected boolean mIsCapturingImage = false; + protected boolean mIsCapturingVideo = false; - protected int mDisplayOffset; - protected int mDeviceOrientation; protected int mState = STATE_STOPPED; // Used for testing. @@ -167,7 +175,7 @@ abstract class CameraController implements onStart(); LOG.i("Start:", "returned from onStart().", "Dispatching.", ss()); mState = STATE_STARTED; - mCameraCallbacks.dispatchOnCameraOpened(mOptions); + mCameraCallbacks.dispatchOnCameraOpened(mCameraOptions); } }); } @@ -228,7 +236,7 @@ abstract class CameraController implements onStart(); mState = STATE_STARTED; LOG.i("Restart: returned from start. Dispatching. State:", ss()); - mCameraCallbacks.dispatchOnCameraOpened(mOptions); + mCameraCallbacks.dispatchOnCameraOpened(mCameraOptions); } }); } @@ -251,22 +259,23 @@ abstract class CameraController implements //region Simple setters - void onDisplayOffset(int displayOrientation) { - // I doubt this will ever change. - mDisplayOffset = displayOrientation; + // This is called before start() and never again. + final void setDisplayOffset(int displayOffset) { + mDisplayOffset = displayOffset; } - void onDeviceOrientation(int deviceOrientation) { + // This can be called multiple times. + final void setDeviceOrientation(int deviceOrientation) { mDeviceOrientation = deviceOrientation; } - void setPictureSizeSelector(SizeSelector selector) { + final void setPictureSizeSelector(SizeSelector selector) { mPictureSizeSelector = selector; } //endregion - //region Abstract setters + //region Abstract setters and APIs // Should restart the session if active. abstract void setSessionType(SessionType sessionType); @@ -298,11 +307,6 @@ abstract class CameraController implements // Throw if capturing. If in video session, recompute capture size, and, if needed, preview size. abstract void setVideoQuality(VideoQuality videoQuality); - - //endregion - - //region APIs - abstract void capturePicture(); abstract void captureSnapshot(); @@ -311,8 +315,6 @@ abstract class CameraController implements abstract void endVideo(); - abstract boolean shouldFlipSizes(); // Wheter the Sizes should be flipped to match the view orientation. - abstract void startAutoFocus(@Nullable Gesture gesture, PointF point); //endregion @@ -326,7 +328,7 @@ abstract class CameraController implements @Nullable final CameraOptions getCameraOptions() { - return mOptions; + return mCameraOptions; } final Facing getFacing() { @@ -383,6 +385,47 @@ abstract class CameraController implements //endregion + //region Orientation utils + + /** + * The result of this should not change after start() is called: the sensor offset is the same, + * and the display offset does not change. + */ + final boolean shouldFlipSizes() { + int offset = computeSensorToViewOffset(); + LOG.i("shouldFlipSizes:", "displayOffset=", mDisplayOffset, "sensorOffset=", mSensorOffset); + LOG.i("shouldFlipSizes:", "sensorToDisplay=", offset); + return offset % 180 != 0; + } + + /** + * Returns how much should the sensor image be rotated before being shown. + * It is meant to be fed to Camera.setDisplayOrientation(). + * The result of this should not change after start() is called: the sensor offset is the same, + * and the display offset does not change. + */ + protected final int computeSensorToViewOffset() { + if (mFacing == Facing.FRONT) { + // or: (360 - ((mSensorOffset + mDisplayOffset) % 360)) % 360; + return ((mSensorOffset - mDisplayOffset) + 360 + 180) % 360; + } else { + return (mSensorOffset - mDisplayOffset + 360) % 360; + } + } + + /** + * Returns the orientation to be set as a exif tag. + */ + protected final int computeSensorToOutputOffset() { + if (mFacing == Facing.FRONT) { + return (mSensorOffset - mDeviceOrientation + 360) % 360; + } else { + return (mSensorOffset + mDeviceOrientation) % 360; + } + } + + //endregion + //region Size utils /** @@ -392,24 +435,14 @@ abstract class CameraController implements * But when it does, the {@link CameraPreview.SurfaceCallback} should be called, * and this should be refreshed. */ - protected Size computePictureSize(List captureSizes) { - SizeSelector selector; - + protected final Size computePictureSize() { // The external selector is expecting stuff in the view world, not in the sensor world. - // Flip before starting, and then flip again. + // Use the list in the camera options, then flip the result if needed. boolean flip = shouldFlipSizes(); - LOG.i("computePictureSize:", "flip:", flip); - if (flip) { - for (Size size : captureSizes) { - size.flip(); - } - } + SizeSelector selector; if (mSessionType == SessionType.PICTURE) { - selector = SizeSelectors.or( - mPictureSizeSelector, - SizeSelectors.biggest() // Fallback to biggest. - ); + selector = SizeSelectors.or(mPictureSizeSelector, SizeSelectors.biggest()); } else { // The Camcorder internally checks for cameraParameters.getSupportedVideoSizes() etc. // And we want the picture size to be the biggest picture consistent with the video aspect ratio. @@ -426,19 +459,20 @@ abstract class CameraController implements ); } - Size result = selector.select(captureSizes).get(0); - LOG.i("computePictureSize:", "result:", result); - if (flip) result.flip(); + List list = new ArrayList<>(mCameraOptions.getSupportedPictureSizes()); + Size result = selector.select(list).get(0); + LOG.i("computePictureSize:", "result:", result, "flip:", flip); + if (flip) result = result.flip(); return result; } - protected Size computePreviewSize(List previewSizes) { + protected final Size computePreviewSize(List previewSizes) { // instead of flipping everything to the view world, we can just flip the // surface size to the sensor world boolean flip = shouldFlipSizes(); AspectRatio targetRatio = AspectRatio.of(mPictureSize.getWidth(), mPictureSize.getHeight()); Size targetMinSize = mPreview.getSurfaceSize(); - if (flip) targetMinSize.flip(); + if (flip) targetMinSize = targetMinSize.flip(); LOG.i("size:", "computePreviewSize:", "targetRatio:", targetRatio, "targetMinSize:", targetMinSize); SizeSelector matchRatio = SizeSelectors.aspectRatio(targetRatio, 0); SizeSelector matchSize = SizeSelectors.and( @@ -450,14 +484,12 @@ abstract class CameraController implements SizeSelectors.biggest() // If couldn't match any, take the biggest. ); Size result = matchAll.select(previewSizes).get(0); - LOG.i("computePreviewSize:", "result:", result); - // Flip back what we flipped. - if (flip) targetMinSize.flip(); + LOG.i("computePreviewSize:", "result:", result, "flip:", flip); return result; } @NonNull - protected CamcorderProfile getCamcorderProfile() { + protected final CamcorderProfile getCamcorderProfile() { switch (mVideoQuality) { case HIGHEST: return CamcorderProfile.get(mCameraId, CamcorderProfile.QUALITY_HIGH); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java index 352d912d..6ef68ed0 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java @@ -20,6 +20,8 @@ public class CameraOptions { private Set supportedFacing = new HashSet<>(2); private Set supportedFlash = new HashSet<>(4); private Set supportedHdr = new HashSet<>(2); + private Set supportedPictureSizes = new HashSet<>(15); + private Set supportedPictureAspectRatio = new HashSet<>(4); private boolean zoomSupported; private boolean videoSnapshotSupported; @@ -31,7 +33,7 @@ public class CameraOptions { // Camera1 constructor. @SuppressWarnings("deprecation") - CameraOptions(Camera.Parameters params) { + CameraOptions(Camera.Parameters params, boolean flipSizes) { List strings; Mapper mapper = new Mapper.Mapper1(); @@ -80,6 +82,15 @@ public class CameraOptions { exposureCorrectionMaxValue = (float) params.getMaxExposureCompensation() * step; exposureCorrectionSupported = params.getMinExposureCompensation() != 0 || params.getMaxExposureCompensation() != 0; + + // Sizes + List sizes = params.getSupportedPictureSizes(); + for (Camera.Size size : sizes) { + int width = flipSizes ? size.height : size.width; + int height = flipSizes ? size.width : size.height; + supportedPictureSizes.add(new Size(width, height)); + supportedPictureAspectRatio.add(AspectRatio.of(width, height)); + } } @@ -155,6 +166,25 @@ public class CameraOptions { return false; } + /** + * Set of supported picture sizes for the currently opened camera. + * + * @return a set of supported values. + */ + @NonNull + public Set getSupportedPictureSizes() { + return Collections.unmodifiableSet(supportedPictureSizes); + } + + /** + * Set of supported picture aspect ratios for the currently opened camera. + * + * @return a set of supported values. + */ + @NonNull + public Set getSupportedPictureAspectRatios() { + return Collections.unmodifiableSet(supportedPictureAspectRatio); + } /** * Set of supported facing values. diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java index 2ff7a8bf..0fd9da39 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java @@ -537,6 +537,7 @@ public class CameraView extends FrameLayout { if (checkPermissions(getSessionType(), getAudio())) { // Update display orientation for current CameraController mOrientationHelper.enable(getContext()); + mCameraController.setDisplayOffset(mOrientationHelper.getDisplayOffset()); mCameraController.start(); } } @@ -1338,7 +1339,7 @@ public class CameraView extends FrameLayout { //region Callbacks and dispatching - interface CameraCallbacks extends OrientationHelper.Callbacks { + interface CameraCallbacks extends OrientationHelper.Callback { void dispatchOnCameraOpened(CameraOptions options); void dispatchOnCameraClosed(); void onCameraPreviewSizeChanged(); @@ -1356,13 +1357,8 @@ public class CameraView extends FrameLayout { private class Callbacks implements CameraCallbacks { - // Outer listeners private CameraLogger mLogger = CameraLogger.create(CameraCallbacks.class.getSimpleName()); - // Orientation TODO: move this logic into OrientationHelper - private Integer mDisplayOffset; - private Integer mDeviceOrientation; - Callbacks() {} @Override @@ -1477,7 +1473,6 @@ public class CameraView extends FrameLayout { }); } - private void dispatchOnPictureTaken(byte[] jpeg) { mLogger.i("dispatchOnPictureTaken"); final byte[] data = jpeg; @@ -1544,31 +1539,12 @@ public class CameraView extends FrameLayout { }); } - @Override - public void onDisplayOffsetChanged(int displayOffset) { - mLogger.i("onDisplayOffsetChanged", displayOffset); - mCameraController.onDisplayOffset(displayOffset); - mDisplayOffset = displayOffset; - if (mDeviceOrientation != null) { - int value = (mDeviceOrientation + mDisplayOffset) % 360; - dispatchOnOrientationChanged(value); - } - } - @Override public void onDeviceOrientationChanged(int deviceOrientation) { mLogger.i("onDeviceOrientationChanged", deviceOrientation); - mCameraController.onDeviceOrientation(deviceOrientation); - mDeviceOrientation = deviceOrientation; - if (mDisplayOffset != null) { - int value = (mDeviceOrientation + mDisplayOffset) % 360; - dispatchOnOrientationChanged(value); - } - } - - - private void dispatchOnOrientationChanged(final int value) { - mLogger.i("dispatchOnOrientationChanged", value); + mCameraController.setDeviceOrientation(deviceOrientation); + int displayOffset = mOrientationHelper.getDisplayOffset(); + final int value = (deviceOrientation + displayOffset) % 360; mUiHandler.post(new Runnable() { @Override public void run() { diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/Size.java b/cameraview/src/main/java/com/otaliastudios/cameraview/Size.java index 79be98d9..2bb71d33 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/Size.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/Size.java @@ -7,8 +7,8 @@ import android.support.annotation.NonNull; */ public class Size implements Comparable { - private int mWidth; - private int mHeight; + private final int mWidth; + private final int mHeight; Size(int width, int height) { mWidth = width; @@ -23,14 +23,9 @@ public class Size implements Comparable { return mHeight; } - /** - * Flips width and height altogether. - */ @SuppressWarnings("SuspiciousNameCombination") - public void flip() { - int temp = mWidth; - mWidth = mHeight; - mHeight = temp; + Size flip() { + return new Size(mHeight, mWidth); } @Override diff --git a/cameraview/src/main/utils/com/otaliastudios/cameraview/OrientationHelper.java b/cameraview/src/main/utils/com/otaliastudios/cameraview/OrientationHelper.java index ba63b7f2..99c2cb17 100644 --- a/cameraview/src/main/utils/com/otaliastudios/cameraview/OrientationHelper.java +++ b/cameraview/src/main/utils/com/otaliastudios/cameraview/OrientationHelper.java @@ -3,7 +3,6 @@ package com.otaliastudios.cameraview; import android.content.Context; import android.hardware.SensorManager; import android.support.annotation.NonNull; -import android.util.SparseIntArray; import android.view.Display; import android.view.OrientationEventListener; import android.view.Surface; @@ -11,28 +10,18 @@ import android.view.WindowManager; class OrientationHelper { - private static final SparseIntArray DISPLAY_ORIENTATIONS = new SparseIntArray(); - static { - DISPLAY_ORIENTATIONS.put(Surface.ROTATION_0, 0); - DISPLAY_ORIENTATIONS.put(Surface.ROTATION_90, 90); - DISPLAY_ORIENTATIONS.put(Surface.ROTATION_180, 180); - DISPLAY_ORIENTATIONS.put(Surface.ROTATION_270, 270); - } - final OrientationEventListener mListener; - Display mDisplay; - private final Callbacks mCallbacks; - int mLastKnownDisplayOffset = -1; - int mLastOrientation = -1; + private final Callback mCallback; + private int mDeviceOrientation = -1; + private int mDisplayOffset = -1; - interface Callbacks { - void onDisplayOffsetChanged(int displayOffset); + interface Callback { void onDeviceOrientationChanged(int deviceOrientation); } - OrientationHelper(Context context, @NonNull Callbacks callbacks) { - mCallbacks = callbacks; + OrientationHelper(Context context, @NonNull Callback callback) { + mCallback = callback; mListener = new OrientationEventListener(context, SensorManager.SENSOR_DELAY_NORMAL) { @Override @@ -50,34 +39,37 @@ class OrientationHelper { or = 270; } - if (or != mLastOrientation) { - mLastOrientation = or; - mCallbacks.onDeviceOrientationChanged(mLastOrientation); - } - - // Let's see if display rotation has changed.. but how could it ever change...?? - // This makes no sense apparently. I'll leave it for now. - if (mDisplay != null) { - final int offset = mDisplay.getRotation(); - if (mLastKnownDisplayOffset != offset) { - mLastKnownDisplayOffset = offset; - mCallbacks.onDisplayOffsetChanged(DISPLAY_ORIENTATIONS.get(offset)); - } + if (or != mDeviceOrientation) { + mDeviceOrientation = or; + mCallback.onDeviceOrientationChanged(mDeviceOrientation); } } - }; } void enable(Context context) { - mDisplay = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); + Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); + switch (display.getRotation()) { + case Surface.ROTATION_0: mDisplayOffset = 0; break; + case Surface.ROTATION_90: mDisplayOffset = 90; break; + case Surface.ROTATION_180: mDisplayOffset = 180; break; + case Surface.ROTATION_270: mDisplayOffset = 270; break; + default: mDisplayOffset = 0; break; + } mListener.enable(); - mLastKnownDisplayOffset = DISPLAY_ORIENTATIONS.get(mDisplay.getRotation()); - mCallbacks.onDisplayOffsetChanged(mLastKnownDisplayOffset); } void disable() { mListener.disable(); - mDisplay = null; + mDisplayOffset = -1; + mDeviceOrientation = -1; + } + + int getDeviceOrientation() { + return mDeviceOrientation; + } + + int getDisplayOffset() { + return mDisplayOffset; } } \ No newline at end of file diff --git a/cameraview/src/test/java/com/otaliastudios/cameraview/SizeTest.java b/cameraview/src/test/java/com/otaliastudios/cameraview/SizeTest.java index fae207d9..1bce75c5 100644 --- a/cameraview/src/test/java/com/otaliastudios/cameraview/SizeTest.java +++ b/cameraview/src/test/java/com/otaliastudios/cameraview/SizeTest.java @@ -19,9 +19,9 @@ public class SizeTest { @Test public void testFlip() { Size size = new Size(10, 20); - size.flip(); - assertEquals(size.getWidth(), 20); - assertEquals(size.getHeight(), 10); + Size flipped = size.flip(); + assertEquals(size.getWidth(), flipped.getHeight()); + assertEquals(size.getHeight(), flipped.getWidth()); } @Test