From bb3b23e69aadcdb41772ae1d0c853466c5ff514c Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Fri, 6 Dec 2019 17:54:47 +0100 Subject: [PATCH] RAW support (#691) * Check engine state after picture metering - Fixes #685 * Ensure actions are only started in a valid holder state - Fixes #669 * Improve size selection * Add PictureFormat definition * Change PictureResult to reflect new format * Throw if toBitmap is called with a DNG file * Update the CameraView interface to support PictureFormat * Implement DNG support into the CameraEngine, restart if needed * Make CameraOptions engine aware * Fix action bug * Make CameraOptions check RAW availability * Complete engine/options logic for RAW * Add RAW control to demo app * Ensure toBitmap does not crash in demo * RAW support inside Full2PictureRecorder * Add DNG test, fix implementation bugs * Add option to see RAW result through share option * Enable RAW toBitmap for API 24+ * Add documentation * Improve documentation * Change tests * Small change --- README.md | 2 + cameraview/build.gradle | 1 - .../cameraview/CameraViewTest.java | 22 +- .../cameraview/PictureResultTest.java | 3 +- ...1Test.java => Camera1IntegrationTest.java} | 4 +- ...2Test.java => Camera2IntegrationTest.java} | 3 +- .../engine/CameraIntegrationTest.java | 27 ++ .../cameraview/engine/MockCameraEngine.java | 6 + .../options/Camera1OptionsTest.java} | 41 +-- .../cameraview/CameraOptions.java | 333 +++--------------- .../otaliastudios/cameraview/CameraUtils.java | 2 +- .../otaliastudios/cameraview/CameraView.java | 30 ++ .../cameraview/PictureResult.java | 31 +- .../cameraview/controls/ControlParser.java | 8 + .../cameraview/controls/PictureFormat.java | 52 +++ .../cameraview/engine/Camera1Engine.java | 14 +- .../cameraview/engine/Camera2Engine.java | 70 +++- .../cameraview/engine/CameraEngine.java | 29 +- .../cameraview/engine/action/BaseAction.java | 16 +- .../engine/options/Camera1Options.java | 132 +++++++ .../engine/options/Camera2Options.java | 178 ++++++++++ .../cameraview/internal/utils/ExifHelper.java | 15 +- .../picture/Full1PictureRecorder.java | 3 +- .../picture/Full2PictureRecorder.java | 69 +++- .../picture/Snapshot1PictureRecorder.java | 1 - .../picture/Snapshot2PictureRecorder.java | 35 +- .../picture/SnapshotGlPictureRecorder.java | 1 - cameraview/src/main/res/values/attrs.xml | 5 + .../internal/utils/ExifHelperTest.java | 29 +- demo/src/main/AndroidManifest.xml | 19 +- .../cameraview/demo/CameraActivity.java | 3 +- .../otaliastudios/cameraview/demo/Option.java | 9 +- .../demo/PicturePreviewActivity.java | 78 +++- .../cameraview/demo/VideoPreviewActivity.java | 36 +- demo/src/main/res/drawable/ic_share.xml | 5 + demo/src/main/res/menu/share.xml | 8 + demo/src/main/res/values/styles.xml | 3 - demo/src/main/res/xml/filepaths.xml | 4 + docs/_posts/2018-12-20-controls.md | 14 + docs/index.md | 1 + 40 files changed, 931 insertions(+), 411 deletions(-) rename cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/{CameraIntegration1Test.java => Camera1IntegrationTest.java} (89%) rename cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/{CameraIntegration2Test.java => Camera2IntegrationTest.java} (96%) rename cameraview/src/androidTest/java/com/otaliastudios/cameraview/{CameraOptions1Test.java => engine/options/Camera1OptionsTest.java} (91%) create mode 100644 cameraview/src/main/java/com/otaliastudios/cameraview/controls/PictureFormat.java create mode 100644 cameraview/src/main/java/com/otaliastudios/cameraview/engine/options/Camera1Options.java create mode 100644 cameraview/src/main/java/com/otaliastudios/cameraview/engine/options/Camera2Options.java create mode 100644 demo/src/main/res/drawable/ic_share.xml create mode 100644 demo/src/main/res/menu/share.xml create mode 100644 demo/src/main/res/xml/filepaths.xml diff --git a/README.md b/README.md index fb313dd7..231b6fa3 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ api 'com.otaliastudios:cameraview:2.4.0' - Take super-fast snapshots with `takePictureSnapshot` and `takeVideoSnapshot` [[docs]](https://natario1.github.io/CameraView/docs/capturing-media.html) - Smart sizing: create a `CameraView` of any size [[docs]](https://natario1.github.io/CameraView/docs/preview-size.html) - Control HDR, flash, zoom, white balance, exposure, location, grid drawing & more [[docs]](https://natario1.github.io/CameraView/docs/controls.html) +- RAW pictures support [[docs]](https://natario1.github.io/CameraView/docs/controls.html) - Lightweight - Works down to API level 15 - Well tested @@ -141,6 +142,7 @@ Using CameraView is extremely simple: app:cameraFilter="@string/real_time_filter" app:cameraPictureMetering="true|false" app:cameraPictureSnapshotMetering="false|true" + app:cameraPictureFormat="jpeg|dng" app:cameraExperimental="false|true"> diff --git a/cameraview/build.gradle b/cameraview/build.gradle index 00367edb..45bf176e 100644 --- a/cameraview/build.gradle +++ b/cameraview/build.gradle @@ -267,6 +267,5 @@ task mergeCoverageReports(type: JacocoReport) { //endregion -// export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_101.jdk/Contents/Home // To deploy ./gradlew bintrayUpload diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java index e92d19ff..fb634090 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java @@ -20,6 +20,7 @@ import com.otaliastudios.cameraview.controls.ControlParser; import com.otaliastudios.cameraview.controls.Engine; import com.otaliastudios.cameraview.controls.Facing; import com.otaliastudios.cameraview.controls.Flash; +import com.otaliastudios.cameraview.controls.PictureFormat; import com.otaliastudios.cameraview.controls.Preview; import com.otaliastudios.cameraview.engine.CameraEngine; import com.otaliastudios.cameraview.filter.Filter; @@ -161,6 +162,7 @@ public class CameraViewTest extends BaseTest { assertEquals(cameraView.getHdr(), controls.getHdr()); assertEquals(cameraView.getAudio(), controls.getAudio()); assertEquals(cameraView.getVideoCodec(), controls.getVideoCodec()); + assertEquals(cameraView.getPictureFormat(), controls.getPictureFormat()); //noinspection SimplifiableJUnitAssertion assertEquals(cameraView.getLocation(), null); assertEquals(cameraView.getExposureCorrection(), 0f, 0f); @@ -321,9 +323,9 @@ public class CameraViewTest extends BaseTest { @Test public void testGestureAction_exposureCorrection() { - CameraOptions o = mock(CameraOptions.class); - when(o.getExposureCorrectionMinValue()).thenReturn(-10f); - when(o.getExposureCorrectionMaxValue()).thenReturn(10f); + CameraOptions o = new CameraOptions() {}; + o.exposureCorrectionMaxValue = 10F; + o.exposureCorrectionMinValue = -10F; mockController.setMockCameraOptions(o); mockController.setMockEngineState(true); mockController.mExposureCorrectionChanged = false; @@ -563,9 +565,9 @@ public class CameraViewTest extends BaseTest { @Test public void testExposureCorrection() { // This needs a valid CameraOptions value. - CameraOptions o = mock(CameraOptions.class); - when(o.getExposureCorrectionMinValue()).thenReturn(-10f); - when(o.getExposureCorrectionMaxValue()).thenReturn(10f); + CameraOptions o = new CameraOptions() {}; + o.exposureCorrectionMaxValue = 10F; + o.exposureCorrectionMinValue = -10F; mockController.setMockCameraOptions(o); cameraView.setExposureCorrection(5f); @@ -745,6 +747,14 @@ public class CameraViewTest extends BaseTest { assertEquals(cameraView.get(VideoCodec.class), VideoCodec.H_264); } + @Test + public void testPictureFormat() { + cameraView.set(PictureFormat.JPEG); + assertEquals(cameraView.get(PictureFormat.class), PictureFormat.JPEG); + cameraView.set(PictureFormat.DNG); + assertEquals(cameraView.get(PictureFormat.class), PictureFormat.DNG); + } + @Test public void testPreviewStreamSizeSelector() { SizeSelector source = SizeSelectors.minHeight(50); diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/PictureResultTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/PictureResultTest.java index 138dbdd9..8bd0cd65 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/PictureResultTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/PictureResultTest.java @@ -4,6 +4,7 @@ package com.otaliastudios.cameraview; import android.location.Location; import com.otaliastudios.cameraview.controls.Facing; +import com.otaliastudios.cameraview.controls.PictureFormat; import com.otaliastudios.cameraview.size.Size; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -24,7 +25,7 @@ public class PictureResultTest extends BaseTest { @Test public void testResult() { - int format = PictureResult.FORMAT_JPEG; + PictureFormat format = PictureFormat.JPEG; int rotation = 90; Size size = new Size(20, 120); byte[] jpeg = new byte[]{2, 4, 1, 5, 2}; diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegration1Test.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/Camera1IntegrationTest.java similarity index 89% rename from cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegration1Test.java rename to cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/Camera1IntegrationTest.java index 73c0ac62..a7d4a8b5 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegration1Test.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/Camera1IntegrationTest.java @@ -3,8 +3,6 @@ package com.otaliastudios.cameraview.engine; import com.otaliastudios.cameraview.DoNotRunOnTravis; import com.otaliastudios.cameraview.controls.Engine; -import org.junit.Ignore; -import org.junit.Test; import org.junit.runner.RunWith; import androidx.annotation.NonNull; @@ -20,7 +18,7 @@ import androidx.test.filters.LargeTest; @RunWith(AndroidJUnit4.class) @LargeTest @DoNotRunOnTravis(because = "These do work but fail on CI emulators, due to bugs in the Camera1 emulated devices.") -public class CameraIntegration1Test extends CameraIntegrationTest { +public class Camera1IntegrationTest extends CameraIntegrationTest { @NonNull @Override diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegration2Test.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/Camera2IntegrationTest.java similarity index 96% rename from cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegration2Test.java rename to cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/Camera2IntegrationTest.java index 9610109c..d6799aa8 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegration2Test.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/Camera2IntegrationTest.java @@ -8,7 +8,6 @@ import com.otaliastudios.cameraview.controls.Engine; import com.otaliastudios.cameraview.engine.action.ActionHolder; import com.otaliastudios.cameraview.engine.action.BaseAction; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; @@ -27,7 +26,7 @@ import java.util.concurrent.CountDownLatch; @RunWith(AndroidJUnit4.class) @LargeTest @DoNotRunOnTravis(because = "These do work but fail on CI emulators.") -public class CameraIntegration2Test extends CameraIntegrationTest { +public class Camera2IntegrationTest extends CameraIntegrationTest { @NonNull @Override diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java index 59c42f1a..5563aba2 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java @@ -23,6 +23,7 @@ import com.otaliastudios.cameraview.controls.Engine; import com.otaliastudios.cameraview.controls.Flash; import com.otaliastudios.cameraview.controls.Hdr; import com.otaliastudios.cameraview.controls.Mode; +import com.otaliastudios.cameraview.controls.PictureFormat; import com.otaliastudios.cameraview.controls.WhiteBalance; import com.otaliastudios.cameraview.frame.Frame; import com.otaliastudios.cameraview.frame.FrameProcessor; @@ -806,6 +807,32 @@ public abstract class CameraIntegrationTest extends BaseTest { //endregion + //region Picture Formats + + // TODO this fails because setPictureFormat triggers a restart() and takePicture can be called + // in the middle of the restart, failing because the engine is not properly set up. To fix this + // we would have to change the whole CameraEngine threading scheme. + // @Test + public void testPictureFormat_DNG() { + openSync(true); + if (camera.getCameraOptions().supports(PictureFormat.DNG)) { + camera.setPictureFormat(PictureFormat.DNG); + camera.takePicture(); + PictureResult result = waitForPictureResult(true); + // assert that result.getData() is a DNG file: + // We can use the first 4 bytes assuming they are the same as a TIFF file + // https://en.wikipedia.org/wiki/List_of_file_signatures + byte[] b = result.getData(); + boolean isII = b[0] == 'I' && b[1] == 'I' && b[2] == '*' && b[3] == '.'; + boolean isMM = b[0] == 'M' && b[1] == 'M' && b[2] == '.' && b[3] == '*'; + if (!isII && !isMM) { + throw new RuntimeException("Not a DNG file."); + } + } + } + + //endregion + //region Frame Processing private void assert30Frames(FrameProcessor mock) throws Exception { 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 2ad4d168..6bbd97d1 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/MockCameraEngine.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/MockCameraEngine.java @@ -11,6 +11,7 @@ import com.otaliastudios.cameraview.PictureResult; import com.otaliastudios.cameraview.VideoResult; import com.otaliastudios.cameraview.controls.Facing; import com.otaliastudios.cameraview.controls.Flash; +import com.otaliastudios.cameraview.controls.PictureFormat; import com.otaliastudios.cameraview.frame.FrameManager; import com.otaliastudios.cameraview.gesture.Gesture; import com.otaliastudios.cameraview.controls.Hdr; @@ -116,6 +117,11 @@ public class MockCameraEngine extends CameraEngine { mLocation = location; } + @Override + public void setPictureFormat(@NonNull PictureFormat pictureFormat) { + mPictureFormat = pictureFormat; + } + @Override public void takePicture(@NonNull PictureResult.Stub stub) { super.takePicture(stub); diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraOptions1Test.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/options/Camera1OptionsTest.java similarity index 91% rename from cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraOptions1Test.java rename to cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/options/Camera1OptionsTest.java index 5aaae714..8db1d61c 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraOptions1Test.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/options/Camera1OptionsTest.java @@ -1,8 +1,10 @@ -package com.otaliastudios.cameraview; +package com.otaliastudios.cameraview.engine.options; import android.hardware.Camera; +import com.otaliastudios.cameraview.BaseTest; +import com.otaliastudios.cameraview.CameraOptions; import com.otaliastudios.cameraview.controls.Audio; import com.otaliastudios.cameraview.controls.Facing; import com.otaliastudios.cameraview.controls.Flash; @@ -22,7 +24,6 @@ import androidx.test.filters.SmallTest; import org.junit.Test; import org.junit.runner.RunWith; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -38,11 +39,11 @@ import static org.mockito.Mockito.when; @RunWith(AndroidJUnit4.class) @SmallTest -public class CameraOptions1Test extends BaseTest { +public class Camera1OptionsTest extends BaseTest { @Test public void testEmpty() { - CameraOptions o = new CameraOptions(mock(Camera.Parameters.class), 0, false); + CameraOptions o = new Camera1Options(mock(Camera.Parameters.class), 0, false); assertTrue(o.getSupportedPictureAspectRatios().isEmpty()); assertTrue(o.getSupportedPictureSizes().isEmpty()); assertTrue(o.getSupportedWhiteBalance().isEmpty()); @@ -72,7 +73,7 @@ public class CameraOptions1Test extends BaseTest { ); Camera.Parameters params = mock(Camera.Parameters.class); when(params.getSupportedPictureSizes()).thenReturn(sizes); - CameraOptions o = new CameraOptions(params, 0, false); + CameraOptions o = new Camera1Options(params, 0, false); Collection supportedSizes = o.getSupportedPictureSizes(); assertEquals(supportedSizes.size(), sizes.size()); for (Camera.Size size : sizes) { @@ -91,7 +92,7 @@ public class CameraOptions1Test extends BaseTest { ); Camera.Parameters params = mock(Camera.Parameters.class); when(params.getSupportedPictureSizes()).thenReturn(sizes); - CameraOptions o = new CameraOptions(params, 0, true); + CameraOptions o = new Camera1Options(params, 0, true); Collection supportedSizes = o.getSupportedPictureSizes(); assertEquals(supportedSizes.size(), sizes.size()); for (Camera.Size size : sizes) { @@ -116,7 +117,7 @@ public class CameraOptions1Test extends BaseTest { Camera.Parameters params = mock(Camera.Parameters.class); when(params.getSupportedPictureSizes()).thenReturn(sizes); - CameraOptions o = new CameraOptions(params, 0, false); + CameraOptions o = new Camera1Options(params, 0, false); Collection supportedRatios = o.getSupportedPictureAspectRatios(); assertEquals(supportedRatios.size(), expected.size()); for (AspectRatio ratio : expected) { @@ -137,7 +138,7 @@ public class CameraOptions1Test extends BaseTest { ); Camera.Parameters params = mock(Camera.Parameters.class); when(params.getSupportedVideoSizes()).thenReturn(sizes); - CameraOptions o = new CameraOptions(params, 0, false); + CameraOptions o = new Camera1Options(params, 0, false); Collection supportedSizes = o.getSupportedVideoSizes(); assertEquals(supportedSizes.size(), sizes.size()); for (Camera.Size size : sizes) { @@ -158,7 +159,7 @@ public class CameraOptions1Test extends BaseTest { Camera.Parameters params = mock(Camera.Parameters.class); when(params.getSupportedVideoSizes()).thenReturn(null); when(params.getSupportedPreviewSizes()).thenReturn(sizes); - CameraOptions o = new CameraOptions(params, 0, false); + CameraOptions o = new Camera1Options(params, 0, false); Collection supportedSizes = o.getSupportedVideoSizes(); assertEquals(supportedSizes.size(), sizes.size()); for (Camera.Size size : sizes) { @@ -177,7 +178,7 @@ public class CameraOptions1Test extends BaseTest { ); Camera.Parameters params = mock(Camera.Parameters.class); when(params.getSupportedVideoSizes()).thenReturn(sizes); - CameraOptions o = new CameraOptions(params, 0, true); + CameraOptions o = new Camera1Options(params, 0, true); Collection supportedSizes = o.getSupportedVideoSizes(); assertEquals(supportedSizes.size(), sizes.size()); for (Camera.Size size : sizes) { @@ -202,7 +203,7 @@ public class CameraOptions1Test extends BaseTest { Camera.Parameters params = mock(Camera.Parameters.class); when(params.getSupportedVideoSizes()).thenReturn(sizes); - CameraOptions o = new CameraOptions(params, 0, false); + CameraOptions o = new Camera1Options(params, 0, false); Collection supportedRatios = o.getSupportedVideoAspectRatios(); assertEquals(supportedRatios.size(), expected.size()); for (AspectRatio ratio : expected) { @@ -218,7 +219,7 @@ public class CameraOptions1Test extends BaseTest { when(params.getMaxExposureCompensation()).thenReturn(0); when(params.getMinExposureCompensation()).thenReturn(0); - CameraOptions o = new CameraOptions(params, 0, false); + CameraOptions o = new Camera1Options(params, 0, false); assertFalse(o.supports(GestureAction.AUTO_FOCUS)); assertTrue(o.supports(GestureAction.TAKE_PICTURE)); assertTrue(o.supports(GestureAction.NONE)); @@ -232,7 +233,7 @@ public class CameraOptions1Test extends BaseTest { public void testAlwaysSupportedControls() { // Grid, VideoQuality, SessionType and Audio are always supported. Camera.Parameters params = mock(Camera.Parameters.class); - CameraOptions o = new CameraOptions(params, 0, false); + CameraOptions o = new Camera1Options(params, 0, false); Collection grids = o.getSupportedControls(Grid.class); Collection video = o.getSupportedControls(VideoCodec.class); @@ -253,7 +254,7 @@ public class CameraOptions1Test extends BaseTest { supported.add(cameraInfo.facing); } - CameraOptions o = new CameraOptions(mock(Camera.Parameters.class), 0, false); + CameraOptions o = new Camera1Options(mock(Camera.Parameters.class), 0, false); Camera1Mapper m = Camera1Mapper.get(); Collection s = o.getSupportedControls(Facing.class); assertEquals(s.size(), supported.size()); @@ -272,7 +273,7 @@ public class CameraOptions1Test extends BaseTest { Camera.Parameters.WHITE_BALANCE_SHADE // Not supported )); - CameraOptions o = new CameraOptions(params, 0, false); + CameraOptions o = new Camera1Options(params, 0, false); Collection w = o.getSupportedControls(WhiteBalance.class); assertEquals(w.size(), 2); assertTrue(w.contains(WhiteBalance.AUTO)); @@ -291,7 +292,7 @@ public class CameraOptions1Test extends BaseTest { Camera.Parameters.FLASH_MODE_RED_EYE // Not supported )); - CameraOptions o = new CameraOptions(params, 0, false); + CameraOptions o = new Camera1Options(params, 0, false); Collection f = o.getSupportedControls(Flash.class); assertEquals(f.size(), 3); assertTrue(f.contains(Flash.OFF)); @@ -312,7 +313,7 @@ public class CameraOptions1Test extends BaseTest { Camera.Parameters.SCENE_MODE_FIREWORKS // Not supported )); - CameraOptions o = new CameraOptions(params, 0, false); + CameraOptions o = new Camera1Options(params, 0, false); Collection h = o.getSupportedControls(Hdr.class); assertEquals(h.size(), 2); assertTrue(h.contains(Hdr.OFF)); @@ -329,7 +330,7 @@ public class CameraOptions1Test extends BaseTest { when(params.isZoomSupported()).thenReturn(true); //noinspection ArraysAsListWithZeroOrOneArgument when(params.getSupportedFocusModes()).thenReturn(Arrays.asList(Camera.Parameters.FOCUS_MODE_AUTO)); - CameraOptions o = new CameraOptions(params, 0, false); + CameraOptions o = new Camera1Options(params, 0, false); assertTrue(o.isZoomSupported()); assertTrue(o.isAutoFocusSupported()); } @@ -340,7 +341,7 @@ public class CameraOptions1Test extends BaseTest { when(params.getMaxExposureCompensation()).thenReturn(10); when(params.getMinExposureCompensation()).thenReturn(-10); when(params.getExposureCompensationStep()).thenReturn(0.5f); - CameraOptions o = new CameraOptions(params, 0, false); + CameraOptions o = new Camera1Options(params, 0, false); assertTrue(o.isExposureCorrectionSupported()); assertEquals(o.getExposureCorrectionMinValue(), -10f * 0.5f, 0f); assertEquals(o.getExposureCorrectionMaxValue(), 10f * 0.5f, 0f); @@ -355,7 +356,7 @@ public class CameraOptions1Test extends BaseTest { new int[]{60000, 120000} ); when(params.getSupportedPreviewFpsRange()).thenReturn(result); - CameraOptions o = new CameraOptions(params, 0, false); + CameraOptions o = new Camera1Options(params, 0, false); assertEquals(20F, o.getPreviewFrameRateMinValue(), 0.001F); assertEquals(120F, o.getPreviewFrameRateMaxValue(), 0.001F); } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java index c68650c2..9095412d 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java @@ -18,6 +18,7 @@ import com.otaliastudios.cameraview.controls.Control; import com.otaliastudios.cameraview.controls.Engine; import com.otaliastudios.cameraview.controls.Facing; import com.otaliastudios.cameraview.controls.Flash; +import com.otaliastudios.cameraview.controls.PictureFormat; import com.otaliastudios.cameraview.controls.Preview; import com.otaliastudios.cameraview.engine.mappers.Camera1Mapper; import com.otaliastudios.cameraview.engine.mappers.Camera2Mapper; @@ -44,263 +45,27 @@ import java.util.Set; /** * Options telling you what is available and what is not. */ -public class CameraOptions { - - private Set supportedWhiteBalance = new HashSet<>(5); - private Set supportedFacing = new HashSet<>(2); - private Set supportedFlash = new HashSet<>(4); - private Set supportedHdr = new HashSet<>(2); - private Set supportedPictureSizes = new HashSet<>(15); - private Set supportedVideoSizes = new HashSet<>(5); - private Set supportedPictureAspectRatio = new HashSet<>(4); - private Set supportedVideoAspectRatio = new HashSet<>(3); - - private boolean zoomSupported; - private boolean exposureCorrectionSupported; - private float exposureCorrectionMinValue; - private float exposureCorrectionMaxValue; - private boolean autoFocusSupported; - private float previewFrameRateMinValue; - private float previewFrameRateMaxValue; - - - public CameraOptions(@NonNull Camera.Parameters params, int cameraId, boolean flipSizes) { - List strings; - Camera1Mapper mapper = Camera1Mapper.get(); - - // Facing - Camera.CameraInfo cameraInfo = new Camera.CameraInfo(); - for (int i = 0, count = Camera.getNumberOfCameras(); i < count; i++) { - Camera.getCameraInfo(i, cameraInfo); - Facing value = mapper.unmapFacing(cameraInfo.facing); - if (value != null) supportedFacing.add(value); - } - - // WB - strings = params.getSupportedWhiteBalance(); - if (strings != null) { - for (String string : strings) { - WhiteBalance value = mapper.unmapWhiteBalance(string); - if (value != null) supportedWhiteBalance.add(value); - } - } - - // Flash - supportedFlash.add(Flash.OFF); - strings = params.getSupportedFlashModes(); - if (strings != null) { - for (String string : strings) { - Flash value = mapper.unmapFlash(string); - if (value != null) supportedFlash.add(value); - } - } - - // Hdr - supportedHdr.add(Hdr.OFF); - strings = params.getSupportedSceneModes(); - if (strings != null) { - for (String string : strings) { - Hdr value = mapper.unmapHdr(string); - if (value != null) supportedHdr.add(value); - } - } - - // zoom - zoomSupported = params.isZoomSupported(); - - // autofocus - autoFocusSupported = params.getSupportedFocusModes() - .contains(Camera.Parameters.FOCUS_MODE_AUTO); - - // Exposure correction - float step = params.getExposureCompensationStep(); - exposureCorrectionMinValue = (float) params.getMinExposureCompensation() * step; - exposureCorrectionMaxValue = (float) params.getMaxExposureCompensation() * step; - exposureCorrectionSupported = params.getMinExposureCompensation() != 0 - || params.getMaxExposureCompensation() != 0; - - // Picture Sizes - List sizes = params.getSupportedPictureSizes(); - for (Camera.Size size : sizes) { - int width = flipSizes ? size.height : size.width; - int height = flipSizes ? size.width : size.height; - supportedPictureSizes.add(new Size(width, height)); - supportedPictureAspectRatio.add(AspectRatio.of(width, height)); - } - - // Video Sizes - // As a safety measure, remove Sizes bigger than CamcorderProfile.highest - CamcorderProfile profile = CamcorderProfiles.get(cameraId, - new Size(Integer.MAX_VALUE, Integer.MAX_VALUE)); - Size videoMaxSize = new Size(profile.videoFrameWidth, profile.videoFrameHeight); - List vsizes = params.getSupportedVideoSizes(); - if (vsizes != null) { - for (Camera.Size size : vsizes) { - if (size.width <= videoMaxSize.getWidth() - && size.height <= videoMaxSize.getHeight()) { - int width = flipSizes ? size.height : size.width; - int height = flipSizes ? size.width : size.height; - supportedVideoSizes.add(new Size(width, height)); - supportedVideoAspectRatio.add(AspectRatio.of(width, height)); - } - } - } else { - // StackOverflow threads seems to agree that if getSupportedVideoSizes is null, - // previews can be used. - List fallback = params.getSupportedPreviewSizes(); - for (Camera.Size size : fallback) { - if (size.width <= videoMaxSize.getWidth() - && size.height <= videoMaxSize.getHeight()) { - int width = flipSizes ? size.height : size.width; - int height = flipSizes ? size.width : size.height; - supportedVideoSizes.add(new Size(width, height)); - supportedVideoAspectRatio.add(AspectRatio.of(width, height)); - } - } - } - - // Preview FPS - previewFrameRateMinValue = Float.MAX_VALUE; - previewFrameRateMaxValue = -Float.MAX_VALUE; - List fpsRanges = params.getSupportedPreviewFpsRange(); - for (int[] fpsRange : fpsRanges) { - float lower = (float) fpsRange[0] / 1000F; - float upper = (float) fpsRange[1] / 1000F; - previewFrameRateMinValue = Math.min(previewFrameRateMinValue, lower); - previewFrameRateMaxValue = Math.max(previewFrameRateMaxValue, upper); - } - } - - // Camera2Engine constructor. - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) - public CameraOptions(@NonNull CameraManager manager, - @NonNull String cameraId, - boolean flipSizes) throws CameraAccessException { - Camera2Mapper mapper = Camera2Mapper.get(); - CameraCharacteristics cameraCharacteristics = manager.getCameraCharacteristics(cameraId); - - // Facing - for (String cameraId1 : manager.getCameraIdList()) { - CameraCharacteristics cameraCharacteristics1 = manager - .getCameraCharacteristics(cameraId1); - Integer cameraFacing = cameraCharacteristics1.get(CameraCharacteristics.LENS_FACING); - if (cameraFacing != null) { - Facing value = mapper.unmapFacing(cameraFacing); - if (value != null) supportedFacing.add(value); - } - } - - // WB - int[] awbModes = cameraCharacteristics.get( - CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES); - //noinspection ConstantConditions - for (int awbMode : awbModes) { - WhiteBalance value = mapper.unmapWhiteBalance(awbMode); - if (value != null) supportedWhiteBalance.add(value); - } - - // Flash - supportedFlash.add(Flash.OFF); - Boolean hasFlash = cameraCharacteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE); - if (hasFlash != null && hasFlash) { - int[] aeModes = cameraCharacteristics.get( - CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES); - //noinspection ConstantConditions - for (int aeMode : aeModes) { - Set flashes = mapper.unmapFlash(aeMode); - supportedFlash.addAll(flashes); - } - } - - // HDR - supportedHdr.add(Hdr.OFF); - int[] sceneModes = cameraCharacteristics.get( - CameraCharacteristics.CONTROL_AVAILABLE_SCENE_MODES); - //noinspection ConstantConditions - for (int sceneMode : sceneModes) { - Hdr value = mapper.unmapHdr(sceneMode); - if (value != null) supportedHdr.add(value); - } - - // Zoom - Float maxZoom = cameraCharacteristics.get( - CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM); - if(maxZoom != null) { - zoomSupported = maxZoom > 1; - } - - - // AutoFocus - // This now means 3A metering with respect to a specific region of the screen. - // Some controls (AF, AE) have special triggers that might or might not be supported. - // But they can also be on some continuous search mode so that the trigger is not needed. - // What really matters in my opinion is the availability of regions. - Integer afRegions = cameraCharacteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF); - Integer aeRegions = cameraCharacteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE); - Integer awbRegions = cameraCharacteristics.get( - CameraCharacteristics.CONTROL_MAX_REGIONS_AWB); - autoFocusSupported = (afRegions != null && afRegions > 0) - || (aeRegions != null && aeRegions > 0) - || (awbRegions != null && awbRegions > 0); - - // Exposure correction - Range exposureRange = cameraCharacteristics.get( - CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); - Rational exposureStep = cameraCharacteristics.get( - CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); - if (exposureRange != null && exposureStep != null && exposureStep.floatValue() != 0) { - exposureCorrectionMinValue = exposureRange.getLower() / exposureStep.floatValue(); - exposureCorrectionMaxValue = exposureRange.getUpper() / exposureStep.floatValue(); - } - exposureCorrectionSupported = exposureCorrectionMinValue != 0 - && exposureCorrectionMaxValue != 0; - - - // Picture Sizes - StreamConfigurationMap streamMap = cameraCharacteristics.get( - CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); - if (streamMap == null) { - throw new RuntimeException("StreamConfigurationMap is null. Should not happen."); - } - android.util.Size[] psizes = streamMap.getOutputSizes(ImageFormat.JPEG); - for (android.util.Size size : psizes) { - int width = flipSizes ? size.getHeight() : size.getWidth(); - int height = flipSizes ? size.getWidth() : size.getHeight(); - supportedPictureSizes.add(new Size(width, height)); - supportedPictureAspectRatio.add(AspectRatio.of(width, height)); - } - - // Video Sizes - // As a safety measure, remove Sizes bigger than CamcorderProfile.highest - CamcorderProfile profile = CamcorderProfiles.get(cameraId, - new Size(Integer.MAX_VALUE, Integer.MAX_VALUE)); - Size videoMaxSize = new Size(profile.videoFrameWidth, profile.videoFrameHeight); - android.util.Size[] vsizes = streamMap.getOutputSizes(MediaRecorder.class); - for (android.util.Size size : vsizes) { - if (size.getWidth() <= videoMaxSize.getWidth() - && size.getHeight() <= videoMaxSize.getHeight()) { - int width = flipSizes ? size.getHeight() : size.getWidth(); - int height = flipSizes ? size.getWidth() : size.getHeight(); - supportedVideoSizes.add(new Size(width, height)); - supportedVideoAspectRatio.add(AspectRatio.of(width, height)); - } - } - - // Preview FPS - Range[] range = cameraCharacteristics.get( - CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); - if (range != null) { - previewFrameRateMinValue = Float.MAX_VALUE; - previewFrameRateMaxValue = -Float.MAX_VALUE; - for (Range fpsRange : range) { - previewFrameRateMinValue = Math.min(previewFrameRateMinValue, fpsRange.getLower()); - previewFrameRateMaxValue = Math.max(previewFrameRateMaxValue, fpsRange.getUpper()); - } - } else { - previewFrameRateMinValue = 0F; - previewFrameRateMaxValue = 0F; - } - } +public abstract class CameraOptions { + + protected Set supportedWhiteBalance = new HashSet<>(5); + protected Set supportedFacing = new HashSet<>(2); + protected Set supportedFlash = new HashSet<>(4); + protected Set supportedHdr = new HashSet<>(2); + protected Set supportedPictureSizes = new HashSet<>(15); + protected Set supportedVideoSizes = new HashSet<>(5); + protected Set supportedPictureAspectRatio = new HashSet<>(4); + protected Set supportedVideoAspectRatio = new HashSet<>(3); + protected Set supportedPictureFormats = new HashSet<>(2); + + protected boolean zoomSupported; + protected boolean exposureCorrectionSupported; + protected float exposureCorrectionMinValue; + protected float exposureCorrectionMaxValue; + protected boolean autoFocusSupported; + protected float previewFrameRateMinValue; + protected float previewFrameRateMaxValue; + + protected CameraOptions() { } /** * Shorthand for getSupported*().contains(value). @@ -308,7 +73,7 @@ public class CameraOptions { * @param control value to check * @return whether it's supported */ - public boolean supports(@NonNull Control control) { + public final boolean supports(@NonNull Control control) { return getSupportedControls(control.getClass()).contains(control); } @@ -319,7 +84,7 @@ public class CameraOptions { * @param action value to be checked * @return whether it's supported */ - public boolean supports(@NonNull GestureAction action) { + public final boolean supports(@NonNull GestureAction action) { switch (action) { case AUTO_FOCUS: return isAutoFocusSupported(); @@ -338,7 +103,8 @@ public class CameraOptions { @SuppressWarnings("unchecked") @NonNull - public Collection getSupportedControls(@NonNull Class controlClass) { + public final Collection getSupportedControls( + @NonNull Class controlClass) { if (controlClass.equals(Audio.class)) { return (Collection) Arrays.asList(Audio.values()); } else if (controlClass.equals(Facing.class)) { @@ -359,6 +125,8 @@ public class CameraOptions { return (Collection) Arrays.asList(Engine.values()); } else if (controlClass.equals(Preview.class)) { return (Collection) Arrays.asList(Preview.values()); + } else if (controlClass.equals(PictureFormat.class)) { + return (Collection) getSupportedPictureFormats(); } // Unrecognized control. return Collections.emptyList(); @@ -370,7 +138,7 @@ public class CameraOptions { * @return a collection of supported values. */ @NonNull - public Collection getSupportedPictureSizes() { + public final Collection getSupportedPictureSizes() { return Collections.unmodifiableSet(supportedPictureSizes); } @@ -379,9 +147,8 @@ public class CameraOptions { * * @return a collection of supported values. */ - @SuppressWarnings("WeakerAccess") @NonNull - public Collection getSupportedPictureAspectRatios() { + public final Collection getSupportedPictureAspectRatios() { return Collections.unmodifiableSet(supportedPictureAspectRatio); } @@ -391,7 +158,7 @@ public class CameraOptions { * @return a collection of supported values. */ @NonNull - public Collection getSupportedVideoSizes() { + public final Collection getSupportedVideoSizes() { return Collections.unmodifiableSet(supportedVideoSizes); } @@ -400,9 +167,8 @@ public class CameraOptions { * * @return a set of supported values. */ - @SuppressWarnings("WeakerAccess") @NonNull - public Collection getSupportedVideoAspectRatios() { + public final Collection getSupportedVideoAspectRatios() { return Collections.unmodifiableSet(supportedVideoAspectRatio); } @@ -414,7 +180,7 @@ public class CameraOptions { * @return a collection of supported values. */ @NonNull - public Collection getSupportedFacing() { + public final Collection getSupportedFacing() { return Collections.unmodifiableSet(supportedFacing); } @@ -428,7 +194,7 @@ public class CameraOptions { * @return a collection of supported values. */ @NonNull - public Collection getSupportedFlash() { + public final Collection getSupportedFlash() { return Collections.unmodifiableSet(supportedFlash); } @@ -443,7 +209,7 @@ public class CameraOptions { * @return a collection of supported values. */ @NonNull - public Collection getSupportedWhiteBalance() { + public final Collection getSupportedWhiteBalance() { return Collections.unmodifiableSet(supportedWhiteBalance); } @@ -454,19 +220,30 @@ public class CameraOptions { * @see Hdr#ON * @return a collection of supported values. */ - @SuppressWarnings("WeakerAccess") @NonNull - public Collection getSupportedHdr() { + public final Collection getSupportedHdr() { return Collections.unmodifiableSet(supportedHdr); } + /** + * Set of supported picture formats. + * + * @see PictureFormat#JPEG + * @see PictureFormat#DNG + * @return a collection of supported values. + */ + @NonNull + public final Collection getSupportedPictureFormats() { + return Collections.unmodifiableSet(supportedPictureFormats); + } + /** * Whether zoom is supported. If this is false, pinch-to-zoom * will not work and {@link CameraView#setZoom(float)} will have no effect. * * @return whether zoom is supported. */ - public boolean isZoomSupported() { + public final boolean isZoomSupported() { return zoomSupported; } @@ -478,7 +255,7 @@ public class CameraOptions { * * @return whether auto focus is supported. */ - public boolean isAutoFocusSupported() { + public final boolean isAutoFocusSupported() { return autoFocusSupported; } @@ -490,7 +267,7 @@ public class CameraOptions { * @see #getExposureCorrectionMaxValue() * @return whether exposure correction is supported. */ - public boolean isExposureCorrectionSupported() { + public final boolean isExposureCorrectionSupported() { return exposureCorrectionSupported; } @@ -500,7 +277,7 @@ public class CameraOptions { * * @return min EV value */ - public float getExposureCorrectionMinValue() { + public final float getExposureCorrectionMinValue() { return exposureCorrectionMinValue; } @@ -511,7 +288,7 @@ public class CameraOptions { * * @return max EV value */ - public float getExposureCorrectionMaxValue() { + public final float getExposureCorrectionMaxValue() { return exposureCorrectionMaxValue; } @@ -520,7 +297,7 @@ public class CameraOptions { * * @return the min value */ - public float getPreviewFrameRateMinValue() { + public final float getPreviewFrameRateMinValue() { return previewFrameRateMinValue; } @@ -529,7 +306,7 @@ public class CameraOptions { * * @return the max value */ - public float getPreviewFrameRateMaxValue() { + public final float getPreviewFrameRateMaxValue() { return previewFrameRateMaxValue; } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraUtils.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraUtils.java index 9f8b6210..e6f017f7 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraUtils.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraUtils.java @@ -284,7 +284,7 @@ public class CameraUtils { ExifInterface exif = new ExifInterface(stream); int exifOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); - orientation = ExifHelper.readExifOrientation(exifOrientation); + orientation = ExifHelper.getOrientation(exifOrientation); flip = exifOrientation == ExifInterface.ORIENTATION_FLIP_HORIZONTAL || exifOrientation == ExifInterface.ORIENTATION_FLIP_VERTICAL || exifOrientation == ExifInterface.ORIENTATION_TRANSPOSE || diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java index 3921abe6..d8eb83c9 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java @@ -40,6 +40,7 @@ import com.otaliastudios.cameraview.controls.Flash; import com.otaliastudios.cameraview.controls.Grid; import com.otaliastudios.cameraview.controls.Hdr; import com.otaliastudios.cameraview.controls.Mode; +import com.otaliastudios.cameraview.controls.PictureFormat; import com.otaliastudios.cameraview.controls.Preview; import com.otaliastudios.cameraview.controls.VideoCodec; import com.otaliastudios.cameraview.controls.WhiteBalance; @@ -246,6 +247,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { setPictureSize(sizeSelectors.getPictureSizeSelector()); setPictureMetering(pictureMetering); setPictureSnapshotMetering(pictureSnapshotMetering); + setPictureFormat(controls.getPictureFormat()); setVideoSize(sizeSelectors.getVideoSizeSelector()); setVideoCodec(controls.getVideoCodec()); setVideoMaxSize(videoMaxSize); @@ -847,6 +849,8 @@ public class CameraView extends FrameLayout implements LifecycleObserver { setPreview((Preview) control); } else if (control instanceof Engine) { setEngine((Engine) control); + } else if (control instanceof PictureFormat) { + setPictureFormat((PictureFormat) control); } } @@ -881,6 +885,8 @@ public class CameraView extends FrameLayout implements LifecycleObserver { return (T) getPreview(); } else if (controlClass == Engine.class) { return (T) getEngine(); + } else if (controlClass == PictureFormat.class) { + return (T) getPictureFormat(); } else { throw new IllegalArgumentException("Unknown control class: " + controlClass); } @@ -948,6 +954,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { setAudio(oldEngine.getAudio()); setAudioBitRate(oldEngine.getAudioBitRate()); setPictureSize(oldEngine.getPictureSizeSelector()); + setPictureFormat(oldEngine.getPictureFormat()); setVideoSize(oldEngine.getVideoSizeSelector()); setVideoCodec(oldEngine.getVideoCodec()); setVideoMaxSize(oldEngine.getVideoMaxSize()); @@ -1418,6 +1425,29 @@ public class CameraView extends FrameLayout implements LifecycleObserver { return mCameraEngine.getPictureSnapshotMetering(); } + /** + * Sets the format for pictures taken with {@link #takePicture()}. This format does not apply + * to picture snapshots taken with {@link #takePictureSnapshot()}. + * The {@link PictureFormat#JPEG} is always supported - for other values, please check + * the {@link CameraOptions#getSupportedPictureFormats()} value. + * + * @param pictureFormat new format + */ + public void setPictureFormat(@NonNull PictureFormat pictureFormat) { + mCameraEngine.setPictureFormat(pictureFormat); + } + + /** + * Returns the current picture format. + * @see #setPictureFormat(PictureFormat) + * @return the picture format + */ + @NonNull + public PictureFormat getPictureFormat() { + return mCameraEngine.getPictureFormat(); + } + + /** * Sets a capture size selector for video mode. * The {@link SizeSelector} will be invoked with the list of available sizes, and the first diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/PictureResult.java b/cameraview/src/main/java/com/otaliastudios/cameraview/PictureResult.java index b653e2f4..763a81ae 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/PictureResult.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/PictureResult.java @@ -2,8 +2,10 @@ package com.otaliastudios.cameraview; import android.graphics.BitmapFactory; import android.location.Location; +import android.os.Build; import com.otaliastudios.cameraview.controls.Facing; +import com.otaliastudios.cameraview.controls.PictureFormat; import com.otaliastudios.cameraview.size.Size; import java.io.File; @@ -31,19 +33,16 @@ public class PictureResult { public Size size; public Facing facing; public byte[] data; - public int format; + public PictureFormat format; } - public final static int FORMAT_JPEG = 0; - // public final static int FORMAT_PNG = 1; - private final boolean isSnapshot; private final Location location; private final int rotation; private final Size size; private final Facing facing; private final byte[] data; - private final int format; + private final PictureFormat format; PictureResult(@NonNull Stub builder) { isSnapshot = builder.isSnapshot; @@ -118,12 +117,12 @@ public class PictureResult { } /** - * Returns the image format. At the moment this will always be - * {@link #FORMAT_JPEG}. + * Returns the format for {@link #getData()}. * - * @return the current format + * @return the format */ - public int getFormat() { + @NonNull + public PictureFormat getFormat() { return format; } @@ -137,8 +136,18 @@ public class PictureResult { * @param callback a callback to be notified of image decoding */ public void toBitmap(int maxWidth, int maxHeight, @NonNull BitmapCallback callback) { - CameraUtils.decodeBitmap(getData(), maxWidth, maxHeight, new BitmapFactory.Options(), - rotation, callback); + if (format == PictureFormat.JPEG) { + CameraUtils.decodeBitmap(getData(), maxWidth, maxHeight, new BitmapFactory.Options(), + rotation, callback); + } else if (format == PictureFormat.DNG && Build.VERSION.SDK_INT >= 24) { + // Apparently: BitmapFactory added DNG support in API 24. + // https://github.com/aosp-mirror/platform_frameworks_base/blob/nougat-mr1-release/core/jni/android/graphics/BitmapFactory.cpp + CameraUtils.decodeBitmap(getData(), maxWidth, maxHeight, new BitmapFactory.Options(), + rotation, callback); + } else { + throw new UnsupportedOperationException("PictureResult.toBitmap() does not support " + + "this picture format: " + format); + } } /** diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/controls/ControlParser.java b/cameraview/src/main/java/com/otaliastudios/cameraview/controls/ControlParser.java index 2b733725..61f20bae 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/controls/ControlParser.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/controls/ControlParser.java @@ -23,6 +23,7 @@ public class ControlParser { private int audio; private int videoCodec; private int engine; + private int pictureFormat; public ControlParser(@NonNull Context context, @NonNull TypedArray array) { preview = array.getInteger(R.styleable.CameraView_cameraPreview, Preview.DEFAULT.value()); @@ -38,6 +39,8 @@ public class ControlParser { videoCodec = array.getInteger(R.styleable.CameraView_cameraVideoCodec, VideoCodec.DEFAULT.value()); engine = array.getInteger(R.styleable.CameraView_cameraEngine, Engine.DEFAULT.value()); + pictureFormat = array.getInteger(R.styleable.CameraView_cameraPictureFormat, + PictureFormat.DEFAULT.value()); } @NonNull @@ -90,4 +93,9 @@ public class ControlParser { public Engine getEngine() { return Engine.fromValue(engine); } + + @NonNull + public PictureFormat getPictureFormat() { + return PictureFormat.fromValue(pictureFormat); + } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/controls/PictureFormat.java b/cameraview/src/main/java/com/otaliastudios/cameraview/controls/PictureFormat.java new file mode 100644 index 00000000..796c590b --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/controls/PictureFormat.java @@ -0,0 +1,52 @@ +package com.otaliastudios.cameraview.controls; + + +import androidx.annotation.NonNull; + +import com.otaliastudios.cameraview.CameraOptions; +import com.otaliastudios.cameraview.CameraView; + +/** + * Format of the picture results for pictures that are taken with {@link CameraView#takePicture()}. + * This does not apply to picture snapshots. + * + * @see CameraView#setPictureFormat(PictureFormat) + */ +public enum PictureFormat implements Control { + + /** + * The picture result data will be a JPEG file. + * This value is always supported. + */ + JPEG(0), + + /** + * The picture result data will be a DNG file. + * This is only supported with the {@link Engine#CAMERA2} engine and only on + * specific devices. Please check {@link CameraOptions#getSupportedPictureFormats()}. + */ + DNG(1); + + static final PictureFormat DEFAULT = JPEG; + + private int value; + + PictureFormat(int value) { + this.value = value; + } + + int value() { + return value; + } + + @NonNull + static PictureFormat fromValue(int value) { + PictureFormat[] list = PictureFormat.values(); + for (PictureFormat action : list) { + if (action.value() == value) { + return action; + } + } + return DEFAULT; + } +} 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 f7111e85..0dbb7b4a 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java @@ -13,17 +13,17 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import android.util.Range; import android.view.SurfaceHolder; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Tasks; import com.otaliastudios.cameraview.CameraException; import com.otaliastudios.cameraview.CameraLogger; -import com.otaliastudios.cameraview.CameraOptions; +import com.otaliastudios.cameraview.controls.PictureFormat; import com.otaliastudios.cameraview.engine.mappers.Camera1Mapper; import com.otaliastudios.cameraview.engine.offset.Axis; import com.otaliastudios.cameraview.engine.offset.Reference; +import com.otaliastudios.cameraview.engine.options.Camera1Options; import com.otaliastudios.cameraview.frame.Frame; import com.otaliastudios.cameraview.PictureResult; import com.otaliastudios.cameraview.VideoResult; @@ -156,7 +156,7 @@ 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, mCameraId, + mCameraOptions = new Camera1Options(params, mCameraId, getAngles().flip(Reference.SENSOR, Reference.VIEW)); applyAllParameters(params); mCamera.setParameters(params); @@ -717,6 +717,14 @@ public class Camera1Engine extends CameraEngine implements return false; } + @Override + public void setPictureFormat(@NonNull PictureFormat pictureFormat) { + if (pictureFormat != PictureFormat.JPEG) { + throw new UnsupportedOperationException("Unsupported picture format: " + pictureFormat); + } + mPictureFormat = pictureFormat; + } + //endregion //region Frame Processing 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 c25dd20e..3f51f481 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java @@ -35,13 +35,13 @@ import com.google.android.gms.tasks.TaskCompletionSource; import com.google.android.gms.tasks.Tasks; import com.otaliastudios.cameraview.CameraException; import com.otaliastudios.cameraview.CameraLogger; -import com.otaliastudios.cameraview.CameraOptions; import com.otaliastudios.cameraview.PictureResult; import com.otaliastudios.cameraview.VideoResult; import com.otaliastudios.cameraview.controls.Facing; import com.otaliastudios.cameraview.controls.Flash; import com.otaliastudios.cameraview.controls.Hdr; import com.otaliastudios.cameraview.controls.Mode; +import com.otaliastudios.cameraview.controls.PictureFormat; import com.otaliastudios.cameraview.controls.WhiteBalance; import com.otaliastudios.cameraview.engine.action.Action; import com.otaliastudios.cameraview.engine.action.ActionHolder; @@ -53,6 +53,7 @@ import com.otaliastudios.cameraview.engine.meter.MeterAction; import com.otaliastudios.cameraview.engine.meter.MeterResetAction; import com.otaliastudios.cameraview.engine.offset.Axis; import com.otaliastudios.cameraview.engine.offset.Reference; +import com.otaliastudios.cameraview.engine.options.Camera2Options; import com.otaliastudios.cameraview.frame.Frame; import com.otaliastudios.cameraview.frame.FrameManager; import com.otaliastudios.cameraview.gesture.Gesture; @@ -386,7 +387,14 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv LOG.i("createCamera:", "Applying default parameters."); mCameraCharacteristics = mManager.getCameraCharacteristics(mCameraId); boolean flip = getAngles().flip(Reference.SENSOR, Reference.VIEW); - mCameraOptions = new CameraOptions(mManager, mCameraId, flip); + int format; + switch (mPictureFormat) { + case JPEG: format = ImageFormat.JPEG; break; + case DNG: format = ImageFormat.RAW_SENSOR; break; + default: throw new IllegalArgumentException("Unknown format:" + + mPictureFormat); + } + mCameraOptions = new Camera2Options(mManager, mCameraId, flip, format); createRepeatingRequestBuilder(CameraDevice.TEMPLATE_PREVIEW); } catch (CameraAccessException e) { task.trySetException(createCameraException(e)); @@ -426,6 +434,12 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv final TaskCompletionSource task = new TaskCompletionSource<>(); // Compute sizes. + // TODO preview stream should never be bigger than 1920x1080 as per + // CameraDevice.createCaptureSession. This should be probably be applied + // before all the other external selectors, to treat it as a hard limit. + // OR: pass an int into these functions to be able to take smaller dims + // when session configuration fails + // OR: both. mCaptureSize = computeCaptureSize(); mPreviewStreamSize = computePreviewStreamSize(); @@ -477,13 +491,18 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv } // 3. PICTURE RECORDING + // Format is supported, or it would have thrown in Camera2Options constructor. if (getMode() == Mode.PICTURE) { + int format; + switch (mPictureFormat) { + case JPEG: format = ImageFormat.JPEG; break; + case DNG: format = ImageFormat.RAW_SENSOR; break; + default: throw new IllegalArgumentException("Unknown format:" + mPictureFormat); + } mPictureReader = ImageReader.newInstance( mCaptureSize.getWidth(), mCaptureSize.getHeight(), - ImageFormat.JPEG, - 2 - ); + format, 2); outputSurfaces.add(mPictureReader.getSurface()); } @@ -501,8 +520,8 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv sizes.add(new Size(aSize.getWidth(), aSize.getHeight())); } mFrameProcessingSize = SizeSelectors.and( - SizeSelectors.maxWidth(Math.min(700, mPreviewStreamSize.getWidth())), - SizeSelectors.maxHeight(Math.min(700, mPreviewStreamSize.getHeight())), + SizeSelectors.maxWidth(Math.min(640, mPreviewStreamSize.getWidth())), + SizeSelectors.maxHeight(Math.min(640, mPreviewStreamSize.getHeight())), SizeSelectors.biggest()).select(sizes).get(0); mFrameProcessingReader = ImageReader.newInstance( mFrameProcessingSize.getWidth(), @@ -613,6 +632,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv throw createCameraException(e); } removeRepeatingRequestBuilderSurfaces(); + mLastRepeatingResult = null; LOG.i("onStopPreview:", "Returning."); return Tasks.forResult(null); } @@ -690,7 +710,10 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv action.addCallback(new CompletionCallback() { @Override protected void onActionCompleted(@NonNull Action action) { - onTakePictureSnapshot(stub, outputRatio, false); + // This is called on any thread, so be careful. + setPictureSnapshotMetering(false); + takePictureSnapshot(stub); + setPictureSnapshotMetering(true); } }); action.start(this); @@ -720,7 +743,10 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv action.addCallback(new CompletionCallback() { @Override protected void onActionCompleted(@NonNull Action action) { - onTakePicture(stub, false); + // This is called on any thread, so be careful. + setPictureMetering(false); + takePicture(stub); + setPictureMetering(true); } }); action.start(this); @@ -1304,6 +1330,28 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv return false; } + @Override + public void setPictureFormat(final @NonNull PictureFormat pictureFormat) { + if (pictureFormat != mPictureFormat) { + mPictureFormat = pictureFormat; + LOG.i("setPictureFormat", "changing to", pictureFormat, "posting."); + mHandler.run(new Runnable() { + @Override + public void run() { + LOG.i("setPictureFormat", "changing to", pictureFormat, + "executing. EngineState:", getEngineState(), + "BindState:", getBindState()); + if (getEngineState() == STATE_STOPPED) { + LOG.i("setPictureFormat", "not started so won't restart."); + } else { + LOG.i("setPictureFormat", "started or starting. Calling restart()"); + restart(); + } + } + }); + } + } + //endregion //region Frame Processing @@ -1518,7 +1566,9 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv @Override public void applyBuilder(@NonNull Action source, @NonNull CaptureRequest.Builder builder) throws CameraAccessException { - mSession.capture(builder.build(), mRepeatingRequestCallback, null); + if (getPreviewState() == STATE_STARTED) { + mSession.capture(builder.build(), mRepeatingRequestCallback, null); + } } //endregion 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 419b61a5..d4345e00 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java @@ -18,7 +18,7 @@ import com.otaliastudios.cameraview.CameraException; import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.CameraOptions; import com.otaliastudios.cameraview.PictureResult; -import com.otaliastudios.cameraview.engine.mappers.Camera1Mapper; +import com.otaliastudios.cameraview.controls.PictureFormat; import com.otaliastudios.cameraview.overlay.Overlay; import com.otaliastudios.cameraview.VideoResult; import com.otaliastudios.cameraview.engine.offset.Angles; @@ -168,6 +168,7 @@ public abstract class CameraEngine implements @SuppressWarnings("WeakerAccess") protected WhiteBalance mWhiteBalance; @SuppressWarnings("WeakerAccess") protected VideoCodec mVideoCodec; @SuppressWarnings("WeakerAccess") protected Hdr mHdr; + @SuppressWarnings("WeakerAccess") protected PictureFormat mPictureFormat; @SuppressWarnings("WeakerAccess") protected Location mLocation; @SuppressWarnings("WeakerAccess") protected float mZoomValue; @SuppressWarnings("WeakerAccess") protected float mExposureCorrectionValue; @@ -1020,6 +1021,11 @@ public abstract class CameraEngine implements return mLocation; } + @NonNull + public final PictureFormat getPictureFormat() { + return mPictureFormat; + } + public final float getZoomValue() { return mZoomValue; } @@ -1109,6 +1115,9 @@ public abstract class CameraEngine implements // If closed, keep. If opened, check supported and apply. public abstract void setLocation(@Nullable Location location); + // If closed, keep. If opened, check supported and apply. + public abstract void setPictureFormat(@NonNull PictureFormat pictureFormat); + public abstract void startAutoFocus(@Nullable Gesture gesture, @NonNull PointF point); public abstract void setPlaySounds(boolean playSounds); @@ -1125,11 +1134,11 @@ public abstract class CameraEngine implements /* not final for tests */ public void takePicture(final @NonNull PictureResult.Stub stub) { - LOG.v("takePicture", "scheduling"); + LOG.i("takePicture", "scheduling"); mHandler.run(new Runnable() { @Override public void run() { - LOG.v("takePicture", "performing. BindState:", getBindState(), + LOG.i("takePicture", "performing. BindState:", getBindState(), "isTakingPicture:", isTakingPicture()); if (mMode == Mode.VIDEO) { throw new IllegalStateException("Can't take hq pictures while in VIDEO mode"); @@ -1139,6 +1148,7 @@ public abstract class CameraEngine implements stub.isSnapshot = false; stub.location = mLocation; stub.facing = mFacing; + stub.format = mPictureFormat; onTakePicture(stub, mPictureMetering); } }); @@ -1150,17 +1160,18 @@ public abstract class CameraEngine implements * @param stub a picture stub */ public final void takePictureSnapshot(final @NonNull PictureResult.Stub stub) { - LOG.v("takePictureSnapshot", "scheduling"); + LOG.i("takePictureSnapshot", "scheduling"); mHandler.run(new Runnable() { @Override public void run() { - LOG.v("takePictureSnapshot", "performing. BindState:", + LOG.i("takePictureSnapshot", "performing. BindState:", getBindState(), "isTakingPicture:", isTakingPicture()); if (getBindState() < STATE_STARTED) return; if (isTakingPicture()) return; stub.location = mLocation; stub.isSnapshot = true; stub.facing = mFacing; + stub.format = PictureFormat.JPEG; // Leave the other parameters to subclasses. //noinspection ConstantConditions AspectRatio ratio = AspectRatio.of(getPreviewSurfaceSize(Reference.OUTPUT)); @@ -1191,11 +1202,11 @@ public abstract class CameraEngine implements } public final void takeVideo(final @NonNull VideoResult.Stub stub, final @NonNull File file) { - LOG.v("takeVideo", "scheduling"); + LOG.i("takeVideo", "scheduling"); mHandler.run(new Runnable() { @Override public void run() { - LOG.v("takeVideo", "performing. BindState:", getBindState(), + LOG.i("takeVideo", "performing. BindState:", getBindState(), "isTakingVideo:", isTakingVideo()); if (getBindState() < STATE_STARTED) return; if (isTakingVideo()) return; @@ -1223,11 +1234,11 @@ public abstract class CameraEngine implements */ public final void takeVideoSnapshot(@NonNull final VideoResult.Stub stub, @NonNull final File file) { - LOG.v("takeVideoSnapshot", "scheduling"); + LOG.i("takeVideoSnapshot", "scheduling"); mHandler.run(new Runnable() { @Override public void run() { - LOG.v("takeVideoSnapshot", "performing. BindState:", getBindState(), + LOG.i("takeVideoSnapshot", "performing. BindState:", getBindState(), "isTakingVideo:", isTakingVideo()); if (getBindState() < STATE_STARTED) return; if (isTakingVideo()) return; diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/BaseAction.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/BaseAction.java index ceff4207..08b02e3a 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/BaseAction.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/BaseAction.java @@ -30,6 +30,7 @@ public abstract class BaseAction implements Action { private final List callbacks = new ArrayList<>(); private int state; private ActionHolder holder; + private boolean needsOnStart; @Override public final int getState() { @@ -39,7 +40,11 @@ public abstract class BaseAction implements Action { @Override public final void start(@NonNull ActionHolder holder) { holder.addAction(this); - onStart(holder); + if (holder.getLastResult(this) != null) { + onStart(holder); + } else { + needsOnStart = true; + } } @Override @@ -49,6 +54,7 @@ public abstract class BaseAction implements Action { onAbort(holder); setState(STATE_COMPLETED); } + needsOnStart = false; } /** @@ -58,7 +64,7 @@ public abstract class BaseAction implements Action { */ @CallSuper protected void onStart(@NonNull ActionHolder holder) { - this.holder = holder; // must be here + this.holder = holder; // Overrideable } @@ -72,9 +78,13 @@ public abstract class BaseAction implements Action { // Overrideable } + @CallSuper @Override public void onCaptureStarted(@NonNull ActionHolder holder, @NonNull CaptureRequest request) { - // Overrideable + if (needsOnStart) { + onStart(holder); + needsOnStart = false; + } } @Override diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/options/Camera1Options.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/options/Camera1Options.java new file mode 100644 index 00000000..dd8e1a7a --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/options/Camera1Options.java @@ -0,0 +1,132 @@ +package com.otaliastudios.cameraview.engine.options; + +import android.hardware.Camera; +import android.media.CamcorderProfile; + +import androidx.annotation.NonNull; + +import com.otaliastudios.cameraview.CameraOptions; +import com.otaliastudios.cameraview.controls.Facing; +import com.otaliastudios.cameraview.controls.Flash; +import com.otaliastudios.cameraview.controls.Hdr; +import com.otaliastudios.cameraview.controls.PictureFormat; +import com.otaliastudios.cameraview.controls.WhiteBalance; +import com.otaliastudios.cameraview.engine.mappers.Camera1Mapper; +import com.otaliastudios.cameraview.internal.utils.CamcorderProfiles; +import com.otaliastudios.cameraview.size.AspectRatio; +import com.otaliastudios.cameraview.size.Size; + +import java.util.List; + +public class Camera1Options extends CameraOptions { + + public Camera1Options(@NonNull Camera.Parameters params, int cameraId, boolean flipSizes) { + List strings; + Camera1Mapper mapper = Camera1Mapper.get(); + + // Facing + Camera.CameraInfo cameraInfo = new Camera.CameraInfo(); + for (int i = 0, count = Camera.getNumberOfCameras(); i < count; i++) { + Camera.getCameraInfo(i, cameraInfo); + Facing value = mapper.unmapFacing(cameraInfo.facing); + if (value != null) supportedFacing.add(value); + } + + // WB + strings = params.getSupportedWhiteBalance(); + if (strings != null) { + for (String string : strings) { + WhiteBalance value = mapper.unmapWhiteBalance(string); + if (value != null) supportedWhiteBalance.add(value); + } + } + + // Flash + supportedFlash.add(Flash.OFF); + strings = params.getSupportedFlashModes(); + if (strings != null) { + for (String string : strings) { + Flash value = mapper.unmapFlash(string); + if (value != null) supportedFlash.add(value); + } + } + + // Hdr + supportedHdr.add(Hdr.OFF); + strings = params.getSupportedSceneModes(); + if (strings != null) { + for (String string : strings) { + Hdr value = mapper.unmapHdr(string); + if (value != null) supportedHdr.add(value); + } + } + + // zoom + zoomSupported = params.isZoomSupported(); + + // autofocus + autoFocusSupported = params.getSupportedFocusModes() + .contains(Camera.Parameters.FOCUS_MODE_AUTO); + + // Exposure correction + float step = params.getExposureCompensationStep(); + exposureCorrectionMinValue = (float) params.getMinExposureCompensation() * step; + exposureCorrectionMaxValue = (float) params.getMaxExposureCompensation() * step; + exposureCorrectionSupported = params.getMinExposureCompensation() != 0 + || params.getMaxExposureCompensation() != 0; + + // Picture Sizes + List sizes = params.getSupportedPictureSizes(); + for (Camera.Size size : sizes) { + int width = flipSizes ? size.height : size.width; + int height = flipSizes ? size.width : size.height; + supportedPictureSizes.add(new Size(width, height)); + supportedPictureAspectRatio.add(AspectRatio.of(width, height)); + } + + // Video Sizes + // As a safety measure, remove Sizes bigger than CamcorderProfile.highest + CamcorderProfile profile = CamcorderProfiles.get(cameraId, + new Size(Integer.MAX_VALUE, Integer.MAX_VALUE)); + Size videoMaxSize = new Size(profile.videoFrameWidth, profile.videoFrameHeight); + List vsizes = params.getSupportedVideoSizes(); + if (vsizes != null) { + for (Camera.Size size : vsizes) { + if (size.width <= videoMaxSize.getWidth() + && size.height <= videoMaxSize.getHeight()) { + int width = flipSizes ? size.height : size.width; + int height = flipSizes ? size.width : size.height; + supportedVideoSizes.add(new Size(width, height)); + supportedVideoAspectRatio.add(AspectRatio.of(width, height)); + } + } + } else { + // StackOverflow threads seems to agree that if getSupportedVideoSizes is null, + // previews can be used. + List fallback = params.getSupportedPreviewSizes(); + for (Camera.Size size : fallback) { + if (size.width <= videoMaxSize.getWidth() + && size.height <= videoMaxSize.getHeight()) { + int width = flipSizes ? size.height : size.width; + int height = flipSizes ? size.width : size.height; + supportedVideoSizes.add(new Size(width, height)); + supportedVideoAspectRatio.add(AspectRatio.of(width, height)); + } + } + } + + // Preview FPS + previewFrameRateMinValue = Float.MAX_VALUE; + previewFrameRateMaxValue = -Float.MAX_VALUE; + List fpsRanges = params.getSupportedPreviewFpsRange(); + for (int[] fpsRange : fpsRanges) { + float lower = (float) fpsRange[0] / 1000F; + float upper = (float) fpsRange[1] / 1000F; + previewFrameRateMinValue = Math.min(previewFrameRateMinValue, lower); + previewFrameRateMaxValue = Math.max(previewFrameRateMaxValue, upper); + } + + // Picture formats + supportedPictureFormats.add(PictureFormat.JPEG); + } +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/options/Camera2Options.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/options/Camera2Options.java new file mode 100644 index 00000000..09791608 --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/options/Camera2Options.java @@ -0,0 +1,178 @@ +package com.otaliastudios.cameraview.engine.options; + +import android.graphics.ImageFormat; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraManager; +import android.hardware.camera2.params.StreamConfigurationMap; +import android.media.CamcorderProfile; +import android.media.MediaRecorder; +import android.os.Build; +import android.util.Range; +import android.util.Rational; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; + +import com.otaliastudios.cameraview.CameraOptions; +import com.otaliastudios.cameraview.controls.Facing; +import com.otaliastudios.cameraview.controls.Flash; +import com.otaliastudios.cameraview.controls.Hdr; +import com.otaliastudios.cameraview.controls.PictureFormat; +import com.otaliastudios.cameraview.controls.WhiteBalance; +import com.otaliastudios.cameraview.engine.mappers.Camera2Mapper; +import com.otaliastudios.cameraview.internal.utils.CamcorderProfiles; +import com.otaliastudios.cameraview.size.AspectRatio; +import com.otaliastudios.cameraview.size.Size; + +import java.util.Set; + +import static android.hardware.camera2.CameraCharacteristics.*; + +@RequiresApi(Build.VERSION_CODES.LOLLIPOP) +public class Camera2Options extends CameraOptions { + + public Camera2Options(@NonNull CameraManager manager, + @NonNull String cameraId, + boolean flipSizes, + int pictureFormat) throws CameraAccessException { + Camera2Mapper mapper = Camera2Mapper.get(); + CameraCharacteristics cameraCharacteristics = manager.getCameraCharacteristics(cameraId); + + // Facing + for (String cameraId1 : manager.getCameraIdList()) { + CameraCharacteristics cameraCharacteristics1 = manager + .getCameraCharacteristics(cameraId1); + Integer cameraFacing = cameraCharacteristics1.get(LENS_FACING); + if (cameraFacing != null) { + Facing value = mapper.unmapFacing(cameraFacing); + if (value != null) supportedFacing.add(value); + } + } + + // WB + int[] awbModes = cameraCharacteristics.get(CONTROL_AWB_AVAILABLE_MODES); + //noinspection ConstantConditions + for (int awbMode : awbModes) { + WhiteBalance value = mapper.unmapWhiteBalance(awbMode); + if (value != null) supportedWhiteBalance.add(value); + } + + // Flash + supportedFlash.add(Flash.OFF); + Boolean hasFlash = cameraCharacteristics.get(FLASH_INFO_AVAILABLE); + if (hasFlash != null && hasFlash) { + int[] aeModes = cameraCharacteristics.get(CONTROL_AE_AVAILABLE_MODES); + //noinspection ConstantConditions + for (int aeMode : aeModes) { + Set flashes = mapper.unmapFlash(aeMode); + supportedFlash.addAll(flashes); + } + } + + // HDR + supportedHdr.add(Hdr.OFF); + int[] sceneModes = cameraCharacteristics.get(CONTROL_AVAILABLE_SCENE_MODES); + //noinspection ConstantConditions + for (int sceneMode : sceneModes) { + Hdr value = mapper.unmapHdr(sceneMode); + if (value != null) supportedHdr.add(value); + } + + // Zoom + Float maxZoom = cameraCharacteristics.get(SCALER_AVAILABLE_MAX_DIGITAL_ZOOM); + if(maxZoom != null) { + zoomSupported = maxZoom > 1; + } + + + // AutoFocus + // This now means 3A metering with respect to a specific region of the screen. + // Some controls (AF, AE) have special triggers that might or might not be supported. + // But they can also be on some continuous search mode so that the trigger is not needed. + // What really matters in my opinion is the availability of regions. + Integer afRegions = cameraCharacteristics.get(CONTROL_MAX_REGIONS_AF); + Integer aeRegions = cameraCharacteristics.get(CONTROL_MAX_REGIONS_AE); + Integer awbRegions = cameraCharacteristics.get(CONTROL_MAX_REGIONS_AWB); + autoFocusSupported = (afRegions != null && afRegions > 0) + || (aeRegions != null && aeRegions > 0) + || (awbRegions != null && awbRegions > 0); + + // Exposure correction + Range exposureRange = cameraCharacteristics.get(CONTROL_AE_COMPENSATION_RANGE); + Rational exposureStep = cameraCharacteristics.get(CONTROL_AE_COMPENSATION_STEP); + if (exposureRange != null && exposureStep != null && exposureStep.floatValue() != 0) { + exposureCorrectionMinValue = exposureRange.getLower() / exposureStep.floatValue(); + exposureCorrectionMaxValue = exposureRange.getUpper() / exposureStep.floatValue(); + } + exposureCorrectionSupported = exposureCorrectionMinValue != 0 + && exposureCorrectionMaxValue != 0; + + + // Picture Sizes + StreamConfigurationMap streamMap = cameraCharacteristics.get( + SCALER_STREAM_CONFIGURATION_MAP); + if (streamMap == null) { + throw new RuntimeException("StreamConfigurationMap is null. Should not happen."); + } + int[] pictureFormats = streamMap.getOutputFormats(); + boolean hasPictureFormat = false; + for (int picFormat : pictureFormats) { + if (picFormat == pictureFormat) { + hasPictureFormat = true; + break; + } + } + if (!hasPictureFormat) { + throw new IllegalStateException("Picture format not supported: " + pictureFormat); + } + android.util.Size[] psizes = streamMap.getOutputSizes(pictureFormat); + for (android.util.Size size : psizes) { + int width = flipSizes ? size.getHeight() : size.getWidth(); + int height = flipSizes ? size.getWidth() : size.getHeight(); + supportedPictureSizes.add(new Size(width, height)); + supportedPictureAspectRatio.add(AspectRatio.of(width, height)); + } + + // Video Sizes + // As a safety measure, remove Sizes bigger than CamcorderProfile.highest + CamcorderProfile profile = CamcorderProfiles.get(cameraId, + new Size(Integer.MAX_VALUE, Integer.MAX_VALUE)); + Size videoMaxSize = new Size(profile.videoFrameWidth, profile.videoFrameHeight); + android.util.Size[] vsizes = streamMap.getOutputSizes(MediaRecorder.class); + for (android.util.Size size : vsizes) { + if (size.getWidth() <= videoMaxSize.getWidth() + && size.getHeight() <= videoMaxSize.getHeight()) { + int width = flipSizes ? size.getHeight() : size.getWidth(); + int height = flipSizes ? size.getWidth() : size.getHeight(); + supportedVideoSizes.add(new Size(width, height)); + supportedVideoAspectRatio.add(AspectRatio.of(width, height)); + } + } + + // Preview FPS + Range[] range = cameraCharacteristics.get(CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); + if (range != null) { + previewFrameRateMinValue = Float.MAX_VALUE; + previewFrameRateMaxValue = -Float.MAX_VALUE; + for (Range fpsRange : range) { + previewFrameRateMinValue = Math.min(previewFrameRateMinValue, fpsRange.getLower()); + previewFrameRateMaxValue = Math.max(previewFrameRateMaxValue, fpsRange.getUpper()); + } + } else { + previewFrameRateMinValue = 0F; + previewFrameRateMaxValue = 0F; + } + + // Picture formats + supportedPictureFormats.add(PictureFormat.JPEG); + int[] caps = cameraCharacteristics.get(REQUEST_AVAILABLE_CAPABILITIES); + if (caps != null) { + for (int cap : caps) { + if (cap == CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW) { + supportedPictureFormats.add(PictureFormat.DNG); + } + } + } + } +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/ExifHelper.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/ExifHelper.java index 83dc359e..a890124f 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/ExifHelper.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/ExifHelper.java @@ -11,7 +11,7 @@ public class ExifHelper { * Maps an {@link ExifInterface} orientation value * to the actual degrees. */ - public static int readExifOrientation(int exifOrientation) { + public static int getOrientation(int exifOrientation) { int orientation; switch (exifOrientation) { case ExifInterface.ORIENTATION_NORMAL: @@ -34,5 +34,18 @@ public class ExifHelper { } return orientation; } + + /** + * Maps a degree value to {@link ExifInterface} constant. + */ + public static int getExifOrientation(int orientation) { + switch ((orientation + 360) % 360) { + case 0: return ExifInterface.ORIENTATION_NORMAL; + case 90: return ExifInterface.ORIENTATION_ROTATE_90; + case 180: return ExifInterface.ORIENTATION_ROTATE_180; + case 270: return ExifInterface.ORIENTATION_ROTATE_270; + default: throw new IllegalArgumentException("Invalid orientation: " + orientation); + } + } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Full1PictureRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Full1PictureRecorder.java index 7bad4173..d93ca9b1 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Full1PictureRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Full1PictureRecorder.java @@ -57,11 +57,10 @@ public class Full1PictureRecorder extends PictureRecorder { int exifOrientation = exif.getAttributeInt( ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); - exifRotation = ExifHelper.readExifOrientation(exifOrientation); + exifRotation = ExifHelper.getOrientation(exifOrientation); } catch (IOException e) { exifRotation = 0; } - mResult.format = PictureResult.FORMAT_JPEG; mResult.data = data; mResult.rotation = exifRotation; camera.startPreview(); // This is needed, read somewhere in the docs. diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Full2PictureRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Full2PictureRecorder.java index 0e2c6fae..6cfe8191 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Full2PictureRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Full2PictureRecorder.java @@ -3,12 +3,15 @@ package com.otaliastudios.cameraview.picture; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraDevice; import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.DngCreator; +import android.hardware.camera2.TotalCaptureResult; import android.media.Image; import android.media.ImageReader; import android.os.Build; import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.PictureResult; +import com.otaliastudios.cameraview.controls.PictureFormat; import com.otaliastudios.cameraview.engine.Camera2Engine; import com.otaliastudios.cameraview.engine.action.Action; import com.otaliastudios.cameraview.engine.action.ActionHolder; @@ -16,7 +19,9 @@ import com.otaliastudios.cameraview.engine.action.BaseAction; import com.otaliastudios.cameraview.internal.utils.ExifHelper; import com.otaliastudios.cameraview.internal.utils.WorkerHandler; +import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.ByteBuffer; @@ -39,6 +44,8 @@ public class Full2PictureRecorder extends PictureRecorder private final ImageReader mPictureReader; private final CaptureRequest.Builder mPictureBuilder; + private DngCreator mDngCreator; + public Full2PictureRecorder(@NonNull PictureResult.Stub stub, @NonNull Camera2Engine engine, @NonNull CaptureRequest.Builder pictureBuilder, @@ -54,7 +61,9 @@ public class Full2PictureRecorder extends PictureRecorder protected void onStart(@NonNull ActionHolder holder) { super.onStart(holder); mPictureBuilder.addTarget(mPictureReader.getSurface()); - mPictureBuilder.set(CaptureRequest.JPEG_ORIENTATION, mResult.rotation); + if (mResult.format == PictureFormat.JPEG) { + mPictureBuilder.set(CaptureRequest.JPEG_ORIENTATION, mResult.rotation); + } mPictureBuilder.setTag(CameraDevice.TEMPLATE_STILL_CAPTURE); try { holder.applyBuilder(this, mPictureBuilder); @@ -75,6 +84,20 @@ public class Full2PictureRecorder extends PictureRecorder setState(STATE_COMPLETED); } } + + @Override + public void onCaptureCompleted(@NonNull ActionHolder holder, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { + super.onCaptureCompleted(holder, request, result); + if (mResult.format == PictureFormat.DNG) { + mDngCreator = new DngCreator(holder.getCharacteristics(this), result); + mDngCreator.setOrientation(ExifHelper.getExifOrientation(mResult.rotation)); + if (mResult.location != null) { + mDngCreator.setLocation(mResult.location); + } + } + } }; } @@ -86,37 +109,59 @@ public class Full2PictureRecorder extends PictureRecorder @Override public void onImageAvailable(ImageReader reader) { LOG.i("onImageAvailable started."); - // Read the JPEG. Image image = null; //noinspection TryFinallyCanBeTryWithResources try { image = reader.acquireNextImage(); - ByteBuffer buffer = image.getPlanes()[0].getBuffer(); - byte[] bytes = new byte[buffer.remaining()]; - buffer.get(bytes); - mResult.data = bytes; + switch (mResult.format) { + case JPEG: readJpegImage(image); break; + case DNG: readRawImage(image); break; + default: throw new IllegalStateException("Unknown format: " + mResult.format); + } } catch (Exception e) { mResult = null; mError = e; dispatchResult(); return; } finally { - if (image != null) image.close(); + if (image != null) { + image.close(); + } } + // Leave. + LOG.i("onImageAvailable ended."); + dispatchResult(); + } + + private void readJpegImage(@NonNull Image image) { + ByteBuffer buffer = image.getPlanes()[0].getBuffer(); + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes); + mResult.data = bytes; + // Just like Camera1, unfortunately, the camera might rotate the image // and put EXIF=0 instead of respecting our EXIF and leave the image unaltered. - mResult.format = PictureResult.FORMAT_JPEG; mResult.rotation = 0; try { ExifInterface exif = new ExifInterface(new ByteArrayInputStream(mResult.data)); int exifOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); - mResult.rotation = ExifHelper.readExifOrientation(exifOrientation); + mResult.rotation = ExifHelper.getOrientation(exifOrientation); } catch (IOException ignore) { } + } - // Leave. - LOG.i("onImageAvailable ended."); - dispatchResult(); + private void readRawImage(@NonNull Image image) { + ByteArrayOutputStream array = new ByteArrayOutputStream(); + BufferedOutputStream stream = new BufferedOutputStream(array); + try { + mDngCreator.writeImage(stream, image); + stream.flush(); + mResult.data = array.toByteArray(); + } catch (IOException e) { + mDngCreator.close(); + try { stream.close(); } catch (IOException ignore) {} + throw new RuntimeException(e); + } } } 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 5b7cbc19..73a841b5 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Snapshot1PictureRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Snapshot1PictureRecorder.java @@ -81,7 +81,6 @@ public class Snapshot1PictureRecorder extends PictureRecorder { mResult.data = data; mResult.size = new Size(outputRect.width(), outputRect.height()); mResult.rotation = 0; - mResult.format = PictureResult.FORMAT_JPEG; dispatchResult(); } }); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Snapshot2PictureRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Snapshot2PictureRecorder.java index 8e074ff3..dc2bfba1 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Snapshot2PictureRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Snapshot2PictureRecorder.java @@ -48,7 +48,7 @@ public class Snapshot2PictureRecorder extends SnapshotGlPictureRecorder { private final static CameraLogger LOG = CameraLogger.create(TAG); private final static long LOCK_TIMEOUT = 2500; - private static class FlashAction extends BaseAction { + private class FlashAction extends BaseAction { @Override protected void onStart(@NonNull ActionHolder holder) { @@ -82,6 +82,26 @@ public class Snapshot2PictureRecorder extends SnapshotGlPictureRecorder { } } + private class ResetFlashAction extends BaseAction { + + @Override + protected void onStart(@NonNull ActionHolder holder) { + super.onStart(holder); + try { + // See Camera2Engine.setFlash() comments: turning TORCH off has bugs and we must do + // as follows. + LOG.i("ResetFlashAction:", "Reverting the flash changes."); + CaptureRequest.Builder builder = holder.getBuilder(this); + builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); + builder.set(CaptureRequest.FLASH_MODE, CaptureResult.FLASH_MODE_OFF); + holder.applyBuilder(this, builder); + builder.set(CaptureRequest.CONTROL_AE_MODE, mOriginalAeMode); + builder.set(CaptureRequest.FLASH_MODE, mOriginalFlashMode); + holder.applyBuilder(this); + } catch (CameraAccessException ignore) {} + } + } + private final Action mAction; private final ActionHolder mHolder; private final boolean mActionNeeded; @@ -135,18 +155,7 @@ public class Snapshot2PictureRecorder extends SnapshotGlPictureRecorder { @Override protected void dispatchResult() { // Revert our changes. - LOG.i("dispatchResult:", "Reverting the flash changes."); - try { - // See Camera2Engine.setFlash() comments: turning TORCH off has bugs and we must do - // as follows. - CaptureRequest.Builder builder = mHolder.getBuilder(mAction); - builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); - builder.set(CaptureRequest.FLASH_MODE, CaptureResult.FLASH_MODE_OFF); - mHolder.applyBuilder(mAction, builder); - builder.set(CaptureRequest.CONTROL_AE_MODE, mOriginalAeMode); - builder.set(CaptureRequest.FLASH_MODE, mOriginalFlashMode); - mHolder.applyBuilder(mAction); - } catch (CameraAccessException ignore) {} + new ResetFlashAction().start(mHolder); super.dispatchResult(); } } 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 5dd5ce20..cdb81e18 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotGlPictureRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotGlPictureRecorder.java @@ -238,7 +238,6 @@ public class SnapshotGlPictureRecorder extends PictureRecorder { LOG.i("takeFrame:", "timestampUs:", timestampUs); mViewport.drawFrame(timestampUs, mTextureId, mTransform); if (mHasOverlay) mOverlayDrawer.render(timestampUs); - mResult.format = PictureResult.FORMAT_JPEG; mResult.data = eglSurface.saveFrameTo(Bitmap.CompressFormat.JPEG); // 6. Cleanup diff --git a/cameraview/src/main/res/values/attrs.xml b/cameraview/src/main/res/values/attrs.xml index 8cbc871a..4411199e 100644 --- a/cameraview/src/main/res/values/attrs.xml +++ b/cameraview/src/main/res/values/attrs.xml @@ -145,6 +145,11 @@ + + + + + diff --git a/cameraview/src/test/java/com/otaliastudios/cameraview/internal/utils/ExifHelperTest.java b/cameraview/src/test/java/com/otaliastudios/cameraview/internal/utils/ExifHelperTest.java index 4f4ccb7f..319f2096 100644 --- a/cameraview/src/test/java/com/otaliastudios/cameraview/internal/utils/ExifHelperTest.java +++ b/cameraview/src/test/java/com/otaliastudios/cameraview/internal/utils/ExifHelperTest.java @@ -3,13 +3,8 @@ package com.otaliastudios.cameraview.internal.utils; import androidx.exifinterface.media.ExifInterface; -import org.junit.After; -import org.junit.Before; import org.junit.Test; -import java.util.ArrayList; -import java.util.List; - import static junit.framework.Assert.assertNotNull; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -20,22 +15,22 @@ public class ExifHelperTest { @Test public void testValues() { - assertEquals(0, ExifHelper.readExifOrientation(ExifInterface.ORIENTATION_NORMAL)); - assertEquals(0, ExifHelper.readExifOrientation(ExifInterface.ORIENTATION_FLIP_HORIZONTAL)); - assertEquals(180, ExifHelper.readExifOrientation(ExifInterface.ORIENTATION_ROTATE_180)); - assertEquals(180, ExifHelper.readExifOrientation(ExifInterface.ORIENTATION_FLIP_VERTICAL)); - assertEquals(90, ExifHelper.readExifOrientation(ExifInterface.ORIENTATION_ROTATE_90)); - assertEquals(90, ExifHelper.readExifOrientation(ExifInterface.ORIENTATION_TRANSPOSE)); - assertEquals(270, ExifHelper.readExifOrientation(ExifInterface.ORIENTATION_ROTATE_270)); - assertEquals(270, ExifHelper.readExifOrientation(ExifInterface.ORIENTATION_TRANSVERSE)); + assertEquals(0, ExifHelper.getOrientation(ExifInterface.ORIENTATION_NORMAL)); + assertEquals(0, ExifHelper.getOrientation(ExifInterface.ORIENTATION_FLIP_HORIZONTAL)); + assertEquals(180, ExifHelper.getOrientation(ExifInterface.ORIENTATION_ROTATE_180)); + assertEquals(180, ExifHelper.getOrientation(ExifInterface.ORIENTATION_FLIP_VERTICAL)); + assertEquals(90, ExifHelper.getOrientation(ExifInterface.ORIENTATION_ROTATE_90)); + assertEquals(90, ExifHelper.getOrientation(ExifInterface.ORIENTATION_TRANSPOSE)); + assertEquals(270, ExifHelper.getOrientation(ExifInterface.ORIENTATION_ROTATE_270)); + assertEquals(270, ExifHelper.getOrientation(ExifInterface.ORIENTATION_TRANSVERSE)); } @Test public void testUnknownValues() { - assertEquals(0, ExifHelper.readExifOrientation(-15)); - assertEquals(0, ExifHelper.readExifOrientation(-1)); - assertEquals(0, ExifHelper.readExifOrientation(195)); - assertEquals(0, ExifHelper.readExifOrientation(Integer.MAX_VALUE)); + assertEquals(0, ExifHelper.getOrientation(-15)); + assertEquals(0, ExifHelper.getOrientation(-1)); + assertEquals(0, ExifHelper.getOrientation(195)); + assertEquals(0, ExifHelper.getOrientation(Integer.MAX_VALUE)); } } diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 77976f9a..e7e1162e 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -15,7 +15,6 @@ @@ -24,9 +23,23 @@ - + + + - + + + diff --git a/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java b/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java index 8e07625a..97923046 100644 --- a/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java +++ b/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java @@ -115,6 +115,7 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis // Some controls new Option.Flash(), new Option.WhiteBalance(), new Option.Hdr(), new Option.PictureMetering(), new Option.PictureSnapshotMetering(), + new Option.PictureFormat(), // Video recording new Option.PreviewFrameRate(), new Option.VideoCodec(), new Option.Audio(), // Gestures @@ -133,7 +134,7 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis // Engine and preview false, false, true, // Some controls - false, false, false, false, true, + false, false, false, false, false, true, // Video recording false, false, true, // Gestures diff --git a/demo/src/main/java/com/otaliastudios/cameraview/demo/Option.java b/demo/src/main/java/com/otaliastudios/cameraview/demo/Option.java index 0144004a..0b43ca5c 100644 --- a/demo/src/main/java/com/otaliastudios/cameraview/demo/Option.java +++ b/demo/src/main/java/com/otaliastudios/cameraview/demo/Option.java @@ -10,6 +10,7 @@ import android.view.ViewGroup; import com.otaliastudios.cameraview.CameraListener; import com.otaliastudios.cameraview.CameraOptions; import com.otaliastudios.cameraview.CameraView; +import com.otaliastudios.cameraview.controls.Control; import com.otaliastudios.cameraview.gesture.Gesture; import com.otaliastudios.cameraview.gesture.GestureAction; import com.otaliastudios.cameraview.overlay.Overlay; @@ -135,7 +136,7 @@ public abstract class Option { } } - private static abstract class ControlOption extends Option { + private static abstract class ControlOption extends Option { private final Class controlClass; ControlOption(@NonNull Class controlClass, String name) { @@ -547,4 +548,10 @@ public abstract class Option { } } + public static class PictureFormat extends ControlOption { + public PictureFormat() { + super(com.otaliastudios.cameraview.controls.PictureFormat.class, "Picture Format"); + } + } + } diff --git a/demo/src/main/java/com/otaliastudios/cameraview/demo/PicturePreviewActivity.java b/demo/src/main/java/com/otaliastudios/cameraview/demo/PicturePreviewActivity.java index bb43ba5f..18321413 100644 --- a/demo/src/main/java/com/otaliastudios/cameraview/demo/PicturePreviewActivity.java +++ b/demo/src/main/java/com/otaliastudios/cameraview/demo/PicturePreviewActivity.java @@ -1,20 +1,36 @@ package com.otaliastudios.cameraview.demo; import android.app.Activity; +import android.content.Context; +import android.content.Intent; import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.net.Uri; import android.os.Bundle; import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.content.FileProvider; import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; import android.widget.ImageView; +import android.widget.Toast; +import com.otaliastudios.cameraview.CameraUtils; +import com.otaliastudios.cameraview.FileCallback; import com.otaliastudios.cameraview.size.AspectRatio; import com.otaliastudios.cameraview.BitmapCallback; import com.otaliastudios.cameraview.PictureResult; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; -public class PicturePreviewActivity extends Activity { + +public class PicturePreviewActivity extends AppCompatActivity { private static PictureResult picture; @@ -42,12 +58,18 @@ public class PicturePreviewActivity extends Activity { captureLatency.setTitleAndMessage("Approx. latency", delay + " milliseconds"); captureResolution.setTitleAndMessage("Resolution", result.getSize() + " (" + ratio + ")"); exifRotation.setTitleAndMessage("EXIF rotation", result.getRotation() + ""); - result.toBitmap(1000, 1000, new BitmapCallback() { - @Override - public void onBitmapReady(Bitmap bitmap) { - imageView.setImageBitmap(bitmap); - } - }); + try { + result.toBitmap(1000, 1000, new BitmapCallback() { + @Override + public void onBitmapReady(Bitmap bitmap) { + imageView.setImageBitmap(bitmap); + } + }); + } catch (UnsupportedOperationException e) { + imageView.setImageDrawable(new ColorDrawable(Color.GREEN)); + Toast.makeText(this, "Can't preview this format: " + picture.getFormat(), + Toast.LENGTH_LONG).show(); + } if (result.isSnapshot()) { // Log the real size for debugging reason. @@ -69,4 +91,46 @@ public class PicturePreviewActivity extends Activity { setPictureResult(null); } } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.share, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.share) { + Toast.makeText(this, "Sharing...", Toast.LENGTH_SHORT).show(); + String extension; + switch (picture.getFormat()) { + case JPEG: extension = "jpg"; break; + case DNG: extension = "dng"; break; + default: throw new RuntimeException("Unknown format."); + } + File file = new File(getFilesDir(), "picture." + extension); + CameraUtils.writeToFile(picture.getData(), file, new FileCallback() { + @Override + public void onFileReady(@Nullable File file) { + if (file != null) { + Context context = PicturePreviewActivity.this; + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType("image/*"); + Uri uri = FileProvider.getUriForFile(context, + context.getPackageName() + ".provider", + file); + intent.putExtra(Intent.EXTRA_STREAM, uri); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + startActivity(intent); + } else { + Toast.makeText(PicturePreviewActivity.this, + "Error while writing file.", + Toast.LENGTH_SHORT).show(); + } + } + }); + return true; + } + return super.onOptionsItemSelected(item); + } } diff --git a/demo/src/main/java/com/otaliastudios/cameraview/demo/VideoPreviewActivity.java b/demo/src/main/java/com/otaliastudios/cameraview/demo/VideoPreviewActivity.java index 1d26fcc9..a37192cc 100644 --- a/demo/src/main/java/com/otaliastudios/cameraview/demo/VideoPreviewActivity.java +++ b/demo/src/main/java/com/otaliastudios/cameraview/demo/VideoPreviewActivity.java @@ -1,22 +1,33 @@ package com.otaliastudios.cameraview.demo; import android.app.Activity; +import android.content.Context; +import android.content.Intent; import android.media.MediaPlayer; import android.net.Uri; import android.os.Bundle; import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.content.FileProvider; import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.MediaController; +import android.widget.Toast; import android.widget.VideoView; +import com.otaliastudios.cameraview.CameraUtils; +import com.otaliastudios.cameraview.FileCallback; import com.otaliastudios.cameraview.VideoResult; import com.otaliastudios.cameraview.size.AspectRatio; +import java.io.File; -public class VideoPreviewActivity extends Activity { + +public class VideoPreviewActivity extends AppCompatActivity { private VideoView videoView; @@ -97,4 +108,27 @@ public class VideoPreviewActivity extends Activity { setVideoResult(null); } } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.share, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.share) { + Toast.makeText(this, "Sharing...", Toast.LENGTH_SHORT).show(); + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType("video/*"); + Uri uri = FileProvider.getUriForFile(this, + this.getPackageName() + ".provider", + videoResult.getFile()); + intent.putExtra(Intent.EXTRA_STREAM, uri); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + startActivity(intent); + return true; + } + return super.onOptionsItemSelected(item); + } } diff --git a/demo/src/main/res/drawable/ic_share.xml b/demo/src/main/res/drawable/ic_share.xml new file mode 100644 index 00000000..f37b0d49 --- /dev/null +++ b/demo/src/main/res/drawable/ic_share.xml @@ -0,0 +1,5 @@ + + + diff --git a/demo/src/main/res/menu/share.xml b/demo/src/main/res/menu/share.xml new file mode 100644 index 00000000..bb3236e7 --- /dev/null +++ b/demo/src/main/res/menu/share.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/demo/src/main/res/values/styles.xml b/demo/src/main/res/values/styles.xml index d1cf4963..5885930d 100644 --- a/demo/src/main/res/values/styles.xml +++ b/demo/src/main/res/values/styles.xml @@ -8,7 +8,4 @@ @color/colorAccent -