diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 863dcfc1a7..ea1dfb038b 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1768,16 +1768,16 @@ PODS: - ReactCommon/turbomodule/core - Yoga - SocketRocket (0.7.0) - - VisionCamera (4.7.2): - - VisionCamera/Core (= 4.7.2) - - VisionCamera/FrameProcessors (= 4.7.2) - - VisionCamera/React (= 4.7.2) - - VisionCamera/Core (4.7.2) - - VisionCamera/FrameProcessors (4.7.2): + - VisionCamera (4.7.3): + - VisionCamera/Core (= 4.7.3) + - VisionCamera/FrameProcessors (= 4.7.3) + - VisionCamera/React (= 4.7.3) + - VisionCamera/Core (4.7.3) + - VisionCamera/FrameProcessors (4.7.3): - React - React-callinvoker - react-native-worklets-core - - VisionCamera/React (4.7.2): + - VisionCamera/React (4.7.3): - React-Core - VisionCamera/FrameProcessors - Yoga (0.0.0) @@ -2097,9 +2097,9 @@ SPEC CHECKSUMS: RNStaticSafeAreaInsets: 4696b82d3a11ba6f3a790159ddb7290f04abd275 RNVectorIcons: 182892e7d1a2f27b52d3c627eca5d2665a22ee28 SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d - VisionCamera: 4146fa2612c154f893a42a9b1feedf868faa6b23 - Yoga: aa3df615739504eebb91925fc9c58b4922ea9a08 + VisionCamera: 0044a94f7489f19e19d5938e97dfc36f4784af3c + Yoga: 055f92ad73f8c8600a93f0e25ac0b2344c3b07e6 PODFILE CHECKSUM: 2ad84241179871ca890f7c65c855d117862f1a68 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/example/src/CameraPage.tsx b/example/src/CameraPage.tsx index af65e71462..ae5a0a5862 100644 --- a/example/src/CameraPage.tsx +++ b/example/src/CameraPage.tsx @@ -221,6 +221,7 @@ export function CameraPage({ navigation }: Props): React.ReactElement { enableZoomGesture={false} animatedProps={cameraAnimatedProps} exposure={0} + whiteBalance={5000} enableFpsGraph={true} outputOrientation="device" photo={true} diff --git a/package/android/src/main/cpp/frameprocessors/FrameHostObject.cpp b/package/android/src/main/cpp/frameprocessors/FrameHostObject.cpp index 09970a9a6a..cf229ee5fb 100644 --- a/package/android/src/main/cpp/frameprocessors/FrameHostObject.cpp +++ b/package/android/src/main/cpp/frameprocessors/FrameHostObject.cpp @@ -55,7 +55,7 @@ std::vector FrameHostObject::getPropertyNames(jsi::Runtime& rt) return result; } -#define JSI_FUNC [=](jsi::Runtime & runtime, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count) -> jsi::Value +#define JSI_FUNC [=](jsi::Runtime & runtime, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count)->jsi::Value jsi::Value FrameHostObject::get(jsi::Runtime& runtime, const jsi::PropNameID& propName) { auto name = propName.utf8(runtime); diff --git a/package/ios/Core/CameraConfiguration.swift b/package/ios/Core/CameraConfiguration.swift index a4b94dd377..2ae8c2a2e6 100644 --- a/package/ios/Core/CameraConfiguration.swift +++ b/package/ios/Core/CameraConfiguration.swift @@ -47,6 +47,9 @@ final class CameraConfiguration { // Exposure var exposure: Float? + // White Balance + var whiteBalance: Float? + // isActive (Start/Stop) var isActive = false @@ -71,6 +74,7 @@ final class CameraConfiguration { torch = other.torch zoom = other.zoom exposure = other.exposure + whiteBalance = other.whiteBalance isActive = other.isActive audio = other.audio } else { @@ -98,6 +102,7 @@ final class CameraConfiguration { let torchChanged: Bool let zoomChanged: Bool let exposureChanged: Bool + let whiteBalanceChanged: Bool let audioSessionChanged: Bool let locationChanged: Bool @@ -112,10 +117,10 @@ final class CameraConfiguration { /** Returns `true` when props that affect the AVCaptureDevice configuration (i.e. props that require lockForConfiguration()) have changed. - [`formatChanged`, `sidePropsChanged`, `zoomChanged`, `exposureChanged`] + [`formatChanged`, `sidePropsChanged`, `zoomChanged`, `exposureChanged`, `whiteBalanceChanged`] */ var isDeviceConfigurationDirty: Bool { - return isSessionConfigurationDirty || formatChanged || sidePropsChanged || zoomChanged || exposureChanged + return isSessionConfigurationDirty || formatChanged || sidePropsChanged || zoomChanged || exposureChanged || whiteBalanceChanged } init(between left: CameraConfiguration?, and right: CameraConfiguration) { @@ -139,6 +144,8 @@ final class CameraConfiguration { zoomChanged = formatChanged || left?.zoom != right.zoom // exposure (depends on device) exposureChanged = inputChanged || left?.exposure != right.exposure + // white balance (depends on device) + whiteBalanceChanged = inputChanged || left?.whiteBalance != right.whiteBalance // audio session audioSessionChanged = left?.audio != right.audio @@ -153,7 +160,7 @@ final class CameraConfiguration { case disabled case enabled(config: T) - public static func == (lhs: OutputConfiguration, rhs: OutputConfiguration) -> Bool { + static func == (lhs: OutputConfiguration, rhs: OutputConfiguration) -> Bool { switch (lhs, rhs) { case (.disabled, .disabled): return true diff --git a/package/ios/Core/CameraSession+Configuration.swift b/package/ios/Core/CameraSession+Configuration.swift index ecd2a94daf..0427a55b5b 100644 --- a/package/ios/Core/CameraSession+Configuration.swift +++ b/package/ios/Core/CameraSession+Configuration.swift @@ -336,6 +336,47 @@ extension CameraSession { device.setExposureTargetBias(clamped) } + // pragma MARK: White Balance + + /** + Configures white balance (`whiteBalance`) as a temperature value in Kelvin. + */ + func configureWhiteBalance(configuration: CameraConfiguration, device: AVCaptureDevice) { + guard let whiteBalance = configuration.whiteBalance else { + return + } + + guard device.isLockingWhiteBalanceWithCustomDeviceGainsSupported else { + VisionLogger.log(level: .warning, message: "White balance lock mode with gains is not supported on this device!") + return + } + + // Clamp temperature to valid range (typically 3000K to 8000K) + let clampedTemperature = min(max(whiteBalance, 3000), 8000) + + // Convert temperature to white balance gains + let tempAndTint = AVCaptureDevice.WhiteBalanceTemperatureAndTintValues( + temperature: clampedTemperature, + tint: 0 + ) + let gains = device.deviceWhiteBalanceGains(for: tempAndTint) + + // Clamp gains to valid range + let clampedGains = AVCaptureDevice.WhiteBalanceGains( + redGain: min(max(gains.redGain, 1.0), device.maxWhiteBalanceGain), + greenGain: min(max(gains.greenGain, 1.0), device.maxWhiteBalanceGain), + blueGain: min(max(gains.blueGain, 1.0), device.maxWhiteBalanceGain) + ) + + // Set the white balance mode to locked with the specified gains + device.setWhiteBalanceModeLocked(with: clampedGains, completionHandler: nil) + + VisionLogger.log( + level: .info, + message: "White balance set to \(clampedTemperature)K (R:\(clampedGains.redGain), G:\(clampedGains.greenGain), B:\(clampedGains.blueGain))" + ) + } + // pragma MARK: Audio /** diff --git a/package/ios/Core/CameraSession.swift b/package/ios/Core/CameraSession.swift index 10b0f3399c..c8319eea07 100644 --- a/package/ios/Core/CameraSession.swift +++ b/package/ios/Core/CameraSession.swift @@ -187,6 +187,10 @@ final class CameraSession: NSObject, AVCaptureVideoDataOutputSampleBufferDelegat if difference.exposureChanged { self.configureExposure(configuration: config, device: device) } + // 10. Configure white balance + if difference.whiteBalanceChanged { + self.configureWhiteBalance(configuration: config, device: device) + } } if difference.isSessionConfigurationDirty { @@ -195,7 +199,7 @@ final class CameraSession: NSObject, AVCaptureVideoDataOutputSampleBufferDelegat self.captureSession.commitConfiguration() } - // 10. Start or stop the session if needed + // 11. Start or stop the session if needed self.checkIsActive(configuration: config) // 11. Enable or disable the Torch if needed (requires session to be running) @@ -265,7 +269,7 @@ final class CameraSession: NSObject, AVCaptureVideoDataOutputSampleBufferDelegat } } - public final func captureOutput(_ captureOutput: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { + final func captureOutput(_ captureOutput: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { switch captureOutput { case is AVCaptureVideoDataOutput: onVideoFrame(sampleBuffer: sampleBuffer, orientation: connection.orientation, isMirrored: connection.isVideoMirrored) diff --git a/package/ios/Core/PreviewView.swift b/package/ios/Core/PreviewView.swift index 9c6ff25cce..81a8d7cba0 100644 --- a/package/ios/Core/PreviewView.swift +++ b/package/ios/Core/PreviewView.swift @@ -48,7 +48,7 @@ final class PreviewView: UIView { } } - override public static var layerClass: AnyClass { + override static var layerClass: AnyClass { return AVCaptureVideoPreviewLayer.self } diff --git a/package/ios/Core/Recording/Track.swift b/package/ios/Core/Recording/Track.swift index 8dec5ce89a..25c51dc346 100644 --- a/package/ios/Core/Recording/Track.swift +++ b/package/ios/Core/Recording/Track.swift @@ -51,7 +51,7 @@ final class Track { /** Returns the last timestamp that was actually written to the track. */ - public private(set) var lastTimestamp: CMTime? + private(set) var lastTimestamp: CMTime? /** Gets the natural size of the asset writer, or zero if it is not a visual track. diff --git a/package/ios/Core/Recording/TrackTimeline.swift b/package/ios/Core/Recording/TrackTimeline.swift index 345322fdd6..5e33e6af9e 100644 --- a/package/ios/Core/Recording/TrackTimeline.swift +++ b/package/ios/Core/Recording/TrackTimeline.swift @@ -25,22 +25,22 @@ final class TrackTimeline { Represents whether the timeline has been marked as finished or not. A timeline will automatically be marked as finished when a timestamp arrives that appears after a stop(). */ - public private(set) var isFinished = false + private(set) var isFinished = false /** Gets the latency of the buffers in this timeline. This is computed by (currentTime - mostRecentBuffer.timestamp) */ - public private(set) var latency: CMTime = .zero + private(set) var latency: CMTime = .zero /** Get the first actually written timestamp of this timeline */ - public private(set) var firstTimestamp: CMTime? + private(set) var firstTimestamp: CMTime? /** Get the last actually written timestamp of this timeline. */ - public private(set) var lastTimestamp: CMTime? + private(set) var lastTimestamp: CMTime? init(ofTrackType type: TrackType, withClock clock: CMClock) { trackType = type diff --git a/package/ios/FrameProcessors/FrameHostObject.mm b/package/ios/FrameProcessors/FrameHostObject.mm index 67bbfe51df..94f7250447 100644 --- a/package/ios/FrameProcessors/FrameHostObject.mm +++ b/package/ios/FrameProcessors/FrameHostObject.mm @@ -40,7 +40,7 @@ return result; } -#define JSI_FUNC [=](jsi::Runtime & runtime, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count) -> jsi::Value +#define JSI_FUNC [=](jsi::Runtime & runtime, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count)->jsi::Value jsi::Value FrameHostObject::get(jsi::Runtime& runtime, const jsi::PropNameID& propName) { auto name = propName.utf8(runtime); diff --git a/package/ios/React/CameraView.swift b/package/ios/React/CameraView.swift index c773975353..9939327a50 100644 --- a/package/ios/React/CameraView.swift +++ b/package/ios/React/CameraView.swift @@ -61,6 +61,7 @@ public final class CameraView: UIView, CameraSessionDelegate, PreviewViewDelegat @objc var torch = "off" @objc var zoom: NSNumber = 1.0 // in "factor" @objc var exposure: NSNumber = 0.0 + @objc var whiteBalance: NSNumber? // in Kelvin @objc var videoStabilizationMode: NSString? @objc var resizeMode: NSString = "cover" { didSet { @@ -270,6 +271,9 @@ public final class CameraView: UIView, CameraSessionDelegate, PreviewViewDelegat // Exposure config.exposure = exposure.floatValue + // White Balance + config.whiteBalance = whiteBalance?.floatValue + // isActive config.isActive = isActive } diff --git a/package/ios/React/CameraViewManager.m b/package/ios/React/CameraViewManager.m index 527c9bc0fd..c8a9b906a0 100644 --- a/package/ios/React/CameraViewManager.m +++ b/package/ios/React/CameraViewManager.m @@ -53,6 +53,7 @@ @interface RCT_EXTERN_REMAP_MODULE (CameraView, CameraViewManager, RCTViewManage RCT_EXPORT_VIEW_PROPERTY(torch, NSString); RCT_EXPORT_VIEW_PROPERTY(zoom, NSNumber); RCT_EXPORT_VIEW_PROPERTY(exposure, NSNumber); +RCT_EXPORT_VIEW_PROPERTY(whiteBalance, NSNumber); RCT_EXPORT_VIEW_PROPERTY(enableZoomGesture, BOOL); RCT_EXPORT_VIEW_PROPERTY(outputOrientation, NSString); RCT_EXPORT_VIEW_PROPERTY(resizeMode, NSString); diff --git a/package/ios/React/Utils/Promise.swift b/package/ios/React/Utils/Promise.swift index d1a7a132a2..946ea5cce1 100644 --- a/package/ios/React/Utils/Promise.swift +++ b/package/ios/React/Utils/Promise.swift @@ -14,7 +14,7 @@ import Foundation * Represents a JavaScript Promise instance. `reject()` and `resolve()` should only be called once. */ class Promise { - public private(set) var didResolve = false + private(set) var didResolve = false init(resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) { self.resolver = resolver diff --git a/package/package.json b/package/package.json index 96ea36fca3..6b3dd829c9 100644 --- a/package/package.json +++ b/package/package.json @@ -31,11 +31,11 @@ "typescript": "tsc --noEmit false", "lint": "eslint \"**/*.{js,ts,tsx}\" --fix", "lint-ci": "eslint \"**/*.{js,ts,tsx}\" -f @jamesacarr/github-actions", - "start": "cd example && bun start", + "start": "cd ../example && bun start", "build": "bob build", "release": "bob build && release-it", - "pods": "cd example && bun pods", - "bootstrap": "bun && cd example && bun && bun pods", + "pods": "cd ../example && bun pods", + "bootstrap": "bun && cd ../example && bun && bun pods", "check-android": "scripts/ktlint.sh && scripts/clang-format.sh", "check-ios": "scripts/swiftlint.sh && scripts/swiftformat.sh && scripts/clang-format.sh", "check-js": "bun lint --fix && bun typescript", diff --git a/package/src/types/CameraProps.ts b/package/src/types/CameraProps.ts index 18bfc9b618..f83367612c 100644 --- a/package/src/types/CameraProps.ts +++ b/package/src/types/CameraProps.ts @@ -149,6 +149,23 @@ export interface CameraProps extends ViewProps { * The value between min- and max supported exposure is considered the default, neutral value. */ exposure?: number + /** + * Specifies the White Balance of the current camera as a color temperature in Kelvin. + * + * This locks the white balance to a specific temperature value in Kelvin (e.g. 5000 for daylight, 3000 for warm indoor lighting). + * + * When set, the camera will use a fixed white balance instead of auto white balance. + * + * Common values: + * - `3000` - Warm indoor lighting + * - `4000` - Fluorescent lighting + * - `5000` - Daylight + * - `6000` - Cloudy daylight + * - `7000` - Shade + * + * @platform iOS + */ + whiteBalance?: number //#endregion //#region Format/Preset selection