From 7e70c1f751ba0b36b4c3aaa6542fa660d0038e3d Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Fri, 12 Jul 2024 10:51:56 +0200 Subject: [PATCH 1/7] feat: Add `minFps` and `maxFps` props --- package/ios/Core/CameraConfiguration.swift | 8 +++-- .../Core/CameraSession+Configuration.swift | 15 +++++----- package/ios/React/CameraView.swift | 6 ++-- package/ios/React/CameraViewManager.m | 3 +- package/src/Camera.tsx | 4 ++- package/src/types/CameraProps.ts | 30 ++++++++++++++++++- 6 files changed, 51 insertions(+), 15 deletions(-) diff --git a/package/ios/Core/CameraConfiguration.swift b/package/ios/Core/CameraConfiguration.swift index c77acbcc5d..a4b94dd377 100644 --- a/package/ios/Core/CameraConfiguration.swift +++ b/package/ios/Core/CameraConfiguration.swift @@ -36,7 +36,8 @@ final class CameraConfiguration { var format: CameraDeviceFormat? // Side-Props - var fps: Int32? + var minFps: Int32? + var maxFps: Int32? var enableLowLightBoost = false var torch: Torch = .off @@ -64,7 +65,8 @@ final class CameraConfiguration { videoStabilizationMode = other.videoStabilizationMode outputOrientation = other.outputOrientation format = other.format - fps = other.fps + minFps = other.minFps + maxFps = other.maxFps enableLowLightBoost = other.enableLowLightBoost torch = other.torch zoom = other.zoom @@ -129,7 +131,7 @@ final class CameraConfiguration { // format (depends on cameraId) formatChanged = inputChanged || left?.format != right.format // side-props (depends on format) - sidePropsChanged = formatChanged || left?.fps != right.fps || left?.enableLowLightBoost != right.enableLowLightBoost + sidePropsChanged = formatChanged || left?.minFps != right.minFps || left?.maxFps != right.maxFps || left?.enableLowLightBoost != right.enableLowLightBoost // torch (depends on isActive) let wasInactiveAndNeedsToEnableTorchAgain = left?.isActive == false && right.isActive == true && right.torch == .on torchChanged = inputChanged || wasInactiveAndNeedsToEnableTorchAgain || left?.torch != right.torch diff --git a/package/ios/Core/CameraSession+Configuration.swift b/package/ios/Core/CameraSession+Configuration.swift index c6f8c8d363..4bceeb40a8 100644 --- a/package/ios/Core/CameraSession+Configuration.swift +++ b/package/ios/Core/CameraSession+Configuration.swift @@ -249,16 +249,17 @@ extension CameraSession { */ func configureSideProps(configuration: CameraConfiguration, device: AVCaptureDevice) throws { // Configure FPS - if let fps = configuration.fps { - let supportsGivenFps = device.activeFormat.videoSupportedFrameRateRanges.contains { range in - return range.includes(fps: Double(fps)) + if let minFps = configuration.minFps, + let maxFps = configuration.maxFps { + let fpsRanges = device.activeFormat.videoSupportedFrameRateRanges + if !fpsRanges.contains(where: { $0.maxFrameRate >= Double(maxFps) }) { + throw CameraError.format(.invalidFps(fps: Int(maxFps))) } - if !supportsGivenFps { - throw CameraError.format(.invalidFps(fps: Int(fps))) + if !fpsRanges.contains(where: { $0.minFrameRate <= Double(minFps) }) { + throw CameraError.format(.invalidFps(fps: Int(minFps))) } - let minFps = configuration.enableLowLightBoost ? fps / 2 : fps - device.activeVideoMinFrameDuration = CMTimeMake(value: 1, timescale: fps) + device.activeVideoMinFrameDuration = CMTimeMake(value: 1, timescale: maxFps) device.activeVideoMaxFrameDuration = CMTimeMake(value: 1, timescale: minFps) } else { device.activeVideoMinFrameDuration = CMTime.invalid diff --git a/package/ios/React/CameraView.swift b/package/ios/React/CameraView.swift index 55115a766c..a6745cd5ff 100644 --- a/package/ios/React/CameraView.swift +++ b/package/ios/React/CameraView.swift @@ -46,7 +46,8 @@ public final class CameraView: UIView, CameraSessionDelegate, PreviewViewDelegat // props that require format reconfiguring @objc var format: NSDictionary? - @objc var fps: NSNumber? + @objc var minFps: NSNumber? + @objc var maxFps: NSNumber? @objc var videoHdr = false @objc var photoHdr = false @objc var photoQualityBalance: NSString? @@ -256,7 +257,8 @@ public final class CameraView: UIView, CameraSessionDelegate, PreviewViewDelegat } // Side-Props - config.fps = fps?.int32Value + config.minFps = minFps?.int32Value + config.maxFps = maxFps?.int32Value config.enableLowLightBoost = lowLightBoost config.torch = try Torch(jsValue: torch) diff --git a/package/ios/React/CameraViewManager.m b/package/ios/React/CameraViewManager.m index 9b631c3c97..dc3ad1accd 100644 --- a/package/ios/React/CameraViewManager.m +++ b/package/ios/React/CameraViewManager.m @@ -39,7 +39,8 @@ @interface RCT_EXTERN_REMAP_MODULE (CameraView, CameraViewManager, RCTViewManage RCT_EXPORT_VIEW_PROPERTY(enableLocation, BOOL); // device format RCT_EXPORT_VIEW_PROPERTY(format, NSDictionary); -RCT_EXPORT_VIEW_PROPERTY(fps, NSNumber); +RCT_EXPORT_VIEW_PROPERTY(minFps, NSNumber); +RCT_EXPORT_VIEW_PROPERTY(maxFps, NSNumber); RCT_EXPORT_VIEW_PROPERTY(videoHdr, BOOL); RCT_EXPORT_VIEW_PROPERTY(photoHdr, BOOL); RCT_EXPORT_VIEW_PROPERTY(photoQualityBalance, NSString); diff --git a/package/src/Camera.tsx b/package/src/Camera.tsx index 4a7539d7ce..caada9a001 100644 --- a/package/src/Camera.tsx +++ b/package/src/Camera.tsx @@ -621,7 +621,7 @@ export class Camera extends React.PureComponent { /** @internal */ public render(): React.ReactNode { // We remove the big `device` object from the props because we only need to pass `cameraId` to native. - const { device, frameProcessor, codeScanner, enableFpsGraph, ...props } = this.props + const { device, frameProcessor, codeScanner, enableFpsGraph, fps, ...props } = this.props // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (device == null) { @@ -642,6 +642,8 @@ export class Camera extends React.PureComponent { cameraId={device.id} ref={this.ref} torch={torch} + minFps={props.minFps ?? fps ?? props.maxFps} + maxFps={props.maxFps ?? fps ?? props.minFps} isMirrored={props.isMirrored ?? shouldBeMirrored} onViewReady={this.onViewReady} onAverageFpsChanged={enableFpsGraph ? this.onAverageFpsChanged : undefined} diff --git a/package/src/types/CameraProps.ts b/package/src/types/CameraProps.ts index 2fac776d1f..b8ca16cbda 100644 --- a/package/src/types/CameraProps.ts +++ b/package/src/types/CameraProps.ts @@ -183,11 +183,39 @@ export interface CameraProps extends ViewProps { */ androidPreviewViewType?: 'surface-view' | 'texture-view' /** - * Specify the frames per second this camera should stream frames at. + * Specify a fixed number of frames per second this camera should stream frames at. + * + * `fps` is just a shorthand for setting {@linkcode minFps} and {@linkcode maxFps} to the same value. + * For setting a variable FPS range, use {@linkcode minFps} and {@linkcode maxFps} instead of `fps`. + * Setting a variable FPS range can be beneficial for better exposure and quality in low-light conditions. * * Make sure the given {@linkcode format} can stream at the target {@linkcode fps} value (see {@linkcode CameraDeviceFormat.minFps format.minFps} and {@linkcode CameraDeviceFormat.maxFps format.maxFps}). */ fps?: number + /** + * Specifies the minimum amount of frames per second this camera should stream frames at. + * + * Under low/dark lighting conditions, the Camera will throttle it's frame rate to receive more + * light, and `minFps` sets the minimum value for this. + * + * Make sure this value is not lower than the given {@linkcode format}'s {@linkcode CameraDeviceFormat.minFps format.minFps} value. + */ + minFps?: number + /** + * Specifies the maximum amount of frames per second this camera should stream frames at. + * + * Under good/bright lighting conditions, the Camera can increase it's frame rate as it does not need + * a lot of light to expose frames. + * + * However under low/dark lighting conditions, the Camera might throttle it's frame rate to receive more light. + * This can be controlled via {@linkcode minFps}: + * - if {@linkcode minFps} is the same value as `maxFps`, the Camera will not throttle FPS. + * - if {@linkcode minFps} is a lower value than `maxFps` (e.g. `minFps={20} maxFps={30}`), the Camera + * can choose between 20 FPS and 30 FPS depending on lighting conditions. + * + * Make sure this value is not higher than the given {@linkcode format}'s {@linkcode CameraDeviceFormat.maxFps format.maxFps} value. + */ + maxFps?: number /** * Enables or disables HDR Video Streaming for Preview, Video and Frame Processor via a 10-bit wide-color pixel format. * From cd9e6e72829c7748a7c05ba367b16678ac08a797 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Fri, 12 Jul 2024 10:56:22 +0200 Subject: [PATCH 2/7] Implement Android --- .../mrousavy/camera/core/CameraConfiguration.kt | 8 ++++---- .../java/com/mrousavy/camera/react/CameraView.kt | 6 ++++-- .../com/mrousavy/camera/react/CameraViewManager.kt | 14 +++++++++++--- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraConfiguration.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraConfiguration.kt index 22eb8a2dee..7109c2ad7e 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraConfiguration.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraConfiguration.kt @@ -29,7 +29,8 @@ data class CameraConfiguration( var format: CameraDeviceFormat? = null, // Side-Props - var fps: Int? = null, + var minFps: Int? = null, + var maxFps: Int? = null, var enableLowLightBoost: Boolean = false, var torch: Torch = Torch.OFF, var videoStabilizationMode: VideoStabilizationMode = VideoStabilizationMode.OFF, @@ -54,9 +55,8 @@ data class CameraConfiguration( val targetFpsRange: Range? get() { - val maxFps = fps ?: return null - val format = format ?: throw PropRequiresFormatToBeNonNullError("fps") - val minFps = format.minFps.toInt() + val minFps = minFps ?: return null + val maxFps = maxFps ?: return null return Range(minFps, maxFps) } diff --git a/package/android/src/main/java/com/mrousavy/camera/react/CameraView.kt b/package/android/src/main/java/com/mrousavy/camera/react/CameraView.kt index fc19ede1e2..f0eb5b90de 100644 --- a/package/android/src/main/java/com/mrousavy/camera/react/CameraView.kt +++ b/package/android/src/main/java/com/mrousavy/camera/react/CameraView.kt @@ -70,7 +70,8 @@ class CameraView(context: Context) : // props that require format reconfiguring var format: CameraDeviceFormat? = null - var fps: Int? = null + var minFps: Int? = null + var maxFps: Int? = null var videoStabilizationMode: VideoStabilizationMode? = null var videoHdr = false var photoHdr = false @@ -218,7 +219,8 @@ class CameraView(context: Context) : config.format = format // Side-Props - config.fps = fps + config.minFps = minFps + config.maxFps = maxFps config.enableLowLightBoost = lowLightBoost config.torch = torch config.exposure = exposure diff --git a/package/android/src/main/java/com/mrousavy/camera/react/CameraViewManager.kt b/package/android/src/main/java/com/mrousavy/camera/react/CameraViewManager.kt index 3c882cb8a8..e890d93a4a 100644 --- a/package/android/src/main/java/com/mrousavy/camera/react/CameraViewManager.kt +++ b/package/android/src/main/java/com/mrousavy/camera/react/CameraViewManager.kt @@ -158,9 +158,17 @@ class CameraViewManager : ViewGroupManager() { // TODO: Change when TurboModules release. // We're treating -1 as "null" here, because when I make the fps parameter // of type "Int?" the react bridge throws an error. - @ReactProp(name = "fps", defaultInt = -1) - fun setFps(view: CameraView, fps: Int) { - view.fps = if (fps > 0) fps else null + @ReactProp(name = "minFps", defaultInt = -1) + fun setMinFps(view: CameraView, minFps: Int) { + view.minFps = if (minFps > 0) minFps else null + } + + // TODO: Change when TurboModules release. + // We're treating -1 as "null" here, because when I make the fps parameter + // of type "Int?" the react bridge throws an error. + @ReactProp(name = "maxFps", defaultInt = -1) + fun setMaxFps(view: CameraView, maxFps: Int) { + view.maxFps = if (maxFps > 0) maxFps else null } @ReactProp(name = "photoHdr") From 79bb3c7f0054a2a3dca424fd20205f77ac28842d Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Fri, 12 Jul 2024 10:57:20 +0200 Subject: [PATCH 3/7] Pods --- package/example/ios/Podfile.lock | 16 ++++++++-------- .../ios/Core/CameraSession+Configuration.swift | 12 ++++++------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package/example/ios/Podfile.lock b/package/example/ios/Podfile.lock index 02946a9743..5b8a8bd799 100644 --- a/package/example/ios/Podfile.lock +++ b/package/example/ios/Podfile.lock @@ -1391,16 +1391,16 @@ PODS: - ReactCommon/turbomodule/core - Yoga - SocketRocket (0.7.0) - - VisionCamera (4.4.1): - - VisionCamera/Core (= 4.4.1) - - VisionCamera/FrameProcessors (= 4.4.1) - - VisionCamera/React (= 4.4.1) - - VisionCamera/Core (4.4.1) - - VisionCamera/FrameProcessors (4.4.1): + - VisionCamera (4.4.2): + - VisionCamera/Core (= 4.4.2) + - VisionCamera/FrameProcessors (= 4.4.2) + - VisionCamera/React (= 4.4.2) + - VisionCamera/Core (4.4.2) + - VisionCamera/FrameProcessors (4.4.2): - React - React-callinvoker - react-native-worklets-core - - VisionCamera/React (4.4.1): + - VisionCamera/React (4.4.2): - React-Core - VisionCamera/FrameProcessors - Yoga (0.0.0) @@ -1688,7 +1688,7 @@ SPEC CHECKSUMS: RNStaticSafeAreaInsets: 055ddbf5e476321720457cdaeec0ff2ba40ec1b8 RNVectorIcons: 2a2f79274248390b80684ea3c4400bd374a15c90 SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d - VisionCamera: 670bd7d5ec35b6e3bfaf9d48f4a102248291b00d + VisionCamera: 0292805e99ae8d121c8ec5bb8a9554f6f1c36b82 Yoga: 2f71ecf38d934aecb366e686278102a51679c308 PODFILE CHECKSUM: 49584be049764895189f1f88ebc9769116621103 diff --git a/package/ios/Core/CameraSession+Configuration.swift b/package/ios/Core/CameraSession+Configuration.swift index 4bceeb40a8..a9c20e9342 100644 --- a/package/ios/Core/CameraSession+Configuration.swift +++ b/package/ios/Core/CameraSession+Configuration.swift @@ -252,18 +252,18 @@ extension CameraSession { if let minFps = configuration.minFps, let maxFps = configuration.maxFps { let fpsRanges = device.activeFormat.videoSupportedFrameRateRanges - if !fpsRanges.contains(where: { $0.maxFrameRate >= Double(maxFps) }) { - throw CameraError.format(.invalidFps(fps: Int(maxFps))) - } if !fpsRanges.contains(where: { $0.minFrameRate <= Double(minFps) }) { throw CameraError.format(.invalidFps(fps: Int(minFps))) } - - device.activeVideoMinFrameDuration = CMTimeMake(value: 1, timescale: maxFps) + if !fpsRanges.contains(where: { $0.maxFrameRate >= Double(maxFps) }) { + throw CameraError.format(.invalidFps(fps: Int(maxFps))) + } + device.activeVideoMaxFrameDuration = CMTimeMake(value: 1, timescale: minFps) + device.activeVideoMinFrameDuration = CMTimeMake(value: 1, timescale: maxFps) } else { - device.activeVideoMinFrameDuration = CMTime.invalid device.activeVideoMaxFrameDuration = CMTime.invalid + device.activeVideoMinFrameDuration = CMTime.invalid } // Configure Low-Light-Boost From e723113e08919aef668666bbee563db7fc1b4262 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Fri, 12 Jul 2024 11:00:33 +0200 Subject: [PATCH 4/7] fix: Fix comparison --- .../java/com/mrousavy/camera/core/CameraConfiguration.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraConfiguration.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraConfiguration.kt index 7109c2ad7e..745ed9d214 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraConfiguration.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraConfiguration.kt @@ -20,6 +20,8 @@ data class CameraConfiguration( var video: Output