diff --git a/docs/docs/guides/EXPOSURE.mdx b/docs/docs/guides/EXPOSURE.mdx
new file mode 100644
index 0000000..206d0c3
--- /dev/null
+++ b/docs/docs/guides/EXPOSURE.mdx
@@ -0,0 +1,71 @@
+---
+id: exposure
+title: Exposure
+sidebar_label: Exposure
+---
+
+### Adjusting exposure
+
+To adjust the exposure of the Camera, you can use the Camera's [`exposure`](/docs/api/interfaces/CameraProps#exposure) property:
+
+```tsx
+
+```
+
+Values for the `exposure` prop range from [`format.minExposure`](/docs/api/interfaces/CameraDeviceFormat#maxExposure) to [`format.maxExposure`](/docs/api/interfaces/CameraDeviceFormat#minExposure), inclusively. By default (`undefined`), it is set to neutral auto exposure.
+
+Instead of manually adjusting ISO and Exposure-Duration, this acts as an "exposure compensation bias", meaning the Camera will still continuously automatically adjust exposure as it goes, but premultiplies the given exposure value to it's ISO and Exposure Duration settings.
+
+### Examples
+
+![Exposure Example (-2, 0, 2)](/img/exposure.jpg)
+
+### Animating
+
+Just like [`zoom`](zooming), this property can be animated using Reanimated.
+
+1. Add the `exposure` prop to the whitelisted animateable properties:
+
+ ```tsx
+ import Reanimated, { addWhitelistedNativeProps } from "react-native-reanimated"
+
+ const ReanimatedCamera = Reanimated.createAnimatedComponent(Camera)
+ addWhitelistedNativeProps({
+ exposure: true,
+ })
+ ```
+
+2. Implement your animation, for example with an exposure slider:
+
+ ```jsx
+ function App() {
+ // 1. create shared value for exposure slider (from -1..0..1)
+ const exposureSlider = useSharedValue(0)
+
+ // 2. map slider to [minExposure, 0, maxExposure]
+ const exposureValue = useDerivedValue(() => {
+ if (format == null) return 0
+
+ return interpolate(exposureSlider.value,
+ [-1, 0, 1],
+ [format.minExposure, 0, format.maxExposure])
+ }, [exposureSlider, format])
+
+ // 3. pass it as an animated prop
+ const animatedProps = useAnimatedProps(() => ({
+ exposure: exposureValue.value
+ }), [exposureValue])
+
+ // 4. render Camera
+ return (
+
+ )
+ }
+ ```
+
+
+
+#### 🚀 Next section: [HDR](hdr)
diff --git a/docs/docs/guides/FOCUSING.mdx b/docs/docs/guides/FOCUSING.mdx
index e949c69..408cd98 100644
--- a/docs/docs/guides/FOCUSING.mdx
+++ b/docs/docs/guides/FOCUSING.mdx
@@ -22,4 +22,4 @@ Focussing adjusts auto-focus (AF) and auto-exposure (AE).
-#### 🚀 Next section: [HDR](hdr)
+#### 🚀 Next section: [Exposure](exposure)
diff --git a/docs/sidebars.js b/docs/sidebars.js
index 9f05eb7..beb8618 100644
--- a/docs/sidebars.js
+++ b/docs/sidebars.js
@@ -31,6 +31,7 @@ module.exports = {
},
'guides/zooming',
'guides/focusing',
+ 'guides/exposure',
'guides/hdr',
'guides/stabilization',
'guides/performance',
diff --git a/docs/static/img/exposure.jpg b/docs/static/img/exposure.jpg
new file mode 100644
index 0000000..6500cee
Binary files /dev/null and b/docs/static/img/exposure.jpg differ
diff --git a/package/android/src/main/java/com/mrousavy/camera/CameraView.kt b/package/android/src/main/java/com/mrousavy/camera/CameraView.kt
index 780664a..123a8c6 100644
--- a/package/android/src/main/java/com/mrousavy/camera/CameraView.kt
+++ b/package/android/src/main/java/com/mrousavy/camera/CameraView.kt
@@ -78,6 +78,7 @@ class CameraView(context: Context) :
var isActive = false
var torch: Torch = Torch.OFF
var zoom: Float = 1f // in "factor"
+ var exposure: Float = 1f // TODO: Implement exposure bias
var orientation: Orientation = Orientation.PORTRAIT
var enableZoomGesture: Boolean = false
set(value) {
@@ -198,6 +199,7 @@ class CameraView(context: Context) :
config.fps = fps
config.enableLowLightBoost = lowLightBoost ?: false
config.torch = torch
+ config.exposure = exposure
// Zoom
config.zoom = zoom
diff --git a/package/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt b/package/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt
index 4bbc35a..222f464 100644
--- a/package/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt
+++ b/package/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt
@@ -140,8 +140,12 @@ class CameraViewManager : ViewGroupManager() {
@ReactProp(name = "zoom")
fun setZoom(view: CameraView, zoom: Double) {
- val zoomFloat = zoom.toFloat()
- view.zoom = zoomFloat
+ view.zoom = zoom.toFloat()
+ }
+
+ @ReactProp(name = "exposure")
+ fun setExposure(view: CameraView, exposure: Double) {
+ view.exposure = exposure.toFloat()
}
@ReactProp(name = "orientation")
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 e2adbf9..a0c2769 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
@@ -33,6 +33,7 @@ data class CameraConfiguration(
var enableLowLightBoost: Boolean = false,
var torch: Torch = Torch.OFF,
var videoStabilizationMode: VideoStabilizationMode = VideoStabilizationMode.OFF,
+ var exposure: Float? = null,
// Zoom
var zoom: Float = 1f,
@@ -94,7 +95,8 @@ data class CameraConfiguration(
val sidePropsChanged = outputsChanged || // depend on outputs
left?.torch != right.torch || left.enableLowLightBoost != right.enableLowLightBoost || left.fps != right.fps ||
- left.zoom != right.zoom || left.videoStabilizationMode != right.videoStabilizationMode || left.isActive != right.isActive
+ left.zoom != right.zoom || left.videoStabilizationMode != right.videoStabilizationMode || left.isActive != right.isActive ||
+ left.exposure != right.exposure
return Difference(
deviceChanged,
diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt
index 1798128..89a8692 100644
--- a/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt
+++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt
@@ -6,6 +6,7 @@ import android.hardware.camera2.CameraManager
import android.hardware.camera2.CameraMetadata
import android.os.Build
import android.util.Range
+import android.util.Rational
import android.util.Size
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReadableArray
@@ -68,6 +69,8 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, private val
private val cameraConfig = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
private val isoRange = characteristics.get(CameraCharacteristics.SENSOR_INFO_SENSITIVITY_RANGE) ?: Range(0, 0)
+ private val exposureRange = characteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE) ?: Range(0, 0)
+ private val exposureStep = characteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP) ?: Rational(1, 1)
private val digitalStabilizationModes =
characteristics.get(CameraCharacteristics.CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES) ?: IntArray(0)
private val opticalStabilizationModes =
@@ -174,6 +177,9 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, private val
map.putInt("videoWidth", videoSize.width)
map.putInt("minISO", isoRange.lower)
map.putInt("maxISO", isoRange.upper)
+ // TODO: Implement minExposureBias
+ map.putDouble("minExposure", exposureRange.lower.toDouble() / exposureStep.toDouble())
+ map.putDouble("maxExposure", exposureRange.upper.toDouble() / exposureStep.toDouble())
map.putInt("minFps", fpsRange.lower)
map.putInt("maxFps", fpsRange.upper)
map.putDouble("maxZoom", maxZoom)
diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt
index f432b52..1e0c8b2 100644
--- a/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt
+++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt
@@ -434,6 +434,12 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
captureRequest.set(CaptureRequest.CONTROL_SCENE_MODE, CaptureRequest.CONTROL_SCENE_MODE_NIGHT)
}
+ // Set Exposure Bias
+ val exposure = config.exposure?.toInt()
+ if (exposure != null) {
+ captureRequest.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, exposure)
+ }
+
// Set Zoom
// TODO: Check if that zoom value is even supported
captureRequest.setZoom(config.zoom, cameraCharacteristics)
diff --git a/package/android/src/main/java/com/mrousavy/camera/types/CameraDeviceFormat.kt b/package/android/src/main/java/com/mrousavy/camera/types/CameraDeviceFormat.kt
index ade2acd..163262a 100644
--- a/package/android/src/main/java/com/mrousavy/camera/types/CameraDeviceFormat.kt
+++ b/package/android/src/main/java/com/mrousavy/camera/types/CameraDeviceFormat.kt
@@ -13,6 +13,8 @@ data class CameraDeviceFormat(
val maxFps: Double,
val minISO: Double,
val maxISO: Double,
+ val minExposure: Double,
+ val maxExposure: Double,
val fieldOfView: Double,
val maxZoom: Double,
val videoStabilizationModes: List,
@@ -46,6 +48,8 @@ data class CameraDeviceFormat(
value.getDouble("maxFps"),
value.getDouble("minISO"),
value.getDouble("maxISO"),
+ value.getDouble("minExposure"),
+ value.getDouble("maxExposure"),
value.getDouble("fieldOfView"),
value.getDouble("maxZoom"),
videoStabilizationModes,
diff --git a/package/example/ios/Podfile.lock b/package/example/ios/Podfile.lock
index 8c15864..c960617 100644
--- a/package/example/ios/Podfile.lock
+++ b/package/example/ios/Podfile.lock
@@ -507,7 +507,7 @@ PODS:
- libwebp (~> 1.0)
- SDWebImage/Core (~> 5.10)
- SocketRocket (0.6.1)
- - VisionCamera (3.6.4):
+ - VisionCamera (3.6.6):
- React
- React-callinvoker
- React-Core
@@ -747,7 +747,7 @@ SPEC CHECKSUMS:
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
- VisionCamera: db57ca079c4f933f1ddd07f2f8fb2d171a826b85
+ VisionCamera: 47d2342b724c78fb9ff3e2607c21ceda4ba21e75
Yoga: 8796b55dba14d7004f980b54bcc9833ee45b28ce
PODFILE CHECKSUM: 27f53791141a3303d814e09b55770336416ff4eb
diff --git a/package/example/src/CameraPage.tsx b/package/example/src/CameraPage.tsx
index 4760327..b976586 100644
--- a/package/example/src/CameraPage.tsx
+++ b/package/example/src/CameraPage.tsx
@@ -189,6 +189,7 @@ export function CameraPage({ navigation }: Props): React.ReactElement {
onError={onError}
enableZoomGesture={false}
animatedProps={cameraAnimatedProps}
+ exposure={0}
enableFpsGraph={true}
orientation="portrait"
photo={true}
diff --git a/package/ios/CameraView.swift b/package/ios/CameraView.swift
index c23a5d3..8198d2c 100644
--- a/package/ios/CameraView.swift
+++ b/package/ios/CameraView.swift
@@ -46,6 +46,7 @@ public final class CameraView: UIView, CameraSessionDelegate {
@objc var isActive = false
@objc var torch = "off"
@objc var zoom: NSNumber = 1.0 // in "factor"
+ @objc var exposure: NSNumber = 1.0
@objc var enableFpsGraph = false
@objc var videoStabilizationMode: NSString?
@objc var resizeMode: NSString = "cover" {
@@ -218,6 +219,9 @@ public final class CameraView: UIView, CameraSessionDelegate {
// Zoom
config.zoom = zoom.doubleValue
+ // Exposure
+ config.exposure = exposure.floatValue
+
// isActive
config.isActive = isActive
}
diff --git a/package/ios/CameraViewManager.m b/package/ios/CameraViewManager.m
index 3931b55..4fc8983 100644
--- a/package/ios/CameraViewManager.m
+++ b/package/ios/CameraViewManager.m
@@ -44,6 +44,7 @@ RCT_EXPORT_VIEW_PROPERTY(pixelFormat, NSString);
// other props
RCT_EXPORT_VIEW_PROPERTY(torch, NSString);
RCT_EXPORT_VIEW_PROPERTY(zoom, NSNumber);
+RCT_EXPORT_VIEW_PROPERTY(exposure, NSNumber);
RCT_EXPORT_VIEW_PROPERTY(enableZoomGesture, BOOL);
RCT_EXPORT_VIEW_PROPERTY(enableFpsGraph, BOOL);
RCT_EXPORT_VIEW_PROPERTY(orientation, NSString);
diff --git a/package/ios/Core/CameraConfiguration.swift b/package/ios/Core/CameraConfiguration.swift
index 4895aed..8a97d7f 100644
--- a/package/ios/Core/CameraConfiguration.swift
+++ b/package/ios/Core/CameraConfiguration.swift
@@ -39,6 +39,9 @@ class CameraConfiguration {
// Zoom
var zoom: CGFloat?
+ // Exposure
+ var exposure: Float?
+
// isActive (Start/Stop)
var isActive = false
@@ -59,6 +62,7 @@ class CameraConfiguration {
enableLowLightBoost = other.enableLowLightBoost
torch = other.torch
zoom = other.zoom
+ exposure = other.exposure
isActive = other.isActive
audio = other.audio
} else {
@@ -77,6 +81,7 @@ class CameraConfiguration {
let sidePropsChanged: Bool
let torchChanged: Bool
let zoomChanged: Bool
+ let exposureChanged: Bool
let audioSessionChanged: Bool
@@ -113,6 +118,8 @@ class CameraConfiguration {
torchChanged = left?.isActive != right.isActive || left?.torch != right.torch
// zoom (depends on format)
zoomChanged = formatChanged || left?.zoom != right.zoom
+ // exposure (depends on format)
+ exposureChanged = formatChanged || left?.exposure != right.exposure
// audio session
audioSessionChanged = left?.audio != right.audio
diff --git a/package/ios/Core/CameraSession+Configuration.swift b/package/ios/Core/CameraSession+Configuration.swift
index 7e60742..c278db8 100644
--- a/package/ios/Core/CameraSession+Configuration.swift
+++ b/package/ios/Core/CameraSession+Configuration.swift
@@ -285,6 +285,20 @@ extension CameraSession {
device.videoZoomFactor = clamped
}
+ // pragma MARK: Exposure
+
+ /**
+ Configures exposure (`exposure`) as a bias that adjusts exposureTime and ISO.
+ */
+ func configureExposure(configuration: CameraConfiguration, device: AVCaptureDevice) {
+ guard let exposure = configuration.exposure else {
+ return
+ }
+
+ let clamped = max(min(exposure, device.maxExposureTargetBias), device.minExposureTargetBias)
+ device.setExposureTargetBias(clamped)
+ }
+
// pragma MARK: Audio
/**
diff --git a/package/ios/Core/CameraSession.swift b/package/ios/Core/CameraSession.swift
index a4426a5..ac32609 100644
--- a/package/ios/Core/CameraSession.swift
+++ b/package/ios/Core/CameraSession.swift
@@ -154,13 +154,17 @@ class CameraSession: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, AVC
if difference.zoomChanged {
self.configureZoom(configuration: config, device: device)
}
+ // 8. Configure exposure bias
+ if difference.exposureChanged {
+ self.configureExposure(configuration: config, device: device)
+ }
}
}
- // 8. Start or stop the session if needed
+ // 9. Start or stop the session if needed
self.checkIsActive(configuration: config)
- // 9. Enable or disable the Torch if needed (requires session to be running)
+ // 10. Enable or disable the Torch if needed (requires session to be running)
if difference.torchChanged {
try self.withDeviceLock { device in
try self.configureTorch(configuration: config, device: device)
diff --git a/package/src/CameraDevice.ts b/package/src/CameraDevice.ts
index d5a4888..9501a9a 100644
--- a/package/src/CameraDevice.ts
+++ b/package/src/CameraDevice.ts
@@ -80,6 +80,14 @@ export interface CameraDeviceFormat {
* Minimum supported ISO value
*/
minISO: number
+ /**
+ * The minimum Exposure-Bias value this format supports. When setting the `exposure` to this value, the image is almost completely dark (under-exposed).
+ */
+ minExposure: number
+ /**
+ * The maximum Exposure-Bias value this format supports. When setting the `exposure` to this value, the image is almost completely bright (over-exposed).
+ */
+ maxExposure: number
/**
* The video field of view in degrees
*/
diff --git a/package/src/CameraProps.ts b/package/src/CameraProps.ts
index 85bf8a0..42b008d 100644
--- a/package/src/CameraProps.ts
+++ b/package/src/CameraProps.ts
@@ -110,6 +110,19 @@ export interface CameraProps extends ViewProps {
enableZoomGesture?: boolean
//#endregion
+ //#region Camera Controls
+ /**
+ * Specifies the Exposure bias of the current camera. A lower value means darker images, a higher value means brighter images.
+ *
+ * The Camera will still continue to auto-adjust exposure and focus, but will premultiply the exposure setting with the provided value here.
+ *
+ * This values ranges from {@linkcode CameraDeviceFormat.minExposure format.minExposure} to {@linkcode CameraDeviceFormat.maxExposure format.maxExposure}.
+ *
+ * The value between min- and max supported exposure is considered the default, neutral value.
+ */
+ exposure?: number
+ //#endregion
+
//#region Format/Preset selection
/**
* Selects a given format. By default, the best matching format is chosen. See {@linkcode CameraDeviceFormat}