From 5619dbdca596701ad12a33939f7fc35f4062e69d Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Tue, 1 Oct 2019 12:00:51 +0200 Subject: [PATCH] Small improvements (#588) * Abort actions on exit * Fix VideoRecorder state * Add missing metering tests * Fix default vertex shader definition * Fix #582 * Make Camera2Engine more open * Add timestampUs in Filter.draw signature * Add timestampUs in BaseFilter.onPreDraw, onDraw, onPostDraw * Change README * Fix FrameProcessing segmentation fault * Fix #590 by throwing * Fix StackOverflowErrors in AudioMediaEncoder (Fixes #594) * Enforce 100 character limits * Improve encoder selection * Simplify DeviceEncoders class * Improve overlay add/remove * Add changelog info * Add more info --- README.md | 2 +- .../cameraview/CameraViewTest.java | 27 ++ .../engine/CameraIntegration1Test.java | 5 + .../engine/CameraIntegration2Test.java | 5 + .../engine/CameraIntegrationTest.java | 40 +- .../cameraview/filter/BaseFilterTest.java | 21 +- .../cameraview/filter/MultiFilterTest.java | 17 +- .../internal/DeviceEncodersTest.java | 13 +- .../cameraview/overlay/OverlayDrawerTest.java | 3 +- .../cameraview/BitmapCallback.java | 2 +- .../cameraview/CameraListener.java | 13 +- .../cameraview/CameraLogger.java | 10 +- .../cameraview/CameraOptions.java | 47 ++- .../otaliastudios/cameraview/CameraUtils.java | 45 ++- .../otaliastudios/cameraview/CameraView.java | 156 +++++--- .../cameraview/PictureResult.java | 3 +- .../otaliastudios/cameraview/VideoResult.java | 3 +- .../cameraview/controls/ControlParser.java | 23 +- .../cameraview/engine/Camera1Engine.java | 86 +++-- .../cameraview/engine/Camera2Engine.java | 352 +++++++++++------- .../cameraview/engine/CameraEngine.java | 185 +++++---- .../cameraview/engine/LogAction.java | 35 +- .../otaliastudios/cameraview/engine/Step.java | 20 +- .../cameraview/engine/action/Action.java | 21 +- .../engine/action/ActionHolder.java | 3 +- .../engine/action/ActionWrapper.java | 8 +- .../cameraview/engine/action/BaseAction.java | 10 +- .../engine/action/SequenceAction.java | 6 +- .../engine/action/TogetherAction.java | 6 +- .../cameraview/engine/lock/ExposureLock.java | 13 +- .../cameraview/engine/lock/FocusLock.java | 13 +- .../engine/lock/WhiteBalanceLock.java | 11 +- .../cameraview/engine/meter/BaseReset.java | 3 +- .../engine/meter/ExposureMeter.java | 13 +- .../engine/meter/ExposureReset.java | 15 +- .../cameraview/engine/meter/FocusMeter.java | 3 +- .../cameraview/engine/meter/FocusReset.java | 3 +- .../cameraview/engine/meter/MeterAction.java | 29 +- .../engine/meter/WhiteBalanceMeter.java | 13 +- .../engine/meter/WhiteBalanceReset.java | 6 +- .../cameraview/engine/offset/Axis.java | 2 +- .../cameraview/filter/BaseFilter.java | 102 +++-- .../cameraview/filter/Filter.java | 3 +- .../cameraview/filter/MultiFilter.java | 27 +- .../cameraview/filters/AutoFixFilter.java | 9 +- .../filters/BlackAndWhiteFilter.java | 4 +- .../cameraview/filters/BrightnessFilter.java | 8 +- .../cameraview/filters/ContrastFilter.java | 8 +- .../filters/CrossProcessFilter.java | 4 +- .../cameraview/filters/DocumentaryFilter.java | 12 +- .../cameraview/filters/DuotoneFilter.java | 8 +- .../cameraview/filters/FillLightFilter.java | 8 +- .../cameraview/filters/GammaFilter.java | 9 +- .../cameraview/filters/GrainFilter.java | 26 +- .../cameraview/filters/GrayscaleFilter.java | 4 +- .../cameraview/filters/HueFilter.java | 8 +- .../filters/InvertColorsFilter.java | 4 +- .../cameraview/filters/LomoishFilter.java | 29 +- .../cameraview/filters/PosterizeFilter.java | 4 +- .../cameraview/filters/SaturationFilter.java | 15 +- .../cameraview/filters/SepiaFilter.java | 4 +- .../cameraview/filters/SharpnessFilter.java | 24 +- .../cameraview/filters/TemperatureFilter.java | 8 +- .../cameraview/filters/TintFilter.java | 8 +- .../cameraview/filters/VignetteFilter.java | 13 +- .../otaliastudios/cameraview/frame/Frame.java | 4 +- .../cameraview/frame/FrameManager.java | 23 +- .../cameraview/gesture/Gesture.java | 3 +- .../cameraview/gesture/GestureAction.java | 5 +- .../cameraview/gesture/GestureParser.java | 16 +- .../gesture/PinchGestureFinder.java | 3 +- .../gesture/ScrollGestureFinder.java | 11 +- .../cameraview/gesture/TapGestureFinder.java | 3 +- .../cameraview/internal/DeviceEncoders.java | 87 ++++- .../cameraview/internal/GlUtils.java | 6 +- .../cameraview/internal/GridLinesLayout.java | 3 +- .../internal/Issue514Workaround.java | 28 +- .../internal/egl/EglBaseSurface.java | 3 +- .../cameraview/internal/egl/EglCore.java | 7 +- .../cameraview/internal/egl/EglViewport.java | 19 +- .../internal/egl/EglWindowSurface.java | 21 -- .../internal/utils/CamcorderProfiles.java | 9 +- .../internal/utils/ImageHelper.java | 3 +- .../internal/utils/OrientationHelper.java | 10 +- .../cameraview/internal/utils/Pool.java | 15 +- .../internal/utils/RotationHelper.java | 4 +- .../internal/utils/WorkerHandler.java | 9 +- .../cameraview/markers/AutoFocusMarker.java | 4 +- .../markers/DefaultAutoFocusMarker.java | 13 +- .../cameraview/overlay/OverlayDrawer.java | 8 +- .../cameraview/overlay/OverlayLayout.java | 9 +- .../picture/Full1PictureRecorder.java | 4 +- .../picture/Full2PictureRecorder.java | 6 +- .../cameraview/picture/PictureRecorder.java | 3 +- .../picture/Snapshot1PictureRecorder.java | 6 +- .../picture/Snapshot2PictureRecorder.java | 31 +- .../picture/SnapshotGlPictureRecorder.java | 26 +- .../cameraview/preview/CameraPreview.java | 11 +- .../cameraview/preview/GlCameraPreview.java | 62 +-- .../preview/SurfaceCameraPreview.java | 15 +- .../preview/TextureCameraPreview.java | 7 +- .../cameraview/size/AspectRatio.java | 3 +- .../cameraview/size/SizeSelectorParser.java | 72 +++- .../cameraview/video/Full1VideoRecorder.java | 7 +- .../cameraview/video/Full2VideoRecorder.java | 10 +- .../cameraview/video/FullVideoRecorder.java | 54 ++- .../video/SnapshotVideoRecorder.java | 49 ++- .../cameraview/video/VideoRecorder.java | 41 +- .../video/encoding/AudioConfig.java | 11 +- .../video/encoding/AudioMediaEncoder.java | 89 +++-- .../video/encoding/AudioTimestamp.java | 4 +- .../video/encoding/MediaCodecBuffers.java | 4 +- .../video/encoding/MediaEncoder.java | 42 ++- .../video/encoding/MediaEncoderEngine.java | 32 +- .../video/encoding/TextureMediaEncoder.java | 31 +- .../video/encoding/VideoMediaEncoder.java | 18 +- demo/build.gradle | 4 +- .../cameraview/demo/CameraActivity.java | 6 + docs/_posts/2018-12-20-changelog.md | 17 + .../2019-07-14-watermarks-and-overlays.md | 28 +- 120 files changed, 1733 insertions(+), 926 deletions(-) diff --git a/README.md b/README.md index d5190846..33fba179 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ If you like the project, make profit from it, or simply want to thank back, plea [donating](https://natario1.github.io/CameraView/extra/donate) now! You can either make a one time donation or become a sponsor, in which case your company logo will immediately show up below. -CameraView is trusted and supported by [ShareChat](https://sharechat.com/), a social media app with over 14 million daily active users. +CameraView is trusted and supported by [ShareChat](https://sharechat.com/), a social media app with over 100 million downloads.

diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java index 3d7207dd..654dda74 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java @@ -930,6 +930,33 @@ public class CameraViewTest extends BaseTest { verify(cameraView.mOverlayLayout, never()).addView(overlay, params); } + @Test + public void testOverlays_removeOverlayView() { + // First add one. + cameraView.mOverlayLayout = spy(cameraView.mOverlayLayout); + View overlay = new View(getContext()); + OverlayLayout.LayoutParams params = new OverlayLayout.LayoutParams(10, 10); + cameraView.addView(overlay, 0, params); + reset(cameraView.mOverlayLayout); + // Then remove. + cameraView.removeView(overlay); + verify(cameraView.mOverlayLayout, times(1)).isOverlay(params); + verify(cameraView.mOverlayLayout, times(1)).removeView(overlay); + } + + @Test + public void testOverlays_dontRemoveOverlayView() { + // First add a view. + cameraView.mOverlayLayout = spy(cameraView.mOverlayLayout); + View notOverlay = new View(getContext()); + ViewGroup.LayoutParams notOverlayParams = new ViewGroup.LayoutParams(10, 10); + cameraView.addView(notOverlay, notOverlayParams); + reset(cameraView.mOverlayLayout); + // Then remove. + cameraView.removeView(notOverlay); + verify(cameraView.mOverlayLayout, never()).removeView(notOverlay); + } + //endregion // TODO: test permissions diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegration1Test.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegration1Test.java index 33eeea02..73c0ac62 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegration1Test.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegration1Test.java @@ -27,4 +27,9 @@ public class CameraIntegration1Test extends CameraIntegrationTest { protected Engine getEngine() { return Engine.CAMERA1; } + + @Override + protected long getMeteringTimeoutMillis() { + return Camera1Engine.AUTOFOCUS_END_DELAY_MILLIS; + } } diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegration2Test.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegration2Test.java index 53eb9aa9..9610109c 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegration2Test.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegration2Test.java @@ -55,4 +55,9 @@ public class CameraIntegration2Test extends CameraIntegrationTest { }.start(engine); try { latch.await(); } catch (InterruptedException ignore) {} } + + @Override + protected long getMeteringTimeoutMillis() { + return Camera2Engine.METER_TIMEOUT; + } } 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 f6f80e6d..310cd6ab 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java @@ -658,8 +658,8 @@ public abstract class CameraIntegrationTest extends BaseTest { doEndOp(focus, 1).when(listener).onAutoFocusEnd(anyBoolean(), any(PointF.class)); camera.startAutoFocus(1, 1); - // Stop is not guaranteed to be called, we use a delay. So wait at least the delay time. - PointF point = focus.await(1000 + Camera1Engine.AUTOFOCUS_END_DELAY_MILLIS); + // Stop routine can fail, so engines use a timeout. So wait at least the timeout time. + PointF point = focus.await(1000 + getMeteringTimeoutMillis()); if (o.isAutoFocusSupported()) { assertNotNull(point); assertEquals(point, new PointF(1, 1)); @@ -668,6 +668,8 @@ public abstract class CameraIntegrationTest extends BaseTest { } } + protected abstract long getMeteringTimeoutMillis(); + //endregion //region capture @@ -717,6 +719,24 @@ public abstract class CameraIntegrationTest extends BaseTest { openSync(true); camera.takePicture(); waitForUiException(); + camera.takePicture(); + + } + + @Test + public void testCapturePicture_withMetering() { + openSync(true); + camera.setPictureMetering(true); + camera.takePicture(); + waitForPictureResult(true); + } + + @Test + public void testCapturePicture_withoutMetering() { + openSync(true); + camera.setPictureMetering(false); + camera.takePicture(); + waitForPictureResult(true); } @Test @@ -759,6 +779,22 @@ public abstract class CameraIntegrationTest extends BaseTest { assertTrue(result.isSnapshot()); } + @Test + public void testCaptureSnapshot_withMetering() { + openSync(true); + camera.setPictureSnapshotMetering(true); + camera.takePictureSnapshot(); + waitForPictureResult(true); + } + + @Test + public void testCaptureSnapshot_withoutMetering() { + openSync(true); + camera.setPictureSnapshotMetering(false); + camera.takePictureSnapshot(); + waitForPictureResult(true); + } + //endregion //region Frame Processing diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/BaseFilterTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/BaseFilterTest.java index d6614f52..ff0474f0 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/BaseFilterTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/BaseFilterTest.java @@ -8,21 +8,16 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.otaliastudios.cameraview.BaseEglTest; -import com.otaliastudios.cameraview.BaseTest; import com.otaliastudios.cameraview.internal.GlUtils; import com.otaliastudios.cameraview.internal.egl.EglViewport; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import java.util.HashSet; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; @@ -108,10 +103,10 @@ public class BaseFilterTest extends BaseEglTest { public void testDraw_whenInvalid() { filter = spy(new TestFilter()); float[] matrix = new float[16]; - filter.draw(matrix); - verify(filter, never()).onPreDraw(matrix); - verify(filter, never()).onDraw(); - verify(filter, never()).onPostDraw(); + filter.draw(0L, matrix); + verify(filter, never()).onPreDraw(0L, matrix); + verify(filter, never()).onDraw(0L); + verify(filter, never()).onPostDraw(0L); } @Test @@ -122,10 +117,10 @@ public class BaseFilterTest extends BaseEglTest { int texture = viewport.createTexture(); float[] matrix = new float[16]; - viewport.drawFrame(texture, matrix); - verify(filter, times(1)).onPreDraw(matrix); - verify(filter, times(1)).onDraw(); - verify(filter, times(1)).onPostDraw(); + viewport.drawFrame(0L, texture, matrix); + verify(filter, times(1)).onPreDraw(0L, matrix); + verify(filter, times(1)).onDraw(0L); + verify(filter, times(1)).onPostDraw(0L); viewport.release(); } diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/MultiFilterTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/MultiFilterTest.java index 40c4957b..09591425 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/MultiFilterTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/MultiFilterTest.java @@ -3,7 +3,6 @@ package com.otaliastudios.cameraview.filter; import android.opengl.GLES20; -import androidx.annotation.NonNull; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -30,13 +29,13 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; @RunWith(AndroidJUnit4.class) @@ -146,7 +145,7 @@ public class MultiFilterTest extends BaseEglTest { EglViewport viewport = new EglViewport(multiFilter); int texture = viewport.createTexture(); float[] matrix = new float[16]; - viewport.drawFrame(texture, matrix); + viewport.drawFrame(0L, texture, matrix); viewport.release(); // The child should have experienced the whole lifecycle. @@ -154,7 +153,7 @@ public class MultiFilterTest extends BaseEglTest { verify(filter, atLeastOnce()).getFragmentShader(); verify(filter, atLeastOnce()).setSize(anyInt(), anyInt()); verify(filter, times(1)).onCreate(anyInt()); - verify(filter, times(1)).draw(matrix); + verify(filter, times(1)).draw(0L, matrix); verify(filter, times(1)).onDestroy(); } @@ -181,7 +180,7 @@ public class MultiFilterTest extends BaseEglTest { assertTrue(result[0] != 0); return null; } - }).when(filter1).draw(matrix); + }).when(filter1).draw(0L, matrix); // Note: second filter is drawn with the identity matrix! doAnswer(new Answer() { @@ -198,16 +197,16 @@ public class MultiFilterTest extends BaseEglTest { return null; } - }).when(filter2).draw(any(float[].class)); + }).when(filter2).draw(eq(0L), any(float[].class)); EglViewport viewport = new EglViewport(multiFilter); int texture = viewport.createTexture(); - viewport.drawFrame(texture, matrix); + viewport.drawFrame(0L, texture, matrix); viewport.release(); // Verify that both are drawn. - verify(filter1, times(1)).draw(matrix); - verify(filter2, times(1)).draw(any(float[].class)); + verify(filter1, times(1)).draw(0L, matrix); + verify(filter2, times(1)).draw(eq(0L), any(float[].class)); } } diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/DeviceEncodersTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/DeviceEncodersTest.java index c5ef9084..4355e3fe 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/DeviceEncodersTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/DeviceEncodersTest.java @@ -6,17 +6,13 @@ import android.media.MediaCodecInfo; import androidx.annotation.NonNull; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.MediumTest; -import androidx.test.rule.ActivityTestRule; import com.otaliastudios.cameraview.BaseTest; -import com.otaliastudios.cameraview.TestActivity; -import com.otaliastudios.cameraview.controls.Grid; import com.otaliastudios.cameraview.size.AspectRatio; import com.otaliastudios.cameraview.size.Size; import org.junit.After; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -30,8 +26,6 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; @RunWith(AndroidJUnit4.class) @MediumTest @@ -55,10 +49,11 @@ public class DeviceEncodersTest extends BaseTest { @NonNull private DeviceEncoders create() { - return new DeviceEncoders( + return new DeviceEncoders(DeviceEncoders.MODE_RESPECT_ORDER, "video/avc", "audio/mp4a-latm", - DeviceEncoders.MODE_TAKE_FIRST); + 0, + 0); } @Test @@ -87,7 +82,7 @@ public class DeviceEncodersTest extends BaseTest { if (DeviceEncoders.ENABLED) { List allEncoders = deviceEncoders.getDeviceEncoders(); MediaCodecInfo encoder = deviceEncoders.findDeviceEncoder(allEncoders, - "video/avc", DeviceEncoders.MODE_TAKE_FIRST); + "video/avc", DeviceEncoders.MODE_RESPECT_ORDER, 0); assertNotNull(encoder); List encoderTypes = Arrays.asList(encoder.getSupportedTypes()); assertTrue(encoderTypes.contains("video/avc")); diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/overlay/OverlayDrawerTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/overlay/OverlayDrawerTest.java index 81d69666..b763c83e 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/overlay/OverlayDrawerTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/overlay/OverlayDrawerTest.java @@ -81,8 +81,9 @@ public class OverlayDrawerTest extends BaseEglTest { OverlayDrawer drawer = new OverlayDrawer(mock(Overlay.class), new Size(WIDTH, HEIGHT)); drawer.mViewport = spy(drawer.mViewport); drawer.draw(Overlay.Target.PICTURE_SNAPSHOT); - drawer.render(); + drawer.render(0L); verify(drawer.mViewport, times(1)).drawFrame( + 0L, drawer.mTextureId, drawer.getTransform() ); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/BitmapCallback.java b/cameraview/src/main/java/com/otaliastudios/cameraview/BitmapCallback.java index 3d607b91..e6d244bc 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/BitmapCallback.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/BitmapCallback.java @@ -10,7 +10,7 @@ import androidx.annotation.UiThread; public interface BitmapCallback { /** - * Notifies that the bitmap was succesfully decoded. + * Notifies that the bitmap was successfully decoded. * This is run on the UI thread. * Returns a null object if a {@link OutOfMemoryError} was encountered. * diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java index fcc76515..9f8a71de 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java @@ -48,8 +48,9 @@ public abstract class CameraListener { * Notifies that a picture previously captured with {@link CameraView#takePicture()} * or {@link CameraView#takePictureSnapshot()} is ready to be shown or saved to file. * - * If planning to show a bitmap, you can use {@link PictureResult#toBitmap(int, int, BitmapCallback)} - * to decode the byte array taking care about orientation and threading. + * If planning to show a bitmap, you can use + * {@link PictureResult#toBitmap(int, int, BitmapCallback)} to decode the byte array + * taking care about orientation and threading. * * @param result captured picture */ @@ -114,7 +115,9 @@ public abstract class CameraListener { * @param fingers finger positions that caused the event, null if not caused by touch */ @UiThread - public void onZoomChanged(float newValue, @NonNull float[] bounds, @Nullable PointF[] fingers) { } + public void onZoomChanged(float newValue, + @NonNull float[] bounds, + @Nullable PointF[] fingers) { } /** @@ -126,7 +129,9 @@ public abstract class CameraListener { * @param fingers finger positions that caused the event, null if not caused by touch */ @UiThread - public void onExposureCorrectionChanged(float newValue, @NonNull float[] bounds, @Nullable PointF[] fingers) { } + public void onExposureCorrectionChanged(float newValue, + @NonNull float[] bounds, + @Nullable PointF[] fingers) { } /** diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraLogger.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraLogger.java index 6c2c61c4..2219455c 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraLogger.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraLogger.java @@ -49,7 +49,10 @@ public final class CameraLogger { * @param message the log message * @param throwable an optional throwable */ - void log(@LogLevel int level, @NonNull String tag, @NonNull String message, @Nullable Throwable throwable); + void log(@LogLevel int level, + @NonNull String tag, + @NonNull String message, + @Nullable Throwable throwable); } @VisibleForTesting static String lastMessage; @@ -60,7 +63,10 @@ public final class CameraLogger { @VisibleForTesting static Logger sAndroidLogger = new Logger() { @Override - public void log(int level, @NonNull String tag, @NonNull String message, @Nullable Throwable throwable) { + public void log(int level, + @NonNull String tag, + @NonNull String message, + @Nullable Throwable throwable) { switch (level) { case LEVEL_VERBOSE: Log.v(tag, message, throwable); break; case LEVEL_INFO: Log.i(tag, message, throwable); break; diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java index cde2f9b9..4380a620 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java @@ -107,7 +107,8 @@ public class CameraOptions { zoomSupported = params.isZoomSupported(); // autofocus - autoFocusSupported = params.getSupportedFocusModes().contains(Camera.Parameters.FOCUS_MODE_AUTO); + autoFocusSupported = params.getSupportedFocusModes() + .contains(Camera.Parameters.FOCUS_MODE_AUTO); // Exposure correction float step = params.getExposureCompensationStep(); @@ -135,7 +136,8 @@ public class CameraOptions { supportedVideoAspectRatio.add(AspectRatio.of(width, height)); } } else { - // StackOverflow threads seems to agree that if getSupportedVideoSizes is null, previews can be used. + // StackOverflow threads seems to agree that if getSupportedVideoSizes is null, + // previews can be used. List fallback = params.getSupportedPreviewSizes(); for (Camera.Size size : fallback) { int width = flipSizes ? size.height : size.width; @@ -148,13 +150,16 @@ public class CameraOptions { // Camera2Engine constructor. @RequiresApi(Build.VERSION_CODES.LOLLIPOP) - public CameraOptions(@NonNull CameraManager manager, @NonNull String cameraId, boolean flipSizes) throws CameraAccessException { + 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); + CameraCharacteristics cameraCharacteristics1 = manager + .getCameraCharacteristics(cameraId1); Integer cameraFacing = cameraCharacteristics1.get(CameraCharacteristics.LENS_FACING); if (cameraFacing != null) { Facing value = mapper.unmapFacing(cameraFacing); @@ -163,7 +168,8 @@ public class CameraOptions { } // WB - int[] awbModes = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES); + int[] awbModes = cameraCharacteristics.get( + CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES); //noinspection ConstantConditions for (int awbMode : awbModes) { WhiteBalance value = mapper.unmapWhiteBalance(awbMode); @@ -174,7 +180,8 @@ public class CameraOptions { 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); + int[] aeModes = cameraCharacteristics.get( + CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES); //noinspection ConstantConditions for (int aeMode : aeModes) { Set flashes = mapper.unmapFlash(aeMode); @@ -184,7 +191,8 @@ public class CameraOptions { // HDR supportedHdr.add(Hdr.OFF); - int[] sceneModes = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AVAILABLE_SCENE_MODES); + int[] sceneModes = cameraCharacteristics.get( + CameraCharacteristics.CONTROL_AVAILABLE_SCENE_MODES); //noinspection ConstantConditions for (int sceneMode : sceneModes) { Hdr value = mapper.unmapHdr(sceneMode); @@ -192,7 +200,8 @@ public class CameraOptions { } // Zoom - Float maxZoom = cameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM); + Float maxZoom = cameraCharacteristics.get( + CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM); if(maxZoom != null) { zoomSupported = maxZoom > 1; } @@ -205,24 +214,31 @@ public class CameraOptions { // 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); + 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); + 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; + 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."); + 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(); @@ -238,7 +254,8 @@ public class CameraOptions { 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()) { + 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)); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraUtils.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraUtils.java index 246a6db5..9f8b6210 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraUtils.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraUtils.java @@ -108,7 +108,9 @@ public class CameraUtils { * @param callback a callback */ @SuppressWarnings("WeakerAccess") - public static void writeToFile(@NonNull final byte[] data, @NonNull final File file, @NonNull final FileCallback callback) { + public static void writeToFile(@NonNull final byte[] data, + @NonNull final File file, + @NonNull final FileCallback callback) { final Handler ui = new Handler(); WorkerHandler.execute(new Runnable() { @Override @@ -149,7 +151,8 @@ public class CameraUtils { * @param callback a callback to be notified */ @SuppressWarnings("WeakerAccess") - public static void decodeBitmap(@NonNull final byte[] source, @NonNull final BitmapCallback callback) { + public static void decodeBitmap(@NonNull final byte[] source, + @NonNull final BitmapCallback callback) { decodeBitmap(source, Integer.MAX_VALUE, Integer.MAX_VALUE, callback); } @@ -167,7 +170,10 @@ public class CameraUtils { * @param callback a callback to be notified */ @SuppressWarnings("WeakerAccess") - public static void decodeBitmap(@NonNull final byte[] source, final int maxWidth, final int maxHeight, @NonNull final BitmapCallback callback) { + public static void decodeBitmap(@NonNull final byte[] source, + final int maxWidth, + final int maxHeight, + @NonNull final BitmapCallback callback) { decodeBitmap(source, maxWidth, maxHeight, new BitmapFactory.Options(), callback); } @@ -186,12 +192,20 @@ public class CameraUtils { * @param callback a callback to be notified */ @SuppressWarnings("WeakerAccess") - public static void decodeBitmap(@NonNull final byte[] source, final int maxWidth, final int maxHeight, @NonNull final BitmapFactory.Options options, @NonNull final BitmapCallback callback) { + public static void decodeBitmap(@NonNull final byte[] source, + final int maxWidth, + final int maxHeight, + @NonNull final BitmapFactory.Options options, + @NonNull final BitmapCallback callback) { decodeBitmap(source, maxWidth, maxHeight, options, -1, callback); } - @SuppressWarnings("WeakerAccess") - static void decodeBitmap(@NonNull final byte[] source, final int maxWidth, final int maxHeight, @NonNull final BitmapFactory.Options options, final int rotation, @NonNull final BitmapCallback callback) { + static void decodeBitmap(@NonNull final byte[] source, + final int maxWidth, + final int maxHeight, + @NonNull final BitmapFactory.Options options, + final int rotation, + @NonNull final BitmapCallback callback) { final Handler ui = new Handler(); WorkerHandler.execute(new Runnable() { @Override @@ -239,10 +253,13 @@ public class CameraUtils { * @param options the options to be passed to decodeByteArray * @return decoded bitmap or null if error is encountered */ - @SuppressWarnings({"SuspiciousNameCombination", "WeakerAccess"}) + @SuppressWarnings("WeakerAccess") @Nullable @WorkerThread - public static Bitmap decodeBitmap(@NonNull byte[] source, int maxWidth, int maxHeight, @NonNull BitmapFactory.Options options) { + public static Bitmap decodeBitmap(@NonNull byte[] source, + int maxWidth, + int maxHeight, + @NonNull BitmapFactory.Options options) { return decodeBitmap(source, maxWidth, maxHeight, options, -1); } @@ -250,7 +267,11 @@ public class CameraUtils { // Ignores flipping, but it should be super rare. @SuppressWarnings("TryFinallyCanBeTryWithResources") @Nullable - private static Bitmap decodeBitmap(@NonNull byte[] source, int maxWidth, int maxHeight, @NonNull BitmapFactory.Options options, int rotation) { + private static Bitmap decodeBitmap(@NonNull byte[] source, + int maxWidth, + int maxHeight, + @NonNull BitmapFactory.Options options, + int rotation) { if (maxWidth <= 0) maxWidth = Integer.MAX_VALUE; if (maxHeight <= 0) maxHeight = Integer.MAX_VALUE; int orientation; @@ -261,7 +282,8 @@ public class CameraUtils { // http://sylvana.net/jpegcrop/exif_orientation.html stream = new ByteArrayInputStream(source); ExifInterface exif = new ExifInterface(stream); - int exifOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); + int exifOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, + ExifInterface.ORIENTATION_NORMAL); orientation = ExifHelper.readExifOrientation(exifOrientation); flip = exifOrientation == ExifInterface.ORIENTATION_FLIP_HORIZONTAL || exifOrientation == ExifInterface.ORIENTATION_FLIP_VERTICAL || @@ -310,7 +332,8 @@ public class CameraUtils { Matrix matrix = new Matrix(); matrix.setRotate(orientation); Bitmap temp = bitmap; - bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); + bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), + bitmap.getHeight(), matrix, true); temp.recycle(); } } catch (OutOfMemoryError e) { diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java index b21cfd58..ccbf7428 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java @@ -167,25 +167,36 @@ public class CameraView extends FrameLayout implements LifecycleObserver { if (mInEditor) return; setWillNotDraw(false); - TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CameraView, 0, 0); + TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CameraView, + 0, 0); ControlParser controls = new ControlParser(context, a); // Self managed - boolean playSounds = a.getBoolean(R.styleable.CameraView_cameraPlaySounds, DEFAULT_PLAY_SOUNDS); - boolean useDeviceOrientation = a.getBoolean(R.styleable.CameraView_cameraUseDeviceOrientation, DEFAULT_USE_DEVICE_ORIENTATION); + boolean playSounds = a.getBoolean(R.styleable.CameraView_cameraPlaySounds, + DEFAULT_PLAY_SOUNDS); + boolean useDeviceOrientation = a.getBoolean( + R.styleable.CameraView_cameraUseDeviceOrientation, DEFAULT_USE_DEVICE_ORIENTATION); mExperimental = a.getBoolean(R.styleable.CameraView_cameraExperimental, false); mPreview = controls.getPreview(); mEngine = controls.getEngine(); // Camera engine params - int gridColor = a.getColor(R.styleable.CameraView_cameraGridColor, GridLinesLayout.DEFAULT_COLOR); - long videoMaxSize = (long) a.getFloat(R.styleable.CameraView_cameraVideoMaxSize, 0); - int videoMaxDuration = a.getInteger(R.styleable.CameraView_cameraVideoMaxDuration, 0); + int gridColor = a.getColor(R.styleable.CameraView_cameraGridColor, + GridLinesLayout.DEFAULT_COLOR); + long videoMaxSize = (long) a.getFloat(R.styleable.CameraView_cameraVideoMaxSize, + 0); + int videoMaxDuration = a.getInteger(R.styleable.CameraView_cameraVideoMaxDuration, + 0); int videoBitRate = a.getInteger(R.styleable.CameraView_cameraVideoBitRate, 0); int audioBitRate = a.getInteger(R.styleable.CameraView_cameraAudioBitRate, 0); - long autoFocusResetDelay = (long) a.getInteger(R.styleable.CameraView_cameraAutoFocusResetDelay, (int) DEFAULT_AUTOFOCUS_RESET_DELAY_MILLIS); - boolean pictureMetering = a.getBoolean(R.styleable.CameraView_cameraPictureMetering, DEFAULT_PICTURE_METERING); - boolean pictureSnapshotMetering = a.getBoolean(R.styleable.CameraView_cameraPictureSnapshotMetering, DEFAULT_PICTURE_SNAPSHOT_METERING); + long autoFocusResetDelay = (long) a.getInteger( + R.styleable.CameraView_cameraAutoFocusResetDelay, + (int) DEFAULT_AUTOFOCUS_RESET_DELAY_MILLIS); + boolean pictureMetering = a.getBoolean(R.styleable.CameraView_cameraPictureMetering, + DEFAULT_PICTURE_METERING); + boolean pictureSnapshotMetering = a.getBoolean( + R.styleable.CameraView_cameraPictureSnapshotMetering, + DEFAULT_PICTURE_SNAPSHOT_METERING); // Size selectors and gestures SizeSelectorParser sizeSelectors = new SizeSelectorParser(a); @@ -265,7 +276,8 @@ public class CameraView extends FrameLayout implements LifecycleObserver { private void doInstantiateEngine() { LOG.w("doInstantiateEngine:", "instantiating. engine:", mEngine); mCameraEngine = instantiateCameraEngine(mEngine, mCameraCallbacks); - LOG.w("doInstantiateEngine:", "instantiated. engine:", mCameraEngine.getClass().getSimpleName()); + LOG.w("doInstantiateEngine:", "instantiated. engine:", + mCameraEngine.getClass().getSimpleName()); mCameraEngine.setOverlay(mOverlayLayout); } @@ -278,7 +290,8 @@ public class CameraView extends FrameLayout implements LifecycleObserver { void doInstantiatePreview() { LOG.w("doInstantiateEngine:", "instantiating. preview:", mPreview); mCameraPreview = instantiatePreview(mPreview, getContext(), this); - LOG.w("doInstantiateEngine:", "instantiated. preview:", mCameraPreview.getClass().getSimpleName()); + LOG.w("doInstantiateEngine:", "instantiated. preview:", + mCameraPreview.getClass().getSimpleName()); mCameraEngine.setPreview(mCameraPreview); if (mPendingFilter != null) { setFilter(mPendingFilter); @@ -294,8 +307,11 @@ public class CameraView extends FrameLayout implements LifecycleObserver { * @return the engine */ @NonNull - protected CameraEngine instantiateCameraEngine(@NonNull Engine engine, @NonNull CameraEngine.Callback callback) { - if (mExperimental && engine == Engine.CAMERA2 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + protected CameraEngine instantiateCameraEngine(@NonNull Engine engine, + @NonNull CameraEngine.Callback callback) { + if (mExperimental + && engine == Engine.CAMERA2 + && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { return new Camera2Engine(callback); } else { mEngine = Engine.CAMERA1; @@ -312,7 +328,9 @@ public class CameraView extends FrameLayout implements LifecycleObserver { * @return the preview */ @NonNull - protected CameraPreview instantiatePreview(@NonNull Preview preview, @NonNull Context context, @NonNull ViewGroup container) { + protected CameraPreview instantiatePreview(@NonNull Preview preview, + @NonNull Context context, + @NonNull ViewGroup container) { switch (preview) { case SURFACE: return new SurfaceCameraPreview(context, container); @@ -414,13 +432,15 @@ public class CameraView extends FrameLayout implements LifecycleObserver { if (heightMode == AT_MOST && lp.height == MATCH_PARENT) heightMode = EXACTLY; } - LOG.i("onMeasure:", "requested dimensions are", "(" + widthValue + "[" + ms(widthMode) + "]x" + - heightValue + "[" + ms(heightMode) + "])"); - LOG.i("onMeasure:", "previewSize is", "(" + previewWidth + "x" + previewHeight + ")"); + LOG.i("onMeasure:", "requested dimensions are (" + widthValue + "[" + ms(widthMode) + + "]x" + heightValue + "[" + ms(heightMode) + "])"); + LOG.i("onMeasure:", "previewSize is", "(" + previewWidth + "x" + + previewHeight + ")"); - // (1) If we have fixed dimensions (either 300dp or MATCH_PARENT), there's nothing we should do, - // other than respect it. The preview will eventually be cropped at the sides (by PreviewImpl scaling) - // except the case in which these fixed dimensions manage to fit exactly the preview aspect ratio. + // (1) If we have fixed dimensions (either 300dp or MATCH_PARENT), there's nothing we + // should do, other than respect it. The preview will eventually be cropped at the sides + // (by Preview scaling) except the case in which these fixed dimensions manage to fit + // exactly the preview aspect ratio. if (widthMode == EXACTLY && heightMode == EXACTLY) { LOG.i("onMeasure:", "both are MATCH_PARENT or fixed value. We adapt.", "This means CROP_CENTER.", "(" + widthValue + "x" + heightValue + ")"); @@ -440,8 +460,8 @@ public class CameraView extends FrameLayout implements LifecycleObserver { return; } - // It's sure now that at least one dimension can be determined (either because EXACTLY or AT_MOST). - // This starts to seem a pleasant situation. + // It's sure now that at least one dimension can be determined (either because EXACTLY + // or AT_MOST). This starts to seem a pleasant situation. // (3) If one of the dimension is completely free (e.g. in a scrollable container), // take the other and fit the ratio. @@ -457,16 +477,17 @@ public class CameraView extends FrameLayout implements LifecycleObserver { width = widthValue; height = Math.round(width * ratio); } - LOG.i("onMeasure:", "one dimension was free, we adapted it to fit the aspect ratio.", + LOG.i("onMeasure:", "one dimension was free, we adapted it to fit the ratio.", "(" + width + "x" + height + ")"); super.onMeasure(MeasureSpec.makeMeasureSpec(width, EXACTLY), MeasureSpec.makeMeasureSpec(height, EXACTLY)); return; } - // (4) At this point both dimensions are either AT_MOST-AT_MOST, EXACTLY-AT_MOST or AT_MOST-EXACTLY. - // Let's manage this sanely. If only one is EXACTLY, we can TRY to fit the aspect ratio, - // but it is not guaranteed to succeed. It depends on the AT_MOST value of the other dimensions. + // (4) At this point both dimensions are either AT_MOST-AT_MOST, EXACTLY-AT_MOST or + // AT_MOST-EXACTLY. Let's manage this sanely. If only one is EXACTLY, we can TRY to fit + // the aspect ratio, but it is not guaranteed to succeed. It depends on the AT_MOST + // value of the other dimensions. if (widthMode == EXACTLY || heightMode == EXACTLY) { boolean freeWidth = widthMode == AT_MOST; int height, width; @@ -724,8 +745,10 @@ public class CameraView extends FrameLayout implements LifecycleObserver { boolean needsCamera = true; boolean needsAudio = audio == Audio.ON || audio == Audio.MONO || audio == Audio.STEREO; - needsCamera = needsCamera && c.checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED; - needsAudio = needsAudio && c.checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED; + needsCamera = needsCamera && c.checkSelfPermission(Manifest.permission.CAMERA) + != PackageManager.PERMISSION_GRANTED; + needsAudio = needsAudio && c.checkSelfPermission(Manifest.permission.RECORD_AUDIO) + != PackageManager.PERMISSION_GRANTED; if (needsCamera || needsAudio) { requestPermissions(needsCamera, needsAudio); @@ -742,14 +765,15 @@ public class CameraView extends FrameLayout implements LifecycleObserver { if (audio == Audio.ON || audio == Audio.MONO || audio == Audio.STEREO) { try { PackageManager manager = getContext().getPackageManager(); - PackageInfo info = manager.getPackageInfo(getContext().getPackageName(), PackageManager.GET_PERMISSIONS); + PackageInfo info = manager.getPackageInfo(getContext().getPackageName(), + PackageManager.GET_PERMISSIONS); for (String requestedPermission : info.requestedPermissions) { if (requestedPermission.equals(Manifest.permission.RECORD_AUDIO)) { return; } } - String message = LOG.e("Permission error:", "When audio is enabled (Audio.ON),", - "the RECORD_AUDIO permission should be added to the app manifest file."); + String message = LOG.e("Permission error: when audio is enabled (Audio.ON)" + + " the RECORD_AUDIO permission should be added to the app manifest file."); throw new IllegalStateException(message); } catch (PackageManager.NameNotFoundException e) { // Not possible. @@ -1272,8 +1296,12 @@ public class CameraView extends FrameLayout implements LifecycleObserver { * @param y should be between 0 and getHeight() */ public void startAutoFocus(float x, float y) { - if (x < 0 || x > getWidth()) throw new IllegalArgumentException("x should be >= 0 and <= getWidth()"); - if (y < 0 || y > getHeight()) throw new IllegalArgumentException("y should be >= 0 and <= getHeight()"); + if (x < 0 || x > getWidth()) { + throw new IllegalArgumentException("x should be >= 0 and <= getWidth()"); + } + if (y < 0 || y > getHeight()) { + throw new IllegalArgumentException("y should be >= 0 and <= getHeight()"); + } mCameraEngine.startAutoFocus(null, new PointF(x, y)); } @@ -1510,9 +1538,6 @@ public class CameraView extends FrameLayout implements LifecycleObserver { * This will trigger {@link CameraListener#onPictureTaken(PictureResult)} if a listener * was registered. * - * Note that if sessionType is {@link Mode#VIDEO}, this - * might fall back to {@link #takePictureSnapshot()} (that is, we might capture a preview frame). - * * @see #takePictureSnapshot() */ public void takePicture() { @@ -2000,7 +2025,8 @@ public class CameraView extends FrameLayout implements LifecycleObserver { } @Override - public void dispatchOnFocusStart(@Nullable final Gesture gesture, @NonNull final PointF point) { + public void dispatchOnFocusStart(@Nullable final Gesture gesture, + @NonNull final PointF point) { mLogger.i("dispatchOnFocusStart", gesture, point); mUiHandler.post(new Runnable() { @Override @@ -2020,7 +2046,8 @@ public class CameraView extends FrameLayout implements LifecycleObserver { } @Override - public void dispatchOnFocusEnd(@Nullable final Gesture gesture, final boolean success, + public void dispatchOnFocusEnd(@Nullable final Gesture gesture, + final boolean success, @NonNull final PointF point) { mLogger.i("dispatchOnFocusEnd", gesture, success, point); mUiHandler.post(new Runnable() { @@ -2096,7 +2123,8 @@ public class CameraView extends FrameLayout implements LifecycleObserver { @Override public void dispatchFrame(@NonNull final Frame frame) { - // The getTime() below might crash if developers incorrectly release frames asynchronously. + // The getTime() below might crash if developers incorrectly release + // frames asynchronously. mLogger.v("dispatchFrame:", frame.getTime(), "processors:", mFrameProcessors.size()); if (mFrameProcessors.isEmpty()) { // Mark as released. This instance will be reused. @@ -2106,7 +2134,8 @@ public class CameraView extends FrameLayout implements LifecycleObserver { mFrameProcessorsHandler.run(new Runnable() { @Override public void run() { - mLogger.v("dispatchFrame: dispatching", frame.getTime(), "to processors."); + mLogger.v("dispatchFrame: dispatching", frame.getTime(), + "to processors."); for (FrameProcessor processor : mFrameProcessors) { try { processor.process(frame); @@ -2173,7 +2202,6 @@ public class CameraView extends FrameLayout implements LifecycleObserver { return super.generateLayoutParams(attributeSet); } - // We don't support removeView on overlays for now. @Override public void addView(View child, int index, ViewGroup.LayoutParams params) { if (!mInEditor && mOverlayLayout.isOverlay(params)) { @@ -2183,6 +2211,16 @@ public class CameraView extends FrameLayout implements LifecycleObserver { } } + @Override + public void removeView(View view) { + ViewGroup.LayoutParams params = view.getLayoutParams(); + if (!mInEditor && params != null && mOverlayLayout.isOverlay(params)) { + mOverlayLayout.removeView(view); + } else { + super.removeView(view); + } + } + //endregion //region Filters @@ -2198,8 +2236,8 @@ public class CameraView extends FrameLayout implements LifecycleObserver { * Use {@link NoFilter} to clear the existing filter, * and take a look at the {@link Filters} class for commonly used filters. * - * This method will throw an exception if the current preview does not support real-time filters. - * Make sure you use {@link Preview#GL_SURFACE} (the default). + * This method will throw an exception if the current preview does not support real-time + * filters. Make sure you use {@link Preview#GL_SURFACE} (the default). * * @see Filters * @param filter a new filter @@ -2207,20 +2245,32 @@ public class CameraView extends FrameLayout implements LifecycleObserver { public void setFilter(@NonNull Filter filter) { if (mCameraPreview == null) { mPendingFilter = filter; - } else if (!(filter instanceof NoFilter) && !mExperimental) { - throw new RuntimeException("Filters are an experimental features and need the experimental flag set."); - } else if (mCameraPreview instanceof FilterCameraPreview) { - ((FilterCameraPreview) mCameraPreview).setFilter(filter); } else { - throw new RuntimeException("Filters are only supported by the GL_SURFACE preview. Current:" + mPreview); + boolean isNoFilter = filter instanceof NoFilter; + boolean isFilterPreview = mCameraPreview instanceof FilterCameraPreview; + // If not experimental, we only allow NoFilter (called on creation). + if (!isNoFilter && !mExperimental) { + throw new RuntimeException("Filters are an experimental features and" + + " need the experimental flag set."); + } + // If not a filter preview, we only allow NoFilter (called on creation). + if (!isNoFilter && !isFilterPreview) { + throw new RuntimeException("Filters are only supported by the GL_SURFACE preview." + + " Current preview:" + mPreview); + } + // If we have a filter preview, apply. + if (isFilterPreview) { + ((FilterCameraPreview) mCameraPreview).setFilter(filter); + } + // No-op: !isFilterPreview && isNoPreview } } /** * Returns the current real-time filter applied to the camera preview. * - * This method will throw an exception if the current preview does not support real-time filters. - * Make sure you use {@link Preview#GL_SURFACE} (the default). + * This method will throw an exception if the current preview does not support real-time + * filters. Make sure you use {@link Preview#GL_SURFACE} (the default). * * @see #setFilter(Filter) * @return the current filter @@ -2228,13 +2278,15 @@ public class CameraView extends FrameLayout implements LifecycleObserver { @NonNull public Filter getFilter() { if (!mExperimental) { - throw new RuntimeException("Filters are an experimental features and need the experimental flag set."); + throw new RuntimeException("Filters are an experimental features and need " + + "the experimental flag set."); } else if (mCameraPreview == null) { return mPendingFilter; } else if (mCameraPreview instanceof FilterCameraPreview) { return ((FilterCameraPreview) mCameraPreview).getCurrentFilter(); } else { - throw new RuntimeException("Filters are only supported by the GL_SURFACE preview. Current:" + mPreview); + throw new RuntimeException("Filters are only supported by the GL_SURFACE preview. " + + "Current:" + mPreview); } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/PictureResult.java b/cameraview/src/main/java/com/otaliastudios/cameraview/PictureResult.java index d0369667..b653e2f4 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/PictureResult.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/PictureResult.java @@ -137,7 +137,8 @@ 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); + CameraUtils.decodeBitmap(getData(), maxWidth, maxHeight, new BitmapFactory.Options(), + rotation, callback); } /** diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/VideoResult.java b/cameraview/src/main/java/com/otaliastudios/cameraview/VideoResult.java index 785ca4b0..725838d2 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/VideoResult.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/VideoResult.java @@ -185,7 +185,8 @@ public class VideoResult { /** * Returns the reason why the recording was stopped. - * @return one of {@link #REASON_USER}, {@link #REASON_MAX_DURATION_REACHED} or {@link #REASON_MAX_SIZE_REACHED}. + * @return one of {@link #REASON_USER}, {@link #REASON_MAX_DURATION_REACHED} + * or {@link #REASON_MAX_SIZE_REACHED}. */ public int getTerminationReason() { return endReason; 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 6d8a1b1d..2b733725 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/controls/ControlParser.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/controls/ControlParser.java @@ -25,16 +25,19 @@ public class ControlParser { private int engine; public ControlParser(@NonNull Context context, @NonNull TypedArray array) { - this.preview = array.getInteger(R.styleable.CameraView_cameraPreview, Preview.DEFAULT.value()); - this.facing = array.getInteger(R.styleable.CameraView_cameraFacing, Facing.DEFAULT(context).value()); - this.flash = array.getInteger(R.styleable.CameraView_cameraFlash, Flash.DEFAULT.value()); - this.grid = array.getInteger(R.styleable.CameraView_cameraGrid, Grid.DEFAULT.value()); - this.whiteBalance = array.getInteger(R.styleable.CameraView_cameraWhiteBalance, WhiteBalance.DEFAULT.value()); - this.mode = array.getInteger(R.styleable.CameraView_cameraMode, Mode.DEFAULT.value()); - this.hdr = array.getInteger(R.styleable.CameraView_cameraHdr, Hdr.DEFAULT.value()); - this.audio = array.getInteger(R.styleable.CameraView_cameraAudio, Audio.DEFAULT.value()); - this.videoCodec = array.getInteger(R.styleable.CameraView_cameraVideoCodec, VideoCodec.DEFAULT.value()); - this.engine = array.getInteger(R.styleable.CameraView_cameraEngine, Engine.DEFAULT.value()); + preview = array.getInteger(R.styleable.CameraView_cameraPreview, Preview.DEFAULT.value()); + facing = array.getInteger(R.styleable.CameraView_cameraFacing, + Facing.DEFAULT(context).value()); + flash = array.getInteger(R.styleable.CameraView_cameraFlash, Flash.DEFAULT.value()); + grid = array.getInteger(R.styleable.CameraView_cameraGrid, Grid.DEFAULT.value()); + whiteBalance = array.getInteger(R.styleable.CameraView_cameraWhiteBalance, + WhiteBalance.DEFAULT.value()); + mode = array.getInteger(R.styleable.CameraView_cameraMode, Mode.DEFAULT.value()); + hdr = array.getInteger(R.styleable.CameraView_cameraHdr, Hdr.DEFAULT.value()); + audio = array.getInteger(R.styleable.CameraView_cameraAudio, Audio.DEFAULT.value()); + videoCodec = array.getInteger(R.styleable.CameraView_cameraVideoCodec, + VideoCodec.DEFAULT.value()); + engine = array.getInteger(R.styleable.CameraView_cameraEngine, Engine.DEFAULT.value()); } @NonNull 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 5d43c005..dfb10086 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java @@ -76,7 +76,8 @@ public class Camera1Engine extends CameraEngine implements public void onError(int error, Camera camera) { if (error == Camera.CAMERA_ERROR_SERVER_DIED) { // Looks like this is recoverable. - LOG.w("Recoverable error inside the onError callback.", "CAMERA_ERROR_SERVER_DIED"); + LOG.w("Recoverable error inside the onError callback.", + "CAMERA_ERROR_SERVER_DIED"); restart(); return; } @@ -118,7 +119,10 @@ public class Camera1Engine extends CameraEngine implements @Override protected boolean collectCameraInfo(@NonNull Facing facing) { int internalFacing = mMapper.mapFacing(facing); - LOG.i("collectCameraInfo", "Facing:", facing, "Internal:", internalFacing, "Cameras:", Camera.getNumberOfCameras()); + LOG.i("collectCameraInfo", + "Facing:", facing, + "Internal:", internalFacing, + "Cameras:", Camera.getNumberOfCameras()); Camera.CameraInfo cameraInfo = new Camera.CameraInfo(); for (int i = 0, count = Camera.getNumberOfCameras(); i < count; i++) { Camera.getCameraInfo(i, cameraInfo); @@ -150,10 +154,12 @@ 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, getAngles().flip(Reference.SENSOR, Reference.VIEW)); + mCameraOptions = new CameraOptions(params, getAngles() + .flip(Reference.SENSOR, Reference.VIEW)); applyAllParameters(params); mCamera.setParameters(params); - mCamera.setDisplayOrientation(getAngles().offset(Reference.SENSOR, Reference.VIEW, Axis.ABSOLUTE)); // <- not allowed during preview + mCamera.setDisplayOrientation(getAngles().offset(Reference.SENSOR, Reference.VIEW, + Axis.ABSOLUTE)); // <- not allowed during preview LOG.i("onStartEngine:", "Ended"); return Tasks.forResult(null); } @@ -194,14 +200,19 @@ public class Camera1Engine extends CameraEngine implements mPreview.setStreamSize(previewSize.getWidth(), previewSize.getHeight()); Camera.Parameters params = mCamera.getParameters(); - params.setPreviewFormat(ImageFormat.NV21); // should be the default, but let's make sure, since YuvImage will only support this & a few others - params.setPreviewSize(mPreviewStreamSize.getWidth(), mPreviewStreamSize.getHeight()); // not allowed during preview + // NV21 should be the default, but let's make sure, since YuvImage will only support this + // and a few others + params.setPreviewFormat(ImageFormat.NV21); + // setPreviewSize is not allowed during preview + params.setPreviewSize(mPreviewStreamSize.getWidth(), mPreviewStreamSize.getHeight()); if (getMode() == Mode.PICTURE) { - params.setPictureSize(mCaptureSize.getWidth(), mCaptureSize.getHeight()); // allowed during preview + // setPictureSize is allowed during preview + params.setPictureSize(mCaptureSize.getWidth(), mCaptureSize.getHeight()); } else { - // mCaptureSize in this case is a video size. The available video sizes are not necessarily - // a subset of the picture sizes, so we can't use the mCaptureSize value: it might crash. - // However, the setPictureSize() passed here is useless : we don't allow HQ pictures in video mode. + // mCaptureSize in this case is a video size. The available video sizes are not + // necessarily a subset of the picture sizes, so we can't use the mCaptureSize value: + // it might crash. However, the setPictureSize() passed here is useless : we don't allow + // HQ pictures in video mode. // While this might be lifted in the future, for now, just use a picture capture size. Size pictureSize = computeCaptureSize(Mode.PICTURE); params.setPictureSize(pictureSize.getWidth(), pictureSize.getHeight()); @@ -298,7 +309,8 @@ public class Camera1Engine extends CameraEngine implements @WorkerThread @Override protected void onTakePicture(@NonNull PictureResult.Stub stub, boolean doMetering) { - stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR); + stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, + Axis.RELATIVE_TO_SENSOR); stub.size = getPictureSize(Reference.OUTPUT); mPictureRecorder = new Full1PictureRecorder(stub, Camera1Engine.this, mCamera); mPictureRecorder.take(); @@ -306,10 +318,13 @@ public class Camera1Engine extends CameraEngine implements @WorkerThread @Override - protected void onTakePictureSnapshot(@NonNull PictureResult.Stub stub, @NonNull AspectRatio outputRatio, boolean doMetering) { - stub.size = getUncroppedSnapshotSize(Reference.OUTPUT); // Not the real size: it will be cropped to match the view ratio - stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR); // Actually it will be rotated and set to 0. - + protected void onTakePictureSnapshot(@NonNull PictureResult.Stub stub, + @NonNull AspectRatio outputRatio, + boolean doMetering) { + // Not the real size: it will be cropped to match the view ratio + stub.size = getUncroppedSnapshotSize(Reference.OUTPUT); + // Actually it will be rotated and set to 0. + stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR); if (mPreview instanceof GlCameraPreview && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { mPictureRecorder = new SnapshotGlPictureRecorder(stub, this, (GlCameraPreview) mPreview, outputRatio); } else { @@ -324,8 +339,10 @@ public class Camera1Engine extends CameraEngine implements @Override protected void onTakeVideo(@NonNull VideoResult.Stub stub) { - stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR); - stub.size = getAngles().flip(Reference.SENSOR, Reference.OUTPUT) ? mCaptureSize.flip() : mCaptureSize; + stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, + Axis.RELATIVE_TO_SENSOR); + stub.size = getAngles().flip(Reference.SENSOR, Reference.OUTPUT) ? mCaptureSize.flip() + : mCaptureSize; // Unlock the camera and start recording. try { mCamera.unlock(); @@ -342,12 +359,13 @@ public class Camera1Engine extends CameraEngine implements @SuppressLint("NewApi") @WorkerThread @Override - protected void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub, @NonNull AspectRatio outputRatio) { + protected void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub, + @NonNull AspectRatio outputRatio) { if (!(mPreview instanceof GlCameraPreview)) { - throw new IllegalStateException("Video snapshots are only supported with GlCameraPreview."); + throw new IllegalStateException("Video snapshots are only supported with GL_SURFACE."); } if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { - throw new IllegalStateException("Video snapshots are only supported starting from API 18."); + throw new IllegalStateException("Video snapshots are only supported on API 18+."); } GlCameraPreview glPreview = (GlCameraPreview) mPreview; Size outputSize = getUncroppedSnapshotSize(Reference.OUTPUT); @@ -370,7 +388,8 @@ public class Camera1Engine extends CameraEngine implements LOG.i("onTakeVideoSnapshot", "rotation:", stub.rotation, "size:", stub.size); // Start. - mVideoRecorder = new SnapshotVideoRecorder(Camera1Engine.this, glPreview, getOverlay(), stub.rotation); + mVideoRecorder = new SnapshotVideoRecorder(Camera1Engine.this, glPreview, + getOverlay(), stub.rotation); mVideoRecorder.start(stub); } @@ -494,7 +513,8 @@ public class Camera1Engine extends CameraEngine implements }); } - private boolean applyWhiteBalance(@NonNull Camera.Parameters params, @NonNull WhiteBalance oldWhiteBalance) { + private boolean applyWhiteBalance(@NonNull Camera.Parameters params, + @NonNull WhiteBalance oldWhiteBalance) { if (mCameraOptions.supports(mWhiteBalance)) { params.setWhiteBalance(mMapper.mapWhiteBalance(mWhiteBalance)); return true; @@ -573,7 +593,8 @@ public class Camera1Engine extends CameraEngine implements if (applyExposureCorrection(params, old)) { mCamera.setParameters(params); if (notify) { - mCallback.dispatchOnExposureCorrectionChanged(mExposureCorrectionValue, bounds, points); + mCallback.dispatchOnExposureCorrectionChanged(mExposureCorrectionValue, + bounds, points); } } } @@ -582,7 +603,8 @@ public class Camera1Engine extends CameraEngine implements }); } - private boolean applyExposureCorrection(@NonNull Camera.Parameters params, float oldExposureCorrection) { + private boolean applyExposureCorrection(@NonNull Camera.Parameters params, + float oldExposureCorrection) { if (mCameraOptions.isExposureCorrectionSupported()) { // Just make sure we're inside boundaries. float max = mCameraOptions.getExposureCorrectionMaxValue(); @@ -591,7 +613,8 @@ public class Camera1Engine extends CameraEngine implements val = val < min ? min : val > max ? max : val; // cap mExposureCorrectionValue = val; // Apply. - int indexValue = (int) (mExposureCorrectionValue / params.getExposureCompensationStep()); + int indexValue = (int) (mExposureCorrectionValue + / params.getExposureCompensationStep()); params.setExposureCompensation(indexValue); return true; } @@ -692,7 +715,8 @@ public class Camera1Engine extends CameraEngine implements viewWidthF, viewHeightF, offset); List meteringAreas1 = meteringAreas2.subList(0, 1); - // At this point we are sure that camera supports auto focus... right? Look at CameraView.onTouchEvent(). + // At this point we are sure that camera supports auto focus... right? + // Look at CameraView.onTouchEvent(). Camera.Parameters params = mCamera.getParameters(); int maxAF = params.getMaxNumFocusAreas(); int maxAE = params.getMaxNumMeteringAreas(); @@ -702,8 +726,8 @@ public class Camera1Engine extends CameraEngine implements mCamera.setParameters(params); mCallback.dispatchOnFocusStart(gesture, p); - // The auto focus callback is not guaranteed to be called, but we really want it to be. - // So we remove the old runnable if still present and post a new one. + // The auto focus callback is not guaranteed to be called, but we really want it + // to be. So we remove the old runnable if still present and post a new one. if (mFocusEndRunnable != null) mHandler.remove(mFocusEndRunnable); mFocusEndRunnable = new Runnable() { @Override @@ -778,7 +802,11 @@ public class Camera1Engine extends CameraEngine implements int bottom = (int) Math.min(centerY + delta, 1000); int left = (int) Math.max(centerX - delta, -1000); int right = (int) Math.min(centerX + delta, 1000); - LOG.i("focus:", "computeMeteringArea:", "top:", top, "left:", left, "bottom:", bottom, "right:", right); + LOG.i("focus:", "computeMeteringArea:", + "top:", top, + "left:", left, + "bottom:", bottom, + "right:", right); return new Rect(left, top, right, bottom); } 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 82788f5d..8995b162 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java @@ -27,6 +27,7 @@ import android.view.SurfaceHolder; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; +import androidx.annotation.VisibleForTesting; import androidx.annotation.WorkerThread; import com.google.android.gms.tasks.Task; @@ -81,7 +82,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv private static final int FRAME_PROCESSING_FORMAT = ImageFormat.NV21; private static final int FRAME_PROCESSING_INPUT_FORMAT = ImageFormat.YUV_420_888; - private static final long METER_TIMEOUT = 2500; + @VisibleForTesting static final long METER_TIMEOUT = 2500; private final CameraManager mManager; private String mCameraId; @@ -96,17 +97,19 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv private Size mFrameProcessingSize; private ImageReader mFrameProcessingReader; // need this or the reader surface is collected private final WorkerHandler mFrameConversionHandler; + private final Object mFrameProcessingImageLock = new Object(); private Surface mFrameProcessingSurface; // Preview private Surface mPreviewStreamSurface; // Video recording - private VideoResult.Stub mFullVideoPendingStub; // When takeVideo is called, we have to reset the session. + // When takeVideo is called, we restart the session. + private VideoResult.Stub mFullVideoPendingStub; // Picture capturing private ImageReader mPictureReader; - private final boolean mPictureCaptureStopsPreview = false; // can make configurable at some point + private final boolean mPictureCaptureStopsPreview = false; // can be configurable at some point // Actions private final List mActions = new ArrayList<>(); @@ -135,32 +138,46 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv return value == null ? fallback : value; } - @SuppressWarnings("DuplicateBranchesInSwitch") @NonNull private CameraException createCameraException(@NonNull CameraAccessException exception) { int reason; switch (exception.getReason()) { - case CameraAccessException.CAMERA_DISABLED: reason = CameraException.REASON_FAILED_TO_CONNECT; break; - case CameraAccessException.CAMERA_ERROR: reason = CameraException.REASON_DISCONNECTED; break; - case CameraAccessException.CAMERA_DISCONNECTED: reason = CameraException.REASON_DISCONNECTED; break; - case CameraAccessException.CAMERA_IN_USE: reason = CameraException.REASON_FAILED_TO_CONNECT; break; - case CameraAccessException.MAX_CAMERAS_IN_USE: reason = CameraException.REASON_FAILED_TO_CONNECT; break; - default: reason = CameraException.REASON_UNKNOWN; break; + case CameraAccessException.CAMERA_DISABLED: + case CameraAccessException.CAMERA_IN_USE: + case CameraAccessException.MAX_CAMERAS_IN_USE: { + reason = CameraException.REASON_FAILED_TO_CONNECT; + break; + } + case CameraAccessException.CAMERA_ERROR: + case CameraAccessException.CAMERA_DISCONNECTED: { + reason = CameraException.REASON_DISCONNECTED; + break; + } + default: { + reason = CameraException.REASON_UNKNOWN; + break; + } } return new CameraException(exception, reason); } - @SuppressWarnings("DuplicateBranchesInSwitch") @NonNull private CameraException createCameraException(int stateCallbackError) { int reason; switch (stateCallbackError) { - case CameraDevice.StateCallback.ERROR_CAMERA_DISABLED: reason = CameraException.REASON_FAILED_TO_CONNECT; break; // Device policy - case CameraDevice.StateCallback.ERROR_CAMERA_DEVICE: reason = CameraException.REASON_FAILED_TO_CONNECT; break; // Fatal error - case CameraDevice.StateCallback.ERROR_CAMERA_SERVICE: reason = CameraException.REASON_FAILED_TO_CONNECT; break; // Fatal error, device might have to be restarted - case CameraDevice.StateCallback.ERROR_CAMERA_IN_USE: reason = CameraException.REASON_FAILED_TO_CONNECT; break; - case CameraDevice.StateCallback.ERROR_MAX_CAMERAS_IN_USE: reason = CameraException.REASON_FAILED_TO_CONNECT; break; - default: reason = CameraException.REASON_UNKNOWN; break; + case CameraDevice.StateCallback.ERROR_CAMERA_DISABLED: // Device policy + case CameraDevice.StateCallback.ERROR_CAMERA_DEVICE: // Fatal error + case CameraDevice.StateCallback.ERROR_CAMERA_SERVICE: // Fatal error, might have to + // restart the device + case CameraDevice.StateCallback.ERROR_CAMERA_IN_USE: + case CameraDevice.StateCallback.ERROR_MAX_CAMERAS_IN_USE: { + reason = CameraException.REASON_FAILED_TO_CONNECT; + break; + } + default: { + reason = CameraException.REASON_UNKNOWN; + break; + } } return new CameraException(reason); } @@ -173,7 +190,8 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv */ @SuppressWarnings("UnusedReturnValue") @NonNull - private CaptureRequest.Builder createRepeatingRequestBuilder(int template) throws CameraAccessException { + private CaptureRequest.Builder createRepeatingRequestBuilder(int template) + throws CameraAccessException { CaptureRequest.Builder oldBuilder = mRepeatingRequestBuilder; mRepeatingRequestBuilder = mCamera.createCaptureRequest(template); mRepeatingRequestBuilder.setTag(template); @@ -199,8 +217,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv } /** - * Sets up the repeating request builder with default surfaces and extra ones - * if needed (like a video recording surface). + * Removes default surfaces from the repeating request builder. */ private void removeRepeatingRequestBuilderSurfaces() { mRepeatingRequestBuilder.removeTarget(mPreviewStreamSurface); @@ -209,17 +226,6 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv } } - private void applyRepeatingRequestBuilderAsSingle() { - if (getPreviewState() == STATE_STARTED) { - try { - mSession.capture(mRepeatingRequestBuilder.build(), - mRepeatingRequestCallback, null); - } catch (CameraAccessException e) { - throw createCameraException(e); - } - } - } - /** * Applies the repeating request builder to the preview, assuming we actually have a preview * running. Can be called after changing parameters to the builder. @@ -228,7 +234,8 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv * it should be set before calling this method, for example by calling * {@link #createRepeatingRequestBuilder(int)}. */ - private void applyRepeatingRequestBuilder() { + @SuppressWarnings("WeakerAccess") + protected void applyRepeatingRequestBuilder() { applyRepeatingRequestBuilder(true, CameraException.REASON_DISCONNECTED); } @@ -284,7 +291,8 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv protected List getPreviewStreamAvailableSizes() { try { CameraCharacteristics characteristics = mManager.getCameraCharacteristics(mCameraId); - StreamConfigurationMap streamMap = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); + StreamConfigurationMap streamMap = + characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); if (streamMap == null) { throw new RuntimeException("StreamConfigurationMap is null. Should not happen."); } @@ -309,7 +317,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv } @Override - protected boolean collectCameraInfo(@NonNull Facing facing) { + protected final boolean collectCameraInfo(@NonNull Facing facing) { int internalFacing = mMapper.mapFacing(facing); String[] cameraIds = null; try { @@ -325,7 +333,8 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv for (String cameraId : cameraIds) { try { CameraCharacteristics characteristics = mManager.getCameraCharacteristics(cameraId); - if (internalFacing == readCharacteristic(characteristics, CameraCharacteristics.LENS_FACING, -99)) { + if (internalFacing == readCharacteristic(characteristics, + CameraCharacteristics.LENS_FACING, -99)) { mCameraId = cameraId; int sensorOffset = readCharacteristic(characteristics, CameraCharacteristics.SENSOR_ORIENTATION, 0); @@ -372,12 +381,13 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv @Override public void onDisconnected(@NonNull CameraDevice camera) { - // Not sure if this is called INSTEAD of onOpened() or can be called after as well. - // However, using trySetException should address this problem - it will only trigger - // if the task has no result. + // Not sure if this is called INSTEAD of onOpened() or can be called after + // as well. However, using trySetException should address this problem - + // it will only trigger if the task has no result. // - // Docs say to release this camera instance, however, since we throw an unrecoverable - // CameraException, this will trigger a stop() through the exception handler. + // Docs say to release this camera instance, however, since we throw an + // unrecoverable CameraException, this will trigger a stop() through the + // exception handler. task.trySetException(new CameraException(CameraException.REASON_DISCONNECTED)); } @@ -463,8 +473,11 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv // 4. FRAME PROCESSING if (hasFrameProcessors()) { // Choose the size. - StreamConfigurationMap streamMap = mCameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); - if (streamMap == null) throw new RuntimeException("StreamConfigurationMap is null. Should not happen."); + StreamConfigurationMap streamMap = mCameraCharacteristics + .get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); + if (streamMap == null) { + throw new RuntimeException("StreamConfigurationMap is null. Should not happen."); + } android.util.Size[] aSizes = streamMap.getOutputSizes(FRAME_PROCESSING_INPUT_FORMAT); List sizes = new ArrayList<>(); for (android.util.Size aSize : aSizes) { @@ -479,7 +492,8 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv mFrameProcessingSize.getHeight(), FRAME_PROCESSING_INPUT_FORMAT, 2); - mFrameProcessingReader.setOnImageAvailableListener(this, mFrameConversionHandler.getHandler()); + mFrameProcessingReader.setOnImageAvailableListener(this, + mFrameConversionHandler.getHandler()); mFrameProcessingSurface = mFrameProcessingReader.getSurface(); outputSurfaces.add(mFrameProcessingSurface); } else { @@ -500,7 +514,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv @Override public void onConfigureFailed(@NonNull CameraCaptureSession session) { - // I would say this should be a library error and as such we throw a Runtime Exception. + // This SHOULD be a library error so we throw a RuntimeException. String message = LOG.e("onConfigureFailed! Session", session); throw new RuntimeException(message); } @@ -529,13 +543,14 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv LOG.i("onStartPreview", "Starting preview."); addRepeatingRequestBuilderSurfaces(); - applyRepeatingRequestBuilder(false, CameraException.REASON_FAILED_TO_START_PREVIEW); + applyRepeatingRequestBuilder(false, + CameraException.REASON_FAILED_TO_START_PREVIEW); LOG.i("onStartPreview", "Started preview."); // Start delayed video if needed. if (mFullVideoPendingStub != null) { - // Do not call takeVideo/onTakeVideo. It will reset some stub parameters that the recorder sets. - // Also we are posting this so that doTakeVideo sees a started preview. + // Do not call takeVideo/onTakeVideo. It will reset some stub parameters that + // the recorder sets. Also we are posting so that doTakeVideo sees a started preview. LOG.i("onStartPreview", "Posting doTakeVideo call."); final VideoResult.Stub stub = mFullVideoPendingStub; mFullVideoPendingStub = null; @@ -594,7 +609,12 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv mCaptureSize = null; mFrameProcessingSize = null; if (mFrameProcessingReader != null) { - mFrameProcessingReader.close(); + synchronized (mFrameProcessingImageLock) { + // This call synchronously releases all Images and their underlying properties. + // This can cause a segmentation fault while converting the Image to NV21. + // So we use this lock for the two operations. + mFrameProcessingReader.close(); + } mFrameProcessingReader = null; } if (mPictureReader != null) { @@ -611,7 +631,6 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv @NonNull @Override protected Task onStopEngine() { - LOG.i("onStopEngine:", "About to clean up."); try { LOG.i("onStopEngine:", "Clean up.", "Releasing camera."); mCamera.close(); @@ -620,6 +639,16 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv LOG.w("onStopEngine:", "Clean up.", "Exception while releasing camera.", e); } mCamera = null; + + // After engine is stopping, the repeating request builder will be null, + // so the ActionHolder.getBuilder() contract would be broken. Same for characteristics. + // This can cause crashes if some ongoing Action queries the holder. So we abort them. + LOG.i("onStopEngine:", "Aborting actions."); + for (Action action : mActions) { + action.abort(this); + } + + mCameraCharacteristics = null; mCameraOptions = null; mVideoRecorder = null; mRepeatingRequestBuilder = null; @@ -633,7 +662,9 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv @WorkerThread @Override - protected void onTakePictureSnapshot(@NonNull final PictureResult.Stub stub, @NonNull final AspectRatio outputRatio, boolean doMetering) { + protected void onTakePictureSnapshot(@NonNull final PictureResult.Stub stub, + @NonNull final AspectRatio outputRatio, + boolean doMetering) { if (doMetering) { LOG.i("onTakePictureSnapshot:", "doMetering is true. Delaying."); Action action = Actions.timeout(METER_TIMEOUT, createMeterAction(null)); @@ -653,7 +684,8 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv // stub.size is not the real size: it will be cropped to the given ratio // stub.rotation will be set to 0 - we rotate the texture instead. stub.size = getUncroppedSnapshotSize(Reference.OUTPUT); - stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR); + stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, + Axis.RELATIVE_TO_SENSOR); mPictureRecorder = new Snapshot2PictureRecorder(stub, this, (GlCameraPreview) mPreview, outputRatio); mPictureRecorder.take(); @@ -674,21 +706,24 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv action.start(this); } else { LOG.i("onTakePicture:", "doMetering is false. Performing."); - stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR); + stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, + Axis.RELATIVE_TO_SENSOR); stub.size = getPictureSize(Reference.OUTPUT); try { if (mPictureCaptureStopsPreview) { - // These two are present in official samples and are probably meant to speed things up? - // But from my tests, they actually make everything slower. So this is disabled by default - // with a boolean flag. Maybe in the future we can make this configurable as some - // people might want to stop the preview while picture is being taken even if it - // increases the latency. + // These two are present in official samples and are probably meant to + // speed things up? But from my tests, they actually make everything slower. + // So this is disabled by default with a boolean flag. Maybe in the future + // we can make this configurable as some people might want to stop the preview + // while picture is being taken even if it increases the latency. mSession.stopRepeating(); mSession.abortCaptures(); } - CaptureRequest.Builder builder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); + CaptureRequest.Builder builder + = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); applyAllParameters(builder, mRepeatingRequestBuilder); - mPictureRecorder = new Full2PictureRecorder(stub, this, builder, mPictureReader); + mPictureRecorder = new Full2PictureRecorder(stub, this, builder, + mPictureReader); mPictureRecorder.take(); } catch (CameraAccessException e) { throw createCameraException(e); @@ -721,8 +756,10 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv @Override protected void onTakeVideo(@NonNull VideoResult.Stub stub) { LOG.i("onTakeVideo", "called."); - stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR); - stub.size = getAngles().flip(Reference.SENSOR, Reference.OUTPUT) ? mCaptureSize.flip() : mCaptureSize; + stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, + Axis.RELATIVE_TO_SENSOR); + stub.size = getAngles().flip(Reference.SENSOR, Reference.OUTPUT) ? + mCaptureSize.flip() : mCaptureSize; // We must restart the session at each time. // Save the pending data and restart the session. LOG.w("onTakeVideo", "calling restartBind."); @@ -752,9 +789,10 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv @WorkerThread @Override - protected void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub, @NonNull AspectRatio outputRatio) { + protected void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub, + @NonNull AspectRatio outputRatio) { if (!(mPreview instanceof GlCameraPreview)) { - throw new IllegalStateException("Video snapshots are only supported with GlCameraPreview."); + throw new IllegalStateException("Video snapshots are only supported with GL_SURFACE."); } GlCameraPreview glPreview = (GlCameraPreview) mPreview; Size outputSize = getUncroppedSnapshotSize(Reference.OUTPUT); @@ -779,18 +817,21 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv // Start. // The overlay rotation should alway be VIEW-OUTPUT, just liek Camera1Engine. int overlayRotation = getAngles().offset(Reference.VIEW, Reference.OUTPUT, Axis.ABSOLUTE); - mVideoRecorder = new SnapshotVideoRecorder(this, glPreview, getOverlay(), overlayRotation); + mVideoRecorder = new SnapshotVideoRecorder(this, glPreview, getOverlay(), + overlayRotation); mVideoRecorder.start(stub); } + /** + * When video ends we must stop the recorder and remove the recorder surface from + * camera outputs. This is done in onVideoResult. However, on some devices, order matters. + * If we stop the recorder and AFTER send camera frames to it, the camera will try to fill + * the recorder "abandoned" Surface and on some devices with a poor internal implementation + * (HW_LEVEL_LEGACY) this crashes. So if the conditions are met, we restore here. Issue #549. + */ @Override public void onVideoRecordingEnd() { super.onVideoRecordingEnd(); - // When video ends we must stop the recorder and remove the recorder surface from camera outputs. - // This is done in onVideoResult. However, on some devices, order matters. If we stop the recorder - // and AFTER send camera frames to it, the camera will try to fill the recorder "abandoned" - // Surface and on some devices with a poor internal implementation (HW_LEVEL_LEGACY) this crashes. - // So if the conditions are met, we restore here. Issue #549. boolean needsIssue549Workaround = (mVideoRecorder instanceof Full2VideoRecorder) || (readCharacteristic(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL, -1) == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY); @@ -806,7 +847,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv } /** - * Some video recorders might change the camera template to {@link CameraDevice#TEMPLATE_RECORD}. + * Video recorders might change the camera template to {@link CameraDevice#TEMPLATE_RECORD}. * After the video is taken, we should restore the template preview, which also means that * we'll remove any extra surface target that was added by the video recorder. * @@ -846,26 +887,34 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv // We might be in a metering operation, or the old builder might have some special // metering parameters. Copy these special keys over to the new builder. // These are the keys changed by metering.Parameters, or by us in applyFocusForMetering. - builder.set(CaptureRequest.CONTROL_AF_REGIONS, oldBuilder.get(CaptureRequest.CONTROL_AF_REGIONS)); - builder.set(CaptureRequest.CONTROL_AE_REGIONS, oldBuilder.get(CaptureRequest.CONTROL_AE_REGIONS)); - builder.set(CaptureRequest.CONTROL_AWB_REGIONS, oldBuilder.get(CaptureRequest.CONTROL_AWB_REGIONS)); - builder.set(CaptureRequest.CONTROL_AF_MODE, oldBuilder.get(CaptureRequest.CONTROL_AF_MODE)); + builder.set(CaptureRequest.CONTROL_AF_REGIONS, + oldBuilder.get(CaptureRequest.CONTROL_AF_REGIONS)); + builder.set(CaptureRequest.CONTROL_AE_REGIONS, + oldBuilder.get(CaptureRequest.CONTROL_AE_REGIONS)); + builder.set(CaptureRequest.CONTROL_AWB_REGIONS, + oldBuilder.get(CaptureRequest.CONTROL_AWB_REGIONS)); + builder.set(CaptureRequest.CONTROL_AF_MODE, + oldBuilder.get(CaptureRequest.CONTROL_AF_MODE)); // Do NOT copy exposure or focus triggers! } } - private void applyDefaultFocus(@NonNull CaptureRequest.Builder builder) { - int[] modesArray = readCharacteristic(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES, new int[]{}); + @SuppressWarnings("WeakerAccess") + protected void applyDefaultFocus(@NonNull CaptureRequest.Builder builder) { + int[] modesArray = readCharacteristic(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES, + new int[]{}); List modes = new ArrayList<>(); for (int mode : modesArray) { modes.add(mode); } if (getMode() == Mode.VIDEO && modes.contains(CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO)) { - builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO); + builder.set(CaptureRequest.CONTROL_AF_MODE, + CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO); return; } if (modes.contains(CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)) { - builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); + builder.set(CaptureRequest.CONTROL_AF_MODE, + CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); return; } @@ -882,10 +931,18 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv } } - private void applyFocusForMetering(@NonNull CaptureRequest.Builder builder) { - // All focus modes support the AF trigger, except OFF and EDOF. - // However, unlike the preview, we'd prefer AUTO to any CONTINUOUS value. - int[] modesArray = readCharacteristic(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES, new int[]{}); + /** + * All focus modes support the AF trigger, except OFF and EDOF. + * However, unlike the preview, we'd prefer AUTO to any CONTINUOUS value. + * An AUTO value means that focus is locked unless we run the focus trigger, + * which is what metering does. + * + * @param builder builder + */ + @SuppressWarnings("WeakerAccess") + protected void applyFocusForMetering(@NonNull CaptureRequest.Builder builder) { + int[] modesArray = readCharacteristic(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES, + new int[]{}); List modes = new ArrayList<>(); for (int mode : modesArray) { modes.add(mode); } if (modes.contains(CaptureRequest.CONTROL_AF_MODE_AUTO)) { @@ -894,12 +951,14 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv } if (getMode() == Mode.VIDEO && modes.contains(CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO)) { - builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO); + builder.set(CaptureRequest.CONTROL_AF_MODE, + CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO); return; } if (modes.contains(CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)) { - builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); + builder.set(CaptureRequest.CONTROL_AF_MODE, + CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); //noinspection UnnecessaryReturnStatement return; } @@ -916,13 +975,14 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv boolean shouldApply = applyFlash(mRepeatingRequestBuilder, old); boolean needsWorkaround = getPreviewState() == STATE_STARTED; if (needsWorkaround) { - // Runtime changes to the flash value are not correctly handled by the driver. - // See https://stackoverflow.com/q/53003383/4288782 for example. + // Runtime changes to the flash value are not correctly handled by the + // driver. See https://stackoverflow.com/q/53003383/4288782 for example. // For this reason, we go back to OFF, capture once, then go to the new one. mFlash = Flash.OFF; applyFlash(mRepeatingRequestBuilder, old); try { - mSession.capture(mRepeatingRequestBuilder.build(), null, null); + mSession.capture(mRepeatingRequestBuilder.build(), null, + null); } catch (CameraAccessException e) { throw createCameraException(e); } @@ -946,18 +1006,19 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv * - {@link CaptureRequest#CONTROL_AE_MODE_ON_ALWAYS_FLASH} * * The API offers a high level control through {@link CaptureRequest#CONTROL_AE_MODE}, - * which is what the mapper looks at. It will trigger (if specified) flash only for still captures - * which is exactly what we want. + * which is what the mapper looks at. It will trigger (if specified) flash only for + * still captures which is exactly what we want. * * However, we set CONTROL_AE_MODE to ON/OFF (depending * on which is available) with both {@link Flash#OFF} and {@link Flash#TORCH}. * - * When CONTROL_AE_MODE is ON or OFF, the low level control, called {@link CaptureRequest#FLASH_MODE}, - * becomes effective, and that's where we can actually distinguish between a turned off flash - * and a torch flash. + * When CONTROL_AE_MODE is ON or OFF, the low level control, called + * {@link CaptureRequest#FLASH_MODE}, becomes effective, and that's where we can actually + * distinguish between a turned off flash and a torch flash. */ - private boolean applyFlash(@NonNull CaptureRequest.Builder builder, - @NonNull Flash oldFlash) { + @SuppressWarnings("WeakerAccess") + protected boolean applyFlash(@NonNull CaptureRequest.Builder builder, + @NonNull Flash oldFlash) { if (mCameraOptions.supports(mFlash)) { int[] availableAeModesArray = readCharacteristic( CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES, new int[]{}); @@ -996,8 +1057,9 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv }); } - private boolean applyLocation(@NonNull CaptureRequest.Builder builder, - @SuppressWarnings("unused") @Nullable Location oldLocation) { + @SuppressWarnings("WeakerAccess") + protected boolean applyLocation(@NonNull CaptureRequest.Builder builder, + @SuppressWarnings("unused") @Nullable Location oldLocation) { if (mLocation != null) { builder.set(CaptureRequest.JPEG_GPS_LOCATION, mLocation); } @@ -1021,8 +1083,9 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv }); } - private boolean applyWhiteBalance(@NonNull CaptureRequest.Builder builder, - @NonNull WhiteBalance oldWhiteBalance) { + @SuppressWarnings("WeakerAccess") + protected boolean applyWhiteBalance(@NonNull CaptureRequest.Builder builder, + @NonNull WhiteBalance oldWhiteBalance) { if (mCameraOptions.supports(mWhiteBalance)) { int whiteBalance = mMapper.mapWhiteBalance(mWhiteBalance); builder.set(CaptureRequest.CONTROL_AWB_MODE, whiteBalance); @@ -1049,8 +1112,8 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv }); } - - private boolean applyHdr(@NonNull CaptureRequest.Builder builder, @NonNull Hdr oldHdr) { + @SuppressWarnings("WeakerAccess") + protected boolean applyHdr(@NonNull CaptureRequest.Builder builder, @NonNull Hdr oldHdr) { if (mCameraOptions.supports(mHdr)) { int hdr = mMapper.mapHdr(mHdr); builder.set(CaptureRequest.CONTROL_SCENE_MODE, hdr); @@ -1080,9 +1143,11 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv }); } - private boolean applyZoom(@NonNull CaptureRequest.Builder builder, float oldZoom) { + @SuppressWarnings("WeakerAccess") + protected boolean applyZoom(@NonNull CaptureRequest.Builder builder, float oldZoom) { if (mCameraOptions.isZoomSupported()) { - float maxZoom = readCharacteristic(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM, 1F); + float maxZoom = readCharacteristic( + CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM, 1F); // converting 0.0f-1.0f zoom scale to the actual camera digital zoom scale // (which will be for example, 1.0-10.0) float calculatedZoom = (mZoomValue * (maxZoom - 1.0f)) + 1.0f; @@ -1096,8 +1161,8 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv @NonNull private Rect getZoomRect(float zoomLevel, float maxDigitalZoom) { - Rect activeRect = readCharacteristic(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE, new Rect()); - + Rect activeRect = readCharacteristic(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE, + new Rect()); int minW = (int) (activeRect.width() / maxDigitalZoom); int minH = (int) (activeRect.height() / maxDigitalZoom); int difW = activeRect.width() - minW; @@ -1107,11 +1172,15 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv // When zoom is maxZoom, we want to return a centered rect with minW and minH int cropW = (int) (difW * (zoomLevel - 1) / (maxDigitalZoom - 1) / 2F); int cropH = (int) (difH * (zoomLevel - 1) / (maxDigitalZoom - 1) / 2F); - return new Rect(cropW, cropH, activeRect.width() - cropW, activeRect.height() - cropH); + return new Rect(cropW, cropH, activeRect.width() - cropW, + activeRect.height() - cropH); } @Override - public void setExposureCorrection(final float EVvalue, @NonNull final float[] bounds, @Nullable final PointF[] points, final boolean notify) { + public void setExposureCorrection(final float EVvalue, + @NonNull final float[] bounds, + @Nullable final PointF[] points, + final boolean notify) { final float old = mExposureCorrectionValue; mExposureCorrectionValue = EVvalue; mHandler.run(new Runnable() { @@ -1130,11 +1199,15 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv }); } - private boolean applyExposureCorrection(@NonNull CaptureRequest.Builder builder, float oldEVvalue) { + @SuppressWarnings("WeakerAccess") + protected boolean applyExposureCorrection(@NonNull CaptureRequest.Builder builder, + float oldEVvalue) { if (mCameraOptions.isExposureCorrectionSupported()) { - Rational exposureCorrectionStep = readCharacteristic(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP, + Rational exposureCorrectionStep = readCharacteristic( + CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP, new Rational(1, 1)); - int exposureCorrectionSteps = Math.round(mExposureCorrectionValue * exposureCorrectionStep.floatValue()); + int exposureCorrectionSteps = Math.round(mExposureCorrectionValue + * exposureCorrectionStep.floatValue()); builder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, exposureCorrectionSteps); return true; } @@ -1165,6 +1238,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv LOG.w("onImageAvailable", "no byte buffer!"); return; } + LOG.v("onImageAvailable", "trying to acquire Image."); Image image = null; try { image = reader.acquireLatestImage(); @@ -1174,9 +1248,11 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv getFrameManager().onBufferUnused(data); return; } - LOG.i("onImageAvailable", "we have both a byte buffer and an Image."); + LOG.v("onImageAvailable", "we have both a byte buffer and an Image."); try { - ImageHelper.convertToNV21(image, data); + synchronized (mFrameProcessingImageLock) { + ImageHelper.convertToNV21(image, data); + } } catch (Exception e) { LOG.w("onImageAvailable", "error while converting."); getFrameManager().onBufferUnused(data); @@ -1188,7 +1264,8 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv // After preview, the frame manager is correctly set up Frame frame = getFrameManager().getFrame(data, System.currentTimeMillis(), - getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR)); + getAngles().offset(Reference.SENSOR, Reference.OUTPUT, + Axis.RELATIVE_TO_SENSOR)); mCallback.dispatchFrame(frame); } else { getFrameManager().onBufferUnused(data); @@ -1197,18 +1274,35 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv @Override public void setHasFrameProcessors(final boolean hasFrameProcessors) { - super.setHasFrameProcessors(hasFrameProcessors); - LOG.i("setHasFrameProcessors", "changed to", hasFrameProcessors, "posting."); + LOG.i("setHasFrameProcessors", "changing to", hasFrameProcessors, "posting."); + Camera2Engine.super.setHasFrameProcessors(hasFrameProcessors); mHandler.run(new Runnable() { @Override public void run() { - LOG.i("setHasFrameProcessors", "changed to", hasFrameProcessors, "executing. BindState:", getBindState()); - if (getBindState() == STATE_STARTED) { - LOG.i("setHasFrameProcessors", "triggering a restart."); - // TODO if taking video, this stops it. + LOG.i("setHasFrameProcessors", "changing to", hasFrameProcessors, + "executing. BindState:", getBindState(), + "PreviewState:", getPreviewState()); + + // Frame processing is set up partially when binding and partially when starting + // the preview. We don't want to only check bind state or startPreview can fail. + if (getBindState() == STATE_STOPPED) { + LOG.i("setHasFrameProcessors", "not bound so won't restart."); + } else if (getPreviewState() == STATE_STARTED) { + // This needs a restartBind(). NOTE: if taking video, this stops it. + LOG.i("setHasFrameProcessors", "bound with preview.", + "Calling restartBind()."); restartBind(); } else { - LOG.i("setHasFrameProcessors", "not bound so won't restart."); + // Bind+Preview is not completely started yet not completely stopped. + // This can happen if the user adds a frame processor in onCameraOpened(). + // Supporting this would add lot of complexity to this class, and + // this should be discouraged anyway since changing the frame processor number + // at this time requires restarting the camera when it was just opened. + // For these reasons, let's throw. + throw new IllegalStateException("Added/removed a FrameProcessor at illegal " + + "time. These operations should be done before opening the camera, or " + + "before closing it - NOT when it just opened, for example during the " + + "onCameraOpened() callback."); } } }); @@ -1225,8 +1319,9 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv @Override public void run() { LOG.i("startAutoFocus", "executing. Preview state:", getPreviewState()); - // This will only work when we have a preview, since it launches the preview in the end. - // Even without this it would need the bind state at least, since we need the preview size. + // This will only work when we have a preview, since it launches the preview + // in the end. Even without this it would need the bind state at least, + // since we need the preview size. if (getPreviewState() < STATE_STARTED) return; // The camera options API still has the auto focus API but it really @@ -1244,7 +1339,8 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv mCallback.dispatchOnFocusEnd(gesture, action.isSuccessful(), point); mHandler.remove(mUnlockAndResetMeteringRunnable); if (shouldResetAutoFocus()) { - mHandler.post(getAutoFocusResetDelay(), mUnlockAndResetMeteringRunnable); + mHandler.post(getAutoFocusResetDelay(), + mUnlockAndResetMeteringRunnable); } } }); @@ -1263,7 +1359,8 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv // The last one is under our control because the library has no focus API. // So let's set a good af mode here. This operation is reverted during onMeteringReset(). applyFocusForMetering(mRepeatingRequestBuilder); - mMeterAction = new MeterAction(Camera2Engine.this, point, point == null); + mMeterAction = new MeterAction(Camera2Engine.this, point, + point == null); return mMeterAction; } @@ -1282,8 +1379,10 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv protected void onStart(@NonNull ActionHolder holder) { super.onStart(holder); applyDefaultFocus(holder.getBuilder(this)); - holder.getBuilder(this).set(CaptureRequest.CONTROL_AE_LOCK, false); - holder.getBuilder(this).set(CaptureRequest.CONTROL_AWB_LOCK, false); + holder.getBuilder(this) + .set(CaptureRequest.CONTROL_AE_LOCK, false); + holder.getBuilder(this) + .set(CaptureRequest.CONTROL_AWB_LOCK, false); holder.applyBuilder(this); setState(STATE_COMPLETED); // TODO should wait results? @@ -1348,7 +1447,8 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv } @Override - public void applyBuilder(@NonNull Action source, @NonNull CaptureRequest.Builder builder) throws CameraAccessException { + public void applyBuilder(@NonNull Action source, @NonNull CaptureRequest.Builder builder) + throws CameraAccessException { mSession.capture(builder.build(), mRepeatingRequestCallback, null); } 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 f27f4731..6616b0e0 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java @@ -62,10 +62,12 @@ import java.util.concurrent.TimeUnit; /** * PROCESS * Setting up the Camera is usually a 4 steps process: - * 1. Setting up the Surface. Done by {@link CameraPreview}. - * 2. Starting the camera. Done by us. See {@link #startEngine()}, {@link #onStartEngine()}. - * 3. Binding the camera to the surface. Done by us. See {@link #startBind()}, {@link #onStartBind()} - * 4. Streaming the camera preview. Done by us. See {@link #startPreview()}, {@link #onStartPreview()} + * 1. Setting up the Surface. Done by {@link CameraPreview}. + * 2. Starting the camera. Done by us. See {@link #startEngine()}, {@link #onStartEngine()}. + * 3. Binding the camera to the surface. Done by us. See {@link #startBind()}, + * {@link #onStartBind()} + * 4. Streaming the camera preview. Done by us. See {@link #startPreview()}, + * {@link #onStartPreview()} * * The first two steps can actually happen at the same time, anyway * the order is not guaranteed, we just get a callback from the Preview when 1 happens. @@ -80,14 +82,16 @@ import java.util.concurrent.TimeUnit; * STATE * We only expose generic {@link #start()} and {@link #stop()} calls to the outside. * The external users of this class are most likely interested in whether we have completed step 2 - * or not, since that tells us if we can act on the camera or not, rather than knowing about steps 3 and 4. + * or not, since that tells us if we can act on the camera or not, rather than knowing about + * steps 3 and 4. * * So in the {@link CameraEngine} notation, - * - {@link #start()}: ASYNC - starts the engine (S2). When possible, at a later time, S3 and S4 are also performed. + * - {@link #start()}: ASYNC - starts the engine (S2). When possible, at a later time, + * S3 and S4 are also performed. * - {@link #stop()}: ASYNC - stops everything: undoes S4, then S3, then S2. * - {@link #restart()}: ASYNC - completes a stop then a start. - * - {@link #destroy()}: SYNC - performs a {@link #stop()} that will go on no matter the exceptions, without throwing. - * Makes the engine unusable and clears resources. + * - {@link #destroy()}: SYNC - performs a {@link #stop()} that will go on no matter the exceptions, + * without throwing. Makes the engine unusable and clears resources. * * For example, we expose the engine (S2) state through {@link #getEngineState()}. It will be: * - {@link #STATE_STARTING} if we're into step 2 @@ -109,13 +113,13 @@ import java.util.concurrent.TimeUnit; * * * ERROR HANDLING - * THe {@link #mHandler} thread has a special {@link Thread.UncaughtExceptionHandler} that handles exceptions - * and dispatches error to the callback (instead of crashing the app). This lets subclasses run code - * safely and directly throw {@link CameraException}s when needed. + * THe {@link #mHandler} thread has a special {@link Thread.UncaughtExceptionHandler} that handles + * exceptions and dispatches error to the callback (instead of crashing the app). + * This lets subclasses run code safely and directly throw {@link CameraException}s when needed. * - * For convenience, the two main method {@link #onStartEngine()} and {@link #onStopEngine()} are already - * called on the engine thread, but they can still be asynchronous by returning a Google's - * {@link com.google.android.gms.tasks.Task}. + * For convenience, the two main method {@link #onStartEngine()} and {@link #onStopEngine()} + * are already called on the engine thread, but they can still be asynchronous by returning a + * Google's {@link com.google.android.gms.tasks.Task}. */ public abstract class CameraEngine implements CameraPreview.SurfaceCallback, @@ -133,7 +137,9 @@ public abstract class CameraEngine implements void dispatchOnFocusStart(@Nullable Gesture trigger, @NonNull PointF where); void dispatchOnFocusEnd(@Nullable Gesture trigger, boolean success, @NonNull PointF where); void dispatchOnZoomChanged(final float newValue, @Nullable final PointF[] fingers); - void dispatchOnExposureCorrectionChanged(float newValue, @NonNull float[] bounds, @Nullable PointF[] fingers); + void dispatchOnExposureCorrectionChanged(float newValue, + @NonNull float[] bounds, + @Nullable PointF[] fingers); void dispatchFrame(@NonNull Frame frame); void dispatchError(CameraException exception); void dispatchOnVideoRecordingStart(); @@ -186,8 +192,10 @@ public abstract class CameraEngine implements private int mAudioBitRate; private boolean mHasFrameProcessors; private long mAutoFocusResetDelayMillis; - private int mSnapshotMaxWidth = Integer.MAX_VALUE; // in REF_VIEW for consistency with SizeSelectors - private int mSnapshotMaxHeight = Integer.MAX_VALUE; // in REF_VIEW for consistency with SizeSelectors + // in REF_VIEW, for consistency with SizeSelectors + private int mSnapshotMaxWidth = Integer.MAX_VALUE; + // in REF_VIEW, for consistency with SizeSelectors + private int mSnapshotMaxHeight = Integer.MAX_VALUE; private Overlay overlay; // Steps @@ -205,9 +213,11 @@ public abstract class CameraEngine implements // Ops used for testing. @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op mZoomOp = new Op<>(); - @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op mExposureCorrectionOp = new Op<>(); + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op mExposureCorrectionOp + = new Op<>(); @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op mFlashOp = new Op<>(); - @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op mWhiteBalanceOp = new Op<>(); + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op mWhiteBalanceOp + = new Op<>(); @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op mHdrOp = new Op<>(); @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op mLocationOp = new Op<>(); @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op mPlaySoundsOp = new Op<>(); @@ -258,9 +268,9 @@ public abstract class CameraEngine implements /** * Handles exceptions coming from either runtime errors on the {@link #mHandler} code that is - * not caught (using the {@link CrashExceptionHandler}), as might happen during standard mHandler.post() - * operations that subclasses might do, OR for errors caught by tasks and continuations that - * we launch here. + * not caught (using the {@link CrashExceptionHandler}), as might happen during standard + * mHandler.post() operations that subclasses might do, OR for errors caught by tasks and + * continuations that we launch here. * * In the first case, the thread is about to be terminated. In the second case, * we can actually keep using it. @@ -269,7 +279,9 @@ public abstract class CameraEngine implements * @param throwable the throwable * @param fromExceptionHandler true if coming from exception handler */ - private void handleException(@NonNull Thread thread, final @NonNull Throwable throwable, final boolean fromExceptionHandler) { + private void handleException(@NonNull Thread thread, + final @NonNull Throwable throwable, + final boolean fromExceptionHandler) { if (!(throwable instanceof CameraException)) { // This is unexpected, either a bug or something the developer should know. // Release and crash the UI thread so we get bug reports. @@ -290,7 +302,8 @@ public abstract class CameraEngine implements } final CameraException cameraException = (CameraException) throwable; - LOG.e("uncaughtException:", "Got CameraException:", cameraException, "on engine state:", getEngineStateName()); + LOG.e("uncaughtException:", "Got CameraException:", cameraException, + "on engine state:", getEngineStateName()); if (fromExceptionHandler) { // Got to restart the handler. thread.interrupt(); @@ -478,7 +491,8 @@ public abstract class CameraEngine implements @Override public void run() { LOG.w("restartBind", "executing stopPreview."); - stopPreview(false).continueWithTask(mHandler.getExecutor(), new Continuation>() { + stopPreview(false).continueWithTask(mHandler.getExecutor(), + new Continuation>() { @Override public Task then(@NonNull Task task) { LOG.w("restartBind", "executing stopBind."); @@ -525,7 +539,9 @@ public abstract class CameraEngine implements @NonNull @WorkerThread private Task stopPreview(boolean swallowExceptions) { - LOG.i("stopPreview", "needsStopPreview:", needsStopPreview(), "swallowExceptions:", swallowExceptions); + LOG.i("stopPreview", + "needsStopPreview:", needsStopPreview(), + "swallowExceptions:", swallowExceptions); if (needsStopPreview()) { mPreviewStep.doStop(swallowExceptions, new Callable>() { @Override @@ -595,7 +611,8 @@ public abstract class CameraEngine implements @Override public final void onSurfaceChanged() { - LOG.i("onSurfaceChanged:", "Size is", getPreviewSurfaceSize(Reference.VIEW), "Posting."); + LOG.i("onSurfaceChanged:", "Size is", getPreviewSurfaceSize(Reference.VIEW), + "Posting."); mHandler.run(new Runnable() { @Override public void run() { @@ -608,9 +625,11 @@ public abstract class CameraEngine implements // Compute a new camera preview size and apply. Size newSize = computePreviewStreamSize(); if (newSize.equals(mPreviewStreamSize)) { - LOG.i("onSurfaceChanged:", "The computed preview size is identical. No op."); + LOG.i("onSurfaceChanged:", + "The computed preview size is identical. No op."); } else { - LOG.i("onSurfaceChanged:", "Computed a new preview size. Calling onPreviewStreamSizeChanged()."); + LOG.i("onSurfaceChanged:", + "Computed a new preview size. Calling onPreviewStreamSizeChanged()."); mPreviewStreamSize = newSize; onPreviewStreamSizeChanged(); } @@ -633,7 +652,8 @@ public abstract class CameraEngine implements mHandler.run(new Runnable() { @Override public void run() { - stopPreview(false).onSuccessTask(mHandler.getExecutor(), new SuccessContinuation() { + stopPreview(false).onSuccessTask(mHandler.getExecutor(), + new SuccessContinuation() { @NonNull @Override public Task then(@Nullable Void aVoid) { @@ -663,7 +683,8 @@ public abstract class CameraEngine implements // Stop if needed, synchronously and silently. // Cannot use Tasks.await() because we might be on the UI thread. final CountDownLatch latch = new CountDownLatch(1); - stop(true).addOnCompleteListener(mHandler.getExecutor(), new OnCompleteListener() { + stop(true).addOnCompleteListener(mHandler.getExecutor(), + new OnCompleteListener() { @Override public void onComplete(@NonNull Task task) { latch.countDown(); @@ -695,14 +716,17 @@ public abstract class CameraEngine implements @Override public void run() { LOG.w("Start:", "executing runnable. AllState is", mAllStep.getState()); - // It's better to schedule anyway. allStep might be STARTING and we might be tempted to early return here, - // But the truth is that there might be a stop already scheduled when the STARTING op ends. + // It's better to schedule anyway. allStep might be STARTING and we might be + // tempted to early return here, but the truth is that there might be a stop + // already scheduled when the STARTING op ends. // if (mAllStep.isStoppingOrStopped()) { - // LOG.i("Start:", "executing runnable. AllState is STOPPING or STOPPED, so we schedule a start."); + // LOG.i("Start:", "executing runnable. AllState is STOPPING or STOPPED, + // so we schedule a start."); mAllStep.doStart(false, new Callable>() { @Override public Task call() { - return startEngine().addOnFailureListener(mHandler.getExecutor(), new OnFailureListener() { + return startEngine().addOnFailureListener(mHandler.getExecutor(), + new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { outTask.trySetException(e); @@ -725,7 +749,8 @@ public abstract class CameraEngine implements }); // } else { // // NOTE: this returns early if we were STARTING. - // LOG.i("Start:", "executing runnable. AllState is STARTING or STARTED, so we return early."); + // LOG.i("Start:", + // "executing runnable. AllState is STARTING or STARTED, so we return early."); // outTask.trySetResult(null); // } } @@ -746,14 +771,17 @@ public abstract class CameraEngine implements @Override public void run() { LOG.w("Stop:", "executing runnable. AllState is", mAllStep.getState()); - // It's better to schedule anyway. allStep might be STOPPING and we might be tempted to early return here, - // But the truth is that there might be a start already scheduled when the STOPPING op ends. + // It's better to schedule anyway. allStep might be STOPPING and we might be + // tempted to early return here, but the truth is that there might be a start + // already scheduled when the STOPPING op ends. // if (mAllStep.isStartedOrStarting()) { - // LOG.i("Stop:", "executing runnable. AllState is STARTING or STARTED, so we schedule a stop."); + // LOG.i("Stop:", "executing runnable. AllState is STARTING or STARTED, + // so we schedule a stop."); mAllStep.doStop(swallowExceptions, new Callable>() { @Override public Task call() { - return stopPreview(swallowExceptions).continueWithTask(mHandler.getExecutor(), new Continuation>() { + return stopPreview(swallowExceptions).continueWithTask( + mHandler.getExecutor(), new Continuation>() { @Override public Task then(@NonNull Task task) { return stopBind(swallowExceptions); @@ -779,7 +807,8 @@ public abstract class CameraEngine implements }); // } else { // // NOTE: this returns early if we were STOPPING. - // LOG.i("Stop:", "executing runnable. AllState is STOPPING or STOPPED, so we return early."); + // LOG.i("Stop:", "executing runnable. + // AllState is STOPPING or STOPPED, so we return early."); // outTask.trySetResult(null); // } } @@ -800,7 +829,6 @@ public abstract class CameraEngine implements return overlay; } - @SuppressWarnings("WeakerAccess") public final Angles getAngles() { return mAngles; } @@ -888,9 +916,13 @@ public abstract class CameraEngine implements return mSnapshotMaxHeight; } - public final void setAutoFocusResetDelay(long delayMillis) { mAutoFocusResetDelayMillis = delayMillis; } + public final void setAutoFocusResetDelay(long delayMillis) { + mAutoFocusResetDelayMillis = delayMillis; + } - public final long getAutoFocusResetDelay() { return mAutoFocusResetDelayMillis; } + public final long getAutoFocusResetDelay() { + return mAutoFocusResetDelayMillis; + } /** * Sets a new facing value. This will restart the session (if there's any) @@ -1039,8 +1071,8 @@ public abstract class CameraEngine implements * Camera is about to be opened. Implementors should look into available cameras * and see if anyone matches the given {@link Facing value}. * - * If so, implementors should set {@link Angles#setSensorOffset(Facing, int)} and any other information - * (like camera ID) needed to start the engine. + * If so, implementors should set {@link Angles#setSensorOffset(Facing, int)} + * and any other information (like camera ID) needed to start the engine. * * @param facing the facing value * @return true if we have one @@ -1059,7 +1091,10 @@ public abstract class CameraEngine implements public abstract void setZoom(float zoom, @Nullable PointF[] points, boolean notify); // If closed, no-op. If opened, check supported and apply. - public abstract void setExposureCorrection(float EVvalue, @NonNull float[] bounds, @Nullable PointF[] points, boolean notify); + public abstract void setExposureCorrection(float EVvalue, + @NonNull float[] bounds, + @Nullable PointF[] points, + boolean notify); // If closed, keep. If opened, check supported and apply. public abstract void setFlash(@NonNull Flash flash); @@ -1091,7 +1126,8 @@ public abstract class CameraEngine implements mHandler.run(new Runnable() { @Override public void run() { - LOG.v("takePicture", "performing. BindState:", getBindState(), "isTakingPicture:", isTakingPicture()); + LOG.v("takePicture", "performing. BindState:", getBindState(), + "isTakingPicture:", isTakingPicture()); if (mMode == Mode.VIDEO) { throw new IllegalStateException("Can't take hq pictures while in VIDEO mode"); } @@ -1115,7 +1151,8 @@ public abstract class CameraEngine implements mHandler.run(new Runnable() { @Override public void run() { - LOG.v("takePictureSnapshot", "performing. BindState:", getBindState(), "isTakingPicture:", isTakingPicture()); + LOG.v("takePictureSnapshot", "performing. BindState:", + getBindState(), "isTakingPicture:", isTakingPicture()); if (getBindState() < STATE_STARTED) return; if (isTakingPicture()) return; stub.location = mLocation; @@ -1141,7 +1178,8 @@ public abstract class CameraEngine implements mCallback.dispatchOnPictureTaken(result); } else { LOG.e("onPictureResult", "result is null: something went wrong.", error); - mCallback.dispatchError(new CameraException(error, CameraException.REASON_PICTURE_FAILED)); + mCallback.dispatchError(new CameraException(error, + CameraException.REASON_PICTURE_FAILED)); } } @@ -1154,7 +1192,8 @@ public abstract class CameraEngine implements mHandler.run(new Runnable() { @Override public void run() { - LOG.v("takeVideo", "performing. BindState:", getBindState(), "isTakingVideo:", isTakingVideo()); + LOG.v("takeVideo", "performing. BindState:", getBindState(), + "isTakingVideo:", isTakingVideo()); if (getBindState() < STATE_STARTED) return; if (isTakingVideo()) return; if (mMode == Mode.PICTURE) { @@ -1179,12 +1218,14 @@ public abstract class CameraEngine implements * @param stub a video stub * @param file the output file */ - public final void takeVideoSnapshot(final @NonNull VideoResult.Stub stub, @NonNull final File file) { + public final void takeVideoSnapshot(@NonNull final VideoResult.Stub stub, + @NonNull final File file) { LOG.v("takeVideoSnapshot", "scheduling"); mHandler.run(new Runnable() { @Override public void run() { - LOG.v("takeVideoSnapshot", "performing. BindState:", getBindState(), "isTakingVideo:", isTakingVideo()); + LOG.v("takeVideoSnapshot", "performing. BindState:", getBindState(), + "isTakingVideo:", isTakingVideo()); if (getBindState() < STATE_STARTED) return; if (isTakingVideo()) return; stub.file = file; @@ -1215,10 +1256,13 @@ public abstract class CameraEngine implements }); } + @SuppressWarnings("WeakerAccess") protected void onStopVideo() { if (mVideoRecorder != null) { mVideoRecorder.stop(false); - mVideoRecorder = null; + // Do not null this, so we respond correctly to isTakingVideo(), + // which checks for recorder presence and recorder.isRecording(). + // It will be nulled in onVideoResult. } } @@ -1230,7 +1274,8 @@ public abstract class CameraEngine implements mCallback.dispatchOnVideoTaken(result); } else { LOG.e("onVideoResult", "result is null: something went wrong.", exception); - mCallback.dispatchError(new CameraException(exception, CameraException.REASON_VIDEO_FAILED)); + mCallback.dispatchError(new CameraException(exception, + CameraException.REASON_VIDEO_FAILED)); } } @@ -1248,10 +1293,13 @@ public abstract class CameraEngine implements protected abstract void onTakePicture(@NonNull PictureResult.Stub stub, boolean doMetering); @WorkerThread - protected abstract void onTakePictureSnapshot(@NonNull PictureResult.Stub stub, @NonNull AspectRatio outputRatio, boolean doMetering); + protected abstract void onTakePictureSnapshot(@NonNull PictureResult.Stub stub, + @NonNull AspectRatio outputRatio, + boolean doMetering); @WorkerThread - protected abstract void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub, @NonNull AspectRatio outputRatio); + protected abstract void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub, + @NonNull AspectRatio outputRatio); @WorkerThread protected abstract void onTakeVideo(@NonNull VideoResult.Stub stub); @@ -1286,7 +1334,8 @@ public abstract class CameraEngine implements private Size getPreviewSurfaceSize(@NonNull Reference reference) { CameraPreview preview = mPreview; if (preview == null) return null; - return getAngles().flip(Reference.VIEW, reference) ? preview.getSurfaceSize().flip() : preview.getSurfaceSize(); + return getAngles().flip(Reference.VIEW, reference) ? preview.getSurfaceSize().flip() + : preview.getSurfaceSize(); } /** @@ -1295,7 +1344,7 @@ public abstract class CameraEngine implements * levels so we don't want to perform the op here. * * The base snapshot size is based on PreviewStreamSize (later cropped with view ratio). Why? - * One might be tempted to say that it is the SurfaceSize (which already matches the view ratio). + * One might be tempted to say that it's the SurfaceSize (which already matches the view ratio). * * The camera sensor will capture preview frames with PreviewStreamSize and that's it. Then they * are hardware-scaled by the preview surface, but this does not affect the snapshot, as the @@ -1367,7 +1416,8 @@ public abstract class CameraEngine implements List list = new ArrayList<>(sizes); Size result = selector.select(list).get(0); if (!list.contains(result)) { - throw new RuntimeException("SizeSelectors must not return Sizes other than those in the input list."); + throw new RuntimeException("SizeSelectors must not return Sizes other than " + + "those in the input list."); } LOG.i("computeCaptureSize:", "result:", result, "flip:", flip, "mode:", mode); if (flip) result = result.flip(); // Go back to REF_SENSOR @@ -1395,13 +1445,17 @@ public abstract class CameraEngine implements sizes.add(flip ? size.flip() : size); } - // Create our own default selector, which will be used if the external mPreviewStreamSizeSelector - // is null, or if it fails in finding a size. + // Create our own default selector, which will be used if the external + // mPreviewStreamSizeSelector is null, or if it fails in finding a size. Size targetMinSize = getPreviewSurfaceSize(Reference.VIEW); - if (targetMinSize == null) throw new IllegalStateException("targetMinSize should not be null here."); + if (targetMinSize == null) { + throw new IllegalStateException("targetMinSize should not be null here."); + } AspectRatio targetRatio = AspectRatio.of(mCaptureSize.getWidth(), mCaptureSize.getHeight()); if (flip) targetRatio = targetRatio.flip(); - LOG.i("computePreviewStreamSize:", "targetRatio:", targetRatio, "targetMinSize:", targetMinSize); + LOG.i("computePreviewStreamSize:", + "targetRatio:", targetRatio, + "targetMinSize:", targetMinSize); SizeSelector matchRatio = SizeSelectors.and( // Match this aspect ratio and sort by biggest SizeSelectors.aspectRatio(targetRatio, 0), SizeSelectors.biggest()); @@ -1426,7 +1480,8 @@ public abstract class CameraEngine implements } Size result = selector.select(sizes).get(0); if (!sizes.contains(result)) { - throw new RuntimeException("SizeSelectors must not return Sizes other than those in the input list."); + throw new RuntimeException("SizeSelectors must not return Sizes other than " + + "those in the input list."); } if (flip) result = result.flip(); LOG.i("computePreviewStreamSize:", "result:", result, "flip:", flip); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/LogAction.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/LogAction.java index 72de4d6e..d51567a2 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/LogAction.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/LogAction.java @@ -15,12 +15,14 @@ import com.otaliastudios.cameraview.engine.action.BaseAction; @RequiresApi(Build.VERSION_CODES.LOLLIPOP) class LogAction extends BaseAction { - private final static CameraLogger LOG = CameraLogger.create(Camera2Engine.class.getSimpleName()); + private final static CameraLogger LOG + = CameraLogger.create(Camera2Engine.class.getSimpleName()); private String lastLog; @Override - public void onCaptureCompleted(@NonNull ActionHolder holder, @NonNull CaptureRequest request, + public void onCaptureCompleted(@NonNull ActionHolder holder, + @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { super.onCaptureCompleted(holder, request, result); Integer aeMode = result.get(CaptureResult.CONTROL_AE_MODE); @@ -34,29 +36,14 @@ class LogAction extends BaseAction { " afState: " + afState + " afTriggerState: " + afTriggerState; if (!log.equals(lastLog)) { lastLog = log; - LOG.w(log); + LOG.i(log); } + } - // START - // aeMode: 3 aeLock: false aeState: 4 aeTriggerState: 0 afState: 2 afTriggerState: 0 - // - // DURING metering (focus skips) - // aeMode: 3 aeLock: false aeState: 4 aeTriggerState: 0 afState: 0 afTriggerState: 0 - // aeMode: 3 aeLock: false aeState: 5 aeTriggerState: 1 afState: 0 afTriggerState: 0 - // - // DURING locking (focus skips) - // aeMode: 3 aeLock: false aeState: 4 aeTriggerState: 1 afState: 0 afTriggerState: 0 - // aeMode: 3 aeLock: true aeState: 5 aeTriggerState: 1 afState: 0 afTriggerState: 0 - // - // AFTER locked - // aeMode: 3 aeLock: true aeState: 3 aeTriggerState: 1 afState: 0 afTriggerState: 0 - // - // AFTER super.take() called - // aeMode: 1 aeLock: true aeState: 5 aeTriggerState: 1 afState: 0 afTriggerState: 0 - // aeMode: 1 aeLock: true aeState: 3 aeTriggerState: 1 afState: 0 afTriggerState: 0 - // - // Reverting flash changes + reset lock + reset metering - // aeMode: 3 aeLock: false aeState: 4 aeTriggerState: 2(1 now) afState: 2 afTriggerState: 0 - // aeMode: 3 aeLock: false aeState: 1 aeTriggerState: 2(1 now) afState: 2 afTriggerState: 0 + @Override + protected void onCompleted(@NonNull ActionHolder holder) { + super.onCompleted(holder); + setState(0); // set another state. + start(holder); // restart. } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Step.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Step.java index 349c17f5..5769e7c4 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Step.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Step.java @@ -105,17 +105,21 @@ class Step { return doStart(swallowExceptions, op, null); } - Task doStart(final boolean swallowExceptions, final @NonNull Callable> op, final @Nullable Runnable onStarted) { + Task doStart(final boolean swallowExceptions, + final @NonNull Callable> op, + final @Nullable Runnable onStarted) { LOG.i(name, "doStart", "Called. Enqueuing."); task = task.continueWithTask(callback.getExecutor(), new Continuation>() { @Override public Task then(@NonNull Task task) throws Exception { LOG.i(name, "doStart", "About to start. Setting state to STARTING"); setState(STATE_STARTING); - return op.call().addOnFailureListener(callback.getExecutor(), new OnFailureListener() { + return op.call().addOnFailureListener(callback.getExecutor(), + new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { - LOG.w(name, "doStart", "Failed with error", e, "Setting state to STOPPED"); + LOG.w(name, "doStart", "Failed with error", e, + "Setting state to STOPPED"); setState(STATE_STOPPED); if (!swallowExceptions) callback.handleException(e); } @@ -139,17 +143,21 @@ class Step { return doStop(swallowExceptions, op, null); } - Task doStop(final boolean swallowExceptions, final @NonNull Callable> op, final @Nullable Runnable onStopped) { + Task doStop(final boolean swallowExceptions, + final @NonNull Callable> op, + final @Nullable Runnable onStopped) { LOG.i(name, "doStop", "Called. Enqueuing."); task = task.continueWithTask(callback.getExecutor(), new Continuation>() { @Override public Task then(@NonNull Task task) throws Exception { LOG.i(name, "doStop", "About to stop. Setting state to STOPPING"); state = STATE_STOPPING; - return op.call().addOnFailureListener(callback.getExecutor(), new OnFailureListener() { + return op.call().addOnFailureListener(callback.getExecutor(), + new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { - LOG.w(name, "doStop", "Failed with error", e, "Setting state to STOPPED"); + LOG.w(name, "doStop", "Failed with error", e, + "Setting state to STOPPED"); state = STATE_STOPPED; if (!swallowExceptions) callback.handleException(e); } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/Action.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/Action.java index ba4a00de..d04bd257 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/Action.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/Action.java @@ -13,8 +13,8 @@ import androidx.annotation.RequiresApi; /** * The Action class encapsulates logic for completing an action in a Camera2 environment. * In this case, we are often interested in constantly receiving the {@link CaptureResult} - * and {@link CaptureRequest} callbacks, as well as applying changes to a {@link CaptureRequest.Builder} - * and having them applied to the sensor. + * and {@link CaptureRequest} callbacks, as well as applying changes to a + * {@link CaptureRequest.Builder} and having them applied to the sensor. * * The Action class receives the given callbacks and can operate over the engine * through the {@link ActionHolder} object. @@ -61,25 +61,32 @@ public interface Action { void removeCallback(@NonNull ActionCallback callback); /** - * Called from {@link CaptureCallback#onCaptureStarted(CameraCaptureSession, CaptureRequest, long, long)}. + * Called from {@link CaptureCallback#onCaptureStarted(CameraCaptureSession, CaptureRequest, + * long, long)}. * @param holder the holder * @param request the request */ void onCaptureStarted(@NonNull ActionHolder holder, @NonNull CaptureRequest request); /** - * Called from {@link CaptureCallback#onCaptureProgressed(CameraCaptureSession, CaptureRequest, CaptureResult)}. + * Called from {@link CaptureCallback#onCaptureProgressed(CameraCaptureSession, CaptureRequest, + * CaptureResult)}. * @param holder the holder * @param request the request * @param result the result */ - void onCaptureProgressed(@NonNull ActionHolder holder, @NonNull CaptureRequest request, @NonNull CaptureResult result); + void onCaptureProgressed(@NonNull ActionHolder holder, + @NonNull CaptureRequest request, + @NonNull CaptureResult result); /** - * Called from {@link CaptureCallback#onCaptureCompleted(CameraCaptureSession, CaptureRequest, TotalCaptureResult)}. + * Called from {@link CaptureCallback#onCaptureCompleted(CameraCaptureSession, CaptureRequest, + * TotalCaptureResult)}. * @param holder the holder * @param request the request * @param result the result */ - void onCaptureCompleted(@NonNull ActionHolder holder, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result); + void onCaptureCompleted(@NonNull ActionHolder holder, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result); } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/ActionHolder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/ActionHolder.java index 1673d10a..792aa590 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/ActionHolder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/ActionHolder.java @@ -77,5 +77,6 @@ public interface ActionHolder { * @param builder builder * @throws CameraAccessException camera exception */ - void applyBuilder(@NonNull Action source, @NonNull CaptureRequest.Builder builder) throws CameraAccessException; + void applyBuilder(@NonNull Action source, @NonNull CaptureRequest.Builder builder) + throws CameraAccessException; } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/ActionWrapper.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/ActionWrapper.java index 799cf31e..17c32506 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/ActionWrapper.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/ActionWrapper.java @@ -50,13 +50,17 @@ public abstract class ActionWrapper extends BaseAction { } @Override - public void onCaptureProgressed(@NonNull ActionHolder holder, @NonNull CaptureRequest request, @NonNull CaptureResult result) { + public void onCaptureProgressed(@NonNull ActionHolder holder, + @NonNull CaptureRequest request, + @NonNull CaptureResult result) { super.onCaptureProgressed(holder, request, result); getAction().onCaptureProgressed(holder, request, result); } @Override - public void onCaptureCompleted(@NonNull ActionHolder holder, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { + public void onCaptureCompleted(@NonNull ActionHolder holder, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { super.onCaptureCompleted(holder, request, result); getAction().onCaptureCompleted(holder, request, result); } 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 0c3d29e2..ceff4207 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 @@ -78,12 +78,16 @@ public abstract class BaseAction implements Action { } @Override - public void onCaptureProgressed(@NonNull ActionHolder holder, @NonNull CaptureRequest request, @NonNull CaptureResult result) { + public void onCaptureProgressed(@NonNull ActionHolder holder, + @NonNull CaptureRequest request, + @NonNull CaptureResult result) { // Overrideable } @Override - public void onCaptureCompleted(@NonNull ActionHolder holder, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { + public void onCaptureCompleted(@NonNull ActionHolder holder, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { // Overrideable } @@ -92,7 +96,7 @@ public abstract class BaseAction implements Action { * this removes this action from the holder. * @param newState new state */ - protected void setState(int newState) { + protected final void setState(int newState) { if (newState != state) { state = newState; for (ActionCallback callback : callbacks) { diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/SequenceAction.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/SequenceAction.java index 4453877a..0f3b4668 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/SequenceAction.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/SequenceAction.java @@ -75,7 +75,8 @@ class SequenceAction extends BaseAction { } @Override - public void onCaptureProgressed(@NonNull ActionHolder holder, @NonNull CaptureRequest request, + public void onCaptureProgressed(@NonNull ActionHolder holder, + @NonNull CaptureRequest request, @NonNull CaptureResult result) { super.onCaptureProgressed(holder, request, result); if (runningAction >= 0) { @@ -84,7 +85,8 @@ class SequenceAction extends BaseAction { } @Override - public void onCaptureCompleted(@NonNull ActionHolder holder, @NonNull CaptureRequest request, + public void onCaptureCompleted(@NonNull ActionHolder holder, + @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { super.onCaptureCompleted(holder, request, result); if (runningAction >= 0) { diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/TogetherAction.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/TogetherAction.java index a21da923..ef72f430 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/TogetherAction.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/TogetherAction.java @@ -65,7 +65,8 @@ class TogetherAction extends BaseAction { } @Override - public void onCaptureProgressed(@NonNull ActionHolder holder, @NonNull CaptureRequest request, + public void onCaptureProgressed(@NonNull ActionHolder holder, + @NonNull CaptureRequest request, @NonNull CaptureResult result) { super.onCaptureProgressed(holder, request, result); for (BaseAction action : actions) { @@ -74,7 +75,8 @@ class TogetherAction extends BaseAction { } @Override - public void onCaptureCompleted(@NonNull ActionHolder holder, @NonNull CaptureRequest request, + public void onCaptureCompleted(@NonNull ActionHolder holder, + @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { super.onCaptureCompleted(holder, request, result); for (BaseAction action : actions) { diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/lock/ExposureLock.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/lock/ExposureLock.java index 4e221280..dd3f91ab 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/lock/ExposureLock.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/lock/ExposureLock.java @@ -20,7 +20,8 @@ public class ExposureLock extends BaseLock { @Override protected boolean checkIsSupported(@NonNull ActionHolder holder) { - boolean isNotLegacy = readCharacteristic(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL, -1) + boolean isNotLegacy = readCharacteristic( + CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL, -1) != CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY; // Not sure we should check aeMode as well, probably all aeModes support locking, // but this should not be a big issue since we're not even using different AE modes. @@ -30,7 +31,8 @@ public class ExposureLock extends BaseLock { || aeMode == CameraCharacteristics.CONTROL_AE_MODE_ON_ALWAYS_FLASH || aeMode == CameraCharacteristics.CONTROL_AE_MODE_ON_AUTO_FLASH || aeMode == CameraCharacteristics.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE - || aeMode == 5 /* CameraCharacteristics.CONTROL_AE_MODE_ON_EXTERNAL_FLASH, API 28 */); + || aeMode == 5 + /* CameraCharacteristics.CONTROL_AE_MODE_ON_EXTERNAL_FLASH, API 28 */); boolean result = isNotLegacy && isAEOn; LOG.i("checkIsSupported:", result); return result; @@ -49,13 +51,16 @@ public class ExposureLock extends BaseLock { int cancelTrigger = Build.VERSION.SDK_INT >= 23 ? CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL : CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE; - holder.getBuilder(this).set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, cancelTrigger); + holder.getBuilder(this).set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + cancelTrigger); holder.getBuilder(this).set(CaptureRequest.CONTROL_AE_LOCK, true); holder.applyBuilder(this); } @Override - public void onCaptureCompleted(@NonNull ActionHolder holder, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { + public void onCaptureCompleted(@NonNull ActionHolder holder, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { super.onCaptureCompleted(holder, request, result); Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); LOG.i("processCapture:", "aeState:", aeState); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/lock/FocusLock.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/lock/FocusLock.java index adfa4d2f..fc0d69a5 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/lock/FocusLock.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/lock/FocusLock.java @@ -22,7 +22,8 @@ public class FocusLock extends BaseLock { protected boolean checkIsSupported(@NonNull ActionHolder holder) { // We'll lock by changing the AF mode to AUTO. // In that mode, AF won't change unless someone starts a trigger operation. - int[] modes = readCharacteristic(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES, new int[]{}); + int[] modes = readCharacteristic(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES, + new int[]{}); for (int mode : modes) { if (mode == CameraCharacteristics.CONTROL_AF_MODE_AUTO) { return true; @@ -50,13 +51,17 @@ public class FocusLock extends BaseLock { @Override protected void onStarted(@NonNull ActionHolder holder) { - holder.getBuilder(this).set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); - holder.getBuilder(this).set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_CANCEL); + holder.getBuilder(this).set(CaptureRequest.CONTROL_AF_MODE, + CaptureRequest.CONTROL_AF_MODE_AUTO); + holder.getBuilder(this).set(CaptureRequest.CONTROL_AF_TRIGGER, + CaptureRequest.CONTROL_AF_TRIGGER_CANCEL); holder.applyBuilder(this); } @Override - public void onCaptureCompleted(@NonNull ActionHolder holder, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { + public void onCaptureCompleted(@NonNull ActionHolder holder, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { super.onCaptureCompleted(holder, request, result); Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); Integer afMode = result.get(CaptureResult.CONTROL_AF_MODE); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/lock/WhiteBalanceLock.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/lock/WhiteBalanceLock.java index b49c2ab1..c56dcc64 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/lock/WhiteBalanceLock.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/lock/WhiteBalanceLock.java @@ -20,10 +20,13 @@ public class WhiteBalanceLock extends BaseLock { @Override protected boolean checkIsSupported(@NonNull ActionHolder holder) { - boolean isNotLegacy = readCharacteristic(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL, -1) + boolean isNotLegacy = readCharacteristic( + CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL, -1) != CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY; Integer awbMode = holder.getBuilder(this).get(CaptureRequest.CONTROL_AWB_MODE); - boolean result = isNotLegacy && awbMode != null && awbMode == CaptureRequest.CONTROL_AWB_MODE_AUTO; + boolean result = isNotLegacy + && awbMode != null + && awbMode == CaptureRequest.CONTROL_AWB_MODE_AUTO; LOG.i("checkIsSupported:", result); return result; } @@ -43,7 +46,9 @@ public class WhiteBalanceLock extends BaseLock { } @Override - public void onCaptureCompleted(@NonNull ActionHolder holder, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { + public void onCaptureCompleted(@NonNull ActionHolder holder, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { super.onCaptureCompleted(holder, request, result); Integer awbState = result.get(CaptureResult.CONTROL_AWB_STATE); LOG.i("processCapture:", "awbState:", awbState); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/BaseReset.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/BaseReset.java index b4ea622d..5d80a612 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/BaseReset.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/BaseReset.java @@ -29,7 +29,8 @@ public abstract class BaseReset extends BaseAction { super.onStart(holder); MeteringRectangle area = null; if (resetArea) { - Rect rect = readCharacteristic(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE, new Rect()); + Rect rect = readCharacteristic(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE, + new Rect()); area = new MeteringRectangle(rect, MeteringRectangle.METERING_WEIGHT_DONT_CARE); } onStarted(holder, area); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/ExposureMeter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/ExposureMeter.java index cfd0826f..64c6d361 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/ExposureMeter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/ExposureMeter.java @@ -32,7 +32,8 @@ public class ExposureMeter extends BaseMeter { @Override protected boolean checkIsSupported(@NonNull ActionHolder holder) { // In our case, this means checking if we support the AE precapture trigger. - boolean isNotLegacy = readCharacteristic(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL, -1) + boolean isNotLegacy = readCharacteristic( + CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL, -1) != CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY; Integer aeMode = holder.getBuilder(this).get(CaptureRequest.CONTROL_AE_MODE); boolean isAEOn = aeMode != null && @@ -40,7 +41,8 @@ public class ExposureMeter extends BaseMeter { || aeMode == CameraCharacteristics.CONTROL_AE_MODE_ON_ALWAYS_FLASH || aeMode == CameraCharacteristics.CONTROL_AE_MODE_ON_AUTO_FLASH || aeMode == CameraCharacteristics.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE - || aeMode == 5 /* CameraCharacteristics.CONTROL_AE_MODE_ON_EXTERNAL_FLASH, API 28 */); + || aeMode == 5 + /* CameraCharacteristics.CONTROL_AE_MODE_ON_EXTERNAL_FLASH, API 28 */); boolean result = isNotLegacy && isAEOn; LOG.i("checkIsSupported:", result); return result; @@ -63,7 +65,8 @@ public class ExposureMeter extends BaseMeter { CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); // Check the regions. - int maxRegions = readCharacteristic(CameraCharacteristics.CONTROL_MAX_REGIONS_AE, 0); + int maxRegions = readCharacteristic(CameraCharacteristics.CONTROL_MAX_REGIONS_AE, + 0); if (!areas.isEmpty() && maxRegions > 0) { int max = Math.min(maxRegions, areas.size()); holder.getBuilder(this).set(CaptureRequest.CONTROL_AE_REGIONS, @@ -103,8 +106,8 @@ public class ExposureMeter extends BaseMeter { // PRECAPTURE is a transient state. Being here might mean that precapture run // and was successful, OR that the trigger was not even received yet. To // distinguish, check the trigger state. - if (aeTriggerState != null - && aeTriggerState == CaptureResult.CONTROL_AE_PRECAPTURE_TRIGGER_START) { + if (aeTriggerState != null && aeTriggerState + == CaptureResult.CONTROL_AE_PRECAPTURE_TRIGGER_START) { setSuccessful(true); setState(STATE_COMPLETED); } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/ExposureReset.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/ExposureReset.java index 04ace0b9..c3755df7 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/ExposureReset.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/ExposureReset.java @@ -29,29 +29,32 @@ public class ExposureReset extends BaseReset { @Override protected void onStarted(@NonNull ActionHolder holder, @Nullable MeteringRectangle area) { - int maxRegions = readCharacteristic(CameraCharacteristics.CONTROL_MAX_REGIONS_AE, 0); + int maxRegions = readCharacteristic(CameraCharacteristics.CONTROL_MAX_REGIONS_AE, + 0); if (area != null && maxRegions > 0) { holder.getBuilder(this).set(CaptureRequest.CONTROL_AE_REGIONS, new MeteringRectangle[]{area}); } // NOTE: precapture might not be supported, in which case I think it will be ignored. - Integer trigger = holder.getLastResult(this).get(CaptureResult.CONTROL_AE_PRECAPTURE_TRIGGER); + Integer trigger = holder.getLastResult(this) + .get(CaptureResult.CONTROL_AE_PRECAPTURE_TRIGGER); LOG.i("onStarted:", "last precapture trigger is", trigger); if (trigger != null && trigger == CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START) { LOG.i("onStarted:", "canceling precapture."); int newTrigger = Build.VERSION.SDK_INT >= 23 ? CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL : CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE; - holder.getBuilder(this).set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, newTrigger); + holder.getBuilder(this).set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + newTrigger); } // Documentation about CONTROL_AE_PRECAPTURE_TRIGGER says that, if it was started but not // followed by a CAPTURE_INTENT_STILL_PICTURE request, the internal AE routine might remain // locked unless we unlock manually. - // This is often the case for us, since the snapshot picture recorder does not use the intent - // and anyway we use the precapture sequence for touch metering as well. - // To reset, docs suggest the use of CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL, which we do above, + // This is often the case for us, since the snapshot picture recorder does not use the + // intent and anyway we use the precapture sequence for touch metering as well. + // To reset docs suggest the use of CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL, which we do above, // or the technique used below: locking then unlocking. This proved to be the ONLY method // to unlock reliably, unlike the cancel trigger (which we'll run anyway). holder.getBuilder(this).set(CaptureRequest.CONTROL_AE_LOCK, true); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/FocusMeter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/FocusMeter.java index 0356a20b..4d5c2f85 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/FocusMeter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/FocusMeter.java @@ -53,7 +53,8 @@ public class FocusMeter extends BaseMeter { LOG.i("onStarted:", "with areas:", areas); holder.getBuilder(this).set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); - int maxRegions = readCharacteristic(CameraCharacteristics.CONTROL_MAX_REGIONS_AF, 0); + int maxRegions = readCharacteristic(CameraCharacteristics.CONTROL_MAX_REGIONS_AF, + 0); if (!areas.isEmpty() && maxRegions > 0) { int max = Math.min(maxRegions, areas.size()); holder.getBuilder(this).set(CaptureRequest.CONTROL_AF_REGIONS, diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/FocusReset.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/FocusReset.java index 7554a86c..c1d1883a 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/FocusReset.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/FocusReset.java @@ -27,7 +27,8 @@ public class FocusReset extends BaseReset { @Override protected void onStarted(@NonNull ActionHolder holder, @Nullable MeteringRectangle area) { boolean changed = false; - int maxRegions = readCharacteristic(CameraCharacteristics.CONTROL_MAX_REGIONS_AF, 0); + int maxRegions = readCharacteristic(CameraCharacteristics.CONTROL_MAX_REGIONS_AF, + 0); if (area != null && maxRegions > 0) { holder.getBuilder(this).set(CaptureRequest.CONTROL_AF_REGIONS, new MeteringRectangle[]{area}); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/MeterAction.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/MeterAction.java index cc8f0ff4..e8a1266b 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/MeterAction.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/MeterAction.java @@ -82,8 +82,8 @@ public class MeterAction extends ActionWrapper { List areas = new ArrayList<>(); if (point != null) { // This is a good Q/A. https://stackoverflow.com/a/33181620/4288782 - // At first, the point is relative to the View system and does not account our own cropping. - // Will keep updating these two below. + // At first, the point is relative to the View system and does not account + // our own cropping. Will keep updating these two below. final PointF referencePoint = new PointF(point.x, point.y); Size referenceSize = engine.getPreview().getSurfaceSize(); @@ -132,7 +132,7 @@ public class MeterAction extends ActionWrapper { Size previewStreamSize = engine.getPreviewStreamSize(Reference.VIEW); Size previewSurfaceSize = referenceSize; if (previewStreamSize == null) { - throw new IllegalStateException("getPreviewStreamSize should not be null at this point."); + throw new IllegalStateException("getPreviewStreamSize should not be null here."); } int referenceWidth = previewSurfaceSize.getWidth(); int referenceHeight = previewSurfaceSize.getHeight(); @@ -142,13 +142,15 @@ public class MeterAction extends ActionWrapper { if (previewStreamAspectRatio.toFloat() > previewSurfaceAspectRatio.toFloat()) { // Stream is larger. The x coordinate must be increased: a touch on the left side // of the surface is not on the left size of stream (it's more to the right). - float scale = previewStreamAspectRatio.toFloat() / previewSurfaceAspectRatio.toFloat(); + float scale = previewStreamAspectRatio.toFloat() + / previewSurfaceAspectRatio.toFloat(); referencePoint.x += previewSurfaceSize.getWidth() * (scale - 1F) / 2F; referenceWidth = Math.round(previewSurfaceSize.getWidth() * scale); } else { // Stream is taller. The y coordinate must be increased: a touch on the top side // of the surface is not on the top size of stream (it's a bit lower). - float scale = previewSurfaceAspectRatio.toFloat() / previewStreamAspectRatio.toFloat(); + float scale = previewSurfaceAspectRatio.toFloat() + / previewStreamAspectRatio.toFloat(); referencePoint.y += previewSurfaceSize.getHeight() * (scale - 1F) / 2F; referenceHeight = Math.round(previewSurfaceSize.getHeight() * scale); } @@ -169,7 +171,8 @@ public class MeterAction extends ActionWrapper { @SuppressWarnings("SuspiciousNameCombination") @NonNull - private Size applyPreviewToSensorRotation(@NonNull Size referenceSize, @NonNull PointF referencePoint) { + private Size applyPreviewToSensorRotation(@NonNull Size referenceSize, + @NonNull PointF referencePoint) { // Not elegant, but the sin/cos way was failing for some reason. int angle = engine.getAngles().offset(Reference.SENSOR, Reference.VIEW, Axis.ABSOLUTE); boolean flip = angle % 180 != 0; @@ -194,12 +197,13 @@ public class MeterAction extends ActionWrapper { } @NonNull - private Size applyCropRegionCoordinates(@NonNull Size referenceSize, @NonNull PointF referencePoint) { + private Size applyCropRegionCoordinates(@NonNull Size referenceSize, + @NonNull PointF referencePoint) { // The input point and size refer to the stream rect. // The stream rect is part of the 'crop region', as described below. // https://source.android.com/devices/camera/camera3_crop_reprocess.html Rect cropRect = holder.getBuilder(this).get(CaptureRequest.SCALER_CROP_REGION); - // For now, we don't care about x and y position. Rect should be non-null, but let's be safe. + // For now we don't care about x and y position. Rect should not be null, but let's be safe. int cropRectWidth = cropRect == null ? referenceSize.getWidth() : cropRect.width(); int cropRectHeight = cropRect == null ? referenceSize.getHeight() : cropRect.height(); // The stream is always centered inside the crop region, and one of the dimensions @@ -210,16 +214,19 @@ public class MeterAction extends ActionWrapper { } @NonNull - private Size applyActiveArrayCoordinates(@NonNull Size referenceSize, @NonNull PointF referencePoint) { + private Size applyActiveArrayCoordinates(@NonNull Size referenceSize, + @NonNull PointF referencePoint) { // The input point and size refer to the scaler crop region. // We can query for the crop region position inside the active array, so this is easy. Rect cropRect = holder.getBuilder(this).get(CaptureRequest.SCALER_CROP_REGION); referencePoint.x += cropRect == null ? 0 : cropRect.left; referencePoint.y += cropRect == null ? 0 : cropRect.top; // Finally, get the active rect width and height from characteristics. - Rect activeRect = holder.getCharacteristics(this).get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); + Rect activeRect = holder.getCharacteristics(this) + .get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); if (activeRect == null) { // Should never happen - activeRect = new Rect(0, 0, referenceSize.getWidth(), referenceSize.getHeight()); + activeRect = new Rect(0, 0, referenceSize.getWidth(), + referenceSize.getHeight()); } return new Size(activeRect.width(), activeRect.height()); } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/WhiteBalanceMeter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/WhiteBalanceMeter.java index 4828838d..0217b815 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/WhiteBalanceMeter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/WhiteBalanceMeter.java @@ -27,10 +27,13 @@ public class WhiteBalanceMeter extends BaseMeter { @Override protected boolean checkIsSupported(@NonNull ActionHolder holder) { - boolean isNotLegacy = readCharacteristic(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL, -1) + boolean isNotLegacy = readCharacteristic( + CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL, -1) != CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY; Integer awbMode = holder.getBuilder(this).get(CaptureRequest.CONTROL_AWB_MODE); - boolean result = isNotLegacy && awbMode != null && awbMode == CaptureRequest.CONTROL_AWB_MODE_AUTO; + boolean result = isNotLegacy + && awbMode != null + && awbMode == CaptureRequest.CONTROL_AWB_MODE_AUTO; LOG.i("checkIsSupported:", result); return result; } @@ -46,7 +49,8 @@ public class WhiteBalanceMeter extends BaseMeter { @Override protected void onStarted(@NonNull ActionHolder holder, @NonNull List areas) { LOG.i("onStarted:", "with areas:", areas); - int maxRegions = readCharacteristic(CameraCharacteristics.CONTROL_MAX_REGIONS_AWB, 0); + int maxRegions = readCharacteristic(CameraCharacteristics.CONTROL_MAX_REGIONS_AWB, + 0); if (!areas.isEmpty() && maxRegions > 0) { int max = Math.min(maxRegions, areas.size()); holder.getBuilder(this).set(CaptureRequest.CONTROL_AWB_REGIONS, @@ -56,7 +60,8 @@ public class WhiteBalanceMeter extends BaseMeter { } @Override - public void onCaptureCompleted(@NonNull ActionHolder holder, @NonNull CaptureRequest request, + public void onCaptureCompleted(@NonNull ActionHolder holder, + @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { super.onCaptureCompleted(holder, request, result); Integer awbState = result.get(CaptureResult.CONTROL_AWB_STATE); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/WhiteBalanceReset.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/WhiteBalanceReset.java index 5d24fd55..32ca8fe3 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/WhiteBalanceReset.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/WhiteBalanceReset.java @@ -30,9 +30,11 @@ public class WhiteBalanceReset extends BaseReset { @Override protected void onStarted(@NonNull ActionHolder holder, @Nullable MeteringRectangle area) { LOG.w("onStarted:", "with area:", area); - int maxRegions = readCharacteristic(CameraCharacteristics.CONTROL_MAX_REGIONS_AWB, 0); + int maxRegions = readCharacteristic(CameraCharacteristics.CONTROL_MAX_REGIONS_AWB, + 0); if (area != null && maxRegions > 0) { - holder.getBuilder(this).set(CaptureRequest.CONTROL_AWB_REGIONS, new MeteringRectangle[]{area}); + holder.getBuilder(this).set(CaptureRequest.CONTROL_AWB_REGIONS, + new MeteringRectangle[]{area}); holder.applyBuilder(this); } setState(STATE_COMPLETED); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/offset/Axis.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/offset/Axis.java index d5f3eb67..3e30954c 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/offset/Axis.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/offset/Axis.java @@ -21,7 +21,7 @@ public enum Axis { * This rotation axis takes into account the current * {@link com.otaliastudios.cameraview.controls.Facing} value. * - * - for {@link com.otaliastudios.cameraview.controls.Facing#BACK}, this equals {@link #ABSOLUTE} + * - for {@link com.otaliastudios.cameraview.controls.Facing#BACK}, this is {@link #ABSOLUTE} * - for {@link com.otaliastudios.cameraview.controls.Facing#FRONT}, this is inverted */ RELATIVE_TO_SENSOR diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filter/BaseFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filter/BaseFilter.java index 4a1b0334..5edd058a 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filter/BaseFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filter/BaseFilter.java @@ -36,7 +36,8 @@ import java.nio.FloatBuffer; * NOTE - the {@link android.graphics.SurfaceTexture} restrictions apply: * We only support the {@link android.opengl.GLES11Ext#GL_TEXTURE_EXTERNAL_OES} texture target * and it must be specified in the fragment shader as a samplerExternalOES texture. - * You also have to explicitly require the extension: see {@link #createDefaultFragmentShader(String)}. + * You also have to explicitly require the extension: see + * {@link #createDefaultFragmentShader(String)}. * */ public abstract class BaseFilter implements Filter { @@ -44,31 +45,42 @@ public abstract class BaseFilter implements Filter { private final static String TAG = BaseFilter.class.getSimpleName(); private final static CameraLogger LOG = CameraLogger.create(TAG); - private final static String DEFAULT_VERTEX_POSITION_NAME = "aPosition"; - private final static String DEFAULT_VERTEX_TEXTURE_COORDINATE_NAME = "aTextureCoord"; - private final static String DEFAULT_VERTEX_MVP_MATRIX_NAME = "uMVPMatrix"; - private final static String DEFAULT_VERTEX_TRANSFORM_MATRIX_NAME = "uTexMatrix"; - private final static String DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME = "vTextureCoord"; + @SuppressWarnings("WeakerAccess") + protected final static String DEFAULT_VERTEX_POSITION_NAME = "aPosition"; + + @SuppressWarnings("WeakerAccess") + protected final static String DEFAULT_VERTEX_TEXTURE_COORDINATE_NAME = "aTextureCoord"; + + @SuppressWarnings("WeakerAccess") + protected final static String DEFAULT_VERTEX_MVP_MATRIX_NAME = "uMVPMatrix"; + + @SuppressWarnings("WeakerAccess") + protected final static String DEFAULT_VERTEX_TRANSFORM_MATRIX_NAME = "uTexMatrix"; + protected final static String DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME = "vTextureCoord"; @NonNull - private static String createDefaultVertexShader(@NonNull String vertexPositionName, - @NonNull String vertexTextureCoordinateName, - @NonNull String vertexModelViewProjectionMatrixName, - @NonNull String vertexTransformMatrixName, - @NonNull String fragmentTextureCoordinateName) { - return "uniform mat4 "+vertexModelViewProjectionMatrixName+";\n" + - "uniform mat4 "+vertexTransformMatrixName+";\n" + - "attribute vec4 "+vertexPositionName+";\n" + - "attribute vec4 "+vertexTextureCoordinateName+";\n" + - "varying vec2 "+fragmentTextureCoordinateName+";\n" + - "void main() {\n" + - " gl_Position = "+vertexModelViewProjectionMatrixName+" * "+vertexPositionName+";\n" + - " vTextureCoord = ("+vertexTransformMatrixName+" * "+vertexTextureCoordinateName+").xy;\n" + - "}\n"; + private static String createDefaultVertexShader( + @NonNull String vertexPositionName, + @NonNull String vertexTextureCoordinateName, + @NonNull String vertexModelViewProjectionMatrixName, + @NonNull String vertexTransformMatrixName, + @NonNull String fragmentTextureCoordinateName) { + return "uniform mat4 "+vertexModelViewProjectionMatrixName+";\n" + + "uniform mat4 "+vertexTransformMatrixName+";\n" + + "attribute vec4 "+vertexPositionName+";\n" + + "attribute vec4 "+vertexTextureCoordinateName+";\n" + + "varying vec2 "+fragmentTextureCoordinateName+";\n" + + "void main() {\n" + + " gl_Position = " +vertexModelViewProjectionMatrixName+" * " + + vertexPositionName+";\n" + + " "+fragmentTextureCoordinateName+" = ("+vertexTransformMatrixName+" * " + + vertexTextureCoordinateName+").xy;\n" + + "}\n"; } @NonNull - private static String createDefaultFragmentShader(@NonNull String fragmentTextureCoordinateName) { + private static String createDefaultFragmentShader( + @NonNull String fragmentTextureCoordinateName) { return "#extension GL_OES_EGL_image_external : require\n" + "precision mediump float;\n" + "varying vec2 "+fragmentTextureCoordinateName+";\n" @@ -94,7 +106,7 @@ public abstract class BaseFilter implements Filter { }); private int vertexModelViewProjectionMatrixLocation = -1; - private int vertexTranformMatrixLocation = -1; + private int vertexTransformMatrixLocation = -1; private int vertexPositionLocation = -1; private int vertexTextureCoordinateLocation = -1; @VisibleForTesting int programHandle = -1; @@ -121,6 +133,7 @@ public abstract class BaseFilter implements Filter { fragmentTextureCoordinateName); } + @SuppressWarnings("WeakerAccess") @NonNull protected String createDefaultFragmentShader() { return createDefaultFragmentShader(fragmentTextureCoordinateName); @@ -131,12 +144,16 @@ public abstract class BaseFilter implements Filter { this.programHandle = programHandle; vertexPositionLocation = GLES20.glGetAttribLocation(programHandle, vertexPositionName); GlUtils.checkLocation(vertexPositionLocation, vertexPositionName); - vertexTextureCoordinateLocation = GLES20.glGetAttribLocation(programHandle, vertexTextureCoordinateName); + vertexTextureCoordinateLocation = GLES20.glGetAttribLocation(programHandle, + vertexTextureCoordinateName); GlUtils.checkLocation(vertexTextureCoordinateLocation, vertexTextureCoordinateName); - vertexModelViewProjectionMatrixLocation = GLES20.glGetUniformLocation(programHandle, vertexModelViewProjectionMatrixName); - GlUtils.checkLocation(vertexModelViewProjectionMatrixLocation, vertexModelViewProjectionMatrixName); - vertexTranformMatrixLocation = GLES20.glGetUniformLocation(programHandle, vertexTransformMatrixName); - GlUtils.checkLocation(vertexTranformMatrixLocation, vertexTransformMatrixName); + vertexModelViewProjectionMatrixLocation = GLES20.glGetUniformLocation(programHandle, + vertexModelViewProjectionMatrixName); + GlUtils.checkLocation(vertexModelViewProjectionMatrixLocation, + vertexModelViewProjectionMatrixName); + vertexTransformMatrixLocation = GLES20.glGetUniformLocation(programHandle, + vertexTransformMatrixName); + GlUtils.checkLocation(vertexTransformMatrixLocation, vertexTransformMatrixName); } @Override @@ -145,7 +162,7 @@ public abstract class BaseFilter implements Filter { vertexPositionLocation = -1; vertexTextureCoordinateLocation = -1; vertexModelViewProjectionMatrixLocation = -1; - vertexTranformMatrixLocation = -1; + vertexTransformMatrixLocation = -1; } @NonNull @@ -160,48 +177,53 @@ public abstract class BaseFilter implements Filter { } @Override - public void draw(float[] transformMatrix) { + public void draw(long timestampUs, float[] transformMatrix) { if (programHandle == -1) { - LOG.w("Filter.draw() called after destroying the filter. This can happen rarely because of threading."); + LOG.w("Filter.draw() called after destroying the filter. " + + "This can happen rarely because of threading."); } else { - onPreDraw(transformMatrix); - onDraw(); - onPostDraw(); + onPreDraw(timestampUs, transformMatrix); + onDraw(timestampUs); + onPostDraw(timestampUs); } } - protected void onPreDraw(float[] transformMatrix) { + protected void onPreDraw(long timestampUs, float[] transformMatrix) { // Copy the model / view / projection matrix over. - GLES20.glUniformMatrix4fv(vertexModelViewProjectionMatrixLocation, 1, false, GlUtils.IDENTITY_MATRIX, 0); + GLES20.glUniformMatrix4fv(vertexModelViewProjectionMatrixLocation, 1, + false, GlUtils.IDENTITY_MATRIX, 0); GlUtils.checkError("glUniformMatrix4fv"); // Copy the texture transformation matrix over. - GLES20.glUniformMatrix4fv(vertexTranformMatrixLocation, 1, false, transformMatrix, 0); + GLES20.glUniformMatrix4fv(vertexTransformMatrixLocation, 1, + false, transformMatrix, 0); GlUtils.checkError("glUniformMatrix4fv"); // Enable the "aPosition" vertex attribute. // Connect vertexBuffer to "aPosition". GLES20.glEnableVertexAttribArray(vertexPositionLocation); GlUtils.checkError("glEnableVertexAttribArray: " + vertexPositionLocation); - GLES20.glVertexAttribPointer(vertexPositionLocation, 2, GLES20.GL_FLOAT, false, 8, vertexPosition); + GLES20.glVertexAttribPointer(vertexPositionLocation, 2, GLES20.GL_FLOAT, + false, 8, vertexPosition); GlUtils.checkError("glVertexAttribPointer"); // Enable the "aTextureCoord" vertex attribute. // Connect texBuffer to "aTextureCoord". GLES20.glEnableVertexAttribArray(vertexTextureCoordinateLocation); GlUtils.checkError("glEnableVertexAttribArray"); - GLES20.glVertexAttribPointer(vertexTextureCoordinateLocation, 2, GLES20.GL_FLOAT, false, 8, textureCoordinates); + GLES20.glVertexAttribPointer(vertexTextureCoordinateLocation, 2, GLES20.GL_FLOAT, + false, 8, textureCoordinates); GlUtils.checkError("glVertexAttribPointer"); } @SuppressWarnings("WeakerAccess") - protected void onDraw() { + protected void onDraw(long timestampUs) { GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); GlUtils.checkError("glDrawArrays"); } @SuppressWarnings("WeakerAccess") - protected void onPostDraw() { + protected void onPostDraw(long timestampUs) { GLES20.glDisableVertexAttribArray(vertexPositionLocation); GLES20.glDisableVertexAttribArray(vertexTextureCoordinateLocation); } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filter/Filter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filter/Filter.java index 7a03f9c0..b86fee50 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filter/Filter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filter/Filter.java @@ -67,9 +67,10 @@ public interface Filter { * Called to render the actual texture. The given transformation matrix * should be applied. * + * @param timestampUs timestamp in microseconds * @param transformMatrix matrix */ - void draw(float[] transformMatrix); + void draw(long timestampUs, float[] transformMatrix); /** * Called anytime the output size changes. diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filter/MultiFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filter/MultiFilter.java index 440b84cb..aec28f65 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filter/MultiFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filter/MultiFilter.java @@ -26,8 +26,9 @@ import java.util.Map; * New filters can be added at any time through {@link #addFilter(Filter)}, but currently they * can not be removed because we can not easily ensure that they would be correctly released. * - * The {@link MultiFilter} does also implement {@link OneParameterFilter} and {@link TwoParameterFilter}, - * dispatching all the parameter calls to child filters, assuming they support it. + * The {@link MultiFilter} does also implement {@link OneParameterFilter} and + * {@link TwoParameterFilter}, dispatching all the parameter calls to child filters, + * assuming they support it. * * There are some important technical caveats when using {@link MultiFilter}: * - each child filter requires the allocation of a GL framebuffer. Using a large number of filters @@ -146,11 +147,17 @@ public class MultiFilter implements Filter, OneParameterFilter, TwoParameterFilt state.textureId = textureArray[0]; GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, state.textureId); - GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, state.size.getWidth(), state.size.getHeight(), 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null); - GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); - GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); - GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); - GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, + state.size.getWidth(), state.size.getHeight(), 0, GLES20.GL_RGBA, + GLES20.GL_UNSIGNED_BYTE, null); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, + GLES20.GL_LINEAR); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, + GLES20.GL_LINEAR); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, + GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, + GLES20.GL_CLAMP_TO_EDGE); GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, state.framebufferId); GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, @@ -230,7 +237,7 @@ public class MultiFilter implements Filter, OneParameterFilter, TwoParameterFilt } @Override - public void draw(float[] transformMatrix) { + public void draw(long timestampUs, float[] transformMatrix) { synchronized (lock) { for (int i = 0; i < filters.size(); i++) { boolean isFirst = i == 0; @@ -258,9 +265,9 @@ public class MultiFilter implements Filter, OneParameterFilter, TwoParameterFilt // The first filter should apply all the transformations. Then, // since they are applied, we should use a no-op matrix. if (isFirst) { - filter.draw(transformMatrix); + filter.draw(timestampUs, transformMatrix); } else { - filter.draw(GlUtils.IDENTITY_MATRIX); + filter.draw(timestampUs, GlUtils.IDENTITY_MATRIX); } // Set the input for the next cycle: diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/AutoFixFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/AutoFixFilter.java index a05669cb..8b7328f3 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/AutoFixFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/AutoFixFilter.java @@ -24,7 +24,7 @@ public class AutoFixFilter extends BaseFilter implements OneParameterFilter { + "float hist_scale;\n" + "float density_offset;\n" + "float density_scale;\n" - + "varying vec2 vTextureCoord;\n" + + "varying vec2 "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+";\n" + "void main() {\n" + " shift_scale = " + (1.0f / 256f) + ";\n" + " hist_offset = " + (0.5f / 766f) + ";\n" @@ -32,7 +32,8 @@ public class AutoFixFilter extends BaseFilter implements OneParameterFilter { + " density_offset = " + (0.5f / 1024f) + ";\n" + " density_scale = " + (1023f / 1024f) + ";\n" + " const vec3 weights = vec3(0.33333, 0.33333, 0.33333);\n" - + " vec4 color = texture2D(tex_sampler_0, vTextureCoord);\n" + + " vec4 color = texture2D(tex_sampler_0, "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME + + ");\n" + " float energy = dot(color.rgb, weights);\n" + " float mask_value = energy - 0.5;\n" + " float alpha;\n" @@ -116,8 +117,8 @@ public class AutoFixFilter extends BaseFilter implements OneParameterFilter { } @Override - protected void onPreDraw(float[] transformMatrix) { - super.onPreDraw(transformMatrix); + protected void onPreDraw(long timestampUs, float[] transformMatrix) { + super.onPreDraw(timestampUs, transformMatrix); GLES20.glUniform1f(scaleLocation, scale); GlUtils.checkError("glUniform1f"); } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/BlackAndWhiteFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/BlackAndWhiteFilter.java index 4395e713..d523f82a 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/BlackAndWhiteFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/BlackAndWhiteFilter.java @@ -11,9 +11,9 @@ public class BlackAndWhiteFilter extends BaseFilter { private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n" + "precision mediump float;\n" - + "varying vec2 vTextureCoord;\n" + + "varying vec2 "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+";\n" + "uniform samplerExternalOES sTexture;\n" + "void main() {\n" - + " vec4 color = texture2D(sTexture, vTextureCoord);\n" + + " vec4 color = texture2D(sTexture, "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+");\n" + " float colorR = (color.r + color.g + color.b) / 3.0;\n" + " float colorG = (color.r + color.g + color.b) / 3.0;\n" + " float colorB = (color.r + color.g + color.b) / 3.0;\n" diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/BrightnessFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/BrightnessFilter.java index 5aa8e416..1cd6d2e6 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/BrightnessFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/BrightnessFilter.java @@ -17,9 +17,9 @@ public class BrightnessFilter extends BaseFilter implements OneParameterFilter { + "precision mediump float;\n" + "uniform samplerExternalOES sTexture;\n" + "uniform float brightness;\n" - + "varying vec2 vTextureCoord;\n" + + "varying vec2 "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+";\n" + "void main() {\n" - + " vec4 color = texture2D(sTexture, vTextureCoord);\n" + + " vec4 color = texture2D(sTexture, "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+");\n" + " gl_FragColor = brightness * color;\n" + "}\n"; @@ -86,8 +86,8 @@ public class BrightnessFilter extends BaseFilter implements OneParameterFilter { } @Override - protected void onPreDraw(float[] transformMatrix) { - super.onPreDraw(transformMatrix); + protected void onPreDraw(long timestampUs, float[] transformMatrix) { + super.onPreDraw(timestampUs, transformMatrix); GLES20.glUniform1f(brightnessLocation, brightness); GlUtils.checkError("glUniform1f"); } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/ContrastFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/ContrastFilter.java index cc2e601c..ec790e96 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/ContrastFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/ContrastFilter.java @@ -17,9 +17,9 @@ public class ContrastFilter extends BaseFilter implements OneParameterFilter { + "precision mediump float;\n" + "uniform samplerExternalOES sTexture;\n" + "uniform float contrast;\n" - + "varying vec2 vTextureCoord;\n" + + "varying vec2 "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+";\n" + "void main() {\n" - + " vec4 color = texture2D(sTexture, vTextureCoord);\n" + + " vec4 color = texture2D(sTexture, "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+");\n" + " color -= 0.5;\n" + " color *= contrast;\n" + " color += 0.5;\n" @@ -88,8 +88,8 @@ public class ContrastFilter extends BaseFilter implements OneParameterFilter { } @Override - protected void onPreDraw(float[] transformMatrix) { - super.onPreDraw(transformMatrix); + protected void onPreDraw(long timestampUs, float[] transformMatrix) { + super.onPreDraw(timestampUs, transformMatrix); GLES20.glUniform1f(contrastLocation, contrast); GlUtils.checkError("glUniform1f"); } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/CrossProcessFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/CrossProcessFilter.java index 1ff8cca1..7423c576 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/CrossProcessFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/CrossProcessFilter.java @@ -13,9 +13,9 @@ public class CrossProcessFilter extends BaseFilter { private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n" + "precision mediump float;\n" + "uniform samplerExternalOES sTexture;\n" - + "varying vec2 vTextureCoord;\n" + + "varying vec2 "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+";\n" + "void main() {\n" - + " vec4 color = texture2D(sTexture, vTextureCoord);\n" + + " vec4 color = texture2D(sTexture, "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+");\n" + " vec3 ncolor = vec3(0.0, 0.0, 0.0);\n" + " float value;\n" + " if (color.r < 0.5) {\n" diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/DocumentaryFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/DocumentaryFilter.java index c4a1532f..a4f1bfc5 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/DocumentaryFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/DocumentaryFilter.java @@ -22,7 +22,7 @@ public class DocumentaryFilter extends BaseFilter { + "float stepsize;\n" + "uniform float inv_max_dist;\n" + "uniform vec2 scale;\n" - + "varying vec2 vTextureCoord;\n" + + "varying vec2 "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+";\n" + "float rand(vec2 loc) {\n" + " float theta1 = dot(loc, vec2(0.9898, 0.233));\n" + " float theta2 = dot(loc, vec2(12.0, 78.0));\n" @@ -41,8 +41,8 @@ public class DocumentaryFilter extends BaseFilter { + " stepsize = " + 1.0f / 255.0f + ";\n" // black white - + " vec4 color = texture2D(sTexture, vTextureCoord);\n" - + " float dither = rand(vTextureCoord + seed);\n" + + " vec4 color = texture2D(sTexture, "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+");\n" + + " float dither = rand("+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+" + seed);\n" + " vec3 xform = clamp(2.0 * color.rgb, 0.0, 1.0);\n" + " vec3 temp = clamp(2.0 * (color.rgb + stepsize), 0.0, 1.0);\n" + " vec3 new_color = clamp(xform + (temp - xform) * (dither - 0.5), 0.0, 1.0);\n" @@ -50,7 +50,7 @@ public class DocumentaryFilter extends BaseFilter { + " float gray = dot(new_color, vec3(0.299, 0.587, 0.114));\n" + " new_color = vec3(gray, gray, gray);\n" // vignette - + " vec2 coord = vTextureCoord - vec2(0.5, 0.5);\n" + + " vec2 coord = "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+" - vec2(0.5, 0.5);\n" + " float dist = length(coord * scale);\n" + " float lumen = 0.85 / (1.0 + exp((dist * inv_max_dist - 0.83) * 20.0)) + 0.15;\n" + " gl_FragColor = vec4(new_color * lumen, color.a);\n" @@ -93,8 +93,8 @@ public class DocumentaryFilter extends BaseFilter { } @Override - protected void onPreDraw(float[] transformMatrix) { - super.onPreDraw(transformMatrix); + protected void onPreDraw(long timestampUs, float[] transformMatrix) { + super.onPreDraw(timestampUs, transformMatrix); float[] scale = new float[2]; if (mWidth > mHeight) { scale[0] = 1f; diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/DuotoneFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/DuotoneFilter.java index 385ed99b..ee534f23 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/DuotoneFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/DuotoneFilter.java @@ -20,9 +20,9 @@ public class DuotoneFilter extends BaseFilter implements TwoParameterFilter { + "uniform samplerExternalOES sTexture;\n" + "uniform vec3 first;\n" + "uniform vec3 second;\n" - + "varying vec2 vTextureCoord;\n" + + "varying vec2 "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+";\n" + "void main() {\n" - + " vec4 color = texture2D(sTexture, vTextureCoord);\n" + + " vec4 color = texture2D(sTexture, "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+");\n" + " float energy = (color.r + color.g + color.b) * 0.3333;\n" + " vec3 new_color = (1.0 - energy) * first + energy * second;\n" + " gl_FragColor = vec4(new_color.rgb, color.a);\n" @@ -137,8 +137,8 @@ public class DuotoneFilter extends BaseFilter implements TwoParameterFilter { } @Override - protected void onPreDraw(float[] transformMatrix) { - super.onPreDraw(transformMatrix); + protected void onPreDraw(long timestampUs, float[] transformMatrix) { + super.onPreDraw(timestampUs, transformMatrix); float[] first = new float[]{ Color.red(mFirstColor) / 255f, Color.green(mFirstColor) / 255f, diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/FillLightFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/FillLightFilter.java index 8ba50692..399bab45 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/FillLightFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/FillLightFilter.java @@ -18,10 +18,10 @@ public class FillLightFilter extends BaseFilter implements OneParameterFilter { + "uniform samplerExternalOES sTexture;\n" + "uniform float mult;\n" + "uniform float igamma;\n" - + "varying vec2 vTextureCoord;\n" + + "varying vec2 "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+";\n" + "void main() {\n" + " const vec3 color_weights = vec3(0.25, 0.5, 0.25);\n" - + " vec4 color = texture2D(sTexture, vTextureCoord);\n" + + " vec4 color = texture2D(sTexture, "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+");\n" + " float lightmask = dot(color.rgb, color_weights);\n" + " float backmask = (1.0 - lightmask);\n" + " vec3 ones = vec3(1.0, 1.0, 1.0);\n" @@ -95,8 +95,8 @@ public class FillLightFilter extends BaseFilter implements OneParameterFilter { } @Override - protected void onPreDraw(float[] transformMatrix) { - super.onPreDraw(transformMatrix); + protected void onPreDraw(long timestampUs, float[] transformMatrix) { + super.onPreDraw(timestampUs, transformMatrix); float amount = 1.0f - strength; float multiplier = 1.0f / (amount * 0.7f + 0.3f); GLES20.glUniform1f(multiplierLocation, multiplier); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/GammaFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/GammaFilter.java index 55cc2e86..c481ce83 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/GammaFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/GammaFilter.java @@ -15,11 +15,12 @@ public class GammaFilter extends BaseFilter implements OneParameterFilter { private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n" + "precision mediump float;\n" - + "varying vec2 vTextureCoord;\n" + + "varying vec2 "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+";\n" + "uniform samplerExternalOES sTexture;\n" + "uniform float gamma;\n" + "void main() {\n" - + " vec4 textureColor = texture2D(sTexture, vTextureCoord);\n" + + " vec4 textureColor = texture2D(sTexture, "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME + + ");\n" + " gl_FragColor = vec4(pow(textureColor.rgb, vec3(gamma)), textureColor.w);\n" + "}\n"; @@ -82,8 +83,8 @@ public class GammaFilter extends BaseFilter implements OneParameterFilter { } @Override - protected void onPreDraw(float[] transformMatrix) { - super.onPreDraw(transformMatrix); + protected void onPreDraw(long timestampUs, float[] transformMatrix) { + super.onPreDraw(timestampUs, transformMatrix); GLES20.glUniform1f(gammaLocation, gamma); GlUtils.checkError("glUniform1f"); } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/GrainFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/GrainFilter.java index 01ddd853..2ee4b4f0 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/GrainFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/GrainFilter.java @@ -19,7 +19,7 @@ public class GrainFilter extends BaseFilter implements OneParameterFilter { private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n" + "precision mediump float;\n" + "vec2 seed;\n" - + "varying vec2 vTextureCoord;\n" + + "varying vec2 "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+";\n" + "uniform samplerExternalOES tex_sampler_0;\n" + "uniform samplerExternalOES tex_sampler_1;\n" + "uniform float scale;\n" @@ -40,18 +40,25 @@ public class GrainFilter extends BaseFilter implements OneParameterFilter { + "void main() {\n" + " seed[0] = " + RANDOM.nextFloat() + ";\n" + " seed[1] = " + RANDOM.nextFloat() + ";\n" - + " float noise = texture2D(tex_sampler_1, vTextureCoord + vec2(-stepX, -stepY)).r * 0.224;\n" - + " noise += texture2D(tex_sampler_1, vTextureCoord + vec2(-stepX, stepY)).r * 0.224;\n" - + " noise += texture2D(tex_sampler_1, vTextureCoord + vec2(stepX, -stepY)).r * 0.224;\n" - + " noise += texture2D(tex_sampler_1, vTextureCoord + vec2(stepX, stepY)).r * 0.224;\n" + + " float noise = texture2D(tex_sampler_1, "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME + + " + vec2(-stepX, -stepY)).r * 0.224;\n" + + " noise += texture2D(tex_sampler_1, "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME + + " + vec2(-stepX, stepY)).r * 0.224;\n" + + " noise += texture2D(tex_sampler_1, "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME + + " + vec2(stepX, -stepY)).r * 0.224;\n" + + " noise += texture2D(tex_sampler_1, "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME + + " + vec2(stepX, stepY)).r * 0.224;\n" + " noise += 0.4448;\n" + " noise *= scale;\n" - + " vec4 color = texture2D(tex_sampler_0, vTextureCoord);\n" + + " vec4 color = texture2D(tex_sampler_0, "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME + + ");\n" + " float energy = 0.33333 * color.r + 0.33333 * color.g + 0.33333 * color.b;\n" + " float mask = (1.0 - sqrt(energy));\n" + " float weight = 1.0 - 1.333 * mask * noise;\n" + " gl_FragColor = vec4(color.rgb * weight, color.a);\n" - + " gl_FragColor = gl_FragColor+vec4(rand(vTextureCoord + seed), rand(vTextureCoord + seed),rand(vTextureCoord + seed),1);\n" + + " gl_FragColor = gl_FragColor+vec4(rand("+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME + + " + seed), rand("+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+" + seed),rand(" + + DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+" + seed),1);\n" + "}\n"; private float strength = 0.5f; @@ -61,7 +68,6 @@ public class GrainFilter extends BaseFilter implements OneParameterFilter { private int stepXLocation = -1; private int stepYLocation = -1; - @SuppressWarnings("WeakerAccess") public GrainFilter() { } @Override @@ -132,8 +138,8 @@ public class GrainFilter extends BaseFilter implements OneParameterFilter { } @Override - protected void onPreDraw(float[] transformMatrix) { - super.onPreDraw(transformMatrix); + protected void onPreDraw(long timestampUs, float[] transformMatrix) { + super.onPreDraw(timestampUs, transformMatrix); GLES20.glUniform1f(strengthLocation, strength); GlUtils.checkError("glUniform1f"); GLES20.glUniform1f(stepXLocation, 0.5f / width); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/GrayscaleFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/GrayscaleFilter.java index a4a49f3d..884af10f 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/GrayscaleFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/GrayscaleFilter.java @@ -12,9 +12,9 @@ public class GrayscaleFilter extends BaseFilter { private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n" + "precision mediump float;\n" + "uniform samplerExternalOES sTexture;\n" - + "varying vec2 vTextureCoord;\n" + + "varying vec2 "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+";\n" + "void main() {\n" - + " vec4 color = texture2D(sTexture, vTextureCoord);\n" + + " vec4 color = texture2D(sTexture, "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+");\n" + " float y = dot(color, vec4(0.299, 0.587, 0.114, 0));\n" + " gl_FragColor = vec4(y, y, y, color.a);\n" + "}\n"; diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/HueFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/HueFilter.java index a013d5b4..0d790a25 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/HueFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/HueFilter.java @@ -15,7 +15,7 @@ public class HueFilter extends BaseFilter implements OneParameterFilter { private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n" + "precision mediump float;\n" - + "varying vec2 vTextureCoord;\n" + + "varying vec2 "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+";\n" + "uniform samplerExternalOES sTexture;\n" + "uniform float hue;\n" + "void main() {\n" @@ -25,7 +25,7 @@ public class HueFilter extends BaseFilter implements OneParameterFilter { + " vec4 kYIQToR = vec4 (1.0, 0.9563, 0.6210, 0.0);\n" + " vec4 kYIQToG = vec4 (1.0, -0.2721, -0.6474, 0.0);\n" + " vec4 kYIQToB = vec4 (1.0, -1.1070, 1.7046, 0.0);\n" - + " vec4 color = texture2D(sTexture, vTextureCoord);\n" + + " vec4 color = texture2D(sTexture, "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+");\n" + " float YPrime = dot(color, kRGBToYPrime);\n" + " float I = dot(color, kRGBToI);\n" + " float Q = dot(color, kRGBToQ);\n" @@ -96,8 +96,8 @@ public class HueFilter extends BaseFilter implements OneParameterFilter { } @Override - protected void onPreDraw(float[] transformMatrix) { - super.onPreDraw(transformMatrix); + protected void onPreDraw(long timestampUs, float[] transformMatrix) { + super.onPreDraw(timestampUs, transformMatrix); // map it on 360 degree circle float shaderHue = ((hue - 45) / 45f + 0.5f) * -1; GLES20.glUniform1f(hueLocation, shaderHue); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/InvertColorsFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/InvertColorsFilter.java index a5490d88..f4a0f05f 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/InvertColorsFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/InvertColorsFilter.java @@ -11,10 +11,10 @@ public class InvertColorsFilter extends BaseFilter { private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n" + "precision mediump float;\n" - + "varying vec2 vTextureCoord;\n" + + "varying vec2 "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+";\n" + "uniform samplerExternalOES sTexture;\n" + "void main() {\n" - + " vec4 color = texture2D(sTexture, vTextureCoord);\n" + + " vec4 color = texture2D(sTexture, "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+");\n" + " float colorR = (1.0 - color.r) / 1.0;\n" + " float colorG = (1.0 - color.g) / 1.0;\n" + " float colorB = (1.0 - color.b) / 1.0;\n" diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/LomoishFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/LomoishFilter.java index aaaf88a0..87196abc 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/LomoishFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/LomoishFilter.java @@ -7,7 +7,6 @@ import androidx.annotation.NonNull; import com.otaliastudios.cameraview.filter.BaseFilter; import com.otaliastudios.cameraview.internal.GlUtils; -import java.util.Date; import java.util.Random; /** @@ -25,7 +24,7 @@ public class LomoishFilter extends BaseFilter { + "uniform float inv_max_dist;\n" + "vec2 seed;\n" + "float stepsize;\n" - + "varying vec2 vTextureCoord;\n" + + "varying vec2 "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+";\n" + "float rand(vec2 loc) {\n" + " float theta1 = dot(loc, vec2(0.9898, 0.233));\n" + " float theta2 = dot(loc, vec2(12.0, 78.0));\n" @@ -44,18 +43,18 @@ public class LomoishFilter extends BaseFilter { // sharpen + " vec3 nbr_color = vec3(0.0, 0.0, 0.0);\n" + " vec2 coord;\n" - + " vec4 color = texture2D(sTexture, vTextureCoord);\n" - + " coord.x = vTextureCoord.x - 0.5 * stepsizeX;\n" - + " coord.y = vTextureCoord.y - stepsizeY;\n" + + " vec4 color = texture2D(sTexture, "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+");\n" + + " coord.x = "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+".x - 0.5 * stepsizeX;\n" + + " coord.y = "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+".y - stepsizeY;\n" + " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n" - + " coord.x = vTextureCoord.x - stepsizeX;\n" - + " coord.y = vTextureCoord.y + 0.5 * stepsizeY;\n" + + " coord.x = "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+".x - stepsizeX;\n" + + " coord.y = "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+".y + 0.5 * stepsizeY;\n" + " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n" - + " coord.x = vTextureCoord.x + stepsizeX;\n" - + " coord.y = vTextureCoord.y - 0.5 * stepsizeY;\n" + + " coord.x = "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+".x + stepsizeX;\n" + + " coord.y = "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+".y - 0.5 * stepsizeY;\n" + " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n" - + " coord.x = vTextureCoord.x + stepsizeX;\n" - + " coord.y = vTextureCoord.y + 0.5 * stepsizeY;\n" + + " coord.x = "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+".x + stepsizeX;\n" + + " coord.y = "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+".y + 0.5 * stepsizeY;\n" + " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n" + " vec3 s_color = vec3(color.rgb + 0.3 * nbr_color);\n" // cross process @@ -85,12 +84,12 @@ public class LomoishFilter extends BaseFilter { + " }\n" + " c_color.b = s_color.b * 0.5 + 0.25;\n" // blackwhite - + " float dither = rand(vTextureCoord + seed);\n" + + " float dither = rand("+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+" + seed);\n" + " vec3 xform = clamp((c_color.rgb - 0.15) * 1.53846, 0.0, 1.0);\n" + " vec3 temp = clamp((color.rgb + stepsize - 0.15) * 1.53846, 0.0, 1.0);\n" + " vec3 bw_color = clamp(xform + (temp - xform) * (dither - 0.5), 0.0, 1.0);\n" // vignette - + " coord = vTextureCoord - vec2(0.5, 0.5);\n" + + " coord = "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+" - vec2(0.5, 0.5);\n" + " float dist = length(coord * scale);\n" + " float lumen = 0.85 / (1.0 + exp((dist * inv_max_dist - 0.73) * 20.0)) + 0.15;\n" + " gl_FragColor = vec4(bw_color * lumen, color.a);\n" @@ -142,8 +141,8 @@ public class LomoishFilter extends BaseFilter { } @Override - protected void onPreDraw(float[] transformMatrix) { - super.onPreDraw(transformMatrix); + protected void onPreDraw(long timestampUs, float[] transformMatrix) { + super.onPreDraw(timestampUs, transformMatrix); float[] scale = new float[2]; if (width > height) { scale[0] = 1f; diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/PosterizeFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/PosterizeFilter.java index 5ce1d386..12ec8d24 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/PosterizeFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/PosterizeFilter.java @@ -12,8 +12,8 @@ public class PosterizeFilter extends BaseFilter { private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n" + "precision mediump float;\n" + "uniform samplerExternalOES sTexture;\n" - + "varying vec2 vTextureCoord;\n" + "void main() {\n" - + " vec4 color = texture2D(sTexture, vTextureCoord);\n" + + "varying vec2 "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+";\n" + "void main() {\n" + + " vec4 color = texture2D(sTexture, "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+");\n" + " vec3 pcolor;\n" + " pcolor.r = (color.r >= 0.5) ? 0.75 : 0.25;\n" + " pcolor.g = (color.g >= 0.5) ? 0.75 : 0.25;\n" diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/SaturationFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/SaturationFilter.java index 905addbd..32ef78a0 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/SaturationFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/SaturationFilter.java @@ -20,21 +20,24 @@ public class SaturationFilter extends BaseFilter implements OneParameterFilter { + "uniform vec3 exponents;\n" + "float shift;\n" + "vec3 weights;\n" - + "varying vec2 vTextureCoord;\n" + + "varying vec2 "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+";\n" + "void main() {\n" + " weights[0] = " + 2f / 8f + ";\n" + " weights[1] = " + 5f / 8f + ";\n" + " weights[2] = " + 1f / 8f + ";\n" + " shift = " + 1.0f / 255.0f + ";\n" - + " vec4 oldcolor = texture2D(sTexture, vTextureCoord);\n" + + " vec4 oldcolor = texture2D(sTexture, "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME + + ");\n" + " float kv = dot(oldcolor.rgb, weights) + shift;\n" + " vec3 new_color = scale * oldcolor.rgb + (1.0 - scale) * kv;\n" + " gl_FragColor = vec4(new_color, oldcolor.a);\n" - + " vec4 color = texture2D(sTexture, vTextureCoord);\n" + + " vec4 color = texture2D(sTexture, "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME + + ");\n" + " float de = dot(color.rgb, weights);\n" + " float inv_de = 1.0 / de;\n" + " vec3 verynew_color = de * pow(color.rgb * inv_de, exponents);\n" - + " float max_color = max(max(max(verynew_color.r, verynew_color.g), verynew_color.b), 1.0);\n" + + " float max_color = max(max(max(verynew_color.r, verynew_color.g), " + + "verynew_color.b), 1.0);\n" + " gl_FragColor = gl_FragColor+vec4(verynew_color / max_color, color.a);\n" + "}\n"; @@ -103,8 +106,8 @@ public class SaturationFilter extends BaseFilter implements OneParameterFilter { } @Override - protected void onPreDraw(float[] transformMatrix) { - super.onPreDraw(transformMatrix); + protected void onPreDraw(long timestampUs, float[] transformMatrix) { + super.onPreDraw(timestampUs, transformMatrix); if (scale > 0.0f) { GLES20.glUniform1f(scaleLocation, 0F); GlUtils.checkError("glUniform1f"); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/SepiaFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/SepiaFilter.java index 7099bbea..4631a3ef 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/SepiaFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/SepiaFilter.java @@ -13,7 +13,7 @@ public class SepiaFilter extends BaseFilter { + "precision mediump float;\n" + "uniform samplerExternalOES sTexture;\n" + "mat3 matrix;\n" - + "varying vec2 vTextureCoord;\n" + + "varying vec2 "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+";\n" + "void main() {\n" + " matrix[0][0]=" + 805.0f / 2048.0f + ";\n" + " matrix[0][1]=" + 715.0f / 2048.0f + ";\n" @@ -24,7 +24,7 @@ public class SepiaFilter extends BaseFilter { + " matrix[2][0]=" + 387.0f / 2048.0f + ";\n" + " matrix[2][1]=" + 344.0f / 2048.0f + ";\n" + " matrix[2][2]=" + 268.0f / 2048.0f + ";\n" - + " vec4 color = texture2D(sTexture, vTextureCoord);\n" + + " vec4 color = texture2D(sTexture, "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+");\n" + " vec3 new_color = min(matrix * color.rgb, 1.0);\n" + " gl_FragColor = vec4(new_color.rgb, color.a);\n" + "}\n"; diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/SharpnessFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/SharpnessFilter.java index 5e60543a..88b2ae76 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/SharpnessFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/SharpnessFilter.java @@ -19,22 +19,22 @@ public class SharpnessFilter extends BaseFilter implements OneParameterFilter { + "uniform float scale;\n" + "uniform float stepsizeX;\n" + "uniform float stepsizeY;\n" - + "varying vec2 vTextureCoord;\n" + + "varying vec2 "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+";\n" + "void main() {\n" + " vec3 nbr_color = vec3(0.0, 0.0, 0.0);\n" + " vec2 coord;\n" - + " vec4 color = texture2D(sTexture, vTextureCoord);\n" - + " coord.x = vTextureCoord.x - 0.5 * stepsizeX;\n" - + " coord.y = vTextureCoord.y - stepsizeY;\n" + + " vec4 color = texture2D(sTexture, "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+");\n" + + " coord.x = "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+".x - 0.5 * stepsizeX;\n" + + " coord.y = "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+".y - stepsizeY;\n" + " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n" - + " coord.x = vTextureCoord.x - stepsizeX;\n" - + " coord.y = vTextureCoord.y + 0.5 * stepsizeY;\n" + + " coord.x = "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+".x - stepsizeX;\n" + + " coord.y = "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+".y + 0.5 * stepsizeY;\n" + " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n" - + " coord.x = vTextureCoord.x + stepsizeX;\n" - + " coord.y = vTextureCoord.y - 0.5 * stepsizeY;\n" + + " coord.x = "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+".x + stepsizeX;\n" + + " coord.y = "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+".y - 0.5 * stepsizeY;\n" + " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n" - + " coord.x = vTextureCoord.x + stepsizeX;\n" - + " coord.y = vTextureCoord.y + 0.5 * stepsizeY;\n" + + " coord.x = "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+".x + stepsizeX;\n" + + " coord.y = "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+".y + 0.5 * stepsizeY;\n" + " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n" + " gl_FragColor = vec4(color.rgb - 2.0 * scale * nbr_color, color.a);\n" + "}\n"; @@ -116,8 +116,8 @@ public class SharpnessFilter extends BaseFilter implements OneParameterFilter { } @Override - protected void onPreDraw(float[] transformMatrix) { - super.onPreDraw(transformMatrix); + protected void onPreDraw(long timestampUs, float[] transformMatrix) { + super.onPreDraw(timestampUs, transformMatrix); GLES20.glUniform1f(scaleLocation, scale); GlUtils.checkError("glUniform1f"); GLES20.glUniform1f(stepSizeXLocation, 1.0F / width); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/TemperatureFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/TemperatureFilter.java index b9c7e6df..b6137e85 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/TemperatureFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/TemperatureFilter.java @@ -17,9 +17,9 @@ public class TemperatureFilter extends BaseFilter implements OneParameterFilter + "precision mediump float;\n" + "uniform samplerExternalOES sTexture;\n" + "uniform float scale;\n" - + "varying vec2 vTextureCoord;\n" + + "varying vec2 "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+";\n" + "void main() {\n" - + " vec4 color = texture2D(sTexture, vTextureCoord);\n" + + " vec4 color = texture2D(sTexture, "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+");\n" + " vec3 new_color = color.rgb;\n" + " new_color.r = color.r + color.r * ( 1.0 - color.r) * scale;\n" + " new_color.b = color.b - color.b * ( 1.0 - color.b) * scale;\n" @@ -94,8 +94,8 @@ public class TemperatureFilter extends BaseFilter implements OneParameterFilter } @Override - protected void onPreDraw(float[] transformMatrix) { - super.onPreDraw(transformMatrix); + protected void onPreDraw(long timestampUs, float[] transformMatrix) { + super.onPreDraw(timestampUs, transformMatrix); GLES20.glUniform1f(scaleLocation, scale); GlUtils.checkError("glUniform1f"); } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/TintFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/TintFilter.java index 3958c20f..51a00ecc 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/TintFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/TintFilter.java @@ -21,12 +21,12 @@ public class TintFilter extends BaseFilter implements OneParameterFilter { + "uniform samplerExternalOES sTexture;\n" + "uniform vec3 tint;\n" + "vec3 color_ratio;\n" - + "varying vec2 vTextureCoord;\n" + + "varying vec2 "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+";\n" + "void main() {\n" + " color_ratio[0] = " + 0.21f + ";\n" + " color_ratio[1] = " + 0.71f + ";\n" + " color_ratio[2] = " + 0.07f + ";\n" - + " vec4 color = texture2D(sTexture, vTextureCoord);\n" + + " vec4 color = texture2D(sTexture, "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+");\n" + " float avg_color = dot(color_ratio, color.rgb);\n" + " vec3 new_color = min(0.8 * avg_color + 0.2 * tint, 1.0);\n" + " gl_FragColor = vec4(new_color.rgb, color.a);\n" + "}\n"; @@ -91,8 +91,8 @@ public class TintFilter extends BaseFilter implements OneParameterFilter { } @Override - protected void onPreDraw(float[] transformMatrix) { - super.onPreDraw(transformMatrix); + protected void onPreDraw(long timestampUs, float[] transformMatrix) { + super.onPreDraw(timestampUs, transformMatrix); float[] channels = new float[]{ Color.red(tint) / 255f, Color.green(tint) / 255f, diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/VignetteFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/VignetteFilter.java index b4b8af3b..df5089fa 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/VignetteFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/VignetteFilter.java @@ -21,13 +21,14 @@ public class VignetteFilter extends BaseFilter implements TwoParameterFilter { + "uniform float inv_max_dist;\n" + "uniform float shade;\n" + "uniform vec2 scale;\n" - + "varying vec2 vTextureCoord;\n" + + "varying vec2 "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+";\n" + "void main() {\n" + " const float slope = 20.0;\n" - + " vec2 coord = vTextureCoord - vec2(0.5, 0.5);\n" + + " vec2 coord = "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+" - vec2(0.5, 0.5);\n" + " float dist = length(coord * scale);\n" - + " float lumen = shade / (1.0 + exp((dist * inv_max_dist - range) * slope)) + (1.0 - shade);\n" - + " vec4 color = texture2D(sTexture, vTextureCoord);\n" + + " float lumen = shade / (1.0 + exp((dist * inv_max_dist - range) * slope)) " + + "+ (1.0 - shade);\n" + + " vec4 color = texture2D(sTexture, "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+");\n" + " gl_FragColor = vec4(color.rgb * lumen, color.a);\n" + "}\n"; @@ -144,8 +145,8 @@ public class VignetteFilter extends BaseFilter implements TwoParameterFilter { } @Override - protected void onPreDraw(float[] transformMatrix) { - super.onPreDraw(transformMatrix); + protected void onPreDraw(long timestampUs, float[] transformMatrix) { + super.onPreDraw(timestampUs, transformMatrix); float[] scale = new float[2]; if (mWidth > mHeight) { scale[0] = 1f; diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/frame/Frame.java b/cameraview/src/main/java/com/otaliastudios/cameraview/frame/Frame.java index cb9a9d38..8ee2f97c 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/frame/Frame.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/frame/Frame.java @@ -44,8 +44,8 @@ public class Frame { if (!hasContent()) { LOG.e("Frame is dead! time:", mTime, "lastTime:", mLastTime); throw new RuntimeException("You should not access a released frame. " + - "If this frame was passed to a FrameProcessor, you can only use its contents synchronously, " + - "for the duration of the process() method."); + "If this frame was passed to a FrameProcessor, you can only use its contents " + + "synchronously, for the duration of the process() method."); } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/frame/FrameManager.java b/cameraview/src/main/java/com/otaliastudios/cameraview/frame/FrameManager.java index 0ab68947..55f2a5e5 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/frame/FrameManager.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/frame/FrameManager.java @@ -28,18 +28,20 @@ import java.util.concurrent.LinkedBlockingQueue; * For both byte buffers and frames to get back to the FrameManager pool, all you have to do * is call {@link Frame#release()} when done. * - * Other than this, the FrameManager can work in two modes, depending on whether a {@link BufferCallback} - * is passed to the constructor. The modes changes the buffer behavior. + * Other than this, the FrameManager can work in two modes, depending on whether a + * {@link BufferCallback} is passed to the constructor. The modes changes the buffer behavior. * * 1. {@link #BUFFER_MODE_DISPATCH}: in this mode, as soon as we have a buffer, it is dispatched to * the {@link BufferCallback}. The callback should then fill the buffer, and finally call * {@link #getFrame(byte[], long, int)} to receive a frame. * This is used for Camera1. * - * 2. {@link #BUFFER_MODE_ENQUEUE}: in this mode, the manager internally keeps a queue of byte buffers, - * instead of handing them to the callback. The users can ask for buffers through {@link #getBuffer()}. - * This buffer can be filled with data and used to get a frame {@link #getFrame(byte[], long, int)}, - * or, in case it was not filled, returned to the queue using {@link #onBufferUnused(byte[])}. + * 2. {@link #BUFFER_MODE_ENQUEUE}: in this mode, the manager internally keeps a queue of byte + * buffers, instead of handing them to the callback. The users can ask for buffers through + * {@link #getBuffer()}. + * This buffer can be filled with data and used to get a frame + * {@link #getFrame(byte[], long, int)}, or, in case it was not filled, returned to the queue + * using {@link #onBufferUnused(byte[])}. * This is used for Camera2. */ public class FrameManager { @@ -149,7 +151,8 @@ public class FrameManager { @Nullable public byte[] getBuffer() { if (mBufferMode != BUFFER_MODE_ENQUEUE) { - throw new IllegalStateException("Can't call getBuffer() when not in BUFFER_MODE_ENQUEUE."); + throw new IllegalStateException("Can't call getBuffer() " + + "when not in BUFFER_MODE_ENQUEUE."); } return mBufferQueue.poll(); } @@ -161,7 +164,8 @@ public class FrameManager { */ public void onBufferUnused(@NonNull byte[] buffer) { if (mBufferMode != BUFFER_MODE_ENQUEUE) { - throw new IllegalStateException("Can't call onBufferUnused() when not in BUFFER_MODE_ENQUEUE."); + throw new IllegalStateException("Can't call onBufferUnused() " + + "when not in BUFFER_MODE_ENQUEUE."); } if (isSetUp()) { @@ -188,7 +192,8 @@ public class FrameManager { @NonNull public Frame getFrame(@NonNull byte[] data, long time, int rotation) { if (!isSetUp()) { - throw new IllegalStateException("Can't call getFrame() after releasing or before setUp."); + throw new IllegalStateException("Can't call getFrame() after releasing " + + "or before setUp."); } Frame frame = mFrameQueue.poll(); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/Gesture.java b/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/Gesture.java index 8f38f89f..148abf0d 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/Gesture.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/Gesture.java @@ -8,7 +8,8 @@ import androidx.annotation.NonNull; /** * Gestures listen to finger gestures over the {@link CameraView} bounds and can be mapped - * to one or more camera controls using XML attributes or {@link CameraView#mapGesture(Gesture, GestureAction)}. + * to one or more camera controls using XML attributes or {@link CameraView#mapGesture(Gesture, + * GestureAction)}. * * Not every gesture can control a certain action. For example, pinch gestures can only control * continuous values, such as zoom or AE correction. Single point gestures, on the other hand, diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/GestureAction.java b/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/GestureAction.java index e384ad72..ca86a6c3 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/GestureAction.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/GestureAction.java @@ -2,6 +2,7 @@ package com.otaliastudios.cameraview.gesture; import com.otaliastudios.cameraview.CameraView; +import com.otaliastudios.cameraview.filter.Filter; import com.otaliastudios.cameraview.markers.AutoFocusMarker; import androidx.annotation.NonNull; @@ -63,7 +64,7 @@ public enum GestureAction { EXPOSURE_CORRECTION(4, GestureType.CONTINUOUS), /** - * Controls the first parameter of a real-time {@link com.otaliastudios.cameraview.filter.Filter}, + * Controls the first parameter of a real-time {@link Filter}, * if it accepts one. This action can be mapped to continuous gestures: * * - {@link Gesture#PINCH} @@ -73,7 +74,7 @@ public enum GestureAction { FILTER_CONTROL_1(5, GestureType.CONTINUOUS), /** - * Controls the second parameter of a real-time {@link com.otaliastudios.cameraview.filter.Filter}, + * Controls the second parameter of a real-time {@link Filter}, * if it accepts one. This action can be mapped to continuous gestures: * * - {@link Gesture#PINCH} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/GestureParser.java b/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/GestureParser.java index 84fde50a..f6eb0a35 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/GestureParser.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/GestureParser.java @@ -18,11 +18,17 @@ public class GestureParser { private int verticalScrollAction; public GestureParser(@NonNull TypedArray array) { - this.tapAction = array.getInteger(R.styleable.CameraView_cameraGestureTap, GestureAction.DEFAULT_TAP.value()); - this.longTapAction = array.getInteger(R.styleable.CameraView_cameraGestureLongTap, GestureAction.DEFAULT_LONG_TAP.value()); - this.pinchAction = array.getInteger(R.styleable.CameraView_cameraGesturePinch, GestureAction.DEFAULT_PINCH.value()); - this.horizontalScrollAction = array.getInteger(R.styleable.CameraView_cameraGestureScrollHorizontal, GestureAction.DEFAULT_SCROLL_HORIZONTAL.value()); - this.verticalScrollAction = array.getInteger(R.styleable.CameraView_cameraGestureScrollVertical, GestureAction.DEFAULT_SCROLL_VERTICAL.value()); + tapAction = array.getInteger(R.styleable.CameraView_cameraGestureTap, + GestureAction.DEFAULT_TAP.value()); + longTapAction = array.getInteger(R.styleable.CameraView_cameraGestureLongTap, + GestureAction.DEFAULT_LONG_TAP.value()); + pinchAction = array.getInteger(R.styleable.CameraView_cameraGesturePinch, + GestureAction.DEFAULT_PINCH.value()); + horizontalScrollAction = array.getInteger( + R.styleable.CameraView_cameraGestureScrollHorizontal, + GestureAction.DEFAULT_SCROLL_HORIZONTAL.value()); + verticalScrollAction = array.getInteger(R.styleable.CameraView_cameraGestureScrollVertical, + GestureAction.DEFAULT_SCROLL_VERTICAL.value()); } private GestureAction get(int which) { diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/PinchGestureFinder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/PinchGestureFinder.java index 29d693ae..4da92bf5 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/PinchGestureFinder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/PinchGestureFinder.java @@ -19,7 +19,8 @@ public class PinchGestureFinder extends GestureFinder { public PinchGestureFinder(@NonNull Controller controller) { super(controller, 2); setGesture(Gesture.PINCH); - mDetector = new ScaleGestureDetector(controller.getContext(), new ScaleGestureDetector.SimpleOnScaleGestureListener() { + mDetector = new ScaleGestureDetector(controller.getContext(), + new ScaleGestureDetector.SimpleOnScaleGestureListener() { @Override public boolean onScale(ScaleGestureDetector detector) { mNotify = true; diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/ScrollGestureFinder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/ScrollGestureFinder.java index 8eece3ed..742f01ca 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/ScrollGestureFinder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/ScrollGestureFinder.java @@ -21,10 +21,14 @@ public class ScrollGestureFinder extends GestureFinder { public ScrollGestureFinder(final @NonNull Controller controller) { super(controller, 2); - mDetector = new GestureDetector(controller.getContext(), new GestureDetector.SimpleOnGestureListener() { + mDetector = new GestureDetector(controller.getContext(), + new GestureDetector.SimpleOnGestureListener() { @Override - public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + public boolean onScroll(MotionEvent e1, + MotionEvent e2, + float distanceX, + float distanceY) { boolean horizontal; LOG.i("onScroll:", "distanceX="+distanceX, "distanceY="+distanceY); if (e1 == null || e2 == null) return false; // Got some crashes about this. @@ -39,7 +43,8 @@ public class ScrollGestureFinder extends GestureFinder { horizontal = getGesture() == Gesture.SCROLL_HORIZONTAL; } getPoint(1).set(e2.getX(), e2.getY()); - mFactor = horizontal ? (distanceX / controller.getWidth()) : (distanceY / controller.getHeight()); + mFactor = horizontal ? (distanceX / controller.getWidth()) + : (distanceY / controller.getHeight()); mFactor = horizontal ? -mFactor : mFactor; // When vertical, up = positive mNotify = true; return true; diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/TapGestureFinder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/TapGestureFinder.java index 87f581f9..8457933a 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/TapGestureFinder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/TapGestureFinder.java @@ -16,7 +16,8 @@ public class TapGestureFinder extends GestureFinder { public TapGestureFinder(@NonNull Controller controller) { super(controller, 1); - mDetector = new GestureDetector(controller.getContext(), new GestureDetector.SimpleOnGestureListener() { + mDetector = new GestureDetector(controller.getContext(), + new GestureDetector.SimpleOnGestureListener() { @Override public boolean onSingleTapUp(MotionEvent e) { diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/DeviceEncoders.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/DeviceEncoders.java index 69c94a33..54707e3f 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/DeviceEncoders.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/DeviceEncoders.java @@ -20,9 +20,18 @@ import java.util.List; /** * Checks the capabilities of device encoders and adjust parameters to ensure * that they'll be supported by the final encoder. - * This can choose the encoder in two ways, based on the mode flag: * - * 1. {@link #MODE_TAKE_FIRST} + * Methods in this class might throw either a {@link VideoException} or a {@link AudioException}. + * Throwing this exception means that the given parameters will not be supported by the encoder + * for that type, and cannot be tweaked to be. + * + * When this happens, users should retry with a new {@link DeviceEncoders} instance, but with + * the audio or video encoder offset incremented. This offset is the position in the encoder list + * from which we'll choose the potential encoder. + * + * This class will inspect the encoders list in two ways, based on the mode flag: + * + * 1. {@link #MODE_RESPECT_ORDER} * * Chooses the encoder as the first one that matches the given mime type. * This is what {@link android.media.MediaCodec#createEncoderByType(String)} does, @@ -40,11 +49,12 @@ import java.util.List; * - MediaCodecList (https://android.googlesource.com/platform/frameworks/av/+/master/media/libstagefright/MediaCodecList.cpp#322) * * To be fair, what {@link android.media.MediaRecorder} does is actually choose the first one - * that configures itself without errors. We currently do not offer this option here. TODO + * that configures itself without errors. We currently do not offer this option here. + * TODO add a tryConfigure() step, that throws AudioException/VideoException ? * * 2. {@link #MODE_PREFER_HARDWARE} * - * This takes the list - as ordered by the vendor - and just sorts it such that hardware encoders + * This takes the list - as ordered by the vendor - and just sorts it so that hardware encoders * are preferred over software ones. It's questionable whether this is good or not. Some vendors * might forget to put hardware encoders first in the list, some others might put poor hardware * encoders on the bottom of the list on purpose. @@ -56,9 +66,29 @@ public class DeviceEncoders { @VisibleForTesting static boolean ENABLED = Build.VERSION.SDK_INT >= 21; - public final static int MODE_TAKE_FIRST = 0; + public final static int MODE_RESPECT_ORDER = 0; public final static int MODE_PREFER_HARDWARE = 1; + /** + * Exception thrown when trying to find appropriate values + * for a video encoder. + */ + public class VideoException extends RuntimeException { + private VideoException(@NonNull String message) { + super(message); + } + } + + /** + * Exception thrown when trying to find appropriate values + * for an audio encoder. Currently never thrown. + */ + public class AudioException extends RuntimeException { + private AudioException(@NonNull String message) { + super(message); + } + } + @SuppressWarnings("FieldCanBeLocal") private final MediaCodecInfo mVideoEncoder; @SuppressWarnings("FieldCanBeLocal") @@ -67,17 +97,23 @@ public class DeviceEncoders { private final MediaCodecInfo.AudioCapabilities mAudioCapabilities; @SuppressLint("NewApi") - public DeviceEncoders(@NonNull String videoType, @NonNull String audioType, int mode) { + public DeviceEncoders(int mode, + @NonNull String videoType, + @NonNull String audioType, + int videoOffset, + int audioOffset) { // We could still get a list of MediaCodecInfo for API >= 16, but it seems that the APIs // for querying the availability of a specified MediaFormat were only added in 21 anyway. if (ENABLED) { List encoders = getDeviceEncoders(); - mVideoEncoder = findDeviceEncoder(encoders, videoType, mode); + mVideoEncoder = findDeviceEncoder(encoders, videoType, mode, videoOffset); LOG.i("Enabled. Found video encoder:", mVideoEncoder.getName()); - mAudioEncoder = findDeviceEncoder(encoders, audioType, mode); + mAudioEncoder = findDeviceEncoder(encoders, audioType, mode, audioOffset); LOG.i("Enabled. Found audio encoder:", mAudioEncoder.getName()); - mVideoCapabilities = mVideoEncoder.getCapabilitiesForType(videoType).getVideoCapabilities(); - mAudioCapabilities = mAudioEncoder.getCapabilitiesForType(audioType).getAudioCapabilities(); + mVideoCapabilities = mVideoEncoder.getCapabilitiesForType(videoType) + .getVideoCapabilities(); + mAudioCapabilities = mAudioEncoder.getCapabilitiesForType(audioType) + .getAudioCapabilities(); } else { mVideoEncoder = null; mAudioEncoder = null; @@ -123,7 +159,7 @@ public class DeviceEncoders { /** * Finds the encoder we'll be using, depending on the given mode flag: - * - {@link #MODE_TAKE_FIRST} will just take the first of the list + * - {@link #MODE_RESPECT_ORDER} will just take the first of the list * - {@link #MODE_PREFER_HARDWARE} will prefer hardware encoders * Throws if we find no encoder for this type. * @@ -135,7 +171,10 @@ public class DeviceEncoders { @SuppressLint("NewApi") @NonNull @VisibleForTesting - MediaCodecInfo findDeviceEncoder(@NonNull List encoders, @NonNull String mimeType, int mode) { + MediaCodecInfo findDeviceEncoder(@NonNull List encoders, + @NonNull String mimeType, + int mode, + int offset) { ArrayList results = new ArrayList<>(); for (MediaCodecInfo encoder : encoders) { String[] types = encoder.getSupportedTypes(); @@ -160,10 +199,12 @@ public class DeviceEncoders { } }); } - if (results.isEmpty()) { + if (results.size() < offset + 1) { + // This should not be a VideoException or AudioException - we want the process + // to crash here. throw new RuntimeException("No encoders for type:" + mimeType); } - return results.get(0); + return results.get(offset); } /** @@ -199,19 +240,19 @@ public class DeviceEncoders { // It's still possible that we're BELOW the lower. if (!mVideoCapabilities.getSupportedWidths().contains(width)) { - throw new RuntimeException("Width not supported after adjustment." + + throw new VideoException("Width not supported after adjustment." + " Desired:" + width + " Range:" + mVideoCapabilities.getSupportedWidths()); } if (!mVideoCapabilities.getSupportedHeights().contains(height)) { - throw new RuntimeException("Height not supported after adjustment." + + throw new VideoException("Height not supported after adjustment." + " Desired:" + height + " Range:" + mVideoCapabilities.getSupportedHeights()); } // It's still possible that we're unsupported for other reasons. if (!mVideoCapabilities.isSizeSupported(width, height)) { - throw new RuntimeException("Size not supported for unknown reason." + + throw new VideoException("Size not supported for unknown reason." + " Might be an aspect ratio issue." + " Desired size:" + new Size(width, height)); } @@ -231,7 +272,9 @@ public class DeviceEncoders { public int getSupportedVideoBitRate(int bitRate) { if (!ENABLED) return bitRate; int newBitRate = mVideoCapabilities.getBitrateRange().clamp(bitRate); - LOG.i("getSupportedVideoBitRate -", "inputRate:", bitRate, "adjustedRate:", newBitRate); + LOG.i("getSupportedVideoBitRate -", + "inputRate:", bitRate, + "adjustedRate:", newBitRate); return newBitRate; } @@ -248,7 +291,9 @@ public class DeviceEncoders { int newFrameRate = (int) (double) mVideoCapabilities .getSupportedFrameRatesFor(size.getWidth(), size.getHeight()) .clamp((double) frameRate); - LOG.i("getSupportedVideoFrameRate -", "inputRate:", frameRate, "adjustedRate:", newFrameRate); + LOG.i("getSupportedVideoFrameRate -", + "inputRate:", frameRate, + "adjustedRate:", newFrameRate); return newFrameRate; } @@ -263,7 +308,9 @@ public class DeviceEncoders { public int getSupportedAudioBitRate(int bitRate) { if (!ENABLED) return bitRate; int newBitRate = mAudioCapabilities.getBitrateRange().clamp(bitRate); - LOG.i("getSupportedAudioBitRate -", "inputRate:", bitRate, "adjustedRate:", newBitRate); + LOG.i("getSupportedAudioBitRate -", + "inputRate:", bitRate, + "adjustedRate:", newBitRate); return newBitRate; } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/GlUtils.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/GlUtils.java index c5af965b..05d5c4d8 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/GlUtils.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/GlUtils.java @@ -26,7 +26,8 @@ public class GlUtils { public static void checkError(@NonNull String opName) { int error = GLES20.glGetError(); if (error != GLES20.GL_NO_ERROR) { - String message = LOG.e("Error during", opName, "glError 0x", Integer.toHexString(error)); + String message = LOG.e("Error during", opName, "glError 0x", + Integer.toHexString(error)); throw new RuntimeException(message); } } @@ -47,7 +48,8 @@ public class GlUtils { int[] compiled = new int[1]; GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0); if (compiled[0] == 0) { - LOG.e("Could not compile shader", shaderType, ":", GLES20.glGetShaderInfoLog(shader)); + LOG.e("Could not compile shader", shaderType, ":", + GLES20.glGetShaderInfoLog(shader)); GLES20.glDeleteShader(shader); shader = 0; } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/GridLinesLayout.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/GridLinesLayout.java index 844cdc6b..8743858f 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/GridLinesLayout.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/GridLinesLayout.java @@ -43,7 +43,8 @@ public class GridLinesLayout extends View { super(context, attrs); horiz = new ColorDrawable(gridColor); vert = new ColorDrawable(gridColor); - width = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0.9f, context.getResources().getDisplayMetrics()); + width = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0.9f, + context.getResources().getDisplayMetrics()); } @Override diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/Issue514Workaround.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/Issue514Workaround.java index 13745800..828a5aff 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/Issue514Workaround.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/Issue514Workaround.java @@ -36,8 +36,9 @@ import com.otaliastudios.cameraview.preview.RendererThread; * 9B. Publish overlays to GL texture using overlaySurfaceTexture.updateTexImage() * 9C. GLES - draw textureId * 9D. GLES - draw overlayTextureId - * Both textures are drawn on the same EGLWindow and we manage to overlay them with {@link GLES20#GL_BLEND}. - * This is the whole procedure and it works for the majority of devices and situations. + * Both textures are drawn on the same EGLWindow and we manage to overlay them with + * {@link GLES20#GL_BLEND}. This is the whole procedure and it works for the majority of + * devices and situations. * * ISSUE DESCRIPTION * The #514 issue can be described as follows: @@ -66,7 +67,8 @@ import com.otaliastudios.cameraview.preview.RendererThread; * * So a pixel format conversion takes place, when it shouldn't happen. We can't solve this: * - It is done at a lower level, there's no real way for us to specify the surface format, but - * it seems that these devices will prefer a YUV format and misunderstand our {@link Canvas} pixels. + * it seems that these devices will prefer a YUV format and misunderstand our {@link Canvas} + * pixels. * - There is also no way to identify which devices will present this issue, it's a bug somewhere * and it is implementation specific. * @@ -74,16 +76,18 @@ import com.otaliastudios.cameraview.preview.RendererThread; * Hard to say why, but using this class fixes the described issue. * It seems that when the {@link SurfaceTexture#updateTexImage()} method for the overlay surface * is called - the one that updates the overlayTextureId - we must ensure that the CURRENTLY - * BOUND TEXTURE ID IS NOT 0. The id we choose to apply might be cameraTextureId, or overlayTextureId, - * or probably whatever other valid id, and should be passed to {@link #Issue514Workaround(int)}. + * BOUND TEXTURE ID IS NOT 0. The id we choose to apply might be cameraTextureId, or + * overlayTextureId, or probably whatever other valid id, and should be passed to + * {@link #Issue514Workaround(int)}. * [Tested with cameraTextureId and overlayTextureId: both do work.] * [Tested with invalid id like 9999. This won't work.] * - * This makes no sense, since overlaySurfaceTexture.updateTexImage() is setting it to overlayTextureId - * anyway, but it fixes the issue. Specifically, after any draw operation with {@link EglViewport}, - * the bound texture is reset to 0 so this must be undone here. We offer: + * This makes no sense, since overlaySurfaceTexture.updateTexImage() is setting it to + * overlayTextureId anyway, but it fixes the issue. Specifically, after any draw operation with + * {@link EglViewport}, the bound texture is reset to 0 so this must be undone here. We offer: * - * - {@link #beforeOverlayUpdateTexImage()} to be called before the {@link SurfaceTexture#updateTexImage()} call + * - {@link #beforeOverlayUpdateTexImage()} to be called before the + * {@link SurfaceTexture#updateTexImage()} call * - {@link #end()} to release and bring things back to normal state * * Since updating and rendering can happen on different threads with a shared EGL context, @@ -93,9 +97,9 @@ import com.otaliastudios.cameraview.preview.RendererThread; * REFERENCES * https://github.com/natario1/CameraView/issues/514 * https://android.googlesource.com/platform/frameworks/native/+/5c1139f/libs/gui/SurfaceTexture.cpp - * I can see here that SurfaceTexture does indeed call glBindTexture with the same parameters whenever - * updateTexImage is called, but it also does other gl stuff first. This other gl stuff might be - * breaking when we don't have a bound texture on some specific hardware implementation. + * I can see here that SurfaceTexture does indeed call glBindTexture with the same parameters + * whenever updateTexImage is called, but it also does other gl stuff first. This other gl stuff + * might be breaking when we don't have a bound texture on some specific hardware implementation. */ public class Issue514Workaround { diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglBaseSurface.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglBaseSurface.java index 9ec4315a..f9ceec53 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglBaseSurface.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglBaseSurface.java @@ -229,7 +229,8 @@ public class EglBaseSurface { int height = getHeight(); ByteBuffer buf = ByteBuffer.allocateDirect(width * height * 4); buf.order(ByteOrder.LITTLE_ENDIAN); - GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buf); + GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, + buf); GlUtils.checkError("glReadPixels"); buf.rewind(); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglCore.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglCore.java index 76d2a62b..3d9c6b65 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglCore.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglCore.java @@ -172,8 +172,8 @@ public final class EglCore { } EGLConfig[] configs = new EGLConfig[1]; int[] numConfigs = new int[1]; - if (!EGL14.eglChooseConfig(mEGLDisplay, attribList, 0, configs, 0, configs.length, - numConfigs, 0)) { + if (!EGL14.eglChooseConfig(mEGLDisplay, attribList, 0, configs, 0, + configs.length, numConfigs, 0)) { Log.w(TAG, "unable to find RGB8888 / " + version + " EGLConfig"); return null; } @@ -210,7 +210,8 @@ public final class EglCore { // the EGL state, so if a surface or context is still current on another // thread we can't fully release it here. Exceptions thrown from here // are quietly discarded. Complain in the log file. - Log.w(TAG, "WARNING: EglCore was not explicitly released -- state may be leaked"); + Log.w(TAG, "WARNING: EglCore was not explicitly released! " + + "State may be leaked"); release(); } } finally { diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglViewport.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglViewport.java index ad43de64..685b00ca 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglViewport.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglViewport.java @@ -35,7 +35,8 @@ public class EglViewport { } private void createProgram() { - mProgramHandle = GlUtils.createProgram(mFilter.getVertexShader(), mFilter.getFragmentShader()); + mProgramHandle = GlUtils.createProgram(mFilter.getVertexShader(), + mFilter.getFragmentShader()); mFilter.onCreate(mProgramHandle); } @@ -57,10 +58,14 @@ public class EglViewport { GLES20.glBindTexture(mTextureTarget, texId); GlUtils.checkError("glBindTexture " + texId); - GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST); - GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); - GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); - GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, + GLES20.GL_NEAREST); + GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, + GLES20.GL_LINEAR); + GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, + GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, + GLES20.GL_CLAMP_TO_EDGE); GlUtils.checkError("glTexParameter"); return texId; @@ -72,7 +77,7 @@ public class EglViewport { mPendingFilter = filter; } - public void drawFrame(int textureId, float[] textureMatrix) { + public void drawFrame(long timestampUs, int textureId, float[] textureMatrix) { if (mPendingFilter != null) { release(); mFilter = mPendingFilter; @@ -89,7 +94,7 @@ public class EglViewport { GLES20.glBindTexture(mTextureTarget, textureId); // Draw. - mFilter.draw(textureMatrix); + mFilter.draw(timestampUs, textureMatrix); // Release. GLES20.glBindTexture(mTextureTarget, 0); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglWindowSurface.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglWindowSurface.java index 8605f3c6..0cb49776 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglWindowSurface.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglWindowSurface.java @@ -77,25 +77,4 @@ public class EglWindowSurface extends EglBaseSurface { mSurface = null; } } - - /** - * Recreate the EGLSurface, using the new EglBase. The caller should have already - * freed the old EGLSurface with releaseEglSurface(). - *

- * This is useful when we want to update the EGLSurface associated with a Surface. - * For example, if we want to share with a different EGLContext, which can only - * be done by tearing down and recreating the context. (That's handled by the caller; - * this just creates a new EGLSurface for the Surface we were handed earlier.) - *

- * If the previous EGLSurface isn't fully destroyed, e.g. it's still current on a - * context somewhere, the create call will fail with complaints from the Surface - * about already being connected. - */ - public void recreate(EglCore newEglCore) { - if (mSurface == null) { - throw new RuntimeException("not yet implemented for SurfaceTexture"); - } - mEglCore = newEglCore; // switch to new context - createWindowSurface(mSurface); // create new surface - } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/CamcorderProfiles.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/CamcorderProfiles.java index 74d9c8f7..cc70194c 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/CamcorderProfiles.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/CamcorderProfiles.java @@ -31,14 +31,16 @@ public class CamcorderProfiles { sizeToProfileMap.put(new Size(1280, 720), CamcorderProfile.QUALITY_720P); sizeToProfileMap.put(new Size(1920, 1080), CamcorderProfile.QUALITY_1080P); if (Build.VERSION.SDK_INT >= 21) { - sizeToProfileMap.put(new Size(3840, 2160), CamcorderProfile.QUALITY_2160P); + sizeToProfileMap.put(new Size(3840, 2160), + CamcorderProfile.QUALITY_2160P); } } /** * Returns a CamcorderProfile that's somewhat coherent with the target size, - * to ensure we get acceptable video/audio parameters for MediaRecorders (most notably the bitrate). + * to ensure we get acceptable video/audio parameters for MediaRecorders + * (most notably the bitrate). * * @param cameraId the camera2 id * @param targetSize the target video size @@ -57,7 +59,8 @@ public class CamcorderProfiles { /** * Returns a CamcorderProfile that's somewhat coherent with the target size, - * to ensure we get acceptable video/audio parameters for MediaRecorders (most notably the bitrate). + * to ensure we get acceptable video/audio parameters for MediaRecorders + * (most notably the bitrate). * * @param cameraId the camera id * @param targetSize the target video size diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/ImageHelper.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/ImageHelper.java index b099b404..6f6127a0 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/ImageHelper.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/ImageHelper.java @@ -67,7 +67,8 @@ public class ImageHelper { } if (pixelStride == 2 && rowStride == width && uBuffer.get(0) == vBuffer.get(1)) { - // maybe V an U planes overlap as per NV21, which means vBuffer[1] is alias of uBuffer[0] + // maybe V an U planes overlap as per NV21, which means vBuffer[1] + // is alias of uBuffer[0] byte savePixel = vBuffer.get(1); vBuffer.put(1, (byte)0); if (uBuffer.get(0) == 0) { diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/OrientationHelper.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/OrientationHelper.java index 4319ac94..a72a626e 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/OrientationHelper.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/OrientationHelper.java @@ -12,7 +12,8 @@ import android.view.WindowManager; /** * Helps with keeping track of both device orientation (which changes when device is rotated) - * and the display offset (which depends on the activity orientation wrt the device default orientation). + * and the display offset (which depends on the activity orientation + * wrt the device default orientation). */ public class OrientationHelper { @@ -35,7 +36,8 @@ public class OrientationHelper { */ public OrientationHelper(@NonNull Context context, @NonNull Callback callback) { mCallback = callback; - mListener = new OrientationEventListener(context.getApplicationContext(), SensorManager.SENSOR_DELAY_NORMAL) { + mListener = new OrientationEventListener(context.getApplicationContext(), + SensorManager.SENSOR_DELAY_NORMAL) { @SuppressWarnings("ConstantConditions") @Override @@ -66,7 +68,9 @@ public class OrientationHelper { * @param context a context */ public void enable(@NonNull Context context) { - Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); + Display display = ((WindowManager) context + .getSystemService(Context.WINDOW_SERVICE)) + .getDefaultDisplay(); switch (display.getRotation()) { case Surface.ROTATION_0: mDisplayOffset = 0; break; case Surface.ROTATION_90: mDisplayOffset = 90; break; diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/Pool.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/Pool.java index 76df6ccb..d7c8b57c 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/Pool.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/Pool.java @@ -92,14 +92,16 @@ public class Pool { synchronized (lock) { LOG.v("RECYCLE - Recycling item.", this); if (--activeCount < 0) { - throw new IllegalStateException("Trying to recycle an item which makes activeCount < 0." + - "This means that this or some previous items being recycled were not coming from " + - "this pool, or some item was recycled more than once. " + this); + throw new IllegalStateException("Trying to recycle an item which makes " + + "activeCount < 0. This means that this or some previous items being " + + "recycled were not coming from this pool, or some item was recycled " + + "more than once. " + this); } if (!queue.offer(item)) { - throw new IllegalStateException("Trying to recycle an item while the queue is full. " + - "This means that this or some previous items being recycled were not coming from " + - "this pool, or some item was recycled more than once. " + this); + throw new IllegalStateException("Trying to recycle an item while the queue " + + "is full. This means that this or some previous items being recycled " + + "were not coming from this pool, or some item was recycled " + + "more than once. " + this); } } } @@ -121,7 +123,6 @@ public class Pool { * * @return count */ - @SuppressWarnings("WeakerAccess") public final int count() { synchronized (lock) { return activeCount() + recycledCount(); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/RotationHelper.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/RotationHelper.java index bc62c379..bed0f90c 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/RotationHelper.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/RotationHelper.java @@ -19,7 +19,9 @@ public class RotationHelper { * @param rotation desired angle * @return a new yuv array */ - public static byte[] rotate(@NonNull final byte[] yuv, @NonNull final Size size, final int rotation) { + public static byte[] rotate(@NonNull final byte[] yuv, + @NonNull final Size size, + final int rotation) { if (rotation == 0) return yuv; if (rotation % 90 != 0 || rotation < 0 || rotation > 270) { throw new IllegalArgumentException("0 <= rotation < 360, rotation % 90 == 0"); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/WorkerHandler.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/WorkerHandler.java index 6d0a2f49..17584809 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/WorkerHandler.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/WorkerHandler.java @@ -23,8 +23,10 @@ import java.util.concurrent.Executor; */ public class WorkerHandler { - private final static CameraLogger LOG = CameraLogger.create(WorkerHandler.class.getSimpleName()); - private final static ConcurrentHashMap> sCache = new ConcurrentHashMap<>(4); + private final static CameraLogger LOG + = CameraLogger.create(WorkerHandler.class.getSimpleName()); + private final static ConcurrentHashMap> sCache + = new ConcurrentHashMap<>(4); private final static String FALLBACK_NAME = "FallbackCameraThread"; @@ -50,7 +52,8 @@ public class WorkerHandler { } else { // Cleanup the old thread before creating a new one cached.destroy(); - LOG.w("get:", "Thread reference found, but not alive or interrupted. Removing.", name); + LOG.w("get:", "Thread reference found, but not alive or interrupted.", + "Removing.", name); sCache.remove(name); } } else { diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/markers/AutoFocusMarker.java b/cameraview/src/main/java/com/otaliastudios/cameraview/markers/AutoFocusMarker.java index 35f5f9de..c62a4a79 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/markers/AutoFocusMarker.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/markers/AutoFocusMarker.java @@ -32,7 +32,9 @@ public interface AutoFocusMarker extends Marker { * @param successful whether the operation succeeded * @param point coordinates */ - void onAutoFocusEnd(@NonNull AutoFocusTrigger trigger, boolean successful, @NonNull PointF point); + void onAutoFocusEnd(@NonNull AutoFocusTrigger trigger, + boolean successful, + @NonNull PointF point); } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/markers/DefaultAutoFocusMarker.java b/cameraview/src/main/java/com/otaliastudios/cameraview/markers/DefaultAutoFocusMarker.java index 5a206957..d2968c16 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/markers/DefaultAutoFocusMarker.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/markers/DefaultAutoFocusMarker.java @@ -27,7 +27,8 @@ public class DefaultAutoFocusMarker implements AutoFocusMarker { @Nullable @Override public View onAttach(@NonNull Context context, @NonNull ViewGroup container) { - View view = LayoutInflater.from(context).inflate(R.layout.cameraview_layout_focus_marker, container, false); + View view = LayoutInflater.from(context).inflate(R.layout.cameraview_layout_focus_marker, + container, false); mContainer = view.findViewById(R.id.focusMarkerContainer); mFill = view.findViewById(R.id.focusMarkerFill); return view; @@ -49,18 +50,22 @@ public class DefaultAutoFocusMarker implements AutoFocusMarker { } @Override - public void onAutoFocusEnd(@NonNull AutoFocusTrigger trigger, boolean successful, @NonNull PointF point) { + public void onAutoFocusEnd(@NonNull AutoFocusTrigger trigger, + boolean successful, + @NonNull PointF point) { if (trigger == AutoFocusTrigger.METHOD) return; if (successful) { animate(mContainer, 1, 0, 500, 0, null); animate(mFill, 1, 0, 500, 0, null); } else { animate(mFill, 0, 0, 500, 0, null); - animate(mContainer, 1.36f, 1, 500, 0, new AnimatorListenerAdapter() { + animate(mContainer, 1.36f, 1, 500, 0, + new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); - animate(mContainer, 1.36f, 0, 200, 1000, null); + animate(mContainer, 1.36f, 0, 200, 1000, + null); } }); } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/overlay/OverlayDrawer.java b/cameraview/src/main/java/com/otaliastudios/cameraview/overlay/OverlayDrawer.java index ab9b1c2a..03b87222 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/overlay/OverlayDrawer.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/overlay/OverlayDrawer.java @@ -24,7 +24,7 @@ import java.nio.Buffer; * * - Provides a {@link Canvas} to be passed to the Overlay * - Lets the overlay draw there: {@link #draw(Overlay.Target)} - * - Renders this into the current EGL window: {@link #render()} + * - Renders this into the current EGL window: {@link #render(long)} * - Applies the {@link Issue514Workaround} the correct way * * In the future we might want to use a different approach than {@link EglViewport}, @@ -93,8 +93,10 @@ public class OverlayDrawer { * Renders the drawn content in the current EGL surface, assuming there is one. * Should be called after {@link #draw(Overlay.Target)} and any {@link #getTransform()} * modification. + * + * @param timestampUs frame timestamp */ - public void render() { + public void render(long timestampUs) { // Enable blending // Reference http://www.learnopengles.com/android-lesson-five-an-introduction-to-blending/ GLES20.glDisable(GLES20.GL_CULL_FACE); @@ -103,7 +105,7 @@ public class OverlayDrawer { GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA); synchronized (mIssue514WorkaroundLock) { - mViewport.drawFrame(mTextureId, mTransform); + mViewport.drawFrame(timestampUs, mTextureId, mTransform); } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/overlay/OverlayLayout.java b/cameraview/src/main/java/com/otaliastudios/cameraview/overlay/OverlayLayout.java index 931a5905..bed667da 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/overlay/OverlayLayout.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/overlay/OverlayLayout.java @@ -183,9 +183,12 @@ public class OverlayLayout extends FrameLayout implements Overlay { super(context, attrs); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CameraView_Layout); try { - drawOnPreview = a.getBoolean(R.styleable.CameraView_Layout_layout_drawOnPreview, false); - drawOnPictureSnapshot = a.getBoolean(R.styleable.CameraView_Layout_layout_drawOnPictureSnapshot, false); - drawOnVideoSnapshot = a.getBoolean(R.styleable.CameraView_Layout_layout_drawOnVideoSnapshot, false); + drawOnPreview = a.getBoolean(R.styleable.CameraView_Layout_layout_drawOnPreview, + false); + drawOnPictureSnapshot = a.getBoolean( + R.styleable.CameraView_Layout_layout_drawOnPictureSnapshot, false); + drawOnVideoSnapshot = a.getBoolean( + R.styleable.CameraView_Layout_layout_drawOnVideoSnapshot, false); } finally { a.recycle(); } 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 51bd0a53..7bad4173 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Full1PictureRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Full1PictureRecorder.java @@ -54,7 +54,9 @@ public class Full1PictureRecorder extends PictureRecorder { int exifRotation; try { ExifInterface exif = new ExifInterface(new ByteArrayInputStream(data)); - int exifOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); + int exifOrientation = exif.getAttributeInt( + ExifInterface.TAG_ORIENTATION, + ExifInterface.ORIENTATION_NORMAL); exifRotation = ExifHelper.readExifOrientation(exifOrientation); } catch (IOException e) { exifRotation = 0; 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 6a53e49e..0e2c6fae 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Full2PictureRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Full2PictureRecorder.java @@ -28,7 +28,8 @@ import androidx.exifinterface.media.ExifInterface; * A {@link PictureResult} that uses standard APIs. */ @RequiresApi(Build.VERSION_CODES.LOLLIPOP) -public class Full2PictureRecorder extends PictureRecorder implements ImageReader.OnImageAvailableListener { +public class Full2PictureRecorder extends PictureRecorder + implements ImageReader.OnImageAvailableListener { private static final String TAG = Full2PictureRecorder.class.getSimpleName(); private static final CameraLogger LOG = CameraLogger.create(TAG); @@ -65,7 +66,8 @@ public class Full2PictureRecorder extends PictureRecorder implements ImageReader } @Override - public void onCaptureStarted(@NonNull ActionHolder holder, @NonNull CaptureRequest request) { + public void onCaptureStarted(@NonNull ActionHolder holder, + @NonNull CaptureRequest request) { super.onCaptureStarted(holder, request); if (request.getTag() == (Integer) CameraDevice.TEMPLATE_STILL_CAPTURE) { LOG.i("onCaptureStarted:", "Dispatching picture shutter."); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/PictureRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/PictureRecorder.java index b296777a..50178a1f 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/PictureRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/PictureRecorder.java @@ -44,7 +44,8 @@ public abstract class PictureRecorder { * @param listener a listener */ @SuppressWarnings("WeakerAccess") - public PictureRecorder(@NonNull PictureResult.Stub stub, @Nullable PictureResultListener listener) { + public PictureRecorder(@NonNull PictureResult.Stub stub, + @Nullable PictureResultListener listener) { mResult = stub; mListener = listener; } 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 606ecaaa..5b7cbc19 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Snapshot1PictureRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Snapshot1PictureRecorder.java @@ -60,7 +60,8 @@ public class Snapshot1PictureRecorder extends PictureRecorder { final Size outputSize = mResult.size; final Size previewStreamSize = mEngine1.getPreviewStreamSize(Reference.SENSOR); if (previewStreamSize == null) { - throw new IllegalStateException("Preview stream size should never be null here."); + throw new IllegalStateException("Preview stream size " + + "should never be null here."); } WorkerHandler.execute(new Runnable() { @Override @@ -69,7 +70,8 @@ public class Snapshot1PictureRecorder extends PictureRecorder { // then crop if needed. In both cases, transform yuv to jpeg. //noinspection deprecation byte[] data = RotationHelper.rotate(yuv, previewStreamSize, sensorToOutput); - YuvImage yuv = new YuvImage(data, mFormat, outputSize.getWidth(), outputSize.getHeight(), null); + YuvImage yuv = new YuvImage(data, mFormat, outputSize.getWidth(), + outputSize.getHeight(), null); ByteArrayOutputStream stream = new ByteArrayOutputStream(); Rect outputRect = CropHelper.computeCrop(outputSize, mOutputRatio); 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 a9653945..c618b359 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Snapshot2PictureRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/Snapshot2PictureRecorder.java @@ -26,18 +26,20 @@ import com.otaliastudios.cameraview.size.AspectRatio; * Wraps {@link SnapshotGlPictureRecorder} for Camera2. * * Camera2 engine supports metering for snapshots and we expect for them to correctly fire flash as well. - * The first idea, and in theory, the most correct one, was to set {@link CaptureRequest#CONTROL_CAPTURE_INTENT} - * to {@link CaptureRequest#CONTROL_CAPTURE_INTENT_STILL_CAPTURE}. + * The first idea, and in theory, the most correct one, was to set + * {@link CaptureRequest#CONTROL_CAPTURE_INTENT} to + * {@link CaptureRequest#CONTROL_CAPTURE_INTENT_STILL_CAPTURE}. * * According to documentation, this will automatically trigger the flash if parameters says so. * In fact this is what happens, but it is a very fast flash that only lasts for 1 or 2 frames. * It's not easy to call super.take() at the exact time so that we capture the frame that was lit. - * I have tried by comparing {@link SurfaceTexture#getTimestamp()} and {@link CaptureResult#SENSOR_TIMESTAMP} - * to identify the correct frame. These timestamps match, but the frame is not the correct one. + * I have tried by comparing {@link SurfaceTexture#getTimestamp()} and + * {@link CaptureResult#SENSOR_TIMESTAMP} to identify the correct frame. These timestamps match, + * but the frame is not the correct one. * - * So what we do here is ignore the {@link CaptureRequest#CONTROL_CAPTURE_INTENT} and instead open the - * torch, if requested to do so. Then wait for exposure to settle again and finally take a snapshot. - * I'd still love to use the capture intent instead of this, but was not able yet. + * So what we do here is ignore the {@link CaptureRequest#CONTROL_CAPTURE_INTENT} and instead + * open the torch, if requested to do so. Then wait for exposure to settle again and finally + * take a snapshot. I'd still love to use the capture intent instead of this, but was not able yet. */ @RequiresApi(Build.VERSION_CODES.LOLLIPOP) public class Snapshot2PictureRecorder extends SnapshotGlPictureRecorder { @@ -52,8 +54,10 @@ public class Snapshot2PictureRecorder extends SnapshotGlPictureRecorder { protected void onStart(@NonNull ActionHolder holder) { super.onStart(holder); LOG.i("FlashAction:", "Parameters locked, opening torch."); - holder.getBuilder(this).set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH); - holder.getBuilder(this).set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); + holder.getBuilder(this).set(CaptureRequest.FLASH_MODE, + CaptureRequest.FLASH_MODE_TORCH); + holder.getBuilder(this).set(CaptureRequest.CONTROL_AE_MODE, + CaptureRequest.CONTROL_AE_MODE_ON); holder.applyBuilder(this); } @@ -64,10 +68,12 @@ public class Snapshot2PictureRecorder extends SnapshotGlPictureRecorder { super.onCaptureCompleted(holder, request, result); Integer flashState = result.get(CaptureResult.FLASH_STATE); if (flashState == null) { - LOG.w("FlashAction:", "Waiting flash, but flashState is null! Taking snapshot."); + LOG.w("FlashAction:", "Waiting flash, but flashState is null!", + "Taking snapshot."); setState(STATE_COMPLETED); } else if (flashState == CaptureResult.FLASH_STATE_FIRED) { - LOG.i("FlashAction:", "Waiting flash and we have FIRED state! Taking snapshot."); + LOG.i("FlashAction:", "Waiting flash and we have FIRED state!", + "Taking snapshot."); setState(STATE_COMPLETED); } else { LOG.i("FlashAction:", "Waiting flash but flashState is", @@ -111,7 +117,8 @@ public class Snapshot2PictureRecorder extends SnapshotGlPictureRecorder { @Override public void take() { if (!mActionNeeded) { - LOG.i("take:", "Engine does no metering or needs no flash, taking fast snapshot."); + LOG.i("take:", "Engine does no metering or needs no flash.", + "Taking fast snapshot."); super.take(); } else { LOG.i("take:", "Engine needs flash. Starting action"); 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 b4d019e8..5dd5ce20 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotGlPictureRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotGlPictureRecorder.java @@ -46,11 +46,13 @@ import android.view.Surface; * - We move to another thread, and create a new EGL surface for that EGL context. * - We make this new surface current, and re-draw the textureId on it * - [Optional: fill the overlayTextureId and draw it on the same surface] - * - We use glReadPixels (through {@link EglBaseSurface#saveFrameTo(Bitmap.CompressFormat)}) and save to file. + * - We use glReadPixels (through {@link EglBaseSurface#saveFrameTo(Bitmap.CompressFormat)}) + * and save to file. * * We create a new EGL surface and redraw the frame because: * 1. We want to go off the renderer thread as soon as possible - * 2. We have overlays to be drawn - we don't want to draw them on the preview surface, not even for a frame. + * 2. We have overlays to be drawn - we don't want to draw them on the preview surface, + * not even for a frame. */ public class SnapshotGlPictureRecorder extends PictureRecorder { @@ -102,7 +104,9 @@ public class SnapshotGlPictureRecorder extends PictureRecorder { @RendererThread @Override - public void onRendererFrame(@NonNull SurfaceTexture surfaceTexture, final float scaleX, final float scaleY) { + public void onRendererFrame(@NonNull SurfaceTexture surfaceTexture, + final float scaleX, + final float scaleY) { mPreview.removeRendererFrameCallback(this); SnapshotGlPictureRecorder.this.onRendererFrame(surfaceTexture, scaleX, scaleY); } @@ -110,6 +114,7 @@ public class SnapshotGlPictureRecorder extends PictureRecorder { }); } + @SuppressWarnings("WeakerAccess") @RendererThread @TargetApi(Build.VERSION_CODES.KITKAT) protected void onRendererTextureCreated(int textureId) { @@ -126,12 +131,14 @@ public class SnapshotGlPictureRecorder extends PictureRecorder { } } + @SuppressWarnings("WeakerAccess") @RendererThread @TargetApi(Build.VERSION_CODES.KITKAT) protected void onRendererFilterChanged(@NonNull Filter filter) { mViewport.setFilter(filter.copy()); } + @SuppressWarnings("WeakerAccess") @RendererThread @TargetApi(Build.VERSION_CODES.KITKAT) protected void onRendererFrame(@SuppressWarnings("unused") @NonNull final SurfaceTexture surfaceTexture, @@ -175,9 +182,13 @@ public class SnapshotGlPictureRecorder extends PictureRecorder { * @param scaleX frame scale x in {@link Reference#VIEW} * @param scaleY frame scale y in {@link Reference#VIEW} */ + @SuppressWarnings("WeakerAccess") @WorkerThread @TargetApi(Build.VERSION_CODES.KITKAT) - protected void takeFrame(@NonNull SurfaceTexture surfaceTexture, float scaleX, float scaleY, @NonNull EGLContext eglContext) { + protected void takeFrame(@NonNull SurfaceTexture surfaceTexture, + float scaleX, + float scaleY, + @NonNull EGLContext eglContext) { // 0. EGL window will need an output. // We create a fake one as explained in javadocs. @@ -223,9 +234,10 @@ public class SnapshotGlPictureRecorder extends PictureRecorder { } // 5. Draw and save - LOG.i("takeFrame:", "timestamp:", surfaceTexture.getTimestamp()); - mViewport.drawFrame(mTextureId, mTransform); - if (mHasOverlay) mOverlayDrawer.render(); + long timestampUs = surfaceTexture.getTimestamp() / 1000L; + 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); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/preview/CameraPreview.java b/cameraview/src/main/java/com/otaliastudios/cameraview/preview/CameraPreview.java index 29470b7c..29df062d 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/preview/CameraPreview.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/preview/CameraPreview.java @@ -26,11 +26,13 @@ import com.otaliastudios.cameraview.size.Size; * into an output surface that belongs to the view hierarchy. * * @param the type of view which hosts the content surface - * @param the type of output, either {@link android.view.SurfaceHolder} or {@link android.graphics.SurfaceTexture} + * @param the type of output, either {@link android.view.SurfaceHolder} + * or {@link android.graphics.SurfaceTexture} */ public abstract class CameraPreview { - protected final static CameraLogger LOG = CameraLogger.create(CameraPreview.class.getSimpleName()); + protected final static CameraLogger LOG + = CameraLogger.create(CameraPreview.class.getSimpleName()); /** * This is used to notify CameraEngine to recompute its camera Preview size. @@ -267,8 +269,9 @@ public abstract class CameraPreview { } /** - * At this point we undo the work that was done during {@link #onCreateView(Context, ViewGroup)}, - * which basically means removing the root view from the hierarchy. + * At this point we undo the work that was done during + * {@link #onCreateView(Context, ViewGroup)}, which basically means removing the root view + * from the hierarchy. */ @SuppressWarnings("WeakerAccess") @UiThread diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/preview/GlCameraPreview.java b/cameraview/src/main/java/com/otaliastudios/cameraview/preview/GlCameraPreview.java index 9afc1f92..5627303a 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/preview/GlCameraPreview.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/preview/GlCameraPreview.java @@ -32,9 +32,10 @@ import javax.microedition.khronos.opengles.GL10; * * - in the SurfaceTexture constructor we pass the GL texture handle that we have created. * - * - The SurfaceTexture is linked to the Camera1Engine object. The camera will pass down buffers of data with - * a specified size (that is, the Camera1Engine preview size). For this reason we don't have to specify - * surfaceTexture.setDefaultBufferSize() (like we do, for example, in Snapshot1PictureRecorder). + * - The SurfaceTexture is linked to the Camera1Engine object. The camera will pass down + * buffers of data with a specified size (that is, the Camera1Engine preview size). + * For this reason we don't have to specify surfaceTexture.setDefaultBufferSize() + * (like we do, for example, in Snapshot1PictureRecorder). * * - When SurfaceTexture.updateTexImage() is called, it will fetch the latest texture image from the * camera stream and assign it to the GL texture that was passed. @@ -42,12 +43,13 @@ import javax.microedition.khronos.opengles.GL10; * the transformation matrix to be applied. * * - The easy way to render an OpenGL texture is using the {@link GLSurfaceView} class. - * It manages the GL context, hosts a surface and runs a separated rendering thread that will perform - * the rendering. + * It manages the GL context, hosts a surface and runs a separated rendering thread that will + * perform the rendering. * * - As per docs, we ask the GLSurfaceView to delegate rendering to us, using - * {@link GLSurfaceView#setRenderer(GLSurfaceView.Renderer)}. We request a render on the SurfaceView - * anytime the SurfaceTexture notifies that it has new data available (see OnFrameAvailableListener below). + * {@link GLSurfaceView#setRenderer(GLSurfaceView.Renderer)}. We request a render on the + * SurfaceView anytime the SurfaceTexture notifies that it has new data available + * (see OnFrameAvailableListener below). * * - So in short: * - The SurfaceTexture has buffers of data of mInputStreamSize @@ -55,9 +57,10 @@ import javax.microedition.khronos.opengles.GL10; * These are determined by the CameraView.onMeasure method. * - We have a GL rich texture to be drawn (in the given method and thread). * - * This class will provide rendering callbacks to anyone who registers a {@link RendererFrameCallback}. - * Callbacks are guaranteed to be called on the renderer thread, which means that we can fetch - * the GL context that was created and is managed by the {@link GLSurfaceView}. + * This class will provide rendering callbacks to anyone who registers a + * {@link RendererFrameCallback}. Callbacks are guaranteed to be called on the renderer thread, + * which means that we can fetch the GL context that was created and is managed + * by the {@link GLSurfaceView}. */ public class GlCameraPreview extends FilterCameraPreview { @@ -66,7 +69,8 @@ public class GlCameraPreview extends FilterCameraPreview mRendererFrameCallbacks = Collections.synchronizedSet(new HashSet()); + private final Set mRendererFrameCallbacks + = Collections.synchronizedSet(new HashSet()); @VisibleForTesting float mCropScaleX = 1F; @VisibleForTesting float mCropScaleY = 1F; private View mRootView; @@ -79,7 +83,8 @@ public class GlCameraPreview extends FilterCameraPreview op) { op.start(); - if (mInputStreamWidth > 0 && mInputStreamHeight > 0 && mOutputSurfaceWidth > 0 && mOutputSurfaceHeight > 0) { + if (mInputStreamWidth > 0 && mInputStreamHeight > 0 && mOutputSurfaceWidth > 0 + && mOutputSurfaceHeight > 0) { float scaleX = 1f, scaleY = 1f; AspectRatio current = AspectRatio.of(mOutputSurfaceWidth, mOutputSurfaceHeight); AspectRatio target = AspectRatio.of(mInputStreamWidth, mInputStreamHeight); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/preview/SurfaceCameraPreview.java b/cameraview/src/main/java/com/otaliastudios/cameraview/preview/SurfaceCameraPreview.java index ffe44a82..053da497 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/preview/SurfaceCameraPreview.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/preview/SurfaceCameraPreview.java @@ -14,14 +14,15 @@ import com.otaliastudios.cameraview.R; /** * This is the fallback preview when hardware acceleration is off, and is the last resort. - * Currently does not support cropping, which means that {@link com.otaliastudios.cameraview.CameraView} - * is forced to be wrap_content. + * Currently does not support cropping, which means that + * {@link com.otaliastudios.cameraview.CameraView} is forced to be wrap_content. * * Do not use. */ public class SurfaceCameraPreview extends CameraPreview { - private final static CameraLogger LOG = CameraLogger.create(SurfaceCameraPreview.class.getSimpleName()); + private final static CameraLogger LOG + = CameraLogger.create(SurfaceCameraPreview.class.getSimpleName()); private boolean mDispatched; private View mRootView; @@ -33,7 +34,8 @@ public class SurfaceCameraPreview extends CameraPreview { - @VisibleForTesting final static HashMap sCache = new HashMap<>(16); + @VisibleForTesting final static HashMap sCache + = new HashMap<>(16); /** * Creates an aspect ratio for the given size. diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/size/SizeSelectorParser.java b/cameraview/src/main/java/com/otaliastudios/cameraview/size/SizeSelectorParser.java index 6e5da0bb..32802b37 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/size/SizeSelectorParser.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/size/SizeSelectorParser.java @@ -20,30 +20,48 @@ public class SizeSelectorParser { public SizeSelectorParser(@NonNull TypedArray array) { List pictureConstraints = new ArrayList<>(3); if (array.hasValue(R.styleable.CameraView_cameraPictureSizeMinWidth)) { - pictureConstraints.add(SizeSelectors.minWidth(array.getInteger(R.styleable.CameraView_cameraPictureSizeMinWidth, 0))); + pictureConstraints.add(SizeSelectors.minWidth( + array.getInteger(R.styleable.CameraView_cameraPictureSizeMinWidth, 0) + )); } if (array.hasValue(R.styleable.CameraView_cameraPictureSizeMaxWidth)) { - pictureConstraints.add(SizeSelectors.maxWidth(array.getInteger(R.styleable.CameraView_cameraPictureSizeMaxWidth, 0))); + pictureConstraints.add(SizeSelectors.maxWidth( + array.getInteger(R.styleable.CameraView_cameraPictureSizeMaxWidth, 0) + )); } if (array.hasValue(R.styleable.CameraView_cameraPictureSizeMinHeight)) { - pictureConstraints.add(SizeSelectors.minHeight(array.getInteger(R.styleable.CameraView_cameraPictureSizeMinHeight, 0))); + pictureConstraints.add(SizeSelectors.minHeight( + array.getInteger(R.styleable.CameraView_cameraPictureSizeMinHeight, 0) + )); } if (array.hasValue(R.styleable.CameraView_cameraPictureSizeMaxHeight)) { - pictureConstraints.add(SizeSelectors.maxHeight(array.getInteger(R.styleable.CameraView_cameraPictureSizeMaxHeight, 0))); + pictureConstraints.add(SizeSelectors.maxHeight( + array.getInteger(R.styleable.CameraView_cameraPictureSizeMaxHeight, 0) + )); } if (array.hasValue(R.styleable.CameraView_cameraPictureSizeMinArea)) { - pictureConstraints.add(SizeSelectors.minArea(array.getInteger(R.styleable.CameraView_cameraPictureSizeMinArea, 0))); + pictureConstraints.add(SizeSelectors.minArea( + array.getInteger(R.styleable.CameraView_cameraPictureSizeMinArea, 0) + )); } if (array.hasValue(R.styleable.CameraView_cameraPictureSizeMaxArea)) { - pictureConstraints.add(SizeSelectors.maxArea(array.getInteger(R.styleable.CameraView_cameraPictureSizeMaxArea, 0))); + pictureConstraints.add(SizeSelectors.maxArea( + array.getInteger(R.styleable.CameraView_cameraPictureSizeMaxArea, 0) + )); } if (array.hasValue(R.styleable.CameraView_cameraPictureSizeAspectRatio)) { //noinspection ConstantConditions - pictureConstraints.add(SizeSelectors.aspectRatio(AspectRatio.parse(array.getString(R.styleable.CameraView_cameraPictureSizeAspectRatio)), 0)); + pictureConstraints.add(SizeSelectors.aspectRatio( + AspectRatio.parse(array.getString( + R.styleable.CameraView_cameraPictureSizeAspectRatio)), 0)); } - if (array.getBoolean(R.styleable.CameraView_cameraPictureSizeSmallest, false)) pictureConstraints.add(SizeSelectors.smallest()); - if (array.getBoolean(R.styleable.CameraView_cameraPictureSizeBiggest, false)) pictureConstraints.add(SizeSelectors.biggest()); + if (array.getBoolean(R.styleable.CameraView_cameraPictureSizeSmallest, false)) { + pictureConstraints.add(SizeSelectors.smallest()); + } + if (array.getBoolean(R.styleable.CameraView_cameraPictureSizeBiggest, false)) { + pictureConstraints.add(SizeSelectors.biggest()); + } pictureSizeSelector = !pictureConstraints.isEmpty() ? SizeSelectors.and(pictureConstraints.toArray(new SizeSelector[0])) : SizeSelectors.biggest(); @@ -51,29 +69,47 @@ public class SizeSelectorParser { // Video size selector List videoConstraints = new ArrayList<>(3); if (array.hasValue(R.styleable.CameraView_cameraVideoSizeMinWidth)) { - videoConstraints.add(SizeSelectors.minWidth(array.getInteger(R.styleable.CameraView_cameraVideoSizeMinWidth, 0))); + videoConstraints.add(SizeSelectors.minWidth( + array.getInteger(R.styleable.CameraView_cameraVideoSizeMinWidth, 0) + )); } if (array.hasValue(R.styleable.CameraView_cameraVideoSizeMaxWidth)) { - videoConstraints.add(SizeSelectors.maxWidth(array.getInteger(R.styleable.CameraView_cameraVideoSizeMaxWidth, 0))); + videoConstraints.add(SizeSelectors.maxWidth( + array.getInteger(R.styleable.CameraView_cameraVideoSizeMaxWidth, 0) + )); } if (array.hasValue(R.styleable.CameraView_cameraVideoSizeMinHeight)) { - videoConstraints.add(SizeSelectors.minHeight(array.getInteger(R.styleable.CameraView_cameraVideoSizeMinHeight, 0))); + videoConstraints.add(SizeSelectors.minHeight( + array.getInteger(R.styleable.CameraView_cameraVideoSizeMinHeight, 0) + )); } if (array.hasValue(R.styleable.CameraView_cameraVideoSizeMaxHeight)) { - videoConstraints.add(SizeSelectors.maxHeight(array.getInteger(R.styleable.CameraView_cameraVideoSizeMaxHeight, 0))); + videoConstraints.add(SizeSelectors.maxHeight( + array.getInteger(R.styleable.CameraView_cameraVideoSizeMaxHeight, 0) + )); } if (array.hasValue(R.styleable.CameraView_cameraVideoSizeMinArea)) { - videoConstraints.add(SizeSelectors.minArea(array.getInteger(R.styleable.CameraView_cameraVideoSizeMinArea, 0))); + videoConstraints.add(SizeSelectors.minArea( + array.getInteger(R.styleable.CameraView_cameraVideoSizeMinArea, 0) + )); } if (array.hasValue(R.styleable.CameraView_cameraVideoSizeMaxArea)) { - videoConstraints.add(SizeSelectors.maxArea(array.getInteger(R.styleable.CameraView_cameraVideoSizeMaxArea, 0))); + videoConstraints.add(SizeSelectors.maxArea( + array.getInteger(R.styleable.CameraView_cameraVideoSizeMaxArea, 0) + )); } if (array.hasValue(R.styleable.CameraView_cameraVideoSizeAspectRatio)) { //noinspection ConstantConditions - videoConstraints.add(SizeSelectors.aspectRatio(AspectRatio.parse(array.getString(R.styleable.CameraView_cameraVideoSizeAspectRatio)), 0)); + videoConstraints.add(SizeSelectors.aspectRatio( + AspectRatio.parse(array.getString( + R.styleable.CameraView_cameraVideoSizeAspectRatio)), 0)); + } + if (array.getBoolean(R.styleable.CameraView_cameraVideoSizeSmallest, false)) { + videoConstraints.add(SizeSelectors.smallest()); + } + if (array.getBoolean(R.styleable.CameraView_cameraVideoSizeBiggest, false)) { + videoConstraints.add(SizeSelectors.biggest()); } - if (array.getBoolean(R.styleable.CameraView_cameraVideoSizeSmallest, false)) videoConstraints.add(SizeSelectors.smallest()); - if (array.getBoolean(R.styleable.CameraView_cameraVideoSizeBiggest, false)) videoConstraints.add(SizeSelectors.biggest()); videoSizeSelector = !videoConstraints.isEmpty() ? SizeSelectors.and(videoConstraints.toArray(new SizeSelector[0])) : SizeSelectors.biggest(); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/Full1VideoRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/Full1VideoRecorder.java index 010ea055..5d69f102 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/Full1VideoRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/Full1VideoRecorder.java @@ -34,7 +34,8 @@ public class Full1VideoRecorder extends FullVideoRecorder { } @Override - protected boolean onPrepareMediaRecorder(@NonNull VideoResult.Stub stub, @NonNull MediaRecorder mediaRecorder) { + protected boolean onPrepareMediaRecorder(@NonNull VideoResult.Stub stub, + @NonNull MediaRecorder mediaRecorder) { mediaRecorder.setCamera(mCamera); mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); @@ -45,9 +46,9 @@ public class Full1VideoRecorder extends FullVideoRecorder { } @Override - protected void dispatchResult() { + protected void onDispatchResult() { // Restore frame processing. mCamera.setPreviewCallbackWithBuffer(mEngine); - super.dispatchResult(); + super.onDispatchResult(); } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/Full2VideoRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/Full2VideoRecorder.java index 8ef1f65a..d8e78f1f 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/Full2VideoRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/Full2VideoRecorder.java @@ -71,13 +71,15 @@ public class Full2VideoRecorder extends FullVideoRecorder { @SuppressLint("NewApi") @Override - protected boolean onPrepareMediaRecorder(@NonNull VideoResult.Stub stub, @NonNull MediaRecorder mediaRecorder) { + protected boolean onPrepareMediaRecorder(@NonNull VideoResult.Stub stub, + @NonNull MediaRecorder mediaRecorder) { mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); Size size = stub.rotation % 180 != 0 ? stub.size.flip() : stub.size; mProfile = CamcorderProfiles.get(mCameraId, size); - // This was an option: get the surface from outside this class, using MediaCodec.createPersistentInputSurface() - // But it doesn't really help since the Camera2 engine refuses a surface that has not been configured, - // so even with that trick we would have to attach the surface to this recorder before creating the CameraSession. + // This was an option: get the surface from outside this class, using + // MediaCodec.createPersistentInputSurface(). But it doesn't really help since the + // Camera2 engine refuses a surface that has not been configured, so even with that trick + // we would have to attach the surface to this recorder before creating the CameraSession. // mediaRecorder.setInputSurface(mInputSurface); return super.onPrepareMediaRecorder(stub, mediaRecorder); } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/FullVideoRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/FullVideoRecorder.java index 6c1e836e..c7c1310b 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/FullVideoRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/FullVideoRecorder.java @@ -16,7 +16,8 @@ import androidx.annotation.Nullable; /** * A {@link VideoRecorder} that uses {@link android.media.MediaRecorder} APIs. * - * When started, the media recorder will be prepared in {@link #onPrepareMediaRecorder(VideoResult.Stub, MediaRecorder)}. + * When started, the media recorder will be prepared in + * {@link #onPrepareMediaRecorder(VideoResult.Stub, MediaRecorder)}. * Subclasses should override this method and, before calling super(), do two things: * - set the media recorder VideoSource * - define {@link #mProfile} @@ -45,7 +46,8 @@ public abstract class FullVideoRecorder extends VideoRecorder { return onPrepareMediaRecorder(stub, new MediaRecorder()); } - protected boolean onPrepareMediaRecorder(@NonNull VideoResult.Stub stub, @NonNull MediaRecorder mediaRecorder) { + protected boolean onPrepareMediaRecorder(@NonNull VideoResult.Stub stub, + @NonNull MediaRecorder mediaRecorder) { mMediaRecorder = mediaRecorder; boolean hasAudio = stub.audio == Audio.ON || stub.audio == Audio.MONO @@ -74,8 +76,12 @@ public abstract class FullVideoRecorder extends VideoRecorder { // https://android.googlesource.com/platform/frameworks/av/+/master/media/libmediaplayerservice/StagefrightRecorder.cpp#1650 // https://github.com/MrAlex94/Waterfox-Old/blob/master/media/libstagefright/frameworks/av/media/libstagefright/MediaDefs.cpp String videoType; - if (stub.videoCodec == VideoCodec.H_264) mProfile.videoCodec = MediaRecorder.VideoEncoder.H264; - if (stub.videoCodec == VideoCodec.H_263) mProfile.videoCodec = MediaRecorder.VideoEncoder.H263; + if (stub.videoCodec == VideoCodec.H_264) { + mProfile.videoCodec = MediaRecorder.VideoEncoder.H264; + } + if (stub.videoCodec == VideoCodec.H_263) { + mProfile.videoCodec = MediaRecorder.VideoEncoder.H263; + } switch (mProfile.videoCodec) { case MediaRecorder.VideoEncoder.H263: videoType = "video/3gpp"; break; case MediaRecorder.VideoEncoder.H264: videoType = "video/avc"; break; @@ -87,20 +93,43 @@ public abstract class FullVideoRecorder extends VideoRecorder { } // Merge stub and profile - stub.videoFrameRate = stub.videoFrameRate > 0 ? stub.videoFrameRate : mProfile.videoFrameRate; + stub.videoFrameRate = stub.videoFrameRate > 0 ? stub.videoFrameRate + : mProfile.videoFrameRate; stub.videoBitRate = stub.videoBitRate > 0 ? stub.videoBitRate : mProfile.videoBitRate; if (hasAudio) { stub.audioBitRate = stub.audioBitRate > 0 ? stub.audioBitRate : mProfile.audioBitRate; } // Check DeviceEncoders support - DeviceEncoders encoders = new DeviceEncoders(videoType, audioType, DeviceEncoders.MODE_TAKE_FIRST); boolean flip = stub.rotation % 180 != 0; if (flip) stub.size = stub.size.flip(); - stub.size = encoders.getSupportedVideoSize(stub.size); - stub.videoBitRate = encoders.getSupportedVideoBitRate(stub.videoBitRate); - stub.audioBitRate = encoders.getSupportedAudioBitRate(stub.audioBitRate); - stub.videoFrameRate = encoders.getSupportedVideoFrameRate(stub.size, stub.videoFrameRate); + Size newVideoSize = null; + int newVideoBitRate = 0; + int newAudioBitRate = 0; + int newVideoFrameRate = 0; + int videoEncoderOffset = 0; + int audioEncoderOffset = 0; + boolean encodersFound = false; + while (!encodersFound) { + DeviceEncoders encoders = new DeviceEncoders(DeviceEncoders.MODE_RESPECT_ORDER, + videoType, audioType, videoEncoderOffset, audioEncoderOffset); + try { + newVideoSize = encoders.getSupportedVideoSize(stub.size); + newVideoBitRate = encoders.getSupportedVideoBitRate(stub.videoBitRate); + newAudioBitRate = encoders.getSupportedAudioBitRate(stub.audioBitRate); + newVideoFrameRate = encoders.getSupportedVideoFrameRate(newVideoSize, + stub.videoFrameRate); + encodersFound = true; + } catch (DeviceEncoders.VideoException videoException) { + videoEncoderOffset++; + } catch (DeviceEncoders.AudioException audioException) { + audioEncoderOffset++; + } + } + stub.size = newVideoSize; + stub.videoBitRate = newVideoBitRate; + stub.audioBitRate = newAudioBitRate; + stub.videoFrameRate = newVideoFrameRate; if (flip) stub.size = stub.size.flip(); // Set video params @@ -192,8 +221,9 @@ public abstract class FullVideoRecorder extends VideoRecorder { mMediaRecorder.stop(); } catch (Exception e) { LOG.w("stop:", "Error while closing media recorder.", e); - // This can happen if stopVideo() is called right after takeVideo() (in which case we don't care) - // Or when prepare()/start() have failed for some reason and we are not allowed to call stop. + // This can happen if stopVideo() is called right after takeVideo() + // (in which case we don't care). Or when prepare()/start() have failed for + // some reason and we are not allowed to call stop. // Make sure we don't override the error if one exists already. mResult = null; if (mError == null) mError = e; diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java index 0a84133a..e88a9c7e 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java @@ -114,13 +114,16 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram @RendererThread @Override - public void onRendererFrame(@NonNull SurfaceTexture surfaceTexture, float scaleX, float scaleY) { + public void onRendererFrame(@NonNull SurfaceTexture surfaceTexture, + float scaleX, + float scaleY) { if (mCurrentState == STATE_NOT_RECORDING && mDesiredState == STATE_RECORDING) { LOG.i("Starting the encoder engine."); // Set default options if (mResult.videoFrameRate <= 0) mResult.videoFrameRate = DEFAULT_VIDEO_FRAMERATE; - if (mResult.videoBitRate <= 0) mResult.videoBitRate = estimateVideoBitRate(mResult.size, mResult.videoFrameRate); + if (mResult.videoBitRate <= 0) mResult.videoBitRate + = estimateVideoBitRate(mResult.size, mResult.videoFrameRate); if (mResult.audioBitRate <= 0) mResult.audioBitRate = DEFAULT_AUDIO_BITRATE; // Define mime types @@ -133,11 +136,34 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram String audioType = "audio/mp4a-latm"; // Check the availability of values - DeviceEncoders deviceEncoders = new DeviceEncoders(videoType, audioType, DeviceEncoders.MODE_PREFER_HARDWARE); - mResult.size = deviceEncoders.getSupportedVideoSize(mResult.size); - mResult.videoBitRate = deviceEncoders.getSupportedVideoBitRate(mResult.videoBitRate); - mResult.audioBitRate = deviceEncoders.getSupportedAudioBitRate(mResult.audioBitRate); - mResult.videoFrameRate = deviceEncoders.getSupportedVideoFrameRate(mResult.size, mResult.videoFrameRate); + Size newVideoSize = null; + int newVideoBitRate = 0; + int newAudioBitRate = 0; + int newVideoFrameRate = 0; + int videoEncoderOffset = 0; + int audioEncoderOffset = 0; + boolean encodersFound = false; + DeviceEncoders deviceEncoders = null; + while (!encodersFound) { + deviceEncoders = new DeviceEncoders(DeviceEncoders.MODE_PREFER_HARDWARE, + videoType, audioType, videoEncoderOffset, audioEncoderOffset); + try { + newVideoSize = deviceEncoders.getSupportedVideoSize(mResult.size); + newVideoBitRate = deviceEncoders.getSupportedVideoBitRate(mResult.videoBitRate); + newAudioBitRate = deviceEncoders.getSupportedAudioBitRate(mResult.audioBitRate); + newVideoFrameRate = deviceEncoders.getSupportedVideoFrameRate(newVideoSize, + mResult.videoFrameRate); + encodersFound = true; + } catch (DeviceEncoders.VideoException videoException) { + videoEncoderOffset++; + } catch (DeviceEncoders.AudioException audioException) { + audioEncoderOffset++; + } + } + mResult.size = newVideoSize; + mResult.videoBitRate = newVideoBitRate; + mResult.audioBitRate = newAudioBitRate; + mResult.videoFrameRate = newVideoFrameRate; // Video TextureConfig videoConfig = new TextureConfig(); @@ -168,7 +194,8 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram // Audio AudioMediaEncoder audioEncoder = null; - if (mResult.audio == Audio.ON || mResult.audio == Audio.MONO || mResult.audio == Audio.STEREO) { + if (mResult.audio == Audio.ON || mResult.audio == Audio.MONO + || mResult.audio == Audio.STEREO) { AudioConfig audioConfig = new AudioConfig(); audioConfig.bitRate = mResult.audioBitRate; if (mResult.audio == Audio.MONO) audioConfig.channels = 1; @@ -191,10 +218,12 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram if (mCurrentState == STATE_RECORDING) { LOG.v("dispatching frame."); - TextureMediaEncoder textureEncoder = (TextureMediaEncoder) mEncoderEngine.getVideoEncoder(); + TextureMediaEncoder textureEncoder + = (TextureMediaEncoder) mEncoderEngine.getVideoEncoder(); TextureMediaEncoder.Frame frame = textureEncoder.acquireFrame(); frame.timestampNanos = surfaceTexture.getTimestamp(); - frame.timestampMillis = System.currentTimeMillis(); // NOTE: this is an approximation but it seems to work. + // NOTE: this is an approximation but it seems to work: + frame.timestampMillis = System.currentTimeMillis(); surfaceTexture.getTransformMatrix(frame.transform); if (mEncoderEngine != null) { // Can happen on teardown. At least it used to. mEncoderEngine.notify(TextureMediaEncoder.FRAME_EVENT, frame); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/VideoRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/VideoRecorder.java index 394cfbbe..12513740 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/VideoRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/VideoRecorder.java @@ -1,5 +1,6 @@ package com.otaliastudios.cameraview.video; +import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.VideoResult; import androidx.annotation.CallSuper; @@ -13,6 +14,9 @@ import androidx.annotation.VisibleForTesting; */ public abstract class VideoRecorder { + private final static String TAG = VideoRecorder.class.getSimpleName(); + private final static CameraLogger LOG = CameraLogger.create(TAG); + /** * Listens for video recorder events. */ @@ -37,11 +41,15 @@ public abstract class VideoRecorder { void onVideoRecordingEnd(); } + private final static int STATE_IDLE = 0; + private final static int STATE_RECORDING = 1; + private final static int STATE_STOPPING = 2; + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) VideoResult.Stub mResult; private final VideoResultListener mListener; @SuppressWarnings("WeakerAccess") protected Exception mError; - private boolean mIsRecording; + private int mState; /** * Creates a new video recorder. @@ -49,6 +57,7 @@ public abstract class VideoRecorder { */ VideoRecorder(@Nullable VideoResultListener listener) { mListener = listener; + mState = STATE_IDLE; } /** @@ -57,8 +66,13 @@ public abstract class VideoRecorder { * @param stub the video stub */ public final void start(@NonNull VideoResult.Stub stub) { + if (mState != STATE_IDLE) { + LOG.e("start:", "called twice, or while stopping! " + + "Ignoring. state:", mState); + return; + } + mState = STATE_RECORDING; mResult = stub; - mIsRecording = true; onStart(); } @@ -67,6 +81,12 @@ public abstract class VideoRecorder { * @param isCameraShutdown whether this is a full shutdown, camera is being closed */ public final void stop(boolean isCameraShutdown) { + if (mState == STATE_IDLE) { + LOG.e("stop:", "called twice, or called before start! " + + "Ignoring. isCameraShutdown:", isCameraShutdown); + return; + } + mState = STATE_STOPPING; onStop(isCameraShutdown); } @@ -75,7 +95,8 @@ public abstract class VideoRecorder { * @return true if recording */ public boolean isRecording() { - return mIsRecording; + // true if not idle. + return mState != STATE_IDLE; } protected abstract void onStart(); @@ -86,9 +107,10 @@ public abstract class VideoRecorder { * Subclasses can call this to notify that the result was obtained, * either with some error (null result) or with the actual stub, filled. */ - @CallSuper - protected void dispatchResult() { - mIsRecording = false; + protected final void dispatchResult() { + if (!isRecording()) return; + mState = STATE_IDLE; + onDispatchResult(); if (mListener != null) { mListener.onVideoResult(mResult, mError); } @@ -96,6 +118,13 @@ public abstract class VideoRecorder { mError = null; } + /** + * Subclasses can override this to release resources. + */ + protected void onDispatchResult() { + // No-op + } + /** * Subclasses can call this to notify that the video recording has started, * this will be called when camera is prepared and started. diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioConfig.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioConfig.java index 6372eee7..b74f74ba 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioConfig.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioConfig.java @@ -21,7 +21,8 @@ public class AudioConfig { final int encoding = AudioFormat.ENCODING_PCM_16BIT; // Determines the sampleSizePerChannel // The 44.1KHz frequency is the only setting guaranteed to be available on all devices. final int samplingFrequency = 44100; // samples/sec - final int sampleSizePerChannel = 2; // byte/sample/channel [16bit]. If this changes, review noise introduction + // If sampleSizePerChannel changes, review noise introduction + final int sampleSizePerChannel = 2; // byte/sample/channel [16bit]. final int byteRatePerChannel = samplingFrequency * sampleSizePerChannel; // byte/sec/channel @NonNull @@ -84,10 +85,10 @@ public class AudioConfig { /** * We allocate buffers of {@link #frameSize()} each, which is not much. * - * This value indicates the maximum number of these buffers that we can allocate at a given instant. - * This value is the number of runnables that the encoder thread is allowed to be 'behind' - * the recorder thread. It's not safe to have it very large or we can end encoding A LOT AFTER - * the actual recording. It's better to reduce this and skip recording at all. + * This value indicates the maximum number of these buffers that we can allocate at a given + * instant. This value is the number of runnables that the encoder thread is allowed to be + * 'behind' the recorder thread. It's not safe to have it very large or we can end encoding + * A LOT AFTER the actual recording. It's better to reduce this and skip recording at all. * * Should be coordinated with {@link #frameSize()}. * diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioMediaEncoder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioMediaEncoder.java index fb99b596..2348a427 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioMediaEncoder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioMediaEncoder.java @@ -67,7 +67,8 @@ public class AudioMediaEncoder extends MediaEncoder { mConfig.mimeType, mConfig.samplingFrequency, mConfig.channels); - audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC); + audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, + MediaCodecInfo.CodecProfileLevel.AACObjectLC); audioFormat.setInteger(MediaFormat.KEY_CHANNEL_MASK, mConfig.audioFormatChannels()); audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, mConfig.bitRate); try { @@ -79,7 +80,8 @@ public class AudioMediaEncoder extends MediaEncoder { } catch (IOException e) { throw new RuntimeException(e); } - mMediaCodec.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); + mMediaCodec.configure(audioFormat, null, null, + MediaCodec.CONFIGURE_FLAG_ENCODE); mMediaCodec.start(); mByteBufferPool = new ByteBufferPool(mConfig.frameSize(), mConfig.bufferPoolMaxSize()); mAudioNoise = new AudioNoise(mConfig); @@ -180,28 +182,38 @@ public class AudioMediaEncoder extends MediaEncoder { LOG.w("Stop was requested. We're out of the loop. Will post an endOfStream."); // Last input with 0 length. This will signal the endOfStream. // Can't use drain(true); it is only available when writing to the codec InputSurface. - read(true); + boolean didReadEos = false; + while (!didReadEos) { + didReadEos = read(true); + } mAudioRecord.stop(); mAudioRecord.release(); mAudioRecord = null; } - private void read(boolean endOfStream) { + /** + * Returns true if we found a buffer and could proceed, false if we found no buffer + * so the operation should be performed again by the caller. + * @param endOfStream true if last read + * @return true if proceeded + */ + private boolean read(boolean endOfStream) { mCurrentBuffer = mByteBufferPool.get(); if (mCurrentBuffer == null) { // This can happen and it means that encoding is slow with respect to recording. - // One might be tempted to fix precisely the next frame presentation time when this happens, - // but this is not needed because the current increaseTime() algorithm will consider delays - // when they get large. + // One might be tempted to fix precisely the next frame presentation time when + // this happens, but this is not needed because the current increaseTime() + // algorithm will consider delays when they get large. // Sleeping before returning is a good way of balancing the two operations. // However, if endOfStream, we CAN'T lose this frame! if (endOfStream) { LOG.v("read thread - eos: true - No buffer, retrying."); - read(true); // try again } else { - LOG.w("read thread - eos: false - Skipping audio frame, encoding is too slow."); + LOG.w("read thread - eos: false - Skipping audio frame,", + "encoding is too slow."); skipFrames(6); // sleep a bit } + return false; } else { mCurrentBuffer.clear(); // When stereo, we read twice the data here and AudioRecord will fill the buffer @@ -211,24 +223,29 @@ public class AudioMediaEncoder extends MediaEncoder { mCurrentReadBytes = mAudioRecord.read(mCurrentBuffer, mConfig.frameSize()); long after = System.nanoTime(); float delayMillis = (after - before) / 1000000F; - float durationMillis = AudioTimestamp.bytesToMillis(mCurrentReadBytes, mConfig.byteRate()); + float durationMillis = AudioTimestamp.bytesToMillis(mCurrentReadBytes, + mConfig.byteRate()); LOG.v("read thread - reading took:", delayMillis, "should be:", durationMillis, "delay:", delayMillis - durationMillis); } else { mCurrentReadBytes = mAudioRecord.read(mCurrentBuffer, mConfig.frameSize()); } - LOG.i("read thread - eos:", endOfStream, "- Read new audio frame. Bytes:", mCurrentReadBytes); + LOG.i("read thread - eos:", endOfStream, "- Read new audio frame. Bytes:", + mCurrentReadBytes); if (mCurrentReadBytes > 0) { // Good read: increase PTS. increaseTime(mCurrentReadBytes, endOfStream); LOG.i("read thread - eos:", endOfStream, "- mLastTimeUs:", mLastTimeUs); mCurrentBuffer.limit(mCurrentReadBytes); enqueue(mCurrentBuffer, mLastTimeUs, endOfStream); } else if (mCurrentReadBytes == AudioRecord.ERROR_INVALID_OPERATION) { - LOG.e("read thread - eos:", endOfStream, "- Got AudioRecord.ERROR_INVALID_OPERATION"); + LOG.e("read thread - eos:", endOfStream, + "- Got AudioRecord.ERROR_INVALID_OPERATION"); } else if (mCurrentReadBytes == AudioRecord.ERROR_BAD_VALUE) { - LOG.e("read thread - eos:", endOfStream, "- Got AudioRecord.ERROR_BAD_VALUE"); + LOG.e("read thread - eos:", endOfStream, + "- Got AudioRecord.ERROR_BAD_VALUE"); } + return true; } } @@ -254,7 +271,8 @@ public class AudioMediaEncoder extends MediaEncoder { if (!hasReachedMaxLength()) { boolean didReachMaxLength = (mLastTimeUs - mFirstTimeUs) > getMaxLengthUs(); if (didReachMaxLength && !endOfStream) { - LOG.w("read thread - this frame reached the maxLength! deltaUs:", mLastTimeUs - mFirstTimeUs); + LOG.w("read thread - this frame reached the maxLength! deltaUs:", + mLastTimeUs - mFirstTimeUs); notifyMaxLengthReached(); } } @@ -263,7 +281,9 @@ public class AudioMediaEncoder extends MediaEncoder { maybeAddNoise(); } - private void enqueue(@NonNull ByteBuffer byteBuffer, long timestamp, boolean isEndOfStream) { + private void enqueue(@NonNull ByteBuffer byteBuffer, + long timestamp, + boolean isEndOfStream) { if (PERFORMANCE_DEBUG) { mDebugSendStartMap.put(timestamp, System.nanoTime() / 1000000); } @@ -302,7 +322,8 @@ public class AudioMediaEncoder extends MediaEncoder { long gapStart = mTimestamp.getGapStartUs(mLastTimeUs); long frameUs = AudioTimestamp.bytesToUs(mConfig.frameSize(), mConfig.byteRate()); - LOG.w("read thread - GAPS: trying to add", gaps, "noise buffers. PERFORMANCE_MAX_GAPS:", PERFORMANCE_MAX_GAPS); + LOG.w("read thread - GAPS: trying to add", gaps, + "noise buffers. PERFORMANCE_MAX_GAPS:", PERFORMANCE_MAX_GAPS); for (int i = 0; i < Math.min(gaps, PERFORMANCE_MAX_GAPS); i++) { ByteBuffer noiseBuffer = mByteBufferPool.get(); if (noiseBuffer == null) { @@ -337,7 +358,8 @@ public class AudioMediaEncoder extends MediaEncoder { if (mInputBufferQueue.isEmpty()) { skipFrames(2); } else { - LOG.i("encoding thread - performing", mInputBufferQueue.size(), "pending operations."); + LOG.i("encoding thread - performing", mInputBufferQueue.size(), + "pending operations."); InputBuffer inputBuffer; while ((inputBuffer = mInputBufferQueue.peek()) != null) { @@ -347,10 +369,13 @@ public class AudioMediaEncoder extends MediaEncoder { Long sendStart = mDebugSendStartMap.remove(inputBuffer.timestamp); //noinspection StatementWithEmptyBody if (sendStart != null) { - mDebugSendAvgDelay = ((mDebugSendAvgDelay * mDebugSendCount) + (sendEnd - sendStart)) / (++mDebugSendCount); - LOG.v("send delay millis:", sendEnd - sendStart, "average:", mDebugSendAvgDelay); + mDebugSendAvgDelay = ((mDebugSendAvgDelay * mDebugSendCount) + + (sendEnd - sendStart)) / (++mDebugSendCount); + LOG.v("send delay millis:", sendEnd - sendStart, + "average:", mDebugSendAvgDelay); } else { - // This input buffer was already processed (but tryAcquire failed for now). + // This input buffer was already processed + // (but tryAcquire failed for now). } } @@ -371,23 +396,29 @@ public class AudioMediaEncoder extends MediaEncoder { mInputBufferPool.clear(); if (PERFORMANCE_DEBUG) { // After latest changes, the count here is not so different between MONO and STEREO. - // We get about 400 frames in both cases (430 for MONO, but doesn't seem like a big issue). - LOG.e("EXECUTE DELAY MILLIS:", mDebugExecuteAvgDelay, "COUNT:", mDebugExecuteCount); - LOG.e("SEND DELAY MILLIS:", mDebugSendAvgDelay, "COUNT:", mDebugSendCount); + // We get about 400 frames in both cases (430 for MONO, but doesn't seem like + // a big issue). + LOG.e("EXECUTE DELAY MILLIS:", mDebugExecuteAvgDelay, + "COUNT:", mDebugExecuteCount); + LOG.e("SEND DELAY MILLIS:", mDebugSendAvgDelay, + "COUNT:", mDebugSendCount); } } private void encode(@NonNull InputBuffer buffer) { long executeStart = System.nanoTime() / 1000000; - LOG.i("encoding thread - performing pending operation for timestamp:", buffer.timestamp, "- encoding."); - buffer.data.put(buffer.source); // NOTE: this copy is prob. the worst part here for performance + LOG.i("encoding thread - performing pending operation for timestamp:", + buffer.timestamp, "- encoding."); + // NOTE: this copy is prob. the worst part here for performance + buffer.data.put(buffer.source); mByteBufferPool.recycle(buffer.source); mInputBufferQueue.remove(buffer); encodeInputBuffer(buffer); boolean eos = buffer.isEndOfStream; mInputBufferPool.recycle(buffer); - LOG.i("encoding thread - performing pending operation for timestamp:", buffer.timestamp, "- draining."); + LOG.i("encoding thread - performing pending operation for timestamp:", + buffer.timestamp, "- draining."); // NOTE: can consider calling this drainOutput on yet another thread, which would let us // use an even smaller BUFFER_POOL_MAX_SIZE without losing audio frames. But this way // we can accumulate delay on this new thread without noticing (no pool getting empty). @@ -395,8 +426,10 @@ public class AudioMediaEncoder extends MediaEncoder { if (PERFORMANCE_DEBUG) { long executeEnd = System.nanoTime() / 1000000; - mDebugExecuteAvgDelay = ((mDebugExecuteAvgDelay * mDebugExecuteCount) + (executeEnd - executeStart)) / (++mDebugExecuteCount); - LOG.v("execute delay millis:", executeEnd - executeStart, "average:", mDebugExecuteAvgDelay); + mDebugExecuteAvgDelay = ((mDebugExecuteAvgDelay * mDebugExecuteCount) + + (executeEnd - executeStart)) / (++mDebugExecuteCount); + LOG.v("execute delay millis:", executeEnd - executeStart, + "average:", mDebugExecuteAvgDelay); } } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioTimestamp.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioTimestamp.java index 9f639476..865bd916 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioTimestamp.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioTimestamp.java @@ -69,8 +69,8 @@ class AudioTimestamp { //noinspection StatementWithEmptyBody if (correctionUs < 0) { // This means that this method is being called too often, so that the expected start - // time for this buffer is BEFORE the last buffer end. So, respect the last buffer end - // instead. + // time for this buffer is BEFORE the last buffer end. So, respect the last buffer + // end instead. } mGapUs = 0; mBytesSinceBaseTime += readBytes; diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaCodecBuffers.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaCodecBuffers.java index 1b293cc4..658aef85 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaCodecBuffers.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaCodecBuffers.java @@ -8,8 +8,8 @@ import java.nio.ByteBuffer; import androidx.annotation.RequiresApi; /** - * A Wrapper to MediaCodec that facilitates the use of API-dependent get{Input/Output}Buffer methods, - * in order to prevent: http://stackoverflow.com/q/30646885 + * A Wrapper to MediaCodec that facilitates the use of API-dependent get{Input/Output}Buffer + * methods, in order to prevent: http://stackoverflow.com/q/30646885 */ @RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2) class MediaCodecBuffers { diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoder.java index 5cbd4886..8ce51f09 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoder.java @@ -76,15 +76,15 @@ import java.util.concurrent.atomic.AtomicInteger; * milliseconds of the first frame in the {@link System#currentTimeMillis()} reference, so * something that we can coordinate on. */ -// https://github.com/saki4510t/AudioVideoRecordingSample/blob/master/app/src/main/java/com/serenegiant/encoder/MediaEncoder.java @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) public abstract class MediaEncoder { private final static String TAG = MediaEncoder.class.getSimpleName(); private final static CameraLogger LOG = CameraLogger.create(TAG); - // Did some test to see which value would maximize our performance in the current setup (infinite audio pool). - // Measured the time it would take to write a 30 seconds video. Based on this, we'll go with TIMEOUT=0 for now. + // Did some test to see which value would maximize our performance in the current setup + // (infinite audio pool). Measured the time it would take to write a 30 seconds video. + // Based on this, we'll go with TIMEOUT=0 for now. // INPUT_TIMEOUT_US 10000: 46 seconds // INPUT_TIMEOUT_US 1000: 37 seconds // INPUT_TIMEOUT_US 100: 33 seconds @@ -171,7 +171,8 @@ public abstract class MediaEncoder { * works, we might have {@link #onStop()} or {@link #onStopped()} to be executed before * the previous step has completed. */ - final void prepare(@NonNull final MediaEncoderEngine.Controller controller, final long maxLengthUs) { + final void prepare(@NonNull final MediaEncoderEngine.Controller controller, + final long maxLengthUs) { if (mState >= STATE_PREPARING) { LOG.e(mName, "Wrong state while preparing. Aborting.", mState); return; @@ -230,14 +231,16 @@ public abstract class MediaEncoder { */ @SuppressWarnings("ConstantConditions") final void notify(final @NonNull String event, final @Nullable Object data) { - if (!mPendingEvents.containsKey(event)) mPendingEvents.put(event, new AtomicInteger(0)); + if (!mPendingEvents.containsKey(event)) mPendingEvents.put(event, + new AtomicInteger(0)); final AtomicInteger pendingEvents = mPendingEvents.get(event); pendingEvents.incrementAndGet(); LOG.v(mName, "Notify was called. Posting. pendingEvents:", pendingEvents.intValue()); mWorker.post(new Runnable() { @Override public void run() { - LOG.v(mName, "Notify was called. Executing. pendingEvents:", pendingEvents.intValue()); + LOG.v(mName, "Notify was called. Executing. pendingEvents:", + pendingEvents.intValue()); onEvent(event, data); pendingEvents.decrementAndGet(); } @@ -280,7 +283,8 @@ public abstract class MediaEncoder { * @param maxLengthUs the maxLength in microseconds */ @EncoderThread - protected abstract void onPrepare(@NonNull MediaEncoderEngine.Controller controller, long maxLengthUs); + protected abstract void onPrepare(@NonNull MediaEncoderEngine.Controller controller, + long maxLengthUs); /** * Start recording. This might be a lightweight operation @@ -329,8 +333,8 @@ public abstract class MediaEncoder { } /** - * Returns a new input buffer and index, waiting at most {@link #INPUT_TIMEOUT_US} if none is available. - * Callers should check the boolean result - true if the buffer was filled. + * Returns a new input buffer and index, waiting at most {@link #INPUT_TIMEOUT_US} if none + * is available. Callers should check the boolean result - true if the buffer was filled. * * @param holder the input buffer holder * @return true if acquired @@ -413,7 +417,9 @@ public abstract class MediaEncoder { } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { // should happen before receiving buffers, and should only happen once - if (mController.isStarted()) throw new RuntimeException("MediaFormat changed twice."); + if (mController.isStarted()) { + throw new RuntimeException("MediaFormat changed twice."); + } MediaFormat newFormat = mMediaCodec.getOutputFormat(); mTrackIndex = mController.notifyStarted(newFormat); setState(STATE_STARTED); @@ -424,9 +430,10 @@ public abstract class MediaEncoder { } else { ByteBuffer encodedData = mBuffers.getOutputBuffer(encoderStatus); - // Codec config means that config data was pulled out and fed to the muxer when we got - // the INFO_OUTPUT_FORMAT_CHANGED status. Ignore it. - boolean isCodecConfig = (mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0; + // Codec config means that config data was pulled out and fed to the muxer + // when we got the INFO_OUTPUT_FORMAT_CHANGED status. Ignore it. + boolean isCodecConfig = (mBufferInfo.flags + & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0; if (!isCodecConfig && mController.isStarted() && mBufferInfo.size != 0) { // adjust the ByteBuffer values to match BufferInfo (not needed?) @@ -437,7 +444,8 @@ public abstract class MediaEncoder { // reached and stop recording when needed. if (mFirstTimeUs == Long.MIN_VALUE) { mFirstTimeUs = mBufferInfo.presentationTimeUs; - LOG.w(mName, "DRAINING - Got the first presentation time:", mFirstTimeUs); + LOG.w(mName, "DRAINING - Got the first presentation time:", + mFirstTimeUs); } mLastTimeUs = mBufferInfo.presentationTimeUs; @@ -447,10 +455,12 @@ public abstract class MediaEncoder { // To address this, encoders are required to call notifyFirstFrameMillis // so we can adjust here - moving to 1970 reference. // Extra benefit: we never pass a pts equal to 0, which some encoders refuse. - mBufferInfo.presentationTimeUs = (mStartTimeMillis * 1000) + mLastTimeUs - mFirstTimeUs; + mBufferInfo.presentationTimeUs = (mStartTimeMillis * 1000) + + mLastTimeUs - mFirstTimeUs; // Write. - LOG.i(mName, "DRAINING - About to write(). Adjusted presentation:", mBufferInfo.presentationTimeUs); + LOG.i(mName, "DRAINING - About to write(). Adjusted presentation:", + mBufferInfo.presentationTimeUs); OutputBuffer buffer = mOutputBufferPool.get(); //noinspection ConstantConditions buffer.info = mBufferInfo; diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoderEngine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoderEngine.java index 91ae92ba..7ca3eb14 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoderEngine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoderEngine.java @@ -134,7 +134,8 @@ public class MediaEncoderEngine { mEncoders.add(audioEncoder); } try { - mMediaMuxer = new MediaMuxer(file.toString(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); + mMediaMuxer = new MediaMuxer(file.toString(), + MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); } catch (IOException e) { throw new RuntimeException(e); } @@ -151,7 +152,8 @@ public class MediaEncoderEngine { long maxDurationUs = maxDuration * 1000L; long finalMaxDurationUs = Long.MAX_VALUE; if (maxSize > 0 && maxDuration > 0) { - mPossibleEndReason = sizeMaxDurationUs < maxDurationUs ? END_BY_MAX_SIZE : END_BY_MAX_DURATION; + mPossibleEndReason = sizeMaxDurationUs < maxDurationUs ? END_BY_MAX_SIZE + : END_BY_MAX_DURATION; finalMaxDurationUs = Math.min(sizeMaxDurationUs, maxDurationUs); } else if (maxSize > 0) { mPossibleEndReason = END_BY_MAX_SIZE; @@ -207,8 +209,9 @@ public class MediaEncoderEngine { } /** - * Called after all encoders have requested a release using {@link Controller#notifyStopped(int)}. - * At this point we will do cleanup and notify the listener. + * Called after all encoders have requested a release using + * {@link Controller#notifyStopped(int)}. At this point we will do cleanup and notify + * the listener. */ private void end() { LOG.i("end:", "Releasing muxer after all encoders have been released."); @@ -284,9 +287,11 @@ public class MediaEncoderEngine { throw new IllegalStateException("Trying to start but muxer started already"); } int track = mMediaMuxer.addTrack(format); - LOG.w("notifyStarted:", "Assigned track", track, "to format", format.getString(MediaFormat.KEY_MIME)); + LOG.w("notifyStarted:", "Assigned track", track, "to format", + format.getString(MediaFormat.KEY_MIME)); if (++mStartedEncodersCount == mEncoders.size()) { - LOG.w("notifyStarted:", "All encoders have started. Starting muxer and dispatching onEncodingStart()."); + LOG.w("notifyStarted:", "All encoders have started.", + "Starting muxer and dispatching onEncodingStart()."); // Go out of this thread since it might be very important for the // encoders and we don't want to perform expensive operations here. mControllerThread.run(new Runnable() { @@ -347,7 +352,8 @@ public class MediaEncoderEngine { LOG.v("write:", "Writing into muxer -", "track:", buffer.trackIndex, "presentation:", buffer.info.presentationTimeUs, - "readable:", calendar.get(Calendar.SECOND) + ":" + calendar.get(Calendar.MILLISECOND), + "readable:", calendar.get(Calendar.SECOND) + ":" + + calendar.get(Calendar.MILLISECOND), "count:", count); } else { LOG.v("write:", "Writing into muxer -", @@ -360,9 +366,9 @@ public class MediaEncoderEngine { /** * Requests that the engine stops. This is not executed until all encoders call - * this method, so it is a kind of soft request, just like {@link #notifyStarted(MediaFormat)}. - * To be used when maxLength / maxSize constraints are reached, for example. - * + * this method, so it is a kind of soft request, just like + * {@link #notifyStarted(MediaFormat)}. To be used when maxLength / maxSize constraints + * are reached, for example. * When this succeeds, {@link MediaEncoder#stop()} is called. * * @param track track @@ -371,7 +377,8 @@ public class MediaEncoderEngine { synchronized (mControllerLock) { LOG.w("requestStop:", "Called for track", track); if (--mStartedEncodersCount == 0) { - LOG.w("requestStop:", "All encoders have requested a stop. Stopping them."); + LOG.w("requestStop:", "All encoders have requested a stop.", + "Stopping them."); mEndReason = mPossibleEndReason; // Go out of this thread since it might be very important for the // encoders and we don't want to perform expensive operations here. @@ -395,7 +402,8 @@ public class MediaEncoderEngine { synchronized (mControllerLock) { LOG.w("notifyStopped:", "Called for track", track); if (++mStoppedEncodersCount == mEncoders.size()) { - LOG.w("requestStop:", "All encoders have been stopped. Stopping the muxer."); + LOG.w("requestStop:", "All encoders have been stopped.", + "Stopping the muxer."); // Go out of this thread since it might be very important for the // encoders and we don't want to perform expensive operations here. mControllerThread.run(new Runnable() { diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/TextureMediaEncoder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/TextureMediaEncoder.java index cceb82b8..4d30dc41 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/TextureMediaEncoder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/TextureMediaEncoder.java @@ -80,7 +80,8 @@ public class TextureMediaEncoder extends VideoMediaEncoder { @NonNull public Frame acquireFrame() { if (mFramePool.isEmpty()) { - throw new RuntimeException("Need more frames than this! Please increase the pool size."); + throw new RuntimeException("Need more frames than this! " + + "Please increase the pool size."); } else { //noinspection ConstantConditions return mFramePool.get(); @@ -123,7 +124,7 @@ public class TextureMediaEncoder extends VideoMediaEncoder { // Always render the first few frames, or muxer fails. return true; } else if (getPendingEvents(FRAME_EVENT) > 2) { - LOG.w("shouldRenderFrame - Dropping frame because we already have too many pending events:", + LOG.w("shouldRenderFrame - Dropping, we already have too many pending events:", getPendingEvents(FRAME_EVENT)); return false; } else { @@ -188,8 +189,9 @@ public class TextureMediaEncoder extends VideoMediaEncoder { "timestampUs:", frame.timestampUs(), "- rendering."); - // 1. We must scale this matrix like GlCameraPreview does, because it might have some cropping. - // Scaling takes place with respect to the (0, 0, 0) point, so we must apply a Translation to compensate. + // 1. We must scale this matrix like GlCameraPreview does, because it might have some + // cropping. Scaling takes place with respect to the (0, 0, 0) point, so we must apply + // a Translation to compensate. float[] transform = frame.transform; float scaleX = mConfig.scaleX; float scaleY = mConfig.scaleY; @@ -198,10 +200,10 @@ public class TextureMediaEncoder extends VideoMediaEncoder { Matrix.translateM(transform, 0, scaleTranslX, scaleTranslY, 0); Matrix.scaleM(transform, 0, scaleX, scaleY, 1); - // 2. We also must rotate this matrix. In GlCameraPreview it is not needed because it is a live - // stream, but the output video, must be correctly rotated based on the device rotation at the moment. - // Rotation also takes place with respect to the origin (the Z axis), so we must - // translate to origin, rotate, then back to where we were. + // 2. We also must rotate this matrix. In GlCameraPreview it is not needed because it is + // a live stream, but the output video, must be correctly rotated based on the device + // rotation at the moment. Rotation also takes place with respect to the origin + // (the Z axis), so we must translate to origin, rotate, then back to where we were. Matrix.translateM(transform, 0, 0.5F, 0.5F, 0); Matrix.rotateM(transform, 0, mTransformRotation, 0, 0, 1); Matrix.translateM(transform, 0, -0.5F, -0.5F, 0); @@ -209,13 +211,16 @@ public class TextureMediaEncoder extends VideoMediaEncoder { // 3. Do the same for overlays with their own rotation. if (mConfig.hasOverlay()) { mConfig.overlayDrawer.draw(mConfig.overlayTarget); - Matrix.translateM(mConfig.overlayDrawer.getTransform(), 0, 0.5F, 0.5F, 0); - Matrix.rotateM(mConfig.overlayDrawer.getTransform(), 0, mConfig.overlayRotation, 0, 0, 1); - Matrix.translateM(mConfig.overlayDrawer.getTransform(), 0, -0.5F, -0.5F, 0); + Matrix.translateM(mConfig.overlayDrawer.getTransform(), + 0, 0.5F, 0.5F, 0); + Matrix.rotateM(mConfig.overlayDrawer.getTransform(), 0, mConfig.overlayRotation, + 0, 0, 1); + Matrix.translateM(mConfig.overlayDrawer.getTransform(), + 0, -0.5F, -0.5F, 0); } - mViewport.drawFrame(mConfig.textureId, transform); + mViewport.drawFrame(frame.timestampUs(), mConfig.textureId, transform); if (mConfig.hasOverlay()) { - mConfig.overlayDrawer.render(); + mConfig.overlayDrawer.render(frame.timestampUs()); } mWindow.setPresentationTime(frame.timestampNanos); mWindow.swapBuffers(); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/VideoMediaEncoder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/VideoMediaEncoder.java index 5b582f49..770c3ad7 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/VideoMediaEncoder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/VideoMediaEncoder.java @@ -54,13 +54,17 @@ abstract class VideoMediaEncoder extends MediaEncoder { @EncoderThread @Override protected void onPrepare(@NonNull MediaEncoderEngine.Controller controller, long maxLengthUs) { - MediaFormat format = MediaFormat.createVideoFormat(mConfig.mimeType, mConfig.width, mConfig.height); - - // Failing to specify some of these can cause the MediaCodec configure() call to throw an unhelpful exception. - // About COLOR_FormatSurface, see https://stackoverflow.com/q/28027858/4288782 - // This just means it is an opaque, implementation-specific format that the device GPU prefers. - // So as long as we use the GPU to draw, the format will match what the encoder expects. - format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); + MediaFormat format = MediaFormat.createVideoFormat(mConfig.mimeType, mConfig.width, + mConfig.height); + + // Failing to specify some of these can cause the MediaCodec configure() call to throw an + // unhelpful exception. About COLOR_FormatSurface, see + // https://stackoverflow.com/q/28027858/4288782 + // This just means it is an opaque, implementation-specific format that the device + // GPU prefers. So as long as we use the GPU to draw, the format will match what + // the encoder expects. + format.setInteger(MediaFormat.KEY_COLOR_FORMAT, + MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); format.setInteger(MediaFormat.KEY_BIT_RATE, mConfig.bitRate); format.setInteger(MediaFormat.KEY_FRAME_RATE, mConfig.frameRate); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); // seconds between key frames! diff --git a/demo/build.gradle b/demo/build.gradle index be25003a..bbf6cff8 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -23,6 +23,6 @@ android { dependencies { implementation project(':cameraview') - implementation 'androidx.appcompat:appcompat:1.1.0-alpha02' - implementation 'com.google.android.material:material:1.1.0-alpha03' + implementation 'androidx.appcompat:appcompat:1.1.0-rc01' + implementation 'com.google.android.material:material:1.1.0-alpha09' } 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 50508a71..644afa6c 100644 --- a/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java +++ b/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java @@ -190,6 +190,12 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis OptionView view = (OptionView) group.getChildAt(i); view.onCameraOpened(camera, options); } + camera.addFrameProcessor(new FrameProcessor() { + @Override + public void process(@NonNull Frame frame) { + + } + }); } @Override diff --git a/docs/_posts/2018-12-20-changelog.md b/docs/_posts/2018-12-20-changelog.md index 2e8d729c..97aac6dc 100644 --- a/docs/_posts/2018-12-20-changelog.md +++ b/docs/_posts/2018-12-20-changelog.md @@ -8,6 +8,20 @@ order: 3 New versions are released through GitHub, so the reference page is the [GitHub Releases](https://github.com/natario1/CameraView/releases) page. +## v2.3.0 (to be released) + +- [Camera2, Metering] New: `startAutoFocus` is much more powerful and does 3A metering (AF, AE, AWB) ([#574][574]) +- [Camera2, Metering] New: `setPictureMetering(boolean)` decides whether to do metering before `takePicture()`. Defaults to true to improve quality. ([#580][580]) +- [Camera2, Metering] New: `setPictureSnapshotMetering(boolean)` decides whether to do metering before `takePictureSnapshot()`. Defaults to false to improve latency. However, you can set this to true to greatly improve the snapshot quality, for example to support `Flash`. ([#580][580]) +- [Camera2, Metering] New: metering extended to many more cameras, which can now use `startAutoFocus` or the focus gesture ([#574][574]) +- [Camera2, Metering] Improvement: `onAutoFocusEnd` is now guaranteed to be called ([#574][574]) +- [Camera2, Metering] Improvement: taking picture does not invalidate the previous focus ([#574][574]) +- [Camera2, Metering] Improvement: better metering when zoomed in ([#574][574]) +- [Real time filters] **Breaking change**: `Filter` interface signatures now accept timestamps for animations ([#588][588]) +- [Overlays] New: you can now use `addView()` and `removeView()` to add or remove overlays at runtime (see docs) ([#588][588]) +- [Video] Improvement: better encoder selection ([#588][588]) +- Fix: fixed various bugs and improved stability ([#588][588]) + ## v2.2.0 - [Real time filters] New: `SimpleFilter` class accepts a fragment shader in the constructor ([#552][552]) @@ -303,3 +317,6 @@ https://github.com/natario1/CameraView/compare/v1.2.3...v1.3.0 [559]: https://github.com/natario1/CameraView/pull/559 [564]: https://github.com/natario1/CameraView/pull/564 [572]: https://github.com/natario1/CameraView/pull/572 +[574]: https://github.com/natario1/CameraView/pull/574 +[580]: https://github.com/natario1/CameraView/pull/580 +[588]: https://github.com/natario1/CameraView/pull/588 diff --git a/docs/_posts/2019-07-14-watermarks-and-overlays.md b/docs/_posts/2019-07-14-watermarks-and-overlays.md index 22024ce6..da8d2ec9 100644 --- a/docs/_posts/2019-07-14-watermarks-and-overlays.md +++ b/docs/_posts/2019-07-14-watermarks-and-overlays.md @@ -44,16 +44,30 @@ overlay with `findViewById` and: - Change its position or appearance - Do so while video is being recorded - Any changes in the overlay appearance will be recorded in real-time in the picture snapshot - or video snapshot that you are capturing. +Any changes in the overlay appearance will be recorded in real-time in the picture snapshot +or video snapshot that you are capturing. + +As you can see in the example, you can also selectively choose, for each overlay, whether it +will draw on the preview (`layout_drawOnPreview`), on picture snapshots (`layout_drawOnPreview`), +on video snapshots (`layout_drawOnPreview`). - As you can see in the example, you can also selectively choose, for each overlay, whether it - will draw on the preview (`layout_drawOnPreview`), on picture snapshots (`layout_drawOnPreview`), - on video snapshots (`layout_drawOnPreview`). - ### Advanced Usage -If you need to change these flags at runtime, you should cast the overlay `LayoutParams` as follows: +To add an overlay at runtime, simply use `addView()`, but make sure you pass in an instance of +`OverlayLayout.LayoutParams`: + +```java +OverlayLayout.LayoutParams() params = new OverlayLayout.LayoutParams(); +cameraView.addView(overlay, params); +``` + +To remove an overlay at runtime, simply use `removeView()`: + +```java +cameraView.removeView(overlay); +``` + +To change the `layout_` flags at runtime, you should cast the overlay `LayoutParams` as follows: ```java // Cast to OverlayLayout.LayoutParams