diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java index 654dda74..e92d19ff 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java @@ -784,6 +784,12 @@ public class CameraViewTest extends BaseTest { assertEquals(cameraView.getVideoMaxDuration(), 5000); } + @Test + public void testPreviewFrameRate() { + cameraView.setPreviewFrameRate(60); + assertEquals(cameraView.getPreviewFrameRate(), 60, 0); + } + //endregion //region Lists of listeners and processors 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 310cd6ab..59c42f1a 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java @@ -495,6 +495,15 @@ public abstract class CameraIntegrationTest extends BaseTest { // This also ensures there are no crashes when attaching it to camera parameters. } + @Test + public void testSetPreviewFrameRate() { + openSync(true); + controller.mPreviewFrameRateOp.listen(); + camera.setPreviewFrameRate(30); + controller.mPreviewFrameRateOp.await(300); + assertEquals(camera.getPreviewFrameRate(), 30, 0); + } + @Test public void testSetPlaySounds() { controller.mPlaySoundsOp.listen(); diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/MockCameraEngine.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/MockCameraEngine.java index 12dcd5e4..2ad4d168 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/MockCameraEngine.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/MockCameraEngine.java @@ -173,4 +173,8 @@ public class MockCameraEngine extends CameraEngine { protected boolean collectCameraInfo(@NonNull Facing facing) { return true; } + + @Override public void setPreviewFrameRate(float previewFrameRate) { + mPreviewFrameRate = previewFrameRate; + } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java index 7568c66c..d95a4af8 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java @@ -60,6 +60,8 @@ public class CameraOptions { private float exposureCorrectionMinValue; private float exposureCorrectionMaxValue; private boolean autoFocusSupported; + private float fpsRangeMinValue; + private float fpsRangeMaxValue; public CameraOptions(@NonNull Camera.Parameters params, int cameraId, boolean flipSizes) { @@ -156,6 +158,10 @@ public class CameraOptions { } } } + + //fps range + fpsRangeMinValue = 0F; + fpsRangeMaxValue = 0F; } // Camera2Engine constructor. @@ -272,6 +278,24 @@ public class CameraOptions { supportedVideoAspectRatio.add(AspectRatio.of(width, height)); } } + + //fps Range + fpsRangeMinValue = Float.MAX_VALUE; + fpsRangeMaxValue = Float.MIN_VALUE; + Range[] range = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); + if (range != null) { + for (Range fpsRange : range) { + if (fpsRange.getLower() <= fpsRangeMinValue) { + fpsRangeMinValue = fpsRange.getLower(); + } + if (fpsRange.getUpper() >= fpsRangeMaxValue) { + fpsRangeMaxValue = fpsRange.getUpper(); + } + } + } else { + fpsRangeMinValue = 0F; + fpsRangeMaxValue = 0F; + } } /** @@ -498,4 +522,20 @@ public class CameraOptions { public float getExposureCorrectionMaxValue() { return exposureCorrectionMaxValue; } + + /** + * The minimum value for FPS + * @return the min value + */ + public float getFpsRangeMinValue() { + return fpsRangeMinValue; + } + + /** + * The maximum value for FPS + * @return the max value + */ + public float getFpsRangeMaxValue() { + return fpsRangeMaxValue; + } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java index f55e9289..27704ff0 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java @@ -189,6 +189,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { 0); 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); long autoFocusResetDelay = (long) a.getInteger( R.styleable.CameraView_cameraAutoFocusResetDelay, (int) DEFAULT_AUTOFOCUS_RESET_DELAY_MILLIS); @@ -251,6 +252,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { setVideoMaxDuration(videoMaxDuration); setVideoBitRate(videoBitRate); setAutoFocusResetDelay(autoFocusResetDelay); + setPreviewFrameRate(videoFrameRate); // Apply gestures mapGesture(Gesture.TAP, gestures.getTapAction()); @@ -952,6 +954,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { setVideoMaxDuration(oldEngine.getVideoMaxDuration()); setVideoBitRate(oldEngine.getVideoBitRate()); setAutoFocusResetDelay(oldEngine.getAutoFocusResetDelay()); + setPreviewFrameRate(oldEngine.getPreviewFrameRate()); } /** @@ -1446,6 +1449,23 @@ public class CameraView extends FrameLayout implements LifecycleObserver { return mCameraEngine.getVideoBitRate(); } + /** + * Sets the frame rate for the video + * Will be used by {@link #takeVideoSnapshot(File)}. + * @param frameRate desired frame rate + */ + public void setPreviewFrameRate(float frameRate) { + mCameraEngine.setPreviewFrameRate(frameRate); + } + + /** + * Returns the current frame rate. + * @return current frame rate + */ + public float getPreviewFrameRate() { + return mCameraEngine.getPreviewFrameRate(); + } + /** * Sets the bit rate in bits per second for audio capturing. * Will be used by both {@link #takeVideo(File)} and {@link #takeVideoSnapshot(File)}. 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 60d63cb7..f32232e2 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java @@ -666,6 +666,10 @@ public class Camera1Engine extends CameraEngine implements return false; } + @Override public void setPreviewFrameRate(float previewFrameRate) { + // This method does nothing + } + //endregion //region Frame Processing diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java index ac657b46..9df41019 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java @@ -20,6 +20,7 @@ import android.media.Image; import android.media.ImageReader; import android.os.Build; import android.util.Pair; +import android.util.Range; import android.util.Rational; import android.view.Surface; import android.view.SurfaceHolder; @@ -82,6 +83,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 int DEFAULT_FRAME_RATE = 30; @VisibleForTesting static final long METER_TIMEOUT = 2500; private final CameraManager mManager; @@ -813,6 +815,7 @@ 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) { @@ -913,6 +916,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv applyHdr(builder, Hdr.OFF); applyZoom(builder, 0F); applyExposureCorrection(builder, 0F); + applyPreviewFrameRate(builder, 0F); if (oldBuilder != null) { // We might be in a metering operation, or the old builder might have some special @@ -1252,6 +1256,46 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv mPlaySoundsOp.end(null); } + @Override public void setPreviewFrameRate(float previewFrameRate) { + final float oldPreviewFrameRate = mPreviewFrameRate; + mPreviewFrameRate = previewFrameRate; + mHandler.run(new Runnable() { + @Override + public void run() { + if (getEngineState() == STATE_STARTED) { + if (applyPreviewFrameRate(mRepeatingRequestBuilder, oldPreviewFrameRate)) { + applyRepeatingRequestBuilder(); + } + } + mPreviewFrameRateOp.end(null); + } + }); + } + + @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; + } + } + } else { + for (Range fpsRange : fpsRanges) { + if (fpsRange.contains(DEFAULT_FRAME_RATE)) { + builder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange); + return true; + } + } + } + } + mPreviewFrameRate = oldPreviewFrameRate; + return false; + } + //endregion //region Frame Processing 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 2631b2b9..419b61a5 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java @@ -174,6 +174,7 @@ public abstract class CameraEngine implements @SuppressWarnings("WeakerAccess") protected boolean mPlaySounds; @SuppressWarnings("WeakerAccess") protected boolean mPictureMetering; @SuppressWarnings("WeakerAccess") protected boolean mPictureSnapshotMetering; + @SuppressWarnings("WeakerAccess") protected float mPreviewFrameRate; // Can be private @VisibleForTesting Handler mCrashHandler; @@ -220,6 +221,7 @@ public abstract class CameraEngine implements @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op mHdrOp = new Op<>(); @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op mLocationOp = new Op<>(); @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op mPlaySoundsOp = new Op<>(); + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op mPreviewFrameRateOp = new Op<>(); protected CameraEngine(@NonNull Callback callback) { mCallback = callback; @@ -1026,6 +1028,10 @@ public abstract class CameraEngine implements return mExposureCorrectionValue; } + public final float getPreviewFrameRate() { + return mPreviewFrameRate; + } + @CallSuper public void setHasFrameProcessors(boolean hasFrameProcessors) { mHasFrameProcessors = hasFrameProcessors; @@ -1107,6 +1113,8 @@ public abstract class CameraEngine implements public abstract void setPlaySounds(boolean playSounds); + public abstract void setPreviewFrameRate(float previewFrameRate); + //endregion //region picture and video control diff --git a/cameraview/src/main/res/values/attrs.xml b/cameraview/src/main/res/values/attrs.xml index f11dde3b..8cbc871a 100644 --- a/cameraview/src/main/res/values/attrs.xml +++ b/cameraview/src/main/res/values/attrs.xml @@ -27,6 +27,7 @@ +