Improve CameraOptions APIs, cleanup (#101)

* Improve CameraOptions APIs, cleanup

* Test flip

* Remove unwanted API
pull/105/head
Mattia Iavarone 7 years ago committed by GitHub
parent 9e6c4c0919
commit 33162f0e31
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 96
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraOptions1Test.java
  2. 25
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewCallbacksTest.java
  3. 9
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java
  4. 48
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/OrientationHelperTest.java
  5. 122
      cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java
  6. 5
      cameraview/src/main/java/com/otaliastudios/cameraview/Camera2.java
  7. 126
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java
  8. 32
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java
  9. 34
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
  10. 13
      cameraview/src/main/java/com/otaliastudios/cameraview/Size.java
  11. 62
      cameraview/src/main/utils/com/otaliastudios/cameraview/OrientationHelper.java
  12. 6
      cameraview/src/test/java/com/otaliastudios/cameraview/SizeTest.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<Camera.Size> 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<Size> 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<Camera.Size> 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<Size> 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<Camera.Size> sizes = Arrays.asList(
mockCameraSize(100, 200),
mockCameraSize(50, 50),
mockCameraSize(1600, 900),
mockCameraSize(1000, 2000)
);
Set<AspectRatio> 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<AspectRatio> supportedRatios = o.getSupportedPictureAspectRatios();
assertEquals(supportedRatios.size(), expected.size());
for (AspectRatio ratio : expected) {
assertTrue(supportedRatios.contains(ratio));
}
}
@Test
public void testFacing() {
Set<Integer> 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<Facing> 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);

@ -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());

@ -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;

@ -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);
}
}

@ -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<Camera.Area> meteringAreas2 = computeMeteringAreas(p.x, p.y,
viewWidthF, viewHeightF, computeSensorToDisplayOffset());
viewWidthF, viewHeightF, computeSensorToViewOffset());
List<Camera.Area> meteringAreas1 = meteringAreas2.subList(0, 1);
// At this point we are sure that camera supports auto focus... right? Look at CameraView.onTouchEvent().

@ -116,11 +116,6 @@ class Camera2 extends CameraController {
}
@Override
boolean shouldFlipSizes() {
return false;
}
@Override
void startAutoFocus(@Nullable Gesture gesture, PointF point) {

@ -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<Size> 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<Size> 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<Size> previewSizes) {
protected final Size computePreviewSize(List<Size> 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);

@ -20,6 +20,8 @@ public class CameraOptions {
private Set<Facing> supportedFacing = new HashSet<>(2);
private Set<Flash> supportedFlash = new HashSet<>(4);
private Set<Hdr> supportedHdr = new HashSet<>(2);
private Set<Size> supportedPictureSizes = new HashSet<>(15);
private Set<AspectRatio> 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<String> 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<Camera.Size> 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<Size> 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<AspectRatio> getSupportedPictureAspectRatios() {
return Collections.unmodifiableSet(supportedPictureAspectRatio);
}
/**
* Set of supported facing values.

@ -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() {

@ -7,8 +7,8 @@ import android.support.annotation.NonNull;
*/
public class Size implements Comparable<Size> {
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<Size> {
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

@ -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;
}
}

@ -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

Loading…
Cancel
Save