From 218aa9d1088f289ef7fe5a3e5eef59f709fdab3c Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Wed, 23 Oct 2019 22:46:04 +0200 Subject: [PATCH] Preview FPS docs + Camera1 implementation (#661) * Small changes * Camera1 options * Complete Camera1 integration * Add to demo app * Complete docs --- README.md | 1 + .../cameraview/CameraOptions1Test.java | 14 ++++ .../cameraview/CameraOptions.java | 64 +++++++++---------- .../otaliastudios/cameraview/CameraView.java | 13 +++- .../cameraview/engine/Camera1Engine.java | 53 ++++++++++++++- .../cameraview/engine/Camera2Engine.java | 42 +++++++----- .../cameraview/demo/CameraActivity.java | 11 +++- .../otaliastudios/cameraview/demo/Option.java | 43 ++++++++++++- docs/_posts/2018-12-20-controls.md | 36 ++++++++++- 9 files changed, 215 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index 14ef6f9f..2f4a5316 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,7 @@ Using CameraView is extremely simple: app:cameraGestureScrollVertical="none|zoom|exposureCorrection|filterControl1|filterControl2" app:cameraEngine="camera1|camera2" app:cameraPreview="glSurface|surface|texture" + app:cameraPreviewFrameRate="@integer/preview_frame_rate" app:cameraFacing="back|front" app:cameraHdr="on|off" app:cameraFlash="on|auto|torch|off" diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraOptions1Test.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraOptions1Test.java index 25f218ab..5aaae714 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraOptions1Test.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraOptions1Test.java @@ -22,6 +22,7 @@ import androidx.test.filters.SmallTest; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -345,4 +346,17 @@ public class CameraOptions1Test extends BaseTest { assertEquals(o.getExposureCorrectionMaxValue(), 10f * 0.5f, 0f); } + @Test + public void testPreviewFrameRate() { + Camera.Parameters params = mock(Camera.Parameters.class); + List result = Arrays.asList( + new int[]{20000, 30000}, + new int[]{30000, 60000}, + new int[]{60000, 120000} + ); + when(params.getSupportedPreviewFpsRange()).thenReturn(result); + CameraOptions o = new CameraOptions(params, 0, false); + assertEquals(20F, o.getPreviewFrameRateMinValue(), 0.001F); + assertEquals(120F, o.getPreviewFrameRateMaxValue(), 0.001F); + } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java index d95a4af8..c68650c2 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java @@ -60,8 +60,8 @@ public class CameraOptions { private float exposureCorrectionMinValue; private float exposureCorrectionMaxValue; private boolean autoFocusSupported; - private float fpsRangeMinValue; - private float fpsRangeMaxValue; + private float previewFrameRateMinValue; + private float previewFrameRateMaxValue; public CameraOptions(@NonNull Camera.Parameters params, int cameraId, boolean flipSizes) { @@ -159,9 +159,16 @@ public class CameraOptions { } } - //fps range - fpsRangeMinValue = 0F; - fpsRangeMaxValue = 0F; + // Preview FPS + previewFrameRateMinValue = Float.MAX_VALUE; + previewFrameRateMaxValue = -Float.MAX_VALUE; + List fpsRanges = params.getSupportedPreviewFpsRange(); + for (int[] fpsRange : fpsRanges) { + float lower = (float) fpsRange[0] / 1000F; + float upper = (float) fpsRange[1] / 1000F; + previewFrameRateMinValue = Math.min(previewFrameRateMinValue, lower); + previewFrameRateMaxValue = Math.max(previewFrameRateMaxValue, upper); + } } // Camera2Engine constructor. @@ -279,22 +286,19 @@ public class CameraOptions { } } - //fps Range - fpsRangeMinValue = Float.MAX_VALUE; - fpsRangeMaxValue = Float.MIN_VALUE; - Range[] range = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); + // Preview FPS + Range[] range = cameraCharacteristics.get( + CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); if (range != null) { + previewFrameRateMinValue = Float.MAX_VALUE; + previewFrameRateMaxValue = -Float.MAX_VALUE; for (Range fpsRange : range) { - if (fpsRange.getLower() <= fpsRangeMinValue) { - fpsRangeMinValue = fpsRange.getLower(); - } - if (fpsRange.getUpper() >= fpsRangeMaxValue) { - fpsRangeMaxValue = fpsRange.getUpper(); - } + previewFrameRateMinValue = Math.min(previewFrameRateMinValue, fpsRange.getLower()); + previewFrameRateMaxValue = Math.max(previewFrameRateMaxValue, fpsRange.getUpper()); } } else { - fpsRangeMinValue = 0F; - fpsRangeMaxValue = 0F; + previewFrameRateMinValue = 0F; + previewFrameRateMaxValue = 0F; } } @@ -308,7 +312,6 @@ public class CameraOptions { return getSupportedControls(control.getClass()).contains(control); } - /** * Shorthand for other methods in this class, * e.g. supports(GestureAction.ZOOM) == isZoomSupported(). @@ -333,7 +336,6 @@ public class CameraOptions { return false; } - @SuppressWarnings("unchecked") @NonNull public Collection getSupportedControls(@NonNull Class controlClass) { @@ -362,7 +364,6 @@ public class CameraOptions { return Collections.emptyList(); } - /** * Set of supported picture sizes for the currently opened camera. * @@ -373,7 +374,6 @@ public class CameraOptions { return Collections.unmodifiableSet(supportedPictureSizes); } - /** * Set of supported picture aspect ratios for the currently opened camera. * @@ -385,7 +385,6 @@ public class CameraOptions { return Collections.unmodifiableSet(supportedPictureAspectRatio); } - /** * Set of supported video sizes for the currently opened camera. * @@ -396,7 +395,6 @@ public class CameraOptions { return Collections.unmodifiableSet(supportedVideoSizes); } - /** * Set of supported picture aspect ratios for the currently opened camera. * @@ -408,7 +406,6 @@ public class CameraOptions { return Collections.unmodifiableSet(supportedVideoAspectRatio); } - /** * Set of supported facing values. * @@ -421,7 +418,6 @@ public class CameraOptions { return Collections.unmodifiableSet(supportedFacing); } - /** * Set of supported flash values. * @@ -436,7 +432,6 @@ public class CameraOptions { return Collections.unmodifiableSet(supportedFlash); } - /** * Set of supported white balance values. * @@ -452,7 +447,6 @@ public class CameraOptions { return Collections.unmodifiableSet(supportedWhiteBalance); } - /** * Set of supported hdr values. * @@ -488,7 +482,6 @@ public class CameraOptions { return autoFocusSupported; } - /** * Whether exposure correction is supported. If this is false, calling * {@link CameraView#setExposureCorrection(float)} has no effect. @@ -501,7 +494,6 @@ public class CameraOptions { return exposureCorrectionSupported; } - /** * The minimum value of negative exposure correction, in EV stops. * This is presumably negative or 0 if not supported. @@ -524,18 +516,20 @@ public class CameraOptions { } /** - * The minimum value for FPS + * The minimum value for the preview frame rate, in frames per second (FPS). + * * @return the min value */ - public float getFpsRangeMinValue() { - return fpsRangeMinValue; + public float getPreviewFrameRateMinValue() { + return previewFrameRateMinValue; } /** - * The maximum value for FPS + * The maximum value for the preview frame rate, in frames per second (FPS). + * * @return the max value */ - public float getFpsRangeMaxValue() { - return fpsRangeMaxValue; + public float getPreviewFrameRateMaxValue() { + return previewFrameRateMaxValue; } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java index 27704ff0..3921abe6 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java @@ -1450,8 +1450,12 @@ public class CameraView extends FrameLayout implements LifecycleObserver { } /** - * Sets the frame rate for the video - * Will be used by {@link #takeVideoSnapshot(File)}. + * Sets the preview frame rate in frames per second. + * This rate will be used, for example, by the frame processor and in video + * snapshot taken through {@link #takeVideo(File)}. + * + * A value of 0F will restore the rate to a default value. + * * @param frameRate desired frame rate */ public void setPreviewFrameRate(float frameRate) { @@ -1459,7 +1463,10 @@ public class CameraView extends FrameLayout implements LifecycleObserver { } /** - * Returns the current frame rate. + * Returns the current preview frame rate. + * This can return 0F if no frame rate was set. + * + * @see #setPreviewFrameRate(float) * @return current frame rate */ public float getPreviewFrameRate() { 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 f32232e2..f7111e85 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java @@ -12,8 +12,8 @@ import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import androidx.annotation.WorkerThread; +import android.util.Range; import android.view.SurfaceHolder; import com.google.android.gms.tasks.Task; @@ -392,6 +392,7 @@ public class Camera1Engine extends CameraEngine implements // The correct formula seems to be deviceOrientation+displayOffset, // which means offset(Reference.VIEW, Reference.OUTPUT, Axis.ABSOLUTE). stub.rotation = getAngles().offset(Reference.VIEW, Reference.OUTPUT, Axis.ABSOLUTE); + stub.videoFrameRate = Math.round(mPreviewFrameRate); LOG.i("onTakeVideoSnapshot", "rotation:", stub.rotation, "size:", stub.size); // Start. @@ -423,6 +424,7 @@ public class Camera1Engine extends CameraEngine implements applyZoom(params, 0F); applyExposureCorrection(params, 0F); applyPlaySounds(mPlaySounds); + applyPreviewFrameRate(params, 0F); } private void applyDefaultFocus(@NonNull Camera.Parameters params) { @@ -666,8 +668,53 @@ public class Camera1Engine extends CameraEngine implements return false; } - @Override public void setPreviewFrameRate(float previewFrameRate) { - // This method does nothing + @Override + public void setPreviewFrameRate(float previewFrameRate) { + final float old = previewFrameRate; + mPreviewFrameRate = previewFrameRate; + mHandler.run(new Runnable() { + @Override + public void run() { + if (getEngineState() == STATE_STARTED) { + Camera.Parameters params = mCamera.getParameters(); + if (applyPreviewFrameRate(params, old)) mCamera.setParameters(params); + } + mPreviewFrameRateOp.end(null); + } + }); + } + + private boolean applyPreviewFrameRate(@NonNull Camera.Parameters params, + float oldPreviewFrameRate) { + List fpsRanges = params.getSupportedPreviewFpsRange(); + if (mPreviewFrameRate == 0F) { + // 0F is a special value. Fallback to a reasonable default. + for (int[] fpsRange : fpsRanges) { + float lower = (float) fpsRange[0] / 1000F; + float upper = (float) fpsRange[1] / 1000F; + if ((lower <= 30F && 30F <= upper) || (lower <= 24F && 24F <= upper)) { + params.setPreviewFpsRange(fpsRange[0], fpsRange[1]); + return true; + } + } + } else { + // If out of boundaries, adjust it. + mPreviewFrameRate = Math.min(mPreviewFrameRate, + mCameraOptions.getPreviewFrameRateMaxValue()); + mPreviewFrameRate = Math.max(mPreviewFrameRate, + mCameraOptions.getPreviewFrameRateMinValue()); + for (int[] fpsRange : fpsRanges) { + float lower = (float) fpsRange[0] / 1000F; + float upper = (float) fpsRange[1] / 1000F; + float rate = Math.round(mPreviewFrameRate); + if (lower <= rate && rate <= upper) { + params.setPreviewFpsRange(fpsRange[0], fpsRange[1]); + return true; + } + } + } + mPreviewFrameRate = oldPreviewFrameRate; + return false; } //endregion diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java index 9df41019..c25dd20e 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java @@ -83,7 +83,6 @@ 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 int DEFAULT_FRAME_RATE = 30; @VisibleForTesting static final long METER_TIMEOUT = 2500; private final CameraManager mManager; @@ -815,7 +814,6 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv if (!(mPreview instanceof GlCameraPreview)) { throw new IllegalStateException("Video snapshots are only supported with GL_SURFACE."); } - stub.videoFrameRate = (int) mPreviewFrameRate; GlCameraPreview glPreview = (GlCameraPreview) mPreview; Size outputSize = getUncroppedSnapshotSize(Reference.OUTPUT); if (outputSize == null) { @@ -834,6 +832,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv // Unlike Camera1, the correct formula seems to be deviceOrientation, // which means offset(Reference.BASE, Reference.OUTPUT, Axis.ABSOLUTE). stub.rotation = getAngles().offset(Reference.BASE, Reference.OUTPUT, Axis.ABSOLUTE); + stub.videoFrameRate = Math.round(mPreviewFrameRate); LOG.i("onTakeVideoSnapshot", "rotation:", stub.rotation, "size:", stub.size); // Start. @@ -1273,22 +1272,31 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv } @SuppressWarnings("WeakerAccess") - protected boolean applyPreviewFrameRate(@NonNull CaptureRequest.Builder builder, float oldPreviewFrameRate) { - Range[] fpsRanges = mCameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); - if (fpsRanges != null) { - if (mPreviewFrameRate != 0f) { - for (Range fpsRange : fpsRanges) { - if (fpsRange.contains((int) mPreviewFrameRate)) { - builder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange); - return true; - } + protected boolean applyPreviewFrameRate(@NonNull CaptureRequest.Builder builder, + float oldPreviewFrameRate) { + //noinspection unchecked + Range[] fallback = new Range[]{}; + Range[] fpsRanges = readCharacteristic( + CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES, + fallback); + if (mPreviewFrameRate == 0F) { + // 0F is a special value. Fallback to a reasonable default. + for (Range fpsRange : fpsRanges) { + if (fpsRange.contains(30) || fpsRange.contains(24)) { + builder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange); + return true; } - } else { - for (Range fpsRange : fpsRanges) { - if (fpsRange.contains(DEFAULT_FRAME_RATE)) { - builder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange); - return true; - } + } + } else { + // If out of boundaries, adjust it. + mPreviewFrameRate = Math.min(mPreviewFrameRate, + mCameraOptions.getPreviewFrameRateMaxValue()); + mPreviewFrameRate = Math.max(mPreviewFrameRate, + mCameraOptions.getPreviewFrameRateMinValue()); + for (Range fpsRange : fpsRanges) { + if (fpsRange.contains(Math.round(mPreviewFrameRate))) { + builder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange); + return true; } } } 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 e718e921..8e07625a 100644 --- a/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java +++ b/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java @@ -116,7 +116,7 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis new Option.Flash(), new Option.WhiteBalance(), new Option.Hdr(), new Option.PictureMetering(), new Option.PictureSnapshotMetering(), // Video recording - new Option.VideoCodec(), new Option.Audio(), + new Option.PreviewFrameRate(), new Option.VideoCodec(), new Option.Audio(), // Gestures new Option.Pinch(), new Option.HorizontalScroll(), new Option.VerticalScroll(), new Option.Tap(), new Option.LongTap(), @@ -128,12 +128,19 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis new Option.Grid(), new Option.GridColor(), new Option.UseDeviceOrientation() ); List dividers = Arrays.asList( + // Layout false, true, + // Engine and preview false, false, true, + // Some controls false, false, false, false, true, - false, true, + // Video recording + false, false, true, + // Gestures false, false, false, false, true, + // Watermarks false, false, true, + // Other false, false, true ); for (int i = 0; i < options.size(); i++) { diff --git a/demo/src/main/java/com/otaliastudios/cameraview/demo/Option.java b/demo/src/main/java/com/otaliastudios/cameraview/demo/Option.java index 34f66279..0144004a 100644 --- a/demo/src/main/java/com/otaliastudios/cameraview/demo/Option.java +++ b/demo/src/main/java/com/otaliastudios/cameraview/demo/Option.java @@ -60,7 +60,7 @@ public abstract class Option { @Override public Collection getAll(@NonNull CameraView view, @NonNull CameraOptions options) { View root = (View) view.getParent(); - ArrayList list = new ArrayList<>(); + List list = new ArrayList<>(); int boundary = root.getWidth(); if (boundary == 0) boundary = 1000; int step = boundary / 10; @@ -506,4 +506,45 @@ public abstract class Option { view.setUseDeviceOrientation(value); } } + + public static class PreviewFrameRate extends Option { + public PreviewFrameRate() { + super("Preview FPS"); + } + + @NonNull + @Override + public Collection getAll(@NonNull CameraView view, @NonNull CameraOptions options) { + float min = options.getPreviewFrameRateMinValue(); + float max = options.getPreviewFrameRateMaxValue(); + float delta = max - min; + List result = new ArrayList<>(); + if (min == 0F && max == 0F) { + return result; // empty list + } else if (delta < 0.005F) { + result.add(Math.round(min)); + return result; // single value + } else { + final int steps = 3; + final float step = delta / steps; + for (int i = 0; i <= steps; i++) { + result.add(Math.round(min)); + min += step; + } + return result; + } + } + + @NonNull + @Override + public Integer get(@NonNull CameraView view) { + return Math.round(view.getPreviewFrameRate()); + } + + @Override + public void set(@NonNull CameraView view, @NonNull Integer value) { + view.setPreviewFrameRate((float) value); + } + } + } diff --git a/docs/_posts/2018-12-20-controls.md b/docs/_posts/2018-12-20-controls.md index cf89d708..281d4e85 100644 --- a/docs/_posts/2018-12-20-controls.md +++ b/docs/_posts/2018-12-20-controls.md @@ -30,7 +30,8 @@ or `CameraOptions.supports(Control)` to see if it is supported. app:cameraVideoCodec="deviceDefault|h263|h264" app:cameraVideoMaxSize="0" app:cameraVideoMaxDuration="0" - app:cameraVideoBitRate="0"/> + app:cameraVideoBitRate="0" + app:cameraPreviewFrameRate="30"/> ``` ### APIs @@ -40,6 +41,8 @@ or `CameraOptions.supports(Control)` to see if it is supported. Which camera to use, either back facing or front facing. Defaults to the first available value (tries `BACK` first). +The available values are exposed through the `CameraOptions` object. + ```java cameraView.setFacing(Facing.BACK); cameraView.setFacing(Facing.FRONT); @@ -49,6 +52,8 @@ cameraView.setFacing(Facing.FRONT); Flash mode, either off, on, auto or torch. Defaults to `OFF`. +The available values are exposed through the `CameraOptions` object. + ```java cameraView.setFlash(Flash.OFF); cameraView.setFlash(Flash.ON); @@ -61,6 +66,8 @@ cameraView.setFlash(Flash.TORCH); Sets the encoder for video recordings. Defaults to `DEVICE_DEFAULT`, which should typically be H_264. +The available values are exposed through the `CameraOptions` object. + ```java cameraView.setVideoCodec(VideoCodec.DEVICE_DEFAULT); cameraView.setVideoCodec(VideoCodec.H_263); @@ -72,6 +79,8 @@ cameraView.setVideoCodec(VideoCodec.H_264); Sets the desired white balance for the current session. Defaults to `AUTO`. +The available values are exposed through the `CameraOptions` object. + ```java cameraView.setWhiteBalance(WhiteBalance.AUTO); cameraView.setWhiteBalance(WhiteBalance.INCANDESCENT); @@ -84,6 +93,8 @@ cameraView.setWhiteBalance(WhiteBalance.CLOUDY); Turns on or off HDR captures. Defaults to `OFF`. +The available values are exposed through the `CameraOptions` object. + ```java cameraView.setHdr(Hdr.OFF); cameraView.setHdr(Hdr.ON); @@ -94,6 +105,8 @@ cameraView.setHdr(Hdr.ON); Turns on or off audio stream while recording videos. Defaults to `ON`. +The available values are exposed through the `CameraOptions` object. + ```java cameraView.setAudio(Audio.OFF); cameraView.setAudio(Audio.ON); // on but depends on video config @@ -143,6 +156,27 @@ cameraView.setVideoBitRate(0); cameraView.setVideoBitRate(4000000); ``` +##### cameraPreviewFrameRate + +Controls the preview frame rate, in frames per second. +Use a value of 0F to restore the camera default value. + +```java +cameraView.setPreviewFrameRate(30F); +cameraView.setPreviewFrameRate(0F); +``` + +The preview frame rate is an important parameter because it will also +control (broadly) the rate at which frame processor frames are dispatched, +the video snapshots frame rate, and the rate at which real-time filters are invoked. + +The available values are exposed through the `CameraOptions` object: + +```java +float min = options.getPreviewFrameRateMinValue(); +float max = options.getPreviewFrameRateMaxValue(); +``` + ### Zoom There are two ways to control the zoom value: