From 0f9c7f0e17ee68ee6c4f08215b002f8ca2a2e978 Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Wed, 10 Jul 2019 19:03:34 -0300 Subject: [PATCH] Make internals more open (#494) * Simplify GestureLayouts, now simple classes * Reorder CameraEngine members * Refactor CameraPreviews * Reorder Camera1Engine * Fix tests * New angle classes, fix snapshot rotation * Small changes * Add AnglesTest --- .../cameraview/CameraViewCallbacksTest.java | 6 +- .../cameraview/CameraViewTest.java | 32 +- .../cameraview/engine/MockCameraEngine.java | 45 -- ...LayoutTest.java => GestureFinderTest.java} | 43 +- ...tTest.java => PinchGestureFinderTest.java} | 31 +- ...Test.java => ScrollGestureFinderTest.java} | 39 +- ...outTest.java => TapGestureFinderTest.java} | 47 +- .../cameraview/preview/CameraPreviewTest.java | 5 +- .../preview/GlCameraPreviewTest.java | 4 +- .../cameraview/preview/MockCameraPreview.java | 2 +- .../preview/SurfaceCameraPreviewTest.java | 4 +- .../preview/TextureCameraPreviewTest.java | 4 +- .../otaliastudios/cameraview/CameraView.java | 111 +-- .../cameraview/engine/Camera1Engine.java | 691 +++++++++--------- .../cameraview/engine/Camera2Engine.java | 73 +- .../cameraview/engine/CameraEngine.java | 467 ++++++------ .../cameraview/engine/offset/Angles.java | 121 +++ .../cameraview/engine/offset/Axis.java | 28 + .../cameraview/engine/offset/Reference.java | 28 + ...{GestureLayout.java => GestureFinder.java} | 29 +- ...ureLayout.java => PinchGestureFinder.java} | 13 +- ...reLayout.java => ScrollGestureFinder.java} | 15 +- ...stureLayout.java => TapGestureFinder.java} | 22 +- .../cameraview/internal/egl/EglViewport.java | 8 +- .../picture/Snapshot1PictureRecorder.java | 24 +- .../picture/SnapshotGlPictureRecorder.java | 6 +- .../cameraview/preview/CameraPreview.java | 12 +- .../cameraview/preview/GlCameraPreview.java | 217 +++--- .../preview/SurfaceCameraPreview.java | 5 +- .../preview/TextureCameraPreview.java | 6 +- .../video/SnapshotVideoRecorder.java | 9 +- .../cameraview/engine/offset/AnglesTest.java | 80 ++ 32 files changed, 1214 insertions(+), 1013 deletions(-) rename cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/{GestureLayoutTest.java => GestureFinderTest.java} (59%) rename cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/{PinchGestureLayoutTest.java => PinchGestureFinderTest.java} (63%) rename cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/{ScrollGestureLayoutTest.java => ScrollGestureFinderTest.java} (68%) rename cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/{TapGestureLayoutTest.java => TapGestureFinderTest.java} (59%) create mode 100644 cameraview/src/main/java/com/otaliastudios/cameraview/engine/offset/Angles.java create mode 100644 cameraview/src/main/java/com/otaliastudios/cameraview/engine/offset/Axis.java create mode 100644 cameraview/src/main/java/com/otaliastudios/cameraview/engine/offset/Reference.java rename cameraview/src/main/java/com/otaliastudios/cameraview/gesture/{GestureLayout.java => GestureFinder.java} (88%) rename cameraview/src/main/java/com/otaliastudios/cameraview/gesture/{PinchGestureLayout.java => PinchGestureFinder.java} (85%) rename cameraview/src/main/java/com/otaliastudios/cameraview/gesture/{ScrollGestureLayout.java => ScrollGestureFinder.java} (84%) rename cameraview/src/main/java/com/otaliastudios/cameraview/gesture/{TapGestureLayout.java => TapGestureFinder.java} (72%) create mode 100644 cameraview/src/test/java/com/otaliastudios/cameraview/engine/offset/AnglesTest.java diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewCallbacksTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewCallbacksTest.java index bd461ef0..cb69d51a 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewCallbacksTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewCallbacksTest.java @@ -195,7 +195,7 @@ public class CameraViewCallbacksTest extends BaseTest { @Test public void testDispatchOnFocusStart() { // Enable tap gesture. - // Can't mock package protected. camera.mTapGestureLayout = mock(TapGestureLayout.class); + // Can't mock package protected. camera.mTapGestureFinder = mock(TapGestureLayout.class); camera.mapGesture(Gesture.TAP, GestureAction.AUTO_FOCUS); AutoFocusMarker marker = mock(AutoFocusMarker.class); MarkerLayout markerLayout = mock(MarkerLayout.class); @@ -215,7 +215,7 @@ public class CameraViewCallbacksTest extends BaseTest { @Test public void testDispatchOnFocusEnd() { // Enable tap gesture. - // Can't mock package protected. camera.mTapGestureLayout = mock(TapGestureLayout.class); + // Can't mock package protected. camera.mTapGestureFinder = mock(TapGestureLayout.class); camera.mapGesture(Gesture.TAP, GestureAction.AUTO_FOCUS); AutoFocusMarker marker = mock(AutoFocusMarker.class); camera.setAutoFocusMarker(marker); @@ -229,7 +229,7 @@ public class CameraViewCallbacksTest extends BaseTest { verify(listener, times(1)).onAutoFocusEnd(success, point); verify(marker, times(1)).onAutoFocusEnd(AutoFocusTrigger.GESTURE, success, point); - // Can't mock package protected. verify(camera.mTapGestureLayout, times(1)).onAutoFocusEnd(success); + // Can't mock package protected. verify(camera.mTapGestureFinder, times(1)).onAutoFocusEnd(success); } @Test diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java index 13f2419e..a524788b 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java @@ -28,9 +28,9 @@ import com.otaliastudios.cameraview.controls.Mode; import com.otaliastudios.cameraview.controls.VideoCodec; import com.otaliastudios.cameraview.controls.WhiteBalance; import com.otaliastudios.cameraview.gesture.GestureParser; -import com.otaliastudios.cameraview.gesture.PinchGestureLayout; -import com.otaliastudios.cameraview.gesture.ScrollGestureLayout; -import com.otaliastudios.cameraview.gesture.TapGestureLayout; +import com.otaliastudios.cameraview.gesture.PinchGestureFinder; +import com.otaliastudios.cameraview.gesture.ScrollGestureFinder; +import com.otaliastudios.cameraview.gesture.TapGestureFinder; import com.otaliastudios.cameraview.engine.MockCameraEngine; import com.otaliastudios.cameraview.internal.utils.Op; import com.otaliastudios.cameraview.markers.AutoFocusMarker; @@ -201,21 +201,21 @@ public class CameraViewTest extends BaseTest { // PinchGestureLayout cameraView.mapGesture(Gesture.PINCH, GestureAction.ZOOM); - assertTrue(cameraView.mPinchGestureLayout.isActive()); + assertTrue(cameraView.mPinchGestureFinder.isActive()); cameraView.clearGesture(Gesture.PINCH); - assertFalse(cameraView.mPinchGestureLayout.isActive()); + assertFalse(cameraView.mPinchGestureFinder.isActive()); // TapGestureLayout cameraView.mapGesture(Gesture.TAP, GestureAction.TAKE_PICTURE); - assertTrue(cameraView.mTapGestureLayout.isActive()); + assertTrue(cameraView.mTapGestureFinder.isActive()); cameraView.clearGesture(Gesture.TAP); - assertFalse(cameraView.mPinchGestureLayout.isActive()); + assertFalse(cameraView.mPinchGestureFinder.isActive()); // ScrollGestureLayout cameraView.mapGesture(Gesture.SCROLL_HORIZONTAL, GestureAction.ZOOM); - assertTrue(cameraView.mScrollGestureLayout.isActive()); + assertTrue(cameraView.mScrollGestureFinder.isActive()); cameraView.clearGesture(Gesture.SCROLL_HORIZONTAL); - assertFalse(cameraView.mScrollGestureLayout.isActive()); + assertFalse(cameraView.mScrollGestureFinder.isActive()); } //endregion @@ -231,7 +231,7 @@ public class CameraViewTest extends BaseTest { ui(new Runnable() { @Override public void run() { - cameraView.mTapGestureLayout = new TapGestureLayout(cameraView.getContext()) { + cameraView.mTapGestureFinder = new TapGestureFinder(cameraView.mCameraCallbacks) { protected boolean handleTouchEvent(@NonNull MotionEvent event) { setGesture(Gesture.TAP); return true; @@ -253,7 +253,7 @@ public class CameraViewTest extends BaseTest { ui(new Runnable() { @Override public void run() { - cameraView.mTapGestureLayout = new TapGestureLayout(cameraView.getContext()) { + cameraView.mTapGestureFinder = new TapGestureFinder(cameraView.mCameraCallbacks) { protected boolean handleTouchEvent(@NonNull MotionEvent event) { setGesture(Gesture.TAP); return true; @@ -280,7 +280,7 @@ public class CameraViewTest extends BaseTest { ui(new Runnable() { @Override public void run() { - cameraView.mPinchGestureLayout = new PinchGestureLayout(cameraView.getContext()) { + cameraView.mPinchGestureFinder = new PinchGestureFinder(cameraView.mCameraCallbacks) { @Override protected boolean handleTouchEvent(@NonNull MotionEvent event) { setGesture(Gesture.PINCH); @@ -321,7 +321,7 @@ public class CameraViewTest extends BaseTest { ui(new Runnable() { @Override public void run() { - cameraView.mScrollGestureLayout = new ScrollGestureLayout(cameraView.getContext()) { + cameraView.mScrollGestureFinder = new ScrollGestureFinder(cameraView.mCameraCallbacks) { @Override protected boolean handleTouchEvent(@NonNull MotionEvent event) { setGesture(Gesture.SCROLL_HORIZONTAL); @@ -626,7 +626,7 @@ public class CameraViewTest extends BaseTest { public void testPreviewStreamSizeSelector() { SizeSelector source = SizeSelectors.minHeight(50); cameraView.setPreviewStreamSize(source); - SizeSelector result = mockController.getInternalPreviewStreamSizeSelector(); + SizeSelector result = mockController.getPreviewStreamSizeSelector(); assertNotNull(result); assertEquals(result, source); } @@ -635,7 +635,7 @@ public class CameraViewTest extends BaseTest { public void testPictureSizeSelector() { SizeSelector source = SizeSelectors.minHeight(50); cameraView.setPictureSize(source); - SizeSelector result = mockController.getInternalPictureSizeSelector(); + SizeSelector result = mockController.getPictureSizeSelector(); assertNotNull(result); assertEquals(result, source); } @@ -644,7 +644,7 @@ public class CameraViewTest extends BaseTest { public void testVideoSizeSelector() { SizeSelector source = SizeSelectors.minHeight(50); cameraView.setVideoSize(source); - SizeSelector result = mockController.getInternalVideoSizeSelector(); + SizeSelector result = mockController.getVideoSizeSelector(); assertNotNull(result); assertEquals(result, source); } diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/MockCameraEngine.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/MockCameraEngine.java index 8b464897..3c2be982 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/MockCameraEngine.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/MockCameraEngine.java @@ -87,27 +87,6 @@ public class MockCameraEngine extends CameraEngine { mEngineStep.setState(started ? STATE_STARTED : STATE_STOPPED); } - public int getSnapshotMaxWidth() { - return mSnapshotMaxWidth; - } - - public int getSnapshotMaxHeight() { - return mSnapshotMaxHeight; - } - - public SizeSelector getInternalPreviewStreamSizeSelector() { - return super.getPreviewStreamSizeSelector(); - } - - public SizeSelector getInternalPictureSizeSelector() { - return super.getPictureSizeSelector(); - } - - public SizeSelector getInternalVideoSizeSelector() { - return super.getVideoSizeSelector(); - } - - @Override public void setZoom(float zoom, @Nullable PointF[] points, boolean notify) { mZoomValue = zoom; @@ -194,32 +173,8 @@ public class MockCameraEngine extends CameraEngine { } - @Override - public void setHasFrameProcessors(boolean hasFrameProcessors) { - - } - @Override protected boolean collectCameraInfo(@NonNull Facing facing) { return true; } - - /* - - @Override - public void setFacing(@NonNull Facing facing) { - mFacing = facing; - } - - @Override - public void setMode(@NonNull Mode mode) { - mMode = mode; - } - - @Override - public void setAudio(@NonNull Audio audio) { - mAudio = audio; - } - - */ } diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/GestureLayoutTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/GestureFinderTest.java similarity index 59% rename from cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/GestureLayoutTest.java rename to cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/GestureFinderTest.java index f7ed7565..0ffe626b 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/GestureLayoutTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/GestureFinderTest.java @@ -4,11 +4,14 @@ package com.otaliastudios.cameraview.gesture; import android.annotation.TargetApi; import android.content.Context; +import androidx.annotation.NonNull; import androidx.test.espresso.ViewInteraction; import androidx.test.espresso.matcher.RootMatchers; import androidx.test.rule.ActivityTestRule; import android.view.MotionEvent; import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; import com.otaliastudios.cameraview.BaseTest; import com.otaliastudios.cameraview.TestActivity; @@ -21,17 +24,19 @@ import org.junit.Rule; import static androidx.test.espresso.Espresso.onView; @TargetApi(17) -public abstract class GestureLayoutTest extends BaseTest { +public abstract class GestureFinderTest extends BaseTest { - protected abstract T create(Context context); + protected abstract T createFinder(@NonNull GestureFinder.Controller controller); @Rule public ActivityTestRule rule = new ActivityTestRule<>(TestActivity.class); @SuppressWarnings("WeakerAccess") - protected T layout; + protected T finder; @SuppressWarnings("WeakerAccess") - protected Op touch; + protected Op touchOp; + @SuppressWarnings("WeakerAccess") + protected ViewGroup layout; @Before public void setUp() { @@ -39,16 +44,17 @@ public abstract class GestureLayoutTest extends BaseTes @Override public void run() { TestActivity a = rule.getActivity(); - layout = create(a); - layout.setActive(true); + layout = new FrameLayout(a); + finder = createFinder(new Controller()); + finder.setActive(true); a.inflate(layout); - touch = new Op<>(); + touchOp = new Op<>(); layout.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View view, MotionEvent motionEvent) { - boolean found = layout.onTouchEvent(motionEvent); - if (found) touch.end(layout.getGesture()); + boolean found = finder.onTouchEvent(motionEvent); + if (found) touchOp.end(finder.getGesture()); return true; } }); @@ -62,4 +68,23 @@ public abstract class GestureLayoutTest extends BaseTes .inRoot(RootMatchers.withDecorView( Matchers.is(rule.getActivity().getWindow().getDecorView()))); } + + private class Controller implements GestureFinder.Controller { + + @NonNull + @Override + public Context getContext() { + return layout.getContext(); + } + + @Override + public int getWidth() { + return layout.getWidth(); + } + + @Override + public int getHeight() { + return layout.getHeight(); + } + } } diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/PinchGestureLayoutTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/PinchGestureFinderTest.java similarity index 63% rename from cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/PinchGestureLayoutTest.java rename to cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/PinchGestureFinderTest.java index 5411bc1e..c109f3b6 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/PinchGestureLayoutTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/PinchGestureFinderTest.java @@ -3,10 +3,7 @@ package com.otaliastudios.cameraview.gesture; import android.content.Context; -import com.otaliastudios.cameraview.gesture.Gesture; -import com.otaliastudios.cameraview.gesture.GestureLayoutTest; -import com.otaliastudios.cameraview.gesture.PinchGestureLayout; - +import androidx.annotation.NonNull; import androidx.test.espresso.ViewAction; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -22,21 +19,21 @@ import static org.junit.Assert.assertTrue; @RunWith(AndroidJUnit4.class) @SmallTest -public class PinchGestureLayoutTest extends GestureLayoutTest { +public class PinchGestureFinderTest extends GestureFinderTest { @Override - protected PinchGestureLayout create(Context context) { - return new PinchGestureLayout(context); + protected PinchGestureFinder createFinder(@NonNull GestureFinder.Controller controller) { + return new PinchGestureFinder(controller); } @Test public void testDefaults() { - assertEquals(layout.getGesture(), Gesture.PINCH); - assertEquals(layout.getPoints().length, 2); - assertEquals(layout.getPoints()[0].x, 0, 0); - assertEquals(layout.getPoints()[0].y, 0, 0); - assertEquals(layout.getPoints()[1].x, 0, 0); - assertEquals(layout.getPoints()[1].y, 0, 0); + assertEquals(finder.getGesture(), Gesture.PINCH); + assertEquals(finder.getPoints().length, 2); + assertEquals(finder.getPoints()[0].x, 0, 0); + assertEquals(finder.getPoints()[0].y, 0, 0); + assertEquals(finder.getPoints()[1].x, 0, 0); + assertEquals(finder.getPoints()[1].y, 0, 0); } // TODO: test pinch open @@ -51,15 +48,15 @@ public class PinchGestureLayoutTest extends GestureLayoutTest curr); assertTrue(newValue <= max); diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/ScrollGestureLayoutTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/ScrollGestureFinderTest.java similarity index 68% rename from cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/ScrollGestureLayoutTest.java rename to cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/ScrollGestureFinderTest.java index 5d2fe093..3c05c84f 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/ScrollGestureLayoutTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/ScrollGestureFinderTest.java @@ -3,10 +3,7 @@ package com.otaliastudios.cameraview.gesture; import android.content.Context; -import com.otaliastudios.cameraview.gesture.Gesture; -import com.otaliastudios.cameraview.gesture.GestureLayoutTest; -import com.otaliastudios.cameraview.gesture.ScrollGestureLayout; - +import androidx.annotation.NonNull; import androidx.test.espresso.ViewAction; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -27,43 +24,43 @@ import static org.junit.Assert.assertTrue; @RunWith(AndroidJUnit4.class) @SmallTest -public class ScrollGestureLayoutTest extends GestureLayoutTest { +public class ScrollGestureFinderTest extends GestureFinderTest { @Override - protected ScrollGestureLayout create(Context context) { - return new ScrollGestureLayout(context); + protected ScrollGestureFinder createFinder(@NonNull GestureFinder.Controller controller) { + return new ScrollGestureFinder(controller); } @Test public void testDefaults() { - assertNull(layout.getGesture()); - assertEquals(layout.getPoints().length, 2); - assertEquals(layout.getPoints()[0].x, 0, 0); - assertEquals(layout.getPoints()[0].y, 0, 0); - assertEquals(layout.getPoints()[1].x, 0, 0); - assertEquals(layout.getPoints()[1].y, 0, 0); + assertNull(finder.getGesture()); + assertEquals(finder.getPoints().length, 2); + assertEquals(finder.getPoints()[0].x, 0, 0); + assertEquals(finder.getPoints()[0].y, 0, 0); + assertEquals(finder.getPoints()[1].x, 0, 0); + assertEquals(finder.getPoints()[1].y, 0, 0); } @Test public void testScrollDisabled() { - layout.setActive(false); - touch.listen(); - touch.start(); + finder.setActive(false); + touchOp.listen(); + touchOp.start(); onLayout().perform(swipeUp()); - Gesture found = touch.await(500); + Gesture found = touchOp.await(500); assertNull(found); } private void testScroll(ViewAction scroll, Gesture expected, boolean increasing) { - touch.listen(); - touch.start(); + touchOp.listen(); + touchOp.start(); onLayout().perform(scroll); - Gesture found = touch.await(500); + Gesture found = touchOp.await(500); assertEquals(found, expected); // How will this move our parameter? float curr = 0.5f, min = 0f, max = 1f; - float newValue = layout.computeValue(curr, min, max); + float newValue = finder.computeValue(curr, min, max); if (increasing) { assertTrue(newValue >= curr); assertTrue(newValue <= max); diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/TapGestureLayoutTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/TapGestureFinderTest.java similarity index 59% rename from cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/TapGestureLayoutTest.java rename to cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/TapGestureFinderTest.java index 895c4de3..b3f44aa9 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/TapGestureLayoutTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/TapGestureFinderTest.java @@ -2,6 +2,8 @@ package com.otaliastudios.cameraview.gesture; import android.content.Context; + +import androidx.annotation.NonNull; import androidx.test.espresso.action.GeneralClickAction; import androidx.test.espresso.action.GeneralLocation; import androidx.test.espresso.action.Press; @@ -11,9 +13,6 @@ import androidx.test.filters.SmallTest; import android.view.InputDevice; import android.view.MotionEvent; -import com.otaliastudios.cameraview.gesture.Gesture; -import com.otaliastudios.cameraview.gesture.GestureLayoutTest; -import com.otaliastudios.cameraview.gesture.TapGestureLayout; import com.otaliastudios.cameraview.size.Size; import org.junit.Test; @@ -24,59 +23,59 @@ import static org.junit.Assert.*; @RunWith(AndroidJUnit4.class) @SmallTest -public class TapGestureLayoutTest extends GestureLayoutTest { +public class TapGestureFinderTest extends GestureFinderTest { @Override - protected TapGestureLayout create(Context context) { - return new TapGestureLayout(context); + protected TapGestureFinder createFinder(@NonNull GestureFinder.Controller controller) { + return new TapGestureFinder(controller); } @Test public void testDefaults() { - assertNull(layout.getGesture()); - assertEquals(layout.getPoints().length, 1); - assertEquals(layout.getPoints()[0].x, 0, 0); - assertEquals(layout.getPoints()[0].y, 0, 0); + assertNull(finder.getGesture()); + assertEquals(finder.getPoints().length, 1); + assertEquals(finder.getPoints()[0].x, 0, 0); + assertEquals(finder.getPoints()[0].y, 0, 0); } @Test public void testTap() { - touch.listen(); - touch.start(); + touchOp.listen(); + touchOp.start(); GeneralClickAction a = new GeneralClickAction( Tap.SINGLE, GeneralLocation.CENTER, Press.FINGER, InputDevice.SOURCE_UNKNOWN, MotionEvent.BUTTON_PRIMARY); onLayout().perform(a); - Gesture found = touch.await(500); + Gesture found = touchOp.await(500); assertEquals(found, Gesture.TAP); Size size = rule.getActivity().getContentSize(); - assertEquals(layout.getPoints()[0].x, (size.getWidth() / 2f), 1f); - assertEquals(layout.getPoints()[0].y, (size.getHeight() / 2f), 1f); + assertEquals(finder.getPoints()[0].x, (size.getWidth() / 2f), 1f); + assertEquals(finder.getPoints()[0].y, (size.getHeight() / 2f), 1f); } @Test public void testTapWhileDisabled() { - layout.setActive(false); - touch.listen(); - touch.start(); + finder.setActive(false); + touchOp.listen(); + touchOp.start(); onLayout().perform(click()); - Gesture found = touch.await(500); + Gesture found = touchOp.await(500); assertNull(found); } @Test public void testLongTap() { - touch.listen(); - touch.start(); + touchOp.listen(); + touchOp.start(); GeneralClickAction a = new GeneralClickAction( Tap.LONG, GeneralLocation.CENTER, Press.FINGER, InputDevice.SOURCE_UNKNOWN, MotionEvent.BUTTON_PRIMARY); onLayout().perform(a); - Gesture found = touch.await(500); + Gesture found = touchOp.await(500); assertEquals(found, Gesture.LONG_TAP); Size size = rule.getActivity().getContentSize(); - assertEquals(layout.getPoints()[0].x, (size.getWidth() / 2f), 1f); - assertEquals(layout.getPoints()[0].y, (size.getHeight() / 2f), 1f); + assertEquals(finder.getPoints()[0].x, (size.getWidth() / 2f), 1f); + assertEquals(finder.getPoints()[0].y, (size.getHeight() / 2f), 1f); } } diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/CameraPreviewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/CameraPreviewTest.java index 1ae81559..2f8be439 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/CameraPreviewTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/CameraPreviewTest.java @@ -27,7 +27,7 @@ public abstract class CameraPreviewTest extends BaseTest { private final static long DELAY = 4000; - protected abstract CameraPreview createPreview(Context context, ViewGroup parent, CameraPreview.SurfaceCallback callback); + protected abstract CameraPreview createPreview(Context context, ViewGroup parent); @Rule public ActivityTestRule rule = new ActivityTestRule<>(TestActivity.class); @@ -69,7 +69,8 @@ public abstract class CameraPreviewTest extends BaseTest { } }).when(callback).onSurfaceDestroyed(); - preview = createPreview(a, a.getContentView(), callback); + preview = createPreview(a, a.getContentView()); + preview.setSurfaceCallback(callback); } }); } diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/GlCameraPreviewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/GlCameraPreviewTest.java index 059bf268..ff8a085e 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/GlCameraPreviewTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/GlCameraPreviewTest.java @@ -14,8 +14,8 @@ import org.junit.runner.RunWith; public class GlCameraPreviewTest extends CameraPreviewTest { @Override - protected CameraPreview createPreview(Context context, ViewGroup parent, CameraPreview.SurfaceCallback callback) { - return new GlCameraPreview(context, parent, callback); + protected CameraPreview createPreview(Context context, ViewGroup parent) { + return new GlCameraPreview(context, parent); } @Override diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/MockCameraPreview.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/MockCameraPreview.java index 302bfe4f..c0fd2d2c 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/MockCameraPreview.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/MockCameraPreview.java @@ -12,7 +12,7 @@ import com.otaliastudios.cameraview.preview.CameraPreview; public class MockCameraPreview extends CameraPreview { public MockCameraPreview(Context context, ViewGroup parent) { - super(context, parent, null); + super(context, parent); } @Override diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/SurfaceCameraPreviewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/SurfaceCameraPreviewTest.java index 9555d341..d76f9a87 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/SurfaceCameraPreviewTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/SurfaceCameraPreviewTest.java @@ -18,8 +18,8 @@ import org.junit.runner.RunWith; public class SurfaceCameraPreviewTest extends CameraPreviewTest { @Override - protected CameraPreview createPreview(Context context, ViewGroup parent, CameraPreview.SurfaceCallback callback) { - return new SurfaceCameraPreview(context, parent, callback); + protected CameraPreview createPreview(Context context, ViewGroup parent) { + return new SurfaceCameraPreview(context, parent); } @Override diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/TextureCameraPreviewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/TextureCameraPreviewTest.java index 916c4344..3433cac1 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/TextureCameraPreviewTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/TextureCameraPreviewTest.java @@ -18,8 +18,8 @@ import org.junit.runner.RunWith; public class TextureCameraPreviewTest extends CameraPreviewTest { @Override - protected CameraPreview createPreview(Context context, ViewGroup parent, CameraPreview.SurfaceCallback callback) { - return new TextureCameraPreview(context, parent, callback); + protected CameraPreview createPreview(Context context, ViewGroup parent) { + return new TextureCameraPreview(context, parent); } @Override diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java index bb4f39fc..284a1ecc 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java @@ -37,6 +37,7 @@ import com.otaliastudios.cameraview.controls.Engine; import com.otaliastudios.cameraview.controls.Facing; import com.otaliastudios.cameraview.controls.Flash; import com.otaliastudios.cameraview.engine.Camera2Engine; +import com.otaliastudios.cameraview.engine.offset.Reference; import com.otaliastudios.cameraview.markers.MarkerLayout; import com.otaliastudios.cameraview.engine.Camera1Engine; import com.otaliastudios.cameraview.engine.CameraEngine; @@ -50,11 +51,11 @@ import com.otaliastudios.cameraview.controls.Mode; import com.otaliastudios.cameraview.controls.Preview; import com.otaliastudios.cameraview.controls.VideoCodec; import com.otaliastudios.cameraview.controls.WhiteBalance; -import com.otaliastudios.cameraview.gesture.GestureLayout; +import com.otaliastudios.cameraview.gesture.GestureFinder; import com.otaliastudios.cameraview.gesture.GestureParser; -import com.otaliastudios.cameraview.gesture.PinchGestureLayout; -import com.otaliastudios.cameraview.gesture.ScrollGestureLayout; -import com.otaliastudios.cameraview.gesture.TapGestureLayout; +import com.otaliastudios.cameraview.gesture.PinchGestureFinder; +import com.otaliastudios.cameraview.gesture.ScrollGestureFinder; +import com.otaliastudios.cameraview.gesture.TapGestureFinder; import com.otaliastudios.cameraview.internal.GridLinesLayout; import com.otaliastudios.cameraview.internal.utils.CropHelper; import com.otaliastudios.cameraview.internal.utils.OrientationHelper; @@ -115,11 +116,13 @@ public class CameraView extends FrameLayout implements LifecycleObserver { @VisibleForTesting List mFrameProcessors = new CopyOnWriteArrayList<>(); private Lifecycle mLifecycle; + // Gestures + @VisibleForTesting PinchGestureFinder mPinchGestureFinder; + @VisibleForTesting TapGestureFinder mTapGestureFinder; + @VisibleForTesting ScrollGestureFinder mScrollGestureFinder; + // Views GridLinesLayout mGridLinesLayout; - PinchGestureLayout mPinchGestureLayout; - TapGestureLayout mTapGestureLayout; - ScrollGestureLayout mScrollGestureLayout; MarkerLayout mMarkerLayout; private boolean mKeepScreenOn; @SuppressWarnings({"FieldCanBeLocal", "unused"}) @@ -173,16 +176,15 @@ public class CameraView extends FrameLayout implements LifecycleObserver { mUiHandler = new Handler(Looper.getMainLooper()); mFrameProcessorsHandler = WorkerHandler.get("FrameProcessorsWorker"); + // Gestures + mPinchGestureFinder = new PinchGestureFinder(mCameraCallbacks); + mTapGestureFinder = new TapGestureFinder(mCameraCallbacks); + mScrollGestureFinder = new ScrollGestureFinder(mCameraCallbacks); + // Views mGridLinesLayout = new GridLinesLayout(context); - mPinchGestureLayout = new PinchGestureLayout(context); - mTapGestureLayout = new TapGestureLayout(context); - mScrollGestureLayout = new ScrollGestureLayout(context); mMarkerLayout = new MarkerLayout(context); addView(mGridLinesLayout); - addView(mPinchGestureLayout); - addView(mTapGestureLayout); - addView(mScrollGestureLayout); addView(mMarkerLayout); // Create the engine @@ -225,6 +227,26 @@ public class CameraView extends FrameLayout implements LifecycleObserver { } } + /** + * Engine is instantiated on creation and anytime + * {@link #setEngine(Engine)} is called. + */ + private void doInstantiateEngine() { + mCameraEngine = instantiateCameraEngine(mEngine, mCameraCallbacks); + } + + /** + * Preview is instantiated {@link #onAttachedToWindow()}, because + * we want to know if we're hardware accelerated or not. + * However, in tests, we might want to create the preview right after constructor. + */ + @VisibleForTesting + void doInstantiatePreview() { + mCameraPreview = instantiatePreview(mPreview, getContext(), this); + mCameraEngine.setPreview(mCameraPreview); + } + + /** * Instantiates the camera engine. * @@ -255,30 +277,20 @@ public class CameraView extends FrameLayout implements LifecycleObserver { LOG.w("preview:", "isHardwareAccelerated:", isHardwareAccelerated()); switch (preview) { case SURFACE: - return new SurfaceCameraPreview(context, container, null); + return new SurfaceCameraPreview(context, container); case TEXTURE: { if (isHardwareAccelerated()) { // TextureView is not supported without hardware acceleration. - return new TextureCameraPreview(context, container, null); + return new TextureCameraPreview(context, container); } } case GL_SURFACE: default: { mPreview = Preview.GL_SURFACE; - return new GlCameraPreview(context, container, null); + return new GlCameraPreview(context, container); } } } - @VisibleForTesting - void doInstantiatePreview() { - mCameraPreview = instantiatePreview(mPreview, getContext(), this); - mCameraEngine.setPreview(mCameraPreview); - } - - private void doInstantiateEngine() { - mCameraEngine = instantiateCameraEngine(mEngine, mCameraCallbacks); - } - @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); @@ -331,7 +343,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - Size previewSize = mCameraEngine.getPreviewStreamSize(CameraEngine.REF_VIEW); + Size previewSize = mCameraEngine.getPreviewStreamSize(Reference.VIEW); if (previewSize == null) { LOG.w("onMeasure:", "surface is not ready. Calling default behavior."); super.onMeasure(widthMeasureSpec, heightMeasureSpec); @@ -474,19 +486,19 @@ public class CameraView extends FrameLayout implements LifecycleObserver { mGestureMap.put(gesture, action); switch (gesture) { case PINCH: - mPinchGestureLayout.setActive(mGestureMap.get(Gesture.PINCH) != none); + mPinchGestureFinder.setActive(mGestureMap.get(Gesture.PINCH) != none); break; case TAP: // case DOUBLE_TAP: case LONG_TAP: - mTapGestureLayout.setActive( + mTapGestureFinder.setActive( mGestureMap.get(Gesture.TAP) != none || // mGestureMap.get(Gesture.DOUBLE_TAP) != none || mGestureMap.get(Gesture.LONG_TAP) != none); break; case SCROLL_HORIZONTAL: case SCROLL_VERTICAL: - mScrollGestureLayout.setActive( + mScrollGestureFinder.setActive( mGestureMap.get(Gesture.SCROLL_HORIZONTAL) != none || mGestureMap.get(Gesture.SCROLL_VERTICAL) != none); break; @@ -534,15 +546,15 @@ public class CameraView extends FrameLayout implements LifecycleObserver { // Pass to our own GestureLayouts CameraOptions options = mCameraEngine.getCameraOptions(); // Non null if (options == null) throw new IllegalStateException("Options should not be null here."); - if (mPinchGestureLayout.onTouchEvent(event)) { + if (mPinchGestureFinder.onTouchEvent(event)) { LOG.i("onTouchEvent", "pinch!"); - onGesture(mPinchGestureLayout, options); - } else if (mScrollGestureLayout.onTouchEvent(event)) { + onGesture(mPinchGestureFinder, options); + } else if (mScrollGestureFinder.onTouchEvent(event)) { LOG.i("onTouchEvent", "scroll!"); - onGesture(mScrollGestureLayout, options); - } else if (mTapGestureLayout.onTouchEvent(event)) { + onGesture(mScrollGestureFinder, options); + } else if (mTapGestureFinder.onTouchEvent(event)) { LOG.i("onTouchEvent", "tap!"); - onGesture(mTapGestureLayout, options); + onGesture(mTapGestureFinder, options); } return true; } @@ -551,7 +563,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { // 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 (CameraEngine checks) - private void onGesture(GestureLayout source, @NonNull CameraOptions options) { + private void onGesture(GestureFinder source, @NonNull CameraOptions options) { Gesture gesture = source.getGesture(); GestureAction action = mGestureMap.get(gesture); PointF[] points = source.getPoints(); @@ -1538,12 +1550,12 @@ public class CameraView extends FrameLayout implements LifecycleObserver { // Get the preview size and crop according to the current view size. // It's better to do calculations in the REF_VIEW reference, and then flip if needed. - Size preview = mCameraEngine.getUncroppedSnapshotSize(CameraEngine.REF_VIEW); + Size preview = mCameraEngine.getUncroppedSnapshotSize(Reference.VIEW); if (preview == null) return null; // Should never happen. AspectRatio viewRatio = AspectRatio.of(getWidth(), getHeight()); Rect crop = CropHelper.computeCrop(preview, viewRatio); Size cropSize = new Size(crop.width(), crop.height()); - if (mCameraEngine.flip(CameraEngine.REF_VIEW, CameraEngine.REF_OUTPUT)) { + if (mCameraEngine.getAngles().flip(Reference.VIEW, Reference.OUTPUT)) { return cropSize.flip(); } else { return cropSize; @@ -1562,7 +1574,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { */ @Nullable public Size getPictureSize() { - return mCameraEngine.getPictureSize(CameraEngine.REF_OUTPUT); + return mCameraEngine.getPictureSize(Reference.OUTPUT); } @@ -1577,7 +1589,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { */ @Nullable public Size getVideoSize() { - return mCameraEngine.getVideoSize(CameraEngine.REF_OUTPUT); + return mCameraEngine.getVideoSize(Reference.OUTPUT); } @@ -1732,7 +1744,10 @@ public class CameraView extends FrameLayout implements LifecycleObserver { //region Callbacks and dispatching @VisibleForTesting - class CameraCallbacks implements CameraEngine.Callback, OrientationHelper.Callback { + class CameraCallbacks implements + CameraEngine.Callback, + OrientationHelper.Callback, + GestureFinder.Controller { private CameraLogger mLogger = CameraLogger.create(CameraCallbacks.class.getSimpleName()); @@ -1742,6 +1757,16 @@ public class CameraView extends FrameLayout implements LifecycleObserver { return CameraView.this.getContext(); } + @Override + public int getWidth() { + return CameraView.this.getWidth(); + } + + @Override + public int getHeight() { + return CameraView.this.getHeight(); + } + @Override public void dispatchOnCameraOpened(final CameraOptions options) { mLogger.i("dispatchOnCameraOpened", options); @@ -1786,7 +1811,6 @@ public class CameraView extends FrameLayout implements LifecycleObserver { @Override public void onShutter(boolean shouldPlaySound) { if (shouldPlaySound && mPlaySounds) { - //noinspection all playSound(MediaActionSound.SHUTTER_CLICK); } } @@ -1847,7 +1871,6 @@ public class CameraView extends FrameLayout implements LifecycleObserver { @Override public void run() { if (success && mPlaySounds) { - //noinspection all playSound(MediaActionSound.FOCUS_COMPLETE); } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java index 14647ea2..983acc82 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java @@ -14,7 +14,6 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.annotation.WorkerThread; -import android.util.Log; import android.view.SurfaceHolder; import com.google.android.gms.tasks.Task; @@ -23,6 +22,8 @@ import com.otaliastudios.cameraview.CameraException; import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.CameraOptions; import com.otaliastudios.cameraview.controls.Engine; +import com.otaliastudios.cameraview.engine.offset.Axis; +import com.otaliastudios.cameraview.engine.offset.Reference; import com.otaliastudios.cameraview.frame.Frame; import com.otaliastudios.cameraview.PictureResult; import com.otaliastudios.cameraview.VideoResult; @@ -34,7 +35,6 @@ import com.otaliastudios.cameraview.controls.Hdr; import com.otaliastudios.cameraview.controls.Mode; import com.otaliastudios.cameraview.controls.WhiteBalance; import com.otaliastudios.cameraview.internal.utils.CropHelper; -import com.otaliastudios.cameraview.internal.utils.Op; import com.otaliastudios.cameraview.picture.Full1PictureRecorder; import com.otaliastudios.cameraview.picture.Snapshot1PictureRecorder; import com.otaliastudios.cameraview.picture.SnapshotGlPictureRecorder; @@ -57,52 +57,84 @@ public class Camera1Engine extends CameraEngine implements private static final String TAG = Camera1Engine.class.getSimpleName(); private static final CameraLogger LOG = CameraLogger.create(TAG); + + private static final int PREVIEW_FORMAT = ImageFormat.NV21; @VisibleForTesting static final int AUTOFOCUS_END_DELAY_MILLIS = 2500; private Camera mCamera; @VisibleForTesting int mCameraId; - private int mPreviewStreamFormat; - - private Runnable mFocusEndRunnable; - private final Runnable mFocusResetRunnable = new Runnable() { - @Override - public void run() { - if (!isCameraAvailable()) return; - mCamera.cancelAutoFocus(); - Camera.Parameters params = mCamera.getParameters(); - int maxAF = params.getMaxNumFocusAreas(); - int maxAE = params.getMaxNumMeteringAreas(); - if (maxAF > 0) params.setFocusAreas(null); - if (maxAE > 0) params.setMeteringAreas(null); - applyDefaultFocus(params); // Revert to internal focus. - mCamera.setParameters(params); - } - }; public Camera1Engine(@NonNull Callback callback) { super(callback); mMapper = Mapper.get(Engine.CAMERA1); } - private boolean isCameraAvailable() { - return getEngineState() == STATE_STARTED; + //region Utilities + + @Override + public void onError(int error, Camera camera) { + if (error == Camera.CAMERA_ERROR_SERVER_DIED) { + // Looks like this is recoverable. + LOG.w("Recoverable error inside the onError callback.", "CAMERA_ERROR_SERVER_DIED"); + restart(); + return; + } + + String message = LOG.e("Internal Camera1 error.", error); + Exception runtime = new RuntimeException(message); + int reason; + switch (error) { + case Camera.CAMERA_ERROR_EVICTED: reason = CameraException.REASON_DISCONNECTED; break; + case Camera.CAMERA_ERROR_UNKNOWN: reason = CameraException.REASON_UNKNOWN; break; + default: reason = CameraException.REASON_UNKNOWN; + } + throw new CameraException(runtime, reason); } - private void schedule(@Nullable final Op op, final boolean ensureAvailable, final Runnable action) { - mHandler.run(new Runnable() { - @Override - public void run() { - if (ensureAvailable && !isCameraAvailable()) { - if (op != null) op.end(null); - } else { - action.run(); - if (op != null) op.end(null); - } + //endregion + + //region Protected APIs + + @NonNull + @Override + protected List getPreviewStreamAvailableSizes() { + List sizes = mCamera.getParameters().getSupportedPreviewSizes(); + List result = new ArrayList<>(sizes.size()); + for (Camera.Size size : sizes) { + Size add = new Size(size.width, size.height); + if (!result.contains(add)) result.add(add); + } + LOG.i("getPreviewStreamAvailableSizes:", result); + return result; + } + + @WorkerThread + @Override + protected void onPreviewStreamSizeChanged() { + restartPreview(); + } + + @Override + protected boolean collectCameraInfo(@NonNull Facing facing) { + int internalFacing = mMapper.map(facing); + LOG.i("collectCameraInfo", "Facing:", facing, "Internal:", internalFacing, "Cameras:", Camera.getNumberOfCameras()); + Camera.CameraInfo cameraInfo = new Camera.CameraInfo(); + for (int i = 0, count = Camera.getNumberOfCameras(); i < count; i++) { + Camera.getCameraInfo(i, cameraInfo); + if (cameraInfo.facing == internalFacing) { + setSensorOffset(facing, cameraInfo.orientation); + mCameraId = i; + return true; } - }); + } + return false; } + //endregion + + //region Start + @NonNull @WorkerThread @Override @@ -118,16 +150,10 @@ public class Camera1Engine extends CameraEngine implements // Set parameters that might have been set before the camera was opened. LOG.i("onStartEngine:", "Applying default parameters."); Camera.Parameters params = mCamera.getParameters(); - mCameraOptions = new CameraOptions(params, flip(REF_SENSOR, REF_VIEW)); - applyDefaultFocus(params); - applyFlash(params, Flash.OFF); - applyLocation(params, null); - applyWhiteBalance(params, WhiteBalance.AUTO); - applyHdr(params, Hdr.OFF); - applyPlaySounds(mPlaySounds); - params.setRecordingHint(mMode == Mode.VIDEO); + mCameraOptions = new CameraOptions(params, getAngles().flip(Reference.SENSOR, Reference.VIEW)); + applyAllParameters(params); mCamera.setParameters(params); - mCamera.setDisplayOrientation(offset(REF_SENSOR, REF_VIEW)); // <- not allowed during preview + mCamera.setDisplayOrientation(getAngles().offset(Reference.SENSOR, Reference.VIEW, Axis.ABSOLUTE)); // <- not allowed during preview LOG.i("onStartEngine:", "Ended"); return Tasks.forResult(null); } @@ -161,17 +187,17 @@ public class Camera1Engine extends CameraEngine implements LOG.i("onStartPreview", "Dispatching onCameraPreviewStreamSizeChanged."); mCallback.onCameraPreviewStreamSizeChanged(); - Size previewSize = getPreviewStreamSize(REF_VIEW); + Size previewSize = getPreviewStreamSize(Reference.VIEW); if (previewSize == null) { throw new IllegalStateException("previewStreamSize should not be null at this point."); } mPreview.setStreamSize(previewSize.getWidth(), previewSize.getHeight()); Camera.Parameters params = mCamera.getParameters(); - mPreviewStreamFormat = params.getPreviewFormat(); - params.setPreviewSize(mPreviewStreamSize.getWidth(), mPreviewStreamSize.getHeight()); // <- not allowed during preview - if (mMode == Mode.PICTURE) { - params.setPictureSize(mCaptureSize.getWidth(), mCaptureSize.getHeight()); // <- allowed + params.setPreviewFormat(ImageFormat.NV21); // should be the default, but let's make sure, since YuvImage will only support this & a few others + params.setPreviewSize(mPreviewStreamSize.getWidth(), mPreviewStreamSize.getHeight()); // not allowed during preview + if (getMode() == Mode.PICTURE) { + params.setPictureSize(mCaptureSize.getWidth(), mCaptureSize.getHeight()); // allowed during preview } else { // mCaptureSize in this case is a video size. The available video sizes are not necessarily // a subset of the picture sizes, so we can't use the mCaptureSize value: it might crash. @@ -184,7 +210,7 @@ public class Camera1Engine extends CameraEngine implements mCamera.setPreviewCallbackWithBuffer(null); // Release anything left mCamera.setPreviewCallbackWithBuffer(this); // Add ourselves - getFrameManager().setUp(ImageFormat.getBitsPerPixel(mPreviewStreamFormat), mPreviewStreamSize); + getFrameManager().setUp(ImageFormat.getBitsPerPixel(PREVIEW_FORMAT), mPreviewStreamSize); LOG.i("onStartPreview", "Starting preview with startPreview()."); try { @@ -197,6 +223,10 @@ public class Camera1Engine extends CameraEngine implements return Tasks.forResult(null); } + //endregion + + //region Stop + @NonNull @Override protected Task onStopPreview() { @@ -205,7 +235,6 @@ public class Camera1Engine extends CameraEngine implements mVideoRecorder = null; } mPictureRecorder = null; - mPreviewStreamFormat = 0; getFrameManager().release(); mCamera.setPreviewCallbackWithBuffer(null); // Release anything left try { @@ -216,7 +245,6 @@ public class Camera1Engine extends CameraEngine implements return Tasks.forResult(null); } - @NonNull @Override protected Task onStopBind() { @@ -263,76 +291,171 @@ public class Camera1Engine extends CameraEngine implements return Tasks.forResult(null); } + //endregion + + //region Pictures + @WorkerThread @Override - protected void onPreviewStreamSizeChanged() { - restartPreview(); + protected void onTakePicture(@NonNull PictureResult.Stub stub) { + stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR); + stub.size = getPictureSize(Reference.OUTPUT); + mPictureRecorder = new Full1PictureRecorder(stub, Camera1Engine.this, mCamera); + mPictureRecorder.take(); } + @WorkerThread @Override - protected boolean collectCameraInfo(@NonNull Facing facing) { - int internalFacing = mMapper.map(facing); - LOG.i("collectCameraInfo", "Facing:", facing, "Internal:", internalFacing, "Cameras:", Camera.getNumberOfCameras()); - Camera.CameraInfo cameraInfo = new Camera.CameraInfo(); - for (int i = 0, count = Camera.getNumberOfCameras(); i < count; i++) { - Camera.getCameraInfo(i, cameraInfo); - if (cameraInfo.facing == internalFacing) { - mSensorOffset = cameraInfo.orientation; - mCameraId = i; - return true; - } + protected void onTakePictureSnapshot(@NonNull PictureResult.Stub stub, @NonNull AspectRatio viewAspectRatio) { + stub.size = getUncroppedSnapshotSize(Reference.OUTPUT); // Not the real size: it will be cropped to match the view ratio + stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR); // Actually it will be rotated and set to 0. + AspectRatio outputRatio = getAngles().flip(Reference.OUTPUT, Reference.VIEW) ? viewAspectRatio.flip() : viewAspectRatio; + + if (mPreview instanceof GlCameraPreview && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + mPictureRecorder = new SnapshotGlPictureRecorder(stub, this, (GlCameraPreview) mPreview, outputRatio); + } else { + mPictureRecorder = new Snapshot1PictureRecorder(stub, this, mCamera, outputRatio); } - return false; + mPictureRecorder.take(); } + //endregion + + //region Videos + @Override - public void setHasFrameProcessors(boolean hasFrameProcessors) { - mHasFrameProcessors = hasFrameProcessors; + protected void onTakeVideo(@NonNull VideoResult.Stub stub) { + stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR); + stub.size = getAngles().flip(Reference.SENSOR, Reference.OUTPUT) ? mCaptureSize.flip() : mCaptureSize; + // Unlock the camera and start recording. + try { + mCamera.unlock(); + } catch (Exception e) { + // If this failed, we are unlikely able to record the video. + // Dispatch an error. + onVideoResult(null, e); + return; + } + mVideoRecorder = new Full1VideoRecorder(Camera1Engine.this, mCamera, mCameraId); + mVideoRecorder.start(stub); } - @NonNull + @SuppressLint("NewApi") + @WorkerThread @Override - protected FrameManager instantiateFrameManager() { - return new FrameManager(2, this); + protected void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub, @NonNull AspectRatio viewAspectRatio) { + if (!(mPreview instanceof GlCameraPreview)) { + throw new IllegalStateException("Video snapshots are only supported with GlCameraPreview."); + } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { + throw new IllegalStateException("Video snapshots are only supported starting from API 18."); + } + GlCameraPreview glPreview = (GlCameraPreview) mPreview; + + // Output size is easy: + Size outputSize = getUncroppedSnapshotSize(Reference.OUTPUT); + if (outputSize == null) { + throw new IllegalStateException("outputSize should not be null."); + } + AspectRatio outputRatio = getAngles().flip(Reference.OUTPUT, Reference.VIEW) ? viewAspectRatio.flip() : viewAspectRatio; + Rect outputCrop = CropHelper.computeCrop(outputSize, outputRatio); + outputSize = new Size(outputCrop.width(), outputCrop.height()); + stub.size = outputSize; + stub.rotation = getAngles().offset(Reference.VIEW, Reference.OUTPUT, Axis.ABSOLUTE); + + // Start. + mVideoRecorder = new SnapshotVideoRecorder(Camera1Engine.this, glPreview); + mVideoRecorder.start(stub); } @Override - public void onBufferAvailable(@NonNull byte[] buffer) { - // TODO: sync with handler? - if (isCameraAvailable()) { - mCamera.addCallbackBuffer(buffer); + public void onVideoResult(@Nullable VideoResult.Stub result, @Nullable Exception exception) { + super.onVideoResult(result, exception); + if (result == null) { + // Something went wrong, lock the camera again. + mCamera.lock(); } } - @Override - public void onError(int error, Camera camera) { - if (error == Camera.CAMERA_ERROR_SERVER_DIED) { - // Looks like this is recoverable. - LOG.w("Recoverable error inside the onError callback.", "CAMERA_ERROR_SERVER_DIED"); - restart(); + //endregion + + //region Parameters + + private void applyAllParameters(@NonNull Camera.Parameters params) { + params.setRecordingHint(getMode() == Mode.VIDEO); + applyDefaultFocus(params); + applyFlash(params, Flash.OFF); + applyLocation(params, null); + applyWhiteBalance(params, WhiteBalance.AUTO); + applyHdr(params, Hdr.OFF); + applyZoom(params, 0F); + applyExposureCorrection(params, 0F); + applyPlaySounds(mPlaySounds); + } + + private void applyDefaultFocus(@NonNull Camera.Parameters params) { + List modes = params.getSupportedFocusModes(); + + if (getMode() == Mode.VIDEO && + modes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) { + params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO); return; } - String message = LOG.e("Internal Camera1 error.", error); - Exception runtime = new RuntimeException(message); - int reason; - switch (error) { - case Camera.CAMERA_ERROR_EVICTED: reason = CameraException.REASON_DISCONNECTED; break; - case Camera.CAMERA_ERROR_UNKNOWN: reason = CameraException.REASON_UNKNOWN; break; - default: reason = CameraException.REASON_UNKNOWN; + if (modes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) { + params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE); + return; } - throw new CameraException(runtime, reason); + + if (modes.contains(Camera.Parameters.FOCUS_MODE_INFINITY)) { + params.setFocusMode(Camera.Parameters.FOCUS_MODE_INFINITY); + return; + } + + if (modes.contains(Camera.Parameters.FOCUS_MODE_FIXED)) { + params.setFocusMode(Camera.Parameters.FOCUS_MODE_FIXED); + //noinspection UnnecessaryReturnStatement + return; + } + } + + @Override + public void setFlash(@NonNull Flash flash) { + final Flash old = mFlash; + mFlash = flash; + mHandler.run(new Runnable() { + @Override + public void run() { + if (getEngineState() == STATE_STARTED) { + Camera.Parameters params = mCamera.getParameters(); + if (applyFlash(params, old)) mCamera.setParameters(params); + } + mFlashOp.end(null); + } + }); + } + + private boolean applyFlash(@NonNull Camera.Parameters params, @NonNull Flash oldFlash) { + if (mCameraOptions.supports(mFlash)) { + params.setFlashMode((String) mMapper.map(mFlash)); + return true; + } + mFlash = oldFlash; + return false; } @Override public void setLocation(@Nullable Location location) { final Location oldLocation = mLocation; mLocation = location; - schedule(mLocationOp, true, new Runnable() { + mHandler.run(new Runnable() { @Override public void run() { - Camera.Parameters params = mCamera.getParameters(); - if (applyLocation(params, oldLocation)) mCamera.setParameters(params); + if (getEngineState() == STATE_STARTED) { + Camera.Parameters params = mCamera.getParameters(); + if (applyLocation(params, oldLocation)) mCamera.setParameters(params); + } + mLocationOp.end(null); } }); } @@ -353,11 +476,14 @@ public class Camera1Engine extends CameraEngine implements public void setWhiteBalance(@NonNull WhiteBalance whiteBalance) { final WhiteBalance old = mWhiteBalance; mWhiteBalance = whiteBalance; - schedule(mWhiteBalanceOp, true, new Runnable() { + mHandler.run(new Runnable() { @Override public void run() { - Camera.Parameters params = mCamera.getParameters(); - if (applyWhiteBalance(params, old)) mCamera.setParameters(params); + if (getEngineState() == STATE_STARTED) { + Camera.Parameters params = mCamera.getParameters(); + if (applyWhiteBalance(params, old)) mCamera.setParameters(params); + } + mWhiteBalanceOp.end(null); } }); } @@ -375,11 +501,14 @@ public class Camera1Engine extends CameraEngine implements public void setHdr(@NonNull Hdr hdr) { final Hdr old = mHdr; mHdr = hdr; - schedule(mHdrOp, true, new Runnable() { + mHandler.run(new Runnable() { @Override public void run() { - Camera.Parameters params = mCamera.getParameters(); - if (applyHdr(params, old)) mCamera.setParameters(params); + if (getEngineState() == STATE_STARTED) { + Camera.Parameters params = mCamera.getParameters(); + if (applyHdr(params, old)) mCamera.setParameters(params); + } + mHdrOp.end(null); } }); } @@ -393,268 +522,149 @@ public class Camera1Engine extends CameraEngine implements return false; } - @SuppressWarnings("UnusedReturnValue") - @TargetApi(17) - private boolean applyPlaySounds(boolean oldPlaySound) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - Camera.CameraInfo info = new Camera.CameraInfo(); - Camera.getCameraInfo(mCameraId, info); - if (info.canDisableShutterSound) { - try { - // this method is documented to throw on some occasions. #377 - return mCamera.enableShutterSound(mPlaySounds); - } catch (RuntimeException exception) { - return false; + @Override + public void setZoom(final float zoom, @Nullable final PointF[] points, final boolean notify) { + final float old = mZoomValue; + mZoomValue = zoom; + mHandler.run(new Runnable() { + @Override + public void run() { + if (getEngineState() == STATE_STARTED) { + Camera.Parameters params = mCamera.getParameters(); + if (applyZoom(params, old)) { + mCamera.setParameters(params); + if (notify) { + mCallback.dispatchOnZoomChanged(mZoomValue, points); + } + } } + mZoomOp.end(null); } - } - if (mPlaySounds) { + }); + } + + private boolean applyZoom(@NonNull Camera.Parameters params, float oldZoom) { + if (mCameraOptions.isZoomSupported()) { + float max = params.getMaxZoom(); + params.setZoom((int) (mZoomValue * max)); + mCamera.setParameters(params); return true; } - mPlaySounds = oldPlaySound; + mZoomValue = oldZoom; return false; } @Override - public void setFlash(@NonNull Flash flash) { - final Flash old = mFlash; - mFlash = flash; - schedule(mFlashOp, true, new Runnable() { + public void setExposureCorrection(final float EVvalue, @NonNull final float[] bounds, + @Nullable final PointF[] points, final boolean notify) { + final float old = mExposureCorrectionValue; + mExposureCorrectionValue = EVvalue; + mHandler.run(new Runnable() { @Override public void run() { - Camera.Parameters params = mCamera.getParameters(); - if (applyFlash(params, old)) mCamera.setParameters(params); + if (getEngineState() == STATE_STARTED) { + Camera.Parameters params = mCamera.getParameters(); + if (applyExposureCorrection(params, old)) { + mCamera.setParameters(params); + if (notify) { + mCallback.dispatchOnExposureCorrectionChanged(mExposureCorrectionValue, bounds, points); + } + } + } + mExposureCorrectionOp.end(null); } }); } - - private boolean applyFlash(@NonNull Camera.Parameters params, @NonNull Flash oldFlash) { - if (mCameraOptions.supports(mFlash)) { - params.setFlashMode((String) mMapper.map(mFlash)); + private boolean applyExposureCorrection(@NonNull Camera.Parameters params, float oldExposureCorrection) { + if (mCameraOptions.isExposureCorrectionSupported()) { + // Just make sure we're inside boundaries. + float max = mCameraOptions.getExposureCorrectionMaxValue(); + float min = mCameraOptions.getExposureCorrectionMinValue(); + float val = mExposureCorrectionValue; + val = val < min ? min : val > max ? max : val; // cap + mExposureCorrectionValue = val; + // Apply. + int indexValue = (int) (mExposureCorrectionValue / params.getExposureCompensationStep()); + params.setExposureCompensation(indexValue); return true; } - mFlash = oldFlash; + mExposureCorrectionValue = oldExposureCorrection; return false; } + @Override + public void setPlaySounds(boolean playSounds) { + final boolean old = mPlaySounds; + mPlaySounds = playSounds; + mHandler.run(new Runnable() { + @Override + public void run() { + if (getEngineState() == STATE_STARTED) { + applyPlaySounds(old); + } + mPlaySoundsOp.end(null); + } + }); + } - // Choose the best default focus, based on session type. - private void applyDefaultFocus(@NonNull Camera.Parameters params) { - List modes = params.getSupportedFocusModes(); - - if (mMode == Mode.VIDEO && - modes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) { - params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO); - return; - } - - if (modes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) { - params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE); - return; - } - - if (modes.contains(Camera.Parameters.FOCUS_MODE_INFINITY)) { - params.setFocusMode(Camera.Parameters.FOCUS_MODE_INFINITY); - return; + @SuppressWarnings("UnusedReturnValue") + @TargetApi(17) + private boolean applyPlaySounds(boolean oldPlaySound) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + Camera.CameraInfo info = new Camera.CameraInfo(); + Camera.getCameraInfo(mCameraId, info); + if (info.canDisableShutterSound) { + try { + // this method is documented to throw on some occasions. #377 + return mCamera.enableShutterSound(mPlaySounds); + } catch (RuntimeException exception) { + return false; + } + } } - - if (modes.contains(Camera.Parameters.FOCUS_MODE_FIXED)) { - params.setFocusMode(Camera.Parameters.FOCUS_MODE_FIXED); - //noinspection UnnecessaryReturnStatement - return; + if (mPlaySounds) { + return true; } + mPlaySounds = oldPlaySound; + return false; } - // ----------------- - // Picture recording stuff. + //endregion - @WorkerThread + //region Frame Processing + + @NonNull @Override - protected void onTakePicture(@NonNull PictureResult.Stub stub) { - stub.rotation = offset(REF_SENSOR, REF_OUTPUT); - stub.size = getPictureSize(REF_OUTPUT); - mPictureRecorder = new Full1PictureRecorder(stub, Camera1Engine.this, mCamera); - mPictureRecorder.take(); + protected FrameManager instantiateFrameManager() { + return new FrameManager(2, this); } - @WorkerThread @Override - protected void onTakePictureSnapshot(@NonNull PictureResult.Stub stub, @NonNull AspectRatio viewAspectRatio) { - stub.size = getUncroppedSnapshotSize(REF_OUTPUT); // Not the real size: it will be cropped to match the view ratio - stub.rotation = offset(REF_SENSOR, REF_OUTPUT); // Actually it will be rotated and set to 0. - AspectRatio outputRatio = flip(REF_OUTPUT, REF_VIEW) ? viewAspectRatio.flip() : viewAspectRatio; - - if (mPreview instanceof GlCameraPreview && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - mPictureRecorder = new SnapshotGlPictureRecorder(stub, this, (GlCameraPreview) mPreview, outputRatio); - } else { - mPictureRecorder = new Snapshot1PictureRecorder(stub, this, mCamera, outputRatio); + public void onBufferAvailable(@NonNull byte[] buffer) { + if (getEngineState() == STATE_STARTED) { + mCamera.addCallbackBuffer(buffer); } - mPictureRecorder.take(); } @Override public void onPreviewFrame(@NonNull byte[] data, Camera camera) { Frame frame = getFrameManager().getFrame(data, System.currentTimeMillis(), - offset(REF_SENSOR, REF_OUTPUT), + getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR), mPreviewStreamSize, - mPreviewStreamFormat); + PREVIEW_FORMAT); mCallback.dispatchFrame(frame); } - // ----------------- - // Video recording stuff. - - @Override - public void onVideoResult(@Nullable VideoResult.Stub result, @Nullable Exception exception) { - super.onVideoResult(result, exception); - if (result == null) { - // Something went wrong, lock the camera again. - mCamera.lock(); - } - } - - @Override - protected void onTakeVideo(@NonNull VideoResult.Stub stub) { - stub.rotation = offset(REF_SENSOR, REF_OUTPUT); - stub.size = flip(REF_SENSOR, REF_OUTPUT) ? mCaptureSize.flip() : mCaptureSize; - // Unlock the camera and start recording. - try { - mCamera.unlock(); - } catch (Exception e) { - // If this failed, we are unlikely able to record the video. - // Dispatch an error. - onVideoResult(null, e); - return; - } - if (!(mVideoRecorder instanceof Full1VideoRecorder)) { - mVideoRecorder = new Full1VideoRecorder(Camera1Engine.this, mCamera, mCameraId); - } - mVideoRecorder.start(stub); - } - - @SuppressLint("NewApi") - @WorkerThread - @Override - protected void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub, @NonNull AspectRatio viewAspectRatio) { - if (!(mPreview instanceof GlCameraPreview)) { - throw new IllegalStateException("Video snapshots are only supported with GlCameraPreview."); - } - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { - throw new IllegalStateException("Video snapshots are only supported starting from API 18."); - } - GlCameraPreview glPreview = (GlCameraPreview) mPreview; - - // Size and rotation turned out to be extremely tricky. In case of Snapshot1PictureRecorder - // we use the preview size in REF_OUTPUT (cropped) and offset(REF_SENSOR, REF_OUTPUT) as rotation. - // These values mean that we expect input to be in the REF_SENSOR system. - - // Here everything seems different. We would expect a difference because the two snapshot - // recorders have different mechanics (the picture one uses a SurfaceTexture with setBufferSize, - // the video one here uses the MediaCodec input surface which we can't control). - - // The strangest thing is the fact that the correct angle seems to be the same for FRONT and - // BACK sensor, which means that our sensor correction actually screws things up. For this reason - // facing value is temporarily set to BACK. - Facing realFacing = mFacing; - mFacing = Facing.BACK; - - // These are the angles that make it work on a Nexus5X, compared to the offset() results. - // For instance, SV means offset(REF_SENSOR, REF_VIEW). The rest should be clear. - // CONFIG | WANTED | SV | VS | VO | OV | SO | OS | - // ------------|--------|--------|--------|--------|--------|--------|--------| - // Vertical | 0 | 270 | 90 | 0 | 0 | 270 | 90 | - // Left | 270 | 270 | 90 | 270 | 90 | 180 | 180 | - // Right | 90 | 270 | 90 | 90 | 270 | 0 | 0 | - // Upside down | 180 | 270 | 90 | 180 | 180 | 90 | 270 | - - // The VO is the only correct value. Things change when using FRONT camera, in which case, - // no value is actually correct, and the needed values are the same of BACK! - // CONFIG | WANTED | SV | VS | VO | OV | SO | OS | - // ------------|--------|--------|--------|--------|--------|--------|--------| - // Vertical | 0 | 90 | 270 | 180 | 180 | 270 | 90 | - // Left | 270 | 90 | 270 | 270 | 90 | 0 | 0 | - // Right | 90 | 90 | 270 | 90 | 270 | 180 | 180 | - // Upside down | 180 | 90 | 270 | 0 | 0 | 90 | 270 | - - // Based on this we will use VO for everything. See if we get issues about distortion - // and maybe we can improve. The reason why this happen is beyond my understanding. - - Size outputSize = getUncroppedSnapshotSize(REF_OUTPUT); - if (outputSize == null) { - throw new IllegalStateException("outputSize should not be null."); - } - AspectRatio outputRatio = flip(REF_OUTPUT, REF_VIEW) ? viewAspectRatio.flip() : viewAspectRatio; - Rect outputCrop = CropHelper.computeCrop(outputSize, outputRatio); - outputSize = new Size(outputCrop.width(), outputCrop.height()); - stub.size = outputSize; - stub.rotation = offset(REF_VIEW, REF_OUTPUT); - - // Reset facing and start. - mFacing = realFacing; - if (!(mVideoRecorder instanceof SnapshotVideoRecorder)) { - mVideoRecorder = new SnapshotVideoRecorder(Camera1Engine.this, glPreview); - } - mVideoRecorder.start(stub); - } - - // ----------------- - // Zoom and simpler stuff. - - - @Override - public void setZoom(final float zoom, @Nullable final PointF[] points, final boolean notify) { - schedule(mZoomOp, true, new Runnable() { - @Override - public void run() { - if (!mCameraOptions.isZoomSupported()) return; - - mZoomValue = zoom; - Camera.Parameters params = mCamera.getParameters(); - float max = params.getMaxZoom(); - params.setZoom((int) (zoom * max)); - mCamera.setParameters(params); - - if (notify) { - mCallback.dispatchOnZoomChanged(zoom, points); - } - } - }); - } - - @Override - public void setExposureCorrection(final float EVvalue, @NonNull final float[] bounds, - @Nullable final PointF[] points, final boolean notify) { - schedule(mExposureCorrectionOp, true, new Runnable() { - @Override - public void run() { - if (!mCameraOptions.isExposureCorrectionSupported()) return; - - float value = EVvalue; - float max = mCameraOptions.getExposureCorrectionMaxValue(); - float min = mCameraOptions.getExposureCorrectionMinValue(); - value = value < min ? min : value > max ? max : value; // cap - mExposureCorrectionValue = value; - Camera.Parameters params = mCamera.getParameters(); - int indexValue = (int) (value / params.getExposureCompensationStep()); - params.setExposureCompensation(indexValue); - mCamera.setParameters(params); - - if (notify) { - mCallback.dispatchOnExposureCorrectionChanged(value, bounds, points); - } - } - }); - } - - // ----------------- - // Tap to focus stuff. + //endregion + //region Auto Focus @Override public void startAutoFocus(@Nullable final Gesture gesture, @NonNull final PointF point) { // Must get width and height from the UI thread. + // TODO could take mPreview.surfaceSize like Camera2 does? int viewWidth = 0, viewHeight = 0; if (mPreview != null && mPreview.hasSurface()) { viewWidth = mPreview.getView().getWidth(); @@ -662,14 +672,15 @@ public class Camera1Engine extends CameraEngine implements } final int viewWidthF = viewWidth; final int viewHeightF = viewHeight; - // Schedule. - schedule(null, true, new Runnable() { + mHandler.run(new Runnable() { @Override public void run() { + if (getEngineState() < STATE_STARTED) return; if (!mCameraOptions.isAutoFocusSupported()) return; final PointF p = new PointF(point.x, point.y); // copy. + int offset = getAngles().offset(Reference.SENSOR, Reference.VIEW, Axis.ABSOLUTE); List meteringAreas2 = computeMeteringAreas(p.x, p.y, - viewWidthF, viewHeightF, offset(REF_SENSOR, REF_VIEW)); + viewWidthF, viewHeightF, offset); List meteringAreas1 = meteringAreas2.subList(0, 1); // At this point we are sure that camera supports auto focus... right? Look at CameraView.onTouchEvent(). @@ -688,9 +699,7 @@ public class Camera1Engine extends CameraEngine implements mFocusEndRunnable = new Runnable() { @Override public void run() { - if (isCameraAvailable()) { - mCallback.dispatchOnFocusEnd(gesture, false, p); - } + mCallback.dispatchOnFocusEnd(gesture, false, p); } }; mHandler.post(AUTOFOCUS_END_DELAY_MILLIS, mFocusEndRunnable); @@ -717,6 +726,7 @@ public class Camera1Engine extends CameraEngine implements // Let the mFocusEndRunnable do its job. (could remove it and quickly dispatch // onFocusEnd here, but let's make it simpler). } + } }); } @@ -763,38 +773,21 @@ public class Camera1Engine extends CameraEngine implements return new Rect(left, top, right, bottom); } - - // ----------------- - // Size stuff. - - public final int getPreviewStreamFormat() { - return mPreviewStreamFormat; - } - - - @NonNull - @Override - protected List getPreviewStreamAvailableSizes() { - List sizes = mCamera.getParameters().getSupportedPreviewSizes(); - List result = new ArrayList<>(sizes.size()); - for (Camera.Size size : sizes) { - Size add = new Size(size.width, size.height); - if (!result.contains(add)) result.add(add); + private final Runnable mFocusResetRunnable = new Runnable() { + @Override + public void run() { + if (getEngineState() < STATE_STARTED) return; + mCamera.cancelAutoFocus(); + Camera.Parameters params = mCamera.getParameters(); + int maxAF = params.getMaxNumFocusAreas(); + int maxAE = params.getMaxNumMeteringAreas(); + if (maxAF > 0) params.setFocusAreas(null); + if (maxAE > 0) params.setMeteringAreas(null); + applyDefaultFocus(params); // Revert to internal focus. + mCamera.setParameters(params); } - LOG.i("getPreviewStreamAvailableSizes:", result); - return result; - } + }; - @Override - public void setPlaySounds(boolean playSounds) { - final boolean old = mPlaySounds; - mPlaySounds = playSounds; - schedule(mPlaySoundsOp, true, new Runnable() { - @Override - public void run() { - applyPlaySounds(old); - } - }); - } + //endregion } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java index b251d928..7c700d68 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java @@ -44,6 +44,8 @@ import com.otaliastudios.cameraview.controls.Flash; import com.otaliastudios.cameraview.controls.Hdr; import com.otaliastudios.cameraview.controls.Mode; import com.otaliastudios.cameraview.controls.WhiteBalance; +import com.otaliastudios.cameraview.engine.offset.Axis; +import com.otaliastudios.cameraview.engine.offset.Reference; import com.otaliastudios.cameraview.frame.Frame; import com.otaliastudios.cameraview.frame.FrameManager; import com.otaliastudios.cameraview.gesture.Gesture; @@ -299,7 +301,8 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv CameraCharacteristics characteristics = mManager.getCameraCharacteristics(cameraId); if (internalFacing == readCharacteristic(characteristics, CameraCharacteristics.LENS_FACING, -99)) { mCameraId = cameraId; - mSensorOffset = readCharacteristic(characteristics, CameraCharacteristics.SENSOR_ORIENTATION, 0); + int sensorOffset = readCharacteristic(characteristics, CameraCharacteristics.SENSOR_ORIENTATION, 0); + setSensorOffset(facing, sensorOffset); return true; } } catch (CameraAccessException ignore) { @@ -330,7 +333,8 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv try { LOG.i("createCamera:", "Applying default parameters."); mCameraCharacteristics = mManager.getCameraCharacteristics(mCameraId); - mCameraOptions = new CameraOptions(mManager, mCameraId, flip(REF_SENSOR, REF_VIEW)); + boolean flip = getAngles().flip(Reference.SENSOR, Reference.VIEW); + mCameraOptions = new CameraOptions(mManager, mCameraId, flip); createRepeatingRequestBuilder(CameraDevice.TEMPLATE_PREVIEW); } catch (CameraAccessException e) { task.trySetException(createCameraException(e)); @@ -406,7 +410,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv outputSurfaces.add(mPreviewStreamSurface); // 2. VIDEO RECORDING - if (mMode == Mode.VIDEO) { + if (getMode() == Mode.VIDEO) { if (Full2VideoRecorder.SUPPORTS_PERSISTENT_SURFACE) { mFullVideoPersistentSurface = MediaCodec.createPersistentInputSurface(); outputSurfaces.add(mFullVideoPersistentSurface); @@ -422,7 +426,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv } // 3. PICTURE RECORDING - if (mMode == Mode.PICTURE) { + if (getMode() == Mode.PICTURE) { mPictureReader = ImageReader.newInstance( mCaptureSize.getWidth(), mCaptureSize.getHeight(), @@ -433,7 +437,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv } // 4. FRAME PROCESSING - if (mHasFrameProcessors) { + if (hasFrameProcessors()) { // Choose the size. StreamConfigurationMap streamMap = mCameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); if (streamMap == null) throw new RuntimeException("StreamConfigurationMap is null. Should not happen."); @@ -489,13 +493,13 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv LOG.i("onStartPreview", "Dispatching onCameraPreviewStreamSizeChanged."); mCallback.onCameraPreviewStreamSizeChanged(); - Size previewSizeForView = getPreviewStreamSize(REF_VIEW); + Size previewSizeForView = getPreviewStreamSize(Reference.VIEW); if (previewSizeForView == null) { throw new IllegalStateException("previewStreamSize should not be null at this point."); } mPreview.setStreamSize(previewSizeForView.getWidth(), previewSizeForView.getHeight()); - mPreview.setDrawRotation(mDisplayOffset); - if (mHasFrameProcessors) { + mPreview.setDrawRotation(getAngles().offset(Reference.BASE, Reference.VIEW, Axis.ABSOLUTE)); + if (hasFrameProcessors()) { getFrameManager().setUp(ImageFormat.getBitsPerPixel(FRAME_PROCESSING_FORMAT), mFrameProcessingSize); } @@ -537,7 +541,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv mVideoRecorder = null; } mPictureRecorder = null; - if (mHasFrameProcessors) { + if (hasFrameProcessors()) { getFrameManager().release(); } try { @@ -613,9 +617,9 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv @WorkerThread @Override protected void onTakePictureSnapshot(@NonNull PictureResult.Stub stub, @NonNull AspectRatio viewAspectRatio) { - stub.size = getUncroppedSnapshotSize(REF_OUTPUT); // Not the real size: it will be cropped to match the view ratio - stub.rotation = offset(REF_SENSOR, REF_OUTPUT); // Actually it will be rotated and set to 0. - AspectRatio outputRatio = flip(REF_OUTPUT, REF_VIEW) ? viewAspectRatio.flip() : viewAspectRatio; + stub.size = getUncroppedSnapshotSize(Reference.OUTPUT); // Not the real size: it will be cropped to match the view ratio + stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR); // Actually it will be rotated and set to 0. + AspectRatio outputRatio = getAngles().flip(Reference.OUTPUT, Reference.VIEW) ? viewAspectRatio.flip() : viewAspectRatio; if (mPreview instanceof GlCameraPreview) { mPictureRecorder = new SnapshotGlPictureRecorder(stub, this, (GlCameraPreview) mPreview, outputRatio); } else { @@ -626,8 +630,8 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv @Override protected void onTakePicture(@NonNull PictureResult.Stub stub) { - stub.rotation = offset(REF_SENSOR, REF_OUTPUT); - stub.size = getPictureSize(REF_OUTPUT); + stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR); + stub.size = getPictureSize(Reference.OUTPUT); try { CaptureRequest.Builder builder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); applyAllParameters(builder); @@ -647,7 +651,6 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv public void onPictureResult(@Nullable PictureResult.Stub result, @Nullable Exception error) { boolean fullPicture = mPictureRecorder instanceof Full2PictureRecorder; super.onPictureResult(result, error); - //noinspection StatementWithEmptyBody if (fullPicture && mPictureCaptureStopsPreview) { // See comments in Full2PictureRecorder. applyRepeatingRequestBuilder(); @@ -662,8 +665,8 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv @Override protected void onTakeVideo(@NonNull VideoResult.Stub stub) { LOG.i("onTakeVideo", "called."); - stub.rotation = offset(REF_SENSOR, REF_OUTPUT); - stub.size = flip(REF_SENSOR, REF_OUTPUT) ? mCaptureSize.flip() : mCaptureSize; + stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR); + stub.size = getAngles().flip(Reference.SENSOR, Reference.OUTPUT) ? mCaptureSize.flip() : mCaptureSize; if (!Full2VideoRecorder.SUPPORTS_PERSISTENT_SURFACE) { // On API 21 and 22, we must restart the session at each time. // Save the pending data and restart the session. @@ -676,9 +679,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv } private void doTakeVideo(@NonNull final VideoResult.Stub stub) { - if (!(mVideoRecorder instanceof Full2VideoRecorder)) { - mVideoRecorder = new Full2VideoRecorder(this, mCameraId, mFullVideoPersistentSurface); - } + mVideoRecorder = new Full2VideoRecorder(this, mCameraId, mFullVideoPersistentSurface); Full2VideoRecorder recorder = (Full2VideoRecorder) mVideoRecorder; try { createRepeatingRequestBuilder(CameraDevice.TEMPLATE_RECORD); @@ -698,10 +699,6 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv } } - /** - * See {@link CameraEngine#onTakeVideoSnapshot(VideoResult.Stub, AspectRatio)} - * to read about the size and rotation computation. - */ @WorkerThread @Override protected void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub, @NonNull AspectRatio viewAspectRatio) { @@ -709,23 +706,20 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv throw new IllegalStateException("Video snapshots are only supported with GlCameraPreview."); } GlCameraPreview glPreview = (GlCameraPreview) mPreview; - Facing realFacing = mFacing; - mFacing = Facing.BACK; - Size outputSize = getUncroppedSnapshotSize(REF_OUTPUT); + + // Output size is easy: + Size outputSize = getUncroppedSnapshotSize(Reference.OUTPUT); if (outputSize == null) { throw new IllegalStateException("outputSize should not be null."); } - AspectRatio outputRatio = flip(REF_OUTPUT, REF_VIEW) ? viewAspectRatio.flip() : viewAspectRatio; + AspectRatio outputRatio = getAngles().flip(Reference.OUTPUT, Reference.VIEW) ? viewAspectRatio.flip() : viewAspectRatio; Rect outputCrop = CropHelper.computeCrop(outputSize, outputRatio); outputSize = new Size(outputCrop.width(), outputCrop.height()); stub.size = outputSize; - stub.rotation = offset(REF_VIEW, REF_OUTPUT); + stub.rotation = getAngles().offset(Reference.VIEW, Reference.OUTPUT, Axis.ABSOLUTE); - // Reset facing and start. - mFacing = realFacing; - if (!(mVideoRecorder instanceof SnapshotVideoRecorder)) { - mVideoRecorder = new SnapshotVideoRecorder(this, glPreview); - } + // Start. + mVideoRecorder = new SnapshotVideoRecorder(this, glPreview); mVideoRecorder.start(stub); } @@ -764,7 +758,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv int[] modesArray = readCharacteristic(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES, new int[]{}); List modes = new ArrayList<>(); for (int mode : modesArray) { modes.add(mode); } - if (mMode == Mode.VIDEO && + if (getMode() == Mode.VIDEO && modes.contains(CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO)) { builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO); return; @@ -1053,7 +1047,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv if (getEngineState() == STATE_STARTED) { Frame frame = getFrameManager().getFrame(data, System.currentTimeMillis(), - offset(REF_SENSOR, REF_OUTPUT), + getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR), mFrameProcessingSize, FRAME_PROCESSING_FORMAT); mCallback.dispatchFrame(frame); @@ -1064,8 +1058,8 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv @Override public void setHasFrameProcessors(final boolean hasFrameProcessors) { + super.setHasFrameProcessors(hasFrameProcessors); LOG.i("setHasFrameProcessors", "changed to", hasFrameProcessors, "posting."); - mHasFrameProcessors = hasFrameProcessors; mHandler.run(new Runnable() { @Override public void run() { @@ -1102,12 +1096,11 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv // This is a good Q/A. https://stackoverflow.com/a/33181620/4288782 // At first, the point is relative to the View system and does not account our own cropping. // Will keep updating these two below. - //noinspection UnnecessaryLocalVariable PointF referencePoint = new PointF(point.x, point.y); Size referenceSize /* = previewSurfaceSize */; // 1. Account for cropping. - Size previewStreamSize = getPreviewStreamSize(REF_VIEW); + Size previewStreamSize = getPreviewStreamSize(Reference.VIEW); Size previewSurfaceSize = mPreview.getSurfaceSize(); if (previewStreamSize == null) throw new IllegalStateException("getPreviewStreamSize should not be null at this point."); AspectRatio previewStreamAspectRatio = AspectRatio.of(previewStreamSize); @@ -1134,7 +1127,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv // 3. Rotate to the stream coordinate system. // Not elegant, but the sin/cos way was failing. - int angle = offset(REF_SENSOR, REF_VIEW); + int angle = getAngles().offset(Reference.SENSOR, Reference.VIEW, Axis.ABSOLUTE); boolean flip = angle % 180 != 0; float tempX = referencePoint.x; float tempY = referencePoint.y; if (angle == 0) { diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java index 5dedfa43..17bbf096 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java @@ -1,9 +1,7 @@ package com.otaliastudios.cameraview.engine; import android.content.Context; -import android.graphics.Camera; import android.graphics.PointF; -import android.hardware.camera2.CameraCharacteristics; import android.location.Location; @@ -21,6 +19,8 @@ import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.CameraOptions; import com.otaliastudios.cameraview.PictureResult; import com.otaliastudios.cameraview.VideoResult; +import com.otaliastudios.cameraview.engine.offset.Angles; +import com.otaliastudios.cameraview.engine.offset.Reference; import com.otaliastudios.cameraview.frame.Frame; import com.otaliastudios.cameraview.frame.FrameManager; import com.otaliastudios.cameraview.internal.utils.Op; @@ -84,8 +84,8 @@ import java.util.concurrent.TimeUnit; * - {@link #start()}: ASYNC - starts the engine (S2). When possible, at a later time, S3 and S4 are also performed. * - {@link #stop()}: ASYNC - stops everything: undoes S4, then S3, then S2. * - {@link #restart()}: ASYNC - completes a stop then a start. - * - {@link #destroy()}: ASYNC - performs a {@link #stop()} that will go on no matter the exceptions, without throwing. - * Makes the engine unusable and clears resources. + * - {@link #destroy()}: SYNC - performs a {@link #stop()} that will go on no matter the exceptions, without throwing. + * Makes the engine unusable and clears resources. * * For example, we expose the engine (S2) state through {@link #getEngineState()}. It will be: * - {@link #STATE_STARTING} if we're into step 2 @@ -139,59 +139,52 @@ public abstract class CameraEngine implements private static final String TAG = CameraEngine.class.getSimpleName(); private static final CameraLogger LOG = CameraLogger.create(TAG); - @SuppressWarnings("WeakerAccess") + @SuppressWarnings({"WeakerAccess", "unused"}) public static final int STATE_STOPPING = CameraEngineStep.STATE_STOPPING; public static final int STATE_STOPPED = CameraEngineStep.STATE_STOPPED; - @SuppressWarnings("WeakerAccess") + @SuppressWarnings({"WeakerAccess", "unused"}) public static final int STATE_STARTING = CameraEngineStep.STATE_STARTING; public static final int STATE_STARTED = CameraEngineStep.STATE_STARTED; - public static final int REF_SENSOR = 0; - public static final int REF_VIEW = 1; - public static final int REF_OUTPUT = 2; - - protected final Callback mCallback; - private final FrameManager mFrameManager; - protected CameraPreview mPreview; - protected WorkerHandler mHandler; + // Need to be protected + @SuppressWarnings("WeakerAccess") protected WorkerHandler mHandler; + @SuppressWarnings("WeakerAccess") protected final Callback mCallback; + @SuppressWarnings("WeakerAccess") protected CameraPreview mPreview; + @SuppressWarnings("WeakerAccess") protected CameraOptions mCameraOptions; + @SuppressWarnings("WeakerAccess") protected Mapper mMapper; + @SuppressWarnings("WeakerAccess") protected PictureRecorder mPictureRecorder; + @SuppressWarnings("WeakerAccess") protected VideoRecorder mVideoRecorder; + @SuppressWarnings("WeakerAccess") protected Size mCaptureSize; + @SuppressWarnings("WeakerAccess") protected Size mPreviewStreamSize; + @SuppressWarnings("WeakerAccess") protected Flash mFlash; + @SuppressWarnings("WeakerAccess") protected WhiteBalance mWhiteBalance; + @SuppressWarnings("WeakerAccess") protected VideoCodec mVideoCodec; + @SuppressWarnings("WeakerAccess") protected Hdr mHdr; + @SuppressWarnings("WeakerAccess") protected Location mLocation; + @SuppressWarnings("WeakerAccess") protected float mZoomValue; + @SuppressWarnings("WeakerAccess") protected float mExposureCorrectionValue; + @SuppressWarnings("WeakerAccess") protected boolean mPlaySounds; + + // Can be private @VisibleForTesting Handler mCrashHandler; - - protected Facing mFacing; - protected Flash mFlash; - protected WhiteBalance mWhiteBalance; - protected VideoCodec mVideoCodec; - protected Mode mMode; - protected Hdr mHdr; - protected Location mLocation; - protected Audio mAudio; - protected float mZoomValue; - protected float mExposureCorrectionValue; - protected boolean mPlaySounds; - protected boolean mHasFrameProcessors; - + private final FrameManager mFrameManager; + private final Angles mAngles; @Nullable private SizeSelector mPreviewStreamSizeSelector; private SizeSelector mPictureSizeSelector; private SizeSelector mVideoSizeSelector; - - @VisibleForTesting int mSnapshotMaxWidth = Integer.MAX_VALUE; // in REF_VIEW for consistency with SizeSelectors - @VisibleForTesting int mSnapshotMaxHeight = Integer.MAX_VALUE; // in REF_VIEW for consistency with SizeSelectors - - protected CameraOptions mCameraOptions; - protected Mapper mMapper; - protected PictureRecorder mPictureRecorder; - protected VideoRecorder mVideoRecorder; - protected long mVideoMaxSize; - protected int mVideoMaxDuration; - protected int mVideoBitRate; - protected int mAudioBitRate; - protected Size mCaptureSize; - protected Size mPreviewStreamSize; - protected long mAutoFocusResetDelayMillis; - - protected int mSensorOffset; - protected int mDisplayOffset; - protected int mDeviceOrientation; - + private Facing mFacing; + private Mode mMode; + private Audio mAudio; + private long mVideoMaxSize; + private int mVideoMaxDuration; + private int mVideoBitRate; + private int mAudioBitRate; + private boolean mHasFrameProcessors; + private long mAutoFocusResetDelayMillis; + private int mSnapshotMaxWidth = Integer.MAX_VALUE; // in REF_VIEW for consistency with SizeSelectors + private int mSnapshotMaxHeight = Integer.MAX_VALUE; // in REF_VIEW for consistency with SizeSelectors + + // Steps private final CameraEngineStep.Callback mStepCallback = new CameraEngineStep.Callback() { @Override @NonNull public Executor getExecutor() { return mHandler.getExecutor(); } @Override public void handleException(@NonNull Exception exception) { @@ -203,29 +196,23 @@ public abstract class CameraEngine implements private CameraEngineStep mPreviewStep = new CameraEngineStep("preview", mStepCallback); private CameraEngineStep mAllStep = new CameraEngineStep("all", mStepCallback); - // Used for testing. - @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) - Op mZoomOp = new Op<>(); - @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) - Op mExposureCorrectionOp = new Op<>(); - @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) - Op mFlashOp = new Op<>(); - @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) - Op mWhiteBalanceOp = new Op<>(); - @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) - Op mHdrOp = new Op<>(); - @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) - Op mLocationOp = new Op<>(); + // Ops used for testing. @VisibleForTesting Op mStartVideoOp = new Op<>(); - @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) - Op mPlaySoundsOp = new Op<>(); - - protected CameraEngine(Callback callback) { + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op mZoomOp = new Op<>(); + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op mExposureCorrectionOp = new Op<>(); + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op mFlashOp = new Op<>(); + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op mWhiteBalanceOp = new Op<>(); + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op mHdrOp = new Op<>(); + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op mLocationOp = new Op<>(); + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op mPlaySoundsOp = new Op<>(); + + protected CameraEngine(@NonNull Callback callback) { mCallback = callback; mCrashHandler = new Handler(Looper.getMainLooper()); mHandler = WorkerHandler.get("CameraViewEngine"); mHandler.getThread().setUncaughtExceptionHandler(new CrashExceptionHandler()); mFrameManager = instantiateFrameManager(); + mAngles = new Angles(); } public void setPreview(@NonNull CameraPreview cameraPreview) { @@ -320,7 +307,7 @@ public abstract class CameraEngine implements return mBindStep.getState(); } - @SuppressWarnings("unused") + @SuppressWarnings({"unused", "WeakerAccess"}) public final int getPreviewState() { return mPreviewStep.getState(); } @@ -580,7 +567,7 @@ public abstract class CameraEngine implements */ @Override public final void onSurfaceAvailable() { - LOG.i("onSurfaceAvailable:", "Size is", getPreviewSurfaceSize(REF_VIEW)); + LOG.i("onSurfaceAvailable:", "Size is", getPreviewSurfaceSize(Reference.VIEW)); mHandler.run(new Runnable() { @Override public void run() { @@ -597,7 +584,7 @@ public abstract class CameraEngine implements @Override public final void onSurfaceChanged() { - LOG.i("onSurfaceChanged:", "Size is", getPreviewSurfaceSize(REF_VIEW), "Posting."); + LOG.i("onSurfaceChanged:", "Size is", getPreviewSurfaceSize(Reference.VIEW), "Posting."); mHandler.run(new Runnable() { @Override public void run() { @@ -658,7 +645,7 @@ public abstract class CameraEngine implements * that would cause deadlocks due to us awaiting for {@link #stop()} to return. */ public void destroy() { - LOG.i("destroy:", "state:", getEngineStateName()); + LOG.i("destroy:", "state:", getEngineStateName(), "thread:", Thread.currentThread()); // Prevent CameraEngine leaks. Don't set to null, or exceptions // inside the standard stop() method might crash the main thread. mHandler.getThread().setUncaughtExceptionHandler(new NoOpExceptionHandler()); @@ -674,6 +661,7 @@ public abstract class CameraEngine implements try { boolean success = latch.await(3, TimeUnit.SECONDS); if (!success) { + // TODO seems like this is always the case? LOG.e("Probably some deadlock in destroy.", "Current thread:", Thread.currentThread(), "Handler thread: ", mHandler.getThread()); @@ -790,60 +778,115 @@ public abstract class CameraEngine implements //endregion - //region final setters + //region Final setters and getters + + @SuppressWarnings("WeakerAccess") + public final Angles getAngles() { + return mAngles; + } + + @SuppressWarnings("WeakerAccess") + protected final void setSensorOffset(@NonNull Facing facing, int sensorOffset) { + mAngles.setSensorOffset(facing, sensorOffset); + } // This is called before start() and never again. public final void setDisplayOffset(int displayOffset) { - mDisplayOffset = displayOffset; + mAngles.setDisplayOffset(displayOffset); } // This can be called multiple times. public final void setDeviceOrientation(int deviceOrientation) { - mDeviceOrientation = deviceOrientation; + mAngles.setDeviceOrientation(deviceOrientation); } public final void setPreviewStreamSizeSelector(@Nullable SizeSelector selector) { mPreviewStreamSizeSelector = selector; } + @Nullable + public final SizeSelector getPreviewStreamSizeSelector() { + return mPreviewStreamSizeSelector; + } + public final void setPictureSizeSelector(@NonNull SizeSelector selector) { mPictureSizeSelector = selector; } + @NonNull + public final SizeSelector getPictureSizeSelector() { + return mPictureSizeSelector; + } + public final void setVideoSizeSelector(@NonNull SizeSelector selector) { mVideoSizeSelector = selector; } + @NonNull + public final SizeSelector getVideoSizeSelector() { + return mVideoSizeSelector; + } + public final void setVideoMaxSize(long videoMaxSizeBytes) { mVideoMaxSize = videoMaxSizeBytes; } + public final long getVideoMaxSize() { + return mVideoMaxSize; + } + public final void setVideoMaxDuration(int videoMaxDurationMillis) { mVideoMaxDuration = videoMaxDurationMillis; } + public final int getVideoMaxDuration() { + return mVideoMaxDuration; + } + public final void setVideoCodec(@NonNull VideoCodec codec) { mVideoCodec = codec; } + public final VideoCodec getVideoCodec() { + return mVideoCodec; + } + public final void setVideoBitRate(int videoBitRate) { mVideoBitRate = videoBitRate; } + public final int getVideoBitRate() { + return mVideoBitRate; + } + public final void setAudioBitRate(int audioBitRate) { mAudioBitRate = audioBitRate; } + public final int getAudioBitRate() { + return mAudioBitRate; + } + public final void setSnapshotMaxWidth(int maxWidth) { mSnapshotMaxWidth = maxWidth; } + public int getSnapshotMaxWidth() { + return mSnapshotMaxWidth; + } + public final void setSnapshotMaxHeight(int maxHeight) { mSnapshotMaxHeight = maxHeight; } + public int getSnapshotMaxHeight() { + return mSnapshotMaxHeight; + } + public final void setAutoFocusResetDelay(long delayMillis) { mAutoFocusResetDelayMillis = delayMillis; } + public final long getAutoFocusResetDelay() { return mAutoFocusResetDelayMillis; } + /** * Sets a new facing value. This will restart the session (if there's any) * so that we can open the new facing camera. @@ -867,6 +910,11 @@ public abstract class CameraEngine implements } } + @NonNull + public final Facing getFacing() { + return mFacing; + } + /** * Sets a new audio value that will be used for video recordings. * @param audio desired audio @@ -881,6 +929,11 @@ public abstract class CameraEngine implements } } + @NonNull + public final Audio getAudio() { + return mAudio; + } + /** * Sets the desired mode (either picture or video). * @param mode desired mode. @@ -899,6 +952,64 @@ public abstract class CameraEngine implements } } + @NonNull + public final Mode getMode() { + return mMode; + } + + @NonNull + public final FrameManager getFrameManager() { + return mFrameManager; + } + + @Nullable + public final CameraOptions getCameraOptions() { + return mCameraOptions; + } + + @NonNull + public final Flash getFlash() { + return mFlash; + } + + @NonNull + public final WhiteBalance getWhiteBalance() { + return mWhiteBalance; + } + + @NonNull + public final Hdr getHdr() { + return mHdr; + } + + @Nullable + public final Location getLocation() { + return mLocation; + } + + public final float getZoomValue() { + return mZoomValue; + } + + public final float getExposureCorrectionValue() { + return mExposureCorrectionValue; + } + + @CallSuper + public void setHasFrameProcessors(boolean hasFrameProcessors) { + mHasFrameProcessors = hasFrameProcessors; + } + + @SuppressWarnings("WeakerAccess") + public final boolean hasFrameProcessors() { + return mHasFrameProcessors; + } + + @SuppressWarnings("WeakerAccess") + protected final boolean shouldResetAutoFocus() { + return mAutoFocusResetDelayMillis > 0 && mAutoFocusResetDelayMillis != Long.MAX_VALUE; + } + //endregion //region Abstract setters and APIs @@ -907,8 +1018,8 @@ public abstract class CameraEngine implements * Camera is about to be opened. Implementors should look into available cameras * and see if anyone matches the given {@link Facing value}. * - * If so, implementors should set {@link #mSensorOffset} and any other information - * (like camera ID) needed to start teh engine. + * If so, implementors should set {@link #setSensorOffset(Facing, int)} and any other information + * (like camera ID) needed to start the engine. * * @param facing the facing value * @return true if we have one @@ -945,12 +1056,14 @@ public abstract class CameraEngine implements public abstract void setPlaySounds(boolean playSounds); - public abstract void setHasFrameProcessors(boolean hasFrameProcessors); - //endregion //region picture and video control + public final boolean isTakingPicture() { + return mPictureRecorder != null; + } + /* not final for tests */ public void takePicture(final @NonNull PictureResult.Stub stub) { LOG.v("takePicture", "scheduling"); @@ -970,10 +1083,10 @@ public abstract class CameraEngine implements onTakePicture(stub); } }); - }; + } /** - * The snapshot size is the {@link #getPreviewStreamSize(int)}, but cropped based on the + * The snapshot size is the {@link #getPreviewStreamSize(Reference)}, but cropped based on the * view/surface aspect ratio. * @param stub a picture stub * @param viewAspectRatio the view aspect ratio @@ -990,9 +1103,6 @@ public abstract class CameraEngine implements stub.isSnapshot = true; stub.facing = mFacing; // Leave the other parameters to subclasses. - LOG.v("takePictureSnapshot", "Rotations", "SV", offset(REF_SENSOR, REF_VIEW), "VS", offset(REF_VIEW, REF_SENSOR)); - LOG.v("takePictureSnapshot", "Rotations", "SO", offset(REF_SENSOR, REF_OUTPUT), "OS", offset(REF_OUTPUT, REF_SENSOR)); - LOG.v("takePictureSnapshot", "Rotations", "VO", offset(REF_VIEW, REF_OUTPUT), "OV", offset(REF_OUTPUT, REF_VIEW)); onTakePictureSnapshot(stub, viewAspectRatio); } }); @@ -1014,6 +1124,10 @@ public abstract class CameraEngine implements } } + public final boolean isTakingVideo() { + return mVideoRecorder != null && mVideoRecorder.isRecording(); + } + public final void takeVideo(final @NonNull VideoResult.Stub stub, final @NonNull File file) { LOG.v("takeVideo", "scheduling"); mHandler.run(new Runnable() { @@ -1110,187 +1224,31 @@ public abstract class CameraEngine implements //endregion - //region final getters - - @NonNull - public final FrameManager getFrameManager() { - return mFrameManager; - } - - @Nullable - public final CameraOptions getCameraOptions() { - return mCameraOptions; - } - - @NonNull - public final Facing getFacing() { - return mFacing; - } - - @NonNull - public final Flash getFlash() { - return mFlash; - } - - @NonNull - public final WhiteBalance getWhiteBalance() { - return mWhiteBalance; - } - - public final VideoCodec getVideoCodec() { - return mVideoCodec; - } - - public final int getVideoBitRate() { - return mVideoBitRate; - } - - public final long getVideoMaxSize() { - return mVideoMaxSize; - } - - public final int getVideoMaxDuration() { - return mVideoMaxDuration; - } - - @NonNull - public final Mode getMode() { - return mMode; - } - - @NonNull - public final Hdr getHdr() { - return mHdr; - } - - @Nullable - public final Location getLocation() { - return mLocation; - } - - @NonNull - public final Audio getAudio() { - return mAudio; - } - - public final int getAudioBitRate() { - return mAudioBitRate; - } - - @SuppressWarnings("unused") - @Nullable - @VisibleForTesting - final SizeSelector getPreviewStreamSizeSelector() { - return mPreviewStreamSizeSelector; - } - - @SuppressWarnings("unused") - @NonNull - public final SizeSelector getPictureSizeSelector() { - return mPictureSizeSelector; - } - - @SuppressWarnings("unused") - @NonNull - public final SizeSelector getVideoSizeSelector() { - return mVideoSizeSelector; - } - - public final float getZoomValue() { - return mZoomValue; - } - - public final float getExposureCorrectionValue() { - return mExposureCorrectionValue; - } - - public final boolean isTakingVideo() { - return mVideoRecorder != null && mVideoRecorder.isRecording(); - } - - public final boolean isTakingPicture() { - return mPictureRecorder != null; - } - - public final long getAutoFocusResetDelay() { return mAutoFocusResetDelayMillis; } - - public boolean getHasFrameProcessors() { - return mHasFrameProcessors; - } - - //endregion - - final boolean shouldResetAutoFocus() { - return mAutoFocusResetDelayMillis > 0 && mAutoFocusResetDelayMillis != Long.MAX_VALUE; - } - - //region Orientation utils - - private int computeSensorToViewOffset() { - if (mFacing == Facing.FRONT) { - return (360 - ((mSensorOffset + mDisplayOffset) % 360)) % 360; - } else { - return (mSensorOffset - mDisplayOffset + 360) % 360; - } - } - - private int computeSensorToOutputOffset() { - if (mFacing == Facing.FRONT) { - return (mSensorOffset - mDeviceOrientation + 360) % 360; - } else { - return (mSensorOffset + mDeviceOrientation) % 360; - } - } - - // o(S, V) - o(S, O) - // displayOffset - deviceOrientation - - // Returns the offset between two reference systems. - @SuppressWarnings("WeakerAccess") - public final int offset(int fromReference, int toReference) { - if (fromReference == toReference) return 0; - // We only know how to compute offsets with respect to REF_SENSOR. - // That's why we separate the two cases. - if (fromReference == REF_SENSOR) { - return toReference == REF_VIEW ? - computeSensorToViewOffset() : - computeSensorToOutputOffset(); - } - // Maybe the sensor is the other. - if (toReference == REF_SENSOR) { - return (-offset(toReference, fromReference) + 360) % 360; - } - // None of them is the sensor. Use a difference. - return (offset(REF_SENSOR, toReference) - offset(REF_SENSOR, fromReference) + 360) % 360; - } - - public final boolean flip(int reference1, int reference2) { - return offset(reference1, reference2) % 180 != 0; - } + //region Size utilities @Nullable - public final Size getPictureSize(@SuppressWarnings("SameParameterValue") int reference) { + public final Size getPictureSize(@SuppressWarnings("SameParameterValue") @NonNull Reference reference) { if (mCaptureSize == null || mMode == Mode.VIDEO) return null; - return flip(REF_SENSOR, reference) ? mCaptureSize.flip() : mCaptureSize; + return getAngles().flip(Reference.SENSOR, reference) ? mCaptureSize.flip() : mCaptureSize; } @Nullable - public final Size getVideoSize(@SuppressWarnings("SameParameterValue") int reference) { + public final Size getVideoSize(@SuppressWarnings("SameParameterValue") @NonNull Reference reference) { if (mCaptureSize == null || mMode == Mode.PICTURE) return null; - return flip(REF_SENSOR, reference) ? mCaptureSize.flip() : mCaptureSize; + return getAngles().flip(Reference.SENSOR, reference) ? mCaptureSize.flip() : mCaptureSize; } @Nullable - public final Size getPreviewStreamSize(int reference) { + public final Size getPreviewStreamSize(@NonNull Reference reference) { if (mPreviewStreamSize == null) return null; - return flip(REF_SENSOR, reference) ? mPreviewStreamSize.flip() : mPreviewStreamSize; + return getAngles().flip(Reference.SENSOR, reference) ? mPreviewStreamSize.flip() : mPreviewStreamSize; } @SuppressWarnings("SameParameterValue") @Nullable - private Size getPreviewSurfaceSize(int reference) { + private Size getPreviewSurfaceSize(@NonNull Reference reference) { if (mPreview == null) return null; - return flip(REF_VIEW, reference) ? mPreview.getSurfaceSize().flip() : mPreview.getSurfaceSize(); + return getAngles().flip(Reference.VIEW, reference) ? mPreview.getSurfaceSize().flip() : mPreview.getSurfaceSize(); } /** @@ -1314,10 +1272,10 @@ public abstract class CameraEngine implements * apply, despite the capturing mechanism being different. */ @Nullable - public final Size getUncroppedSnapshotSize(int reference) { + public final Size getUncroppedSnapshotSize(@NonNull Reference reference) { Size baseSize = getPreviewStreamSize(reference); if (baseSize == null) return null; - boolean flip = flip(reference, REF_VIEW); + boolean flip = getAngles().flip(reference, Reference.VIEW); int maxWidth = flip ? mSnapshotMaxHeight : mSnapshotMaxWidth; int maxHeight = flip ? mSnapshotMaxWidth : mSnapshotMaxHeight; float baseRatio = AspectRatio.of(baseSize).toFloat(); @@ -1335,11 +1293,6 @@ public abstract class CameraEngine implements } } - - //endregion - - //region Size utils - /** * This is called either on cameraView.start(), or when the underlying surface changes. * It is possible that in the first call the preview surface has not already computed its @@ -1354,10 +1307,10 @@ public abstract class CameraEngine implements } @SuppressWarnings("WeakerAccess") - protected final Size computeCaptureSize(Mode mode) { + protected final Size computeCaptureSize(@NonNull Mode mode) { // We want to pass stuff into the REF_VIEW reference, not the sensor one. // This is already managed by CameraOptions, so we just flip again at the end. - boolean flip = flip(REF_SENSOR, REF_VIEW); + boolean flip = getAngles().flip(Reference.SENSOR, Reference.VIEW); SizeSelector selector; Collection sizes; if (mode == Mode.PICTURE) { @@ -1393,7 +1346,7 @@ public abstract class CameraEngine implements @NonNull List previewSizes = getPreviewStreamAvailableSizes(); // These sizes come in REF_SENSOR. Since there is an external selector involved, // we must convert all of them to REF_VIEW, then flip back when returning. - boolean flip = flip(REF_SENSOR, REF_VIEW); + boolean flip = getAngles().flip(Reference.SENSOR, Reference.VIEW); List sizes = new ArrayList<>(previewSizes.size()); for (Size size : previewSizes) { sizes.add(flip ? size.flip() : size); @@ -1401,7 +1354,7 @@ public abstract class CameraEngine implements // Create our own default selector, which will be used if the external mPreviewStreamSizeSelector // is null, or if it fails in finding a size. - Size targetMinSize = getPreviewSurfaceSize(REF_VIEW); + Size targetMinSize = getPreviewSurfaceSize(Reference.VIEW); if (targetMinSize == null) throw new IllegalStateException("targetMinSize should not be null here."); AspectRatio targetRatio = AspectRatio.of(mCaptureSize.getWidth(), mCaptureSize.getHeight()); if (flip) targetRatio = targetRatio.flip(); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/offset/Angles.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/offset/Angles.java new file mode 100644 index 00000000..56c9199b --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/offset/Angles.java @@ -0,0 +1,121 @@ +package com.otaliastudios.cameraview.engine.offset; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import com.otaliastudios.cameraview.controls.Facing; + +/** + * Manages offsets between different {@link Reference} systems. + * + * These offsets are computed based on the {@link #setSensorOffset(Facing, int)}, + * {@link #setDisplayOffset(int)} and {@link #setDeviceOrientation(int)} values that are coming + * from outside. + * + * When communicating with the sensor, {@link Axis#RELATIVE_TO_SENSOR} should probably be used. + * This means inverting the offset when using the front camera. + * This is often the case when calling offset(SENSOR, OUTPUT), for example when passing a JPEG + * rotation to the sensor. That is meant to be consumed as relative to the sensor plane. + * + * For all other usages, {@link Axis#ABSOLUTE} is probably a better choice. + */ +public class Angles { + + private Facing mSensorFacing; + @VisibleForTesting int mSensorOffset = 0; + @VisibleForTesting int mDisplayOffset = 0; + @VisibleForTesting int mDeviceOrientation = 0; + + /** + * We want to keep everything in the {@link Axis#ABSOLUTE} reference, + * so a front facing sensor offset must be inverted. + * + * @param sensorFacing sensor facing value + * @param sensorOffset sensor offset + */ + public void setSensorOffset(@NonNull Facing sensorFacing, int sensorOffset) { + sanitizeInput(sensorOffset); + mSensorFacing = sensorFacing; + mSensorOffset = sensorOffset; + if (mSensorFacing == Facing.FRONT) { + mSensorOffset = sanitizeOutput(360 - mSensorOffset); + } + } + + /** + * Sets the display offset. + * @param displayOffset the display offset + */ + public void setDisplayOffset(int displayOffset) { + sanitizeInput(displayOffset); + mDisplayOffset = displayOffset; + } + + /** + * Sets the device orientation. + * @param deviceOrientation the device orientation + */ + public void setDeviceOrientation(int deviceOrientation) { + sanitizeInput(deviceOrientation); + mDeviceOrientation = deviceOrientation; + } + + /** + * Returns the offset between two reference systems, computed along the given axis. + * @param from the source reference system + * @param to the destination reference system + * @param axis the axis + * @return the offset + */ + public int offset(@NonNull Reference from, @NonNull Reference to, @NonNull Axis axis) { + int offset = absoluteOffset(from, to); + if (axis == Axis.RELATIVE_TO_SENSOR) { + if (mSensorFacing == Facing.FRONT) { + offset = sanitizeOutput(360 - offset); + } + } + return offset; + } + + private int absoluteOffset(@NonNull Reference from, @NonNull Reference to) { + if (from == to) { + return 0; + } else if (to == Reference.BASE) { + return sanitizeOutput(360 - absoluteOffset(to, from)); + } else if (from == Reference.BASE) { + switch (to) { + case VIEW: return sanitizeOutput(360 - mDisplayOffset); + case OUTPUT: return sanitizeOutput(mDeviceOrientation); + case SENSOR: return sanitizeOutput(360 - mSensorOffset); + default: throw new RuntimeException("Unknown reference: " + to); + } + } else { + return sanitizeOutput( + absoluteOffset(Reference.BASE, to) + - absoluteOffset(Reference.BASE, from)); + } + } + + /** + * Whether the two references systems are flipped. + * @param from source + * @param to destination + * @return true if flipped + */ + public boolean flip(@NonNull Reference from, @NonNull Reference to) { + return offset(from, to, Axis.ABSOLUTE) % 180 != 0; + } + + private void sanitizeInput(int value) { + if (value != 0 + && value != 90 + && value != 180 + && value != 270) { + throw new IllegalStateException("This value is not sanitized: " + value); + } + } + + private int sanitizeOutput(int value) { + return (value + 360) % 360; + } +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/offset/Axis.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/offset/Axis.java new file mode 100644 index 00000000..d5f3eb67 --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/offset/Axis.java @@ -0,0 +1,28 @@ +package com.otaliastudios.cameraview.engine.offset; + +/** + * The axis around which offsets are computed. We have two possibilities: + * - an axis going out of the device screen towards the user's face + * - an axis going out of the device screen towards the back + * + * We are mostly interested in the first one, but some APIs will require + * angles in the sensor reference, in which case, for front cameras, we are + * required to use {@link #RELATIVE_TO_SENSOR}. + */ +public enum Axis { + + /** + * This rotation axis is the one going out of the device screen + * towards the user's face. + */ + ABSOLUTE, + + /** + * This rotation axis takes into account the current + * {@link com.otaliastudios.cameraview.controls.Facing} value. + * + * - for {@link com.otaliastudios.cameraview.controls.Facing#BACK}, this equals {@link #ABSOLUTE} + * - for {@link com.otaliastudios.cameraview.controls.Facing#FRONT}, this is inverted + */ + RELATIVE_TO_SENSOR +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/offset/Reference.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/offset/Reference.java new file mode 100644 index 00000000..640ff19c --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/offset/Reference.java @@ -0,0 +1,28 @@ +package com.otaliastudios.cameraview.engine.offset; + +public enum Reference { + + /** + * The base reference system has its 'north' aligned with the device natural + * orientation. + */ + BASE, + + /** + * This reference system has its 'north' aligned with the camera sensor. + */ + SENSOR, + + /** + * This reference system has its 'north' aligned with the View hierarchy. + * This can be different than {@link #BASE} if the activity is allowed to rotate + * (or forced into a non natural position). + */ + VIEW, + + /** + * This reference system has its 'north' aligned with the output picture/video. + * This means that it takes into account the device orientation. + */ + OUTPUT +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/GestureLayout.java b/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/GestureFinder.java similarity index 88% rename from cameraview/src/main/java/com/otaliastudios/cameraview/gesture/GestureLayout.java rename to cameraview/src/main/java/com/otaliastudios/cameraview/gesture/GestureFinder.java index 69f24d95..4cd15c02 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/GestureLayout.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/GestureFinder.java @@ -4,23 +4,30 @@ import android.content.Context; import android.graphics.PointF; import androidx.annotation.NonNull; import android.view.MotionEvent; -import android.widget.FrameLayout; /** - * Base class for gesture layouts, that is, layouts that will capture - * gestures. + * Base class for gesture finders. + * Gesture finders are passed down touch events to detect gestures. */ -public abstract class GestureLayout extends FrameLayout { +public abstract class GestureFinder { + + public interface Controller { + @NonNull Context getContext(); + int getWidth(); + int getHeight(); + } // The number of possible values between minValue and maxValue, for the getValue method. // We could make this non-static (e.g. larger granularity for exposure correction). private final static int GRANULARITY = 50; + private boolean mActive; private Gesture mType; private PointF[] mPoints; + private Controller mController; - GestureLayout(@NonNull Context context, int points) { - super(context); + GestureFinder(@NonNull Controller controller, int points) { + mController = controller; mPoints = new PointF[points]; for (int i = 0; i < points; i++) { mPoints[i] = new PointF(0, 0); @@ -108,6 +115,7 @@ public abstract class GestureLayout extends FrameLayout { * @param which the array position * @return the point */ + @SuppressWarnings("WeakerAccess") @NonNull protected final PointF getPoint(int which) { return mPoints[which]; @@ -156,4 +164,13 @@ public abstract class GestureLayout extends FrameLayout { } return newValue; } + + /** + * Returns the controller for this finder. + * @return the controller + */ + @NonNull + protected Controller getController() { + return mController; + } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/PinchGestureLayout.java b/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/PinchGestureFinder.java similarity index 85% rename from cameraview/src/main/java/com/otaliastudios/cameraview/gesture/PinchGestureLayout.java rename to cameraview/src/main/java/com/otaliastudios/cameraview/gesture/PinchGestureFinder.java index 3c0acac8..29d693ae 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/PinchGestureLayout.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/PinchGestureFinder.java @@ -1,26 +1,25 @@ package com.otaliastudios.cameraview.gesture; -import android.content.Context; import android.os.Build; import androidx.annotation.NonNull; import android.view.MotionEvent; import android.view.ScaleGestureDetector; /** - * A {@link GestureLayout} that detects {@link Gesture#PINCH} gestures. + * A {@link GestureFinder} that detects {@link Gesture#PINCH} gestures. */ -public class PinchGestureLayout extends GestureLayout { +public class PinchGestureFinder extends GestureFinder { private final static float ADD_SENSITIVITY = 2f; - ScaleGestureDetector mDetector; + private ScaleGestureDetector mDetector; private boolean mNotify; private float mFactor = 0; - public PinchGestureLayout(@NonNull Context context) { - super(context, 2); + public PinchGestureFinder(@NonNull Controller controller) { + super(controller, 2); setGesture(Gesture.PINCH); - mDetector = new ScaleGestureDetector(context, new ScaleGestureDetector.SimpleOnScaleGestureListener() { + mDetector = new ScaleGestureDetector(controller.getContext(), new ScaleGestureDetector.SimpleOnScaleGestureListener() { @Override public boolean onScale(ScaleGestureDetector detector) { mNotify = true; diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/ScrollGestureLayout.java b/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/ScrollGestureFinder.java similarity index 84% rename from cameraview/src/main/java/com/otaliastudios/cameraview/gesture/ScrollGestureLayout.java rename to cameraview/src/main/java/com/otaliastudios/cameraview/gesture/ScrollGestureFinder.java index 97d30f86..8eece3ed 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/ScrollGestureLayout.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/ScrollGestureFinder.java @@ -1,6 +1,5 @@ package com.otaliastudios.cameraview.gesture; -import android.content.Context; import androidx.annotation.NonNull; import android.view.GestureDetector; import android.view.MotionEvent; @@ -8,21 +7,21 @@ import android.view.MotionEvent; import com.otaliastudios.cameraview.CameraLogger; /** - * A {@link GestureLayout} that detects {@link Gesture#SCROLL_HORIZONTAL} + * A {@link GestureFinder} that detects {@link Gesture#SCROLL_HORIZONTAL} * and {@link Gesture#SCROLL_VERTICAL} gestures. */ -public class ScrollGestureLayout extends GestureLayout { +public class ScrollGestureFinder extends GestureFinder { - private static final String TAG = ScrollGestureLayout.class.getSimpleName(); + private static final String TAG = ScrollGestureFinder.class.getSimpleName(); private static final CameraLogger LOG = CameraLogger.create(TAG); private GestureDetector mDetector; private boolean mNotify; private float mFactor; - public ScrollGestureLayout(@NonNull Context context) { - super(context, 2); - mDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() { + public ScrollGestureFinder(final @NonNull Controller controller) { + super(controller, 2); + mDetector = new GestureDetector(controller.getContext(), new GestureDetector.SimpleOnGestureListener() { @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { @@ -40,7 +39,7 @@ public class ScrollGestureLayout extends GestureLayout { horizontal = getGesture() == Gesture.SCROLL_HORIZONTAL; } getPoint(1).set(e2.getX(), e2.getY()); - mFactor = horizontal ? (distanceX / getWidth()) : (distanceY / getHeight()); + mFactor = horizontal ? (distanceX / controller.getWidth()) : (distanceY / controller.getHeight()); mFactor = horizontal ? -mFactor : mFactor; // When vertical, up = positive mNotify = true; return true; diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/TapGestureLayout.java b/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/TapGestureFinder.java similarity index 72% rename from cameraview/src/main/java/com/otaliastudios/cameraview/gesture/TapGestureLayout.java rename to cameraview/src/main/java/com/otaliastudios/cameraview/gesture/TapGestureFinder.java index 2e37785f..87f581f9 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/TapGestureLayout.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/TapGestureFinder.java @@ -1,32 +1,22 @@ package com.otaliastudios.cameraview.gesture; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.content.Context; -import android.graphics.PointF; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; + import android.view.GestureDetector; -import android.view.LayoutInflater; import android.view.MotionEvent; -import android.view.View; -import android.widget.FrameLayout; -import android.widget.ImageView; - -import com.otaliastudios.cameraview.R; /** - * A {@link GestureLayout} that detects {@link Gesture#TAP} + * A {@link GestureFinder} that detects {@link Gesture#TAP} * and {@link Gesture#LONG_TAP} gestures. */ -public class TapGestureLayout extends GestureLayout { +public class TapGestureFinder extends GestureFinder { private GestureDetector mDetector; private boolean mNotify; - public TapGestureLayout(@NonNull Context context) { - super(context, 1); - mDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() { + public TapGestureFinder(@NonNull Controller controller) { + super(controller, 1); + mDetector = new GestureDetector(controller.getContext(), new GestureDetector.SimpleOnGestureListener() { @Override public boolean onSingleTapUp(MotionEvent e) { diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglViewport.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglViewport.java index 9e8da8b8..3496bf95 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglViewport.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglViewport.java @@ -40,16 +40,16 @@ public class EglViewport extends EglElement { // Stuff from Drawable2d.FULL_RECTANGLE // A full square, extending from -1 to +1 in both dimensions. // When the model/view/projection matrix is identity, this will exactly cover the viewport. - private static final float FULL_RECTANGLE_COORDS[] = { + private static final float[] FULL_RECTANGLE_COORDS = { -1.0f, -1.0f, // 0 bottom left 1.0f, -1.0f, // 1 bottom right - -1.0f, 1.0f, // 2 top left - 1.0f, 1.0f, // 3 top right + -1.0f, 1.0f, // 2 top left + 1.0f, 1.0f, // 3 top right }; // Stuff from Drawable2d.FULL_RECTANGLE // A full square, extending from -1 to +1 in both dimensions. - private static final float FULL_RECTANGLE_TEX_COORDS[] = { + private static final float[] FULL_RECTANGLE_TEX_COORDS = { 0.0f, 0.0f, // 0 bottom left 1.0f, 0.0f, // 1 bottom right 0.0f, 1.0f, // 2 top left diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Snapshot1PictureRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Snapshot1PictureRecorder.java index d1802d0a..c0404821 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Snapshot1PictureRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Snapshot1PictureRecorder.java @@ -1,32 +1,18 @@ package com.otaliastudios.cameraview.picture; -import android.annotation.TargetApi; -import android.graphics.Bitmap; import android.graphics.ImageFormat; import android.graphics.Rect; -import android.graphics.SurfaceTexture; import android.graphics.YuvImage; import android.hardware.Camera; -import android.opengl.EGL14; -import android.opengl.EGLContext; -import android.opengl.Matrix; -import android.os.Build; import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.PictureResult; -import com.otaliastudios.cameraview.controls.Facing; import com.otaliastudios.cameraview.engine.Camera1Engine; import com.otaliastudios.cameraview.engine.CameraEngine; -import com.otaliastudios.cameraview.internal.egl.EglCore; -import com.otaliastudios.cameraview.internal.egl.EglViewport; -import com.otaliastudios.cameraview.internal.egl.EglWindowSurface; +import com.otaliastudios.cameraview.engine.offset.Reference; import com.otaliastudios.cameraview.internal.utils.CropHelper; import com.otaliastudios.cameraview.internal.utils.RotationHelper; import com.otaliastudios.cameraview.internal.utils.WorkerHandler; -import com.otaliastudios.cameraview.preview.CameraPreview; -import com.otaliastudios.cameraview.preview.GlCameraPreview; -import com.otaliastudios.cameraview.preview.RendererFrameCallback; -import com.otaliastudios.cameraview.preview.RendererThread; import com.otaliastudios.cameraview.size.AspectRatio; import com.otaliastudios.cameraview.size.Size; @@ -40,6 +26,7 @@ import java.io.ByteArrayOutputStream; public class Snapshot1PictureRecorder extends PictureRecorder { private static final String TAG = Snapshot1PictureRecorder.class.getSimpleName(); + @SuppressWarnings("unused") private static final CameraLogger LOG = CameraLogger.create(TAG); private Camera1Engine mEngine1; @@ -47,9 +34,6 @@ public class Snapshot1PictureRecorder extends PictureRecorder { private AspectRatio mOutputRatio; private int mFormat; - /** - * Camera1 constructor. - */ public Snapshot1PictureRecorder( @NonNull PictureResult.Stub stub, @NonNull Camera1Engine engine, @@ -59,7 +43,7 @@ public class Snapshot1PictureRecorder extends PictureRecorder { mEngine1 = engine; mCamera = camera; mOutputRatio = outputRatio; - mFormat = engine.getPreviewStreamFormat(); + mFormat = camera.getParameters().getPreviewFormat(); } @Override @@ -74,7 +58,7 @@ public class Snapshot1PictureRecorder extends PictureRecorder { // Adding EXIF to a byte array, unfortunately, is hard. final int sensorToOutput = mResult.rotation; final Size outputSize = mResult.size; - final Size previewStreamSize = mEngine1.getPreviewStreamSize(CameraEngine.REF_SENSOR); + final Size previewStreamSize = mEngine1.getPreviewStreamSize(Reference.SENSOR); if (previewStreamSize == null) { throw new IllegalStateException("Preview stream size should never be null here."); } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotGlPictureRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotGlPictureRecorder.java index e4c66728..88ea8a65 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotGlPictureRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotGlPictureRecorder.java @@ -4,7 +4,6 @@ import android.annotation.TargetApi; import android.graphics.Bitmap; import android.graphics.Rect; import android.graphics.SurfaceTexture; -import android.hardware.Camera; import android.opengl.EGL14; import android.opengl.EGLContext; import android.opengl.Matrix; @@ -13,14 +12,13 @@ import android.os.Build; import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.PictureResult; import com.otaliastudios.cameraview.controls.Facing; -import com.otaliastudios.cameraview.engine.Camera1Engine; import com.otaliastudios.cameraview.engine.CameraEngine; +import com.otaliastudios.cameraview.engine.offset.Reference; import com.otaliastudios.cameraview.internal.egl.EglCore; import com.otaliastudios.cameraview.internal.egl.EglViewport; import com.otaliastudios.cameraview.internal.egl.EglWindowSurface; import com.otaliastudios.cameraview.internal.utils.CropHelper; import com.otaliastudios.cameraview.internal.utils.WorkerHandler; -import com.otaliastudios.cameraview.preview.CameraPreview; import com.otaliastudios.cameraview.preview.GlCameraPreview; import com.otaliastudios.cameraview.preview.RendererFrameCallback; import com.otaliastudios.cameraview.preview.RendererThread; @@ -107,7 +105,7 @@ public class SnapshotGlPictureRecorder extends PictureRecorder { // Apply scale and crop: // NOTE: scaleX and scaleY are in REF_VIEW, while our input appears to be in REF_SENSOR. - boolean flip = mEngine.flip(CameraEngine.REF_VIEW, CameraEngine.REF_SENSOR); + boolean flip = mEngine.getAngles().flip(Reference.VIEW, Reference.SENSOR); float realScaleX = flip ? scaleY : scaleX; float realScaleY = flip ? scaleX : scaleY; float scaleTranslX = (1F - realScaleX) / 2F; diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/preview/CameraPreview.java b/cameraview/src/main/java/com/otaliastudios/cameraview/preview/CameraPreview.java index 21c9e405..1f09c94c 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/preview/CameraPreview.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/preview/CameraPreview.java @@ -67,11 +67,9 @@ public abstract class CameraPreview { * Creates a new preview. * @param context a context * @param parent where to inflate our view - * @param callback the callback */ - public CameraPreview(@NonNull Context context, @NonNull ViewGroup parent, @Nullable SurfaceCallback callback) { + public CameraPreview(@NonNull Context context, @NonNull ViewGroup parent) { mView = onCreateView(context, parent); - mSurfaceCallback = callback; } /** @@ -79,10 +77,12 @@ public abstract class CameraPreview { * @param callback a callback */ public final void setSurfaceCallback(@Nullable SurfaceCallback callback) { + if (hasSurface() && mSurfaceCallback != null) { + mSurfaceCallback.onSurfaceDestroyed(); + } mSurfaceCallback = callback; - // If surface already available, dispatch. - if (mOutputSurfaceWidth != 0 || mOutputSurfaceHeight != 0) { - if (callback != null) callback.onSurfaceAvailable(); + if (hasSurface() && mSurfaceCallback != null) { + mSurfaceCallback.onSurfaceAvailable(); } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/preview/GlCameraPreview.java b/cameraview/src/main/java/com/otaliastudios/cameraview/preview/GlCameraPreview.java index ff09b8ee..5cad6af9 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/preview/GlCameraPreview.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/preview/GlCameraPreview.java @@ -57,7 +57,7 @@ import javax.microedition.khronos.opengles.GL10; * Callbacks are guaranteed to be called on the renderer thread, which means that we can fetch * the GL context that was created and is managed by the {@link GLSurfaceView}. */ -public class GlCameraPreview extends CameraPreview implements GLSurfaceView.Renderer { +public class GlCameraPreview extends CameraPreview { private boolean mDispatched; private final float[] mTransformMatrix = new float[16]; @@ -69,32 +69,8 @@ public class GlCameraPreview extends CameraPreview op) { @@ -285,4 +262,46 @@ public class GlCameraPreview extends CameraPreview