diff --git a/README.md b/README.md index 0a28177d..13107a24 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,7 @@ Using CameraView is extremely simple: app:cameraEngine="camera1|camera2" app:cameraPreview="glSurface|surface|texture" app:cameraPreviewFrameRate="@integer/preview_frame_rate" + app:cameraPreviewFrameRateExact="false|true" app:cameraFacing="back|front" app:cameraHdr="on|off" app:cameraFlash="on|auto|torch|off" diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java index cb856275..538703ac 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java @@ -177,6 +177,7 @@ public class CameraViewTest extends BaseTest { assertEquals(cameraView.getFrameProcessingMaxWidth(), 0); assertEquals(cameraView.getFrameProcessingMaxHeight(), 0); assertEquals(cameraView.getFrameProcessingFormat(), 0); + assertFalse(cameraView.getPreviewFrameRateExact()); // Self managed GestureParser gestures = new GestureParser(empty); @@ -833,6 +834,14 @@ public class CameraViewTest extends BaseTest { assertEquals(cameraView.getPreviewFrameRate(), 60, 0); } + @Test + public void testPreviewFrameRateExact() { + cameraView.setPreviewFrameRateExact(true); + assertTrue(cameraView.getPreviewFrameRateExact()); + cameraView.setPreviewFrameRateExact(false); + assertFalse(cameraView.getPreviewFrameRateExact()); + } + @Test public void testSnapshotMaxSize() { cameraView.setSnapshotMaxWidth(500); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java index f5066a42..dec8b883 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java @@ -206,6 +206,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { int videoBitRate = a.getInteger(R.styleable.CameraView_cameraVideoBitRate, 0); int audioBitRate = a.getInteger(R.styleable.CameraView_cameraAudioBitRate, 0); float videoFrameRate = a.getFloat(R.styleable.CameraView_cameraPreviewFrameRate, 0); + boolean videoFrameRateExact = a.getBoolean(R.styleable.CameraView_cameraPreviewFrameRateExact, false); long autoFocusResetDelay = (long) a.getInteger( R.styleable.CameraView_cameraAutoFocusResetDelay, (int) DEFAULT_AUTOFOCUS_RESET_DELAY_MILLIS); @@ -277,6 +278,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { setVideoMaxDuration(videoMaxDuration); setVideoBitRate(videoBitRate); setAutoFocusResetDelay(autoFocusResetDelay); + setPreviewFrameRateExact(videoFrameRateExact); setPreviewFrameRate(videoFrameRate); setSnapshotMaxWidth(snapshotMaxWidth); setSnapshotMaxHeight(snapshotMaxHeight); @@ -999,6 +1001,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { setVideoBitRate(oldEngine.getVideoBitRate()); setAutoFocusResetDelay(oldEngine.getAutoFocusResetDelay()); setPreviewFrameRate(oldEngine.getPreviewFrameRate()); + setPreviewFrameRateExact(oldEngine.getPreviewFrameRateExact()); setSnapshotMaxWidth(oldEngine.getSnapshotMaxWidth()); setSnapshotMaxHeight(oldEngine.getSnapshotMaxHeight()); setFrameProcessingMaxWidth(oldEngine.getFrameProcessingMaxWidth()); @@ -1543,6 +1546,38 @@ public class CameraView extends FrameLayout implements LifecycleObserver { return mCameraEngine.getVideoBitRate(); } + /** + * A flag to control the behavior when calling {@link #setPreviewFrameRate(float)}. + * + * If the value is set to true, {@link #setPreviewFrameRate(float)} will choose the preview + * frame range as close to the desired new frame rate as possible. Which mean it may choose a + * narrow range around the desired frame rate. Note: This option will give you as exact fps as + * you want but the sensor will have less freedom when adapting the exposure to the environment, + * which may lead to dark preview. + * + * If the value is set to false, {@link #setPreviewFrameRate(float)} will choose as broad range + * as it can. + * + * @param videoFrameRateExact whether want a more exact preview frame range + * + * @see #setPreviewFrameRate(float) + */ + public void setPreviewFrameRateExact(boolean videoFrameRateExact) { + mCameraEngine.setPreviewFrameRateExact(videoFrameRateExact); + } + + /** + * Returns whether we want to set preview fps as exact as we set through + * {@link #setPreviewFrameRate(float)}. + * + * @see #setPreviewFrameRateExact(boolean) + * @see #setPreviewFrameRate(float) + * @return current option + */ + public boolean getPreviewFrameRateExact() { + return mCameraEngine.getPreviewFrameRateExact(); + } + /** * Sets the preview frame rate in frames per second. * This rate will be used, for example, by the frame processor and in video 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 47f09bdb..069864c3 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java @@ -52,6 +52,7 @@ import com.otaliastudios.cameraview.video.SnapshotVideoRecorder; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.List; @@ -715,6 +716,7 @@ public class Camera1Engine extends CameraBaseEngine implements private boolean applyPreviewFrameRate(@NonNull Camera.Parameters params, float oldPreviewFrameRate) { List fpsRanges = params.getSupportedPreviewFpsRange(); + sortRanges(fpsRanges); if (mPreviewFrameRate == 0F) { // 0F is a special value. Fallback to a reasonable default. for (int[] fpsRange : fpsRanges) { @@ -745,6 +747,24 @@ public class Camera1Engine extends CameraBaseEngine implements return false; } + private void sortRanges(List fpsRanges) { + if (getPreviewFrameRateExact() && mPreviewFrameRate != 0F) { // sort by range width in ascending order + Collections.sort(fpsRanges, new Comparator() { + @Override + public int compare(int[] range1, int[] range2) { + return (range1[1] - range1[0]) - (range2[1] - range2[0]); + } + }); + } else { // sort by range width in descending order + Collections.sort(fpsRanges, new Comparator() { + @Override + public int compare(int[] range1, int[] range2) { + return (range2[1] - range2[0]) - (range1[1] - range1[0]); + } + }); + } + } + @Override public void setPictureFormat(@NonNull PictureFormat pictureFormat) { if (pictureFormat != PictureFormat.JPEG) { 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 e0c2f1a2..a3313c85 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java @@ -71,6 +71,8 @@ import com.otaliastudios.cameraview.video.Full2VideoRecorder; import com.otaliastudios.cameraview.video.SnapshotVideoRecorder; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.CopyOnWriteArrayList; @@ -1378,6 +1380,7 @@ public class Camera2Engine extends CameraBaseEngine implements Range[] fpsRanges = readCharacteristic( CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES, fallback); + sortRanges(fpsRanges); if (mPreviewFrameRate == 0F) { // 0F is a special value. Fallback to a reasonable default. for (Range fpsRange : fpsRanges) { @@ -1403,6 +1406,26 @@ public class Camera2Engine extends CameraBaseEngine implements return false; } + private void sortRanges(Range[] fpsRanges) { + if (getPreviewFrameRateExact() && mPreviewFrameRate != 0) { // sort by range width in ascending order + Arrays.sort(fpsRanges, new Comparator>() { + @Override + public int compare(Range range1, Range range2) { + return (range1.getUpper() - range1.getLower()) + - (range2.getUpper() - range2.getLower()); + } + }); + } else { // sort by range width in descending order + Arrays.sort(fpsRanges, new Comparator>() { + @Override + public int compare(Range range1, Range range2) { + return (range2.getUpper() - range2.getLower()) + - (range1.getUpper() - range1.getLower()); + } + }); + } + } + @Override public void setPictureFormat(final @NonNull PictureFormat pictureFormat) { if (pictureFormat != mPictureFormat) { diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraBaseEngine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraBaseEngine.java index 0f21ac54..df95098f 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraBaseEngine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraBaseEngine.java @@ -70,6 +70,7 @@ public abstract class CameraBaseEngine extends CameraEngine { @SuppressWarnings("WeakerAccess") protected boolean mPictureMetering; @SuppressWarnings("WeakerAccess") protected boolean mPictureSnapshotMetering; @SuppressWarnings("WeakerAccess") protected float mPreviewFrameRate; + @SuppressWarnings("WeakerAccess") private boolean mPreviewFrameRateExact; private FrameManager mFrameManager; private final Angles mAngles = new Angles(); @@ -441,6 +442,16 @@ public abstract class CameraBaseEngine extends CameraEngine { return mPictureFormat; } + @Override + public final void setPreviewFrameRateExact(boolean previewFrameRateExact) { + mPreviewFrameRateExact = previewFrameRateExact; + } + + @Override + public final boolean getPreviewFrameRateExact() { + return mPreviewFrameRateExact; + } + @Override public final float getPreviewFrameRate() { return mPreviewFrameRate; 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 7b26330f..e2c4733d 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java @@ -690,6 +690,8 @@ public abstract class CameraEngine implements public abstract void setPictureFormat(@NonNull PictureFormat pictureFormat); @NonNull public abstract PictureFormat getPictureFormat(); + public abstract void setPreviewFrameRateExact(boolean previewFrameRateExact); + public abstract boolean getPreviewFrameRateExact(); public abstract void setPreviewFrameRate(float previewFrameRate); public abstract float getPreviewFrameRate(); diff --git a/cameraview/src/main/res/values/attrs.xml b/cameraview/src/main/res/values/attrs.xml index 780566a0..609a69a3 100644 --- a/cameraview/src/main/res/values/attrs.xml +++ b/cameraview/src/main/res/values/attrs.xml @@ -34,6 +34,7 @@ + diff --git a/docs/_docs/controls.md b/docs/_docs/controls.md index 1321bce8..92f2b6ab 100644 --- a/docs/_docs/controls.md +++ b/docs/_docs/controls.md @@ -29,7 +29,8 @@ or `CameraOptions.supports(Control)` to see if it is supported. app:cameraVideoMaxSize="0" app:cameraVideoMaxDuration="0" app:cameraVideoBitRate="0" - app:cameraPreviewFrameRate="30"/> + app:cameraPreviewFrameRate="30" + app:cameraPreviewFrameRateExact="false|true"/> ``` ### APIs @@ -180,6 +181,18 @@ float min = options.getPreviewFrameRateMinValue(); float max = options.getPreviewFrameRateMaxValue(); ``` +##### cameraPreviewFrameRateExact +Controls the behavior of `cameraPreviewFrameRate`. If this option is set to `true`, the narrowest +range containing the new preview fps will be used. If this option is set to `false` the broadest +range containing the new preview fps will be used. Note: If set this option to true, it will give as +exact preview fps as you want, but the sensor will have less freedom when adapting the exposure to +the environment, which may lead to dark preview. + +```java +cameraView.setPreviewFrameRateExact(true); +cameraView.setPreviewFrameRageExact(false); +``` + ### Zoom There are two ways to control the zoom value: