Skip to content

Commit dbe3bda

Browse files
committed
feat: add focusDepth on iOS and android
1 parent 8e919c8 commit dbe3bda

19 files changed

+176
-20
lines changed

bun.lockb

8.38 KB
Binary file not shown.

docs/docs/guides/FOCUSING.mdx

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ This is an Example on how to use [react-native-gesture-handler](https://github.c
2727
```tsx
2828
import { Camera, useCameraDevice } from 'react-native-vision-camera'
2929
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
30-
import { runOnJS } from 'react-native-reanimated';
30+
import { runOnJS } from 'react-native-reanimated'
3131

3232
export function App() {
3333
const camera = useRef<Camera>(null)
@@ -39,25 +39,38 @@ export function App() {
3939
c.focus(point)
4040
}, [])
4141

42-
const gesture = Gesture.Tap()
43-
.onEnd(({ x, y }) => {
44-
runOnJS(focus)({ x, y })
45-
})
42+
const gesture = Gesture.Tap().onEnd(({ x, y }) => {
43+
runOnJS(focus)({ x, y })
44+
})
4645

4746
if (device == null) return <NoCameraDeviceError />
4847
return (
4948
<GestureDetector gesture={gesture}>
50-
<Camera
51-
ref={camera}
52-
style={StyleSheet.absoluteFill}
53-
device={device}
54-
isActive={true}
55-
/>
49+
<Camera ref={camera} style={StyleSheet.absoluteFill} device={device} isActive={true} />
5650
</GestureDetector>
5751
)
5852
}
5953
```
6054

55+
### Focus depth (focus on a fixed manual distance)
56+
57+
You can programmatically set the distance of the focus, or the depth.
58+
59+
```ts
60+
await camera.current.focusDepth(0.5)
61+
```
62+
63+
The value between Android and iOS is reversed:
64+
| distance | Android | iOS |
65+
|---------------|--------------------------------|-----|
66+
| macro (close) | 10 | 0 |
67+
| infinite | 0.2 | 1 |
68+
69+
In practice, you will probably use these values to prevent any issues on **Android**:
70+
71+
- macro: `device.minFocusDistance + 0.1` (0.1 prevents some weird focus changes)
72+
- infinite: `Math.min(0.2, device.minFocusDistance)` (the lowest value for infinite focus, but setting it too low also produces focus artifacts)
73+
6174
<br />
6275

6376
#### 🚀 Next section: [Orientation](orientation)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.mrousavy.camera.core
2+
3+
import android.annotation.SuppressLint
4+
import android.hardware.camera2.CameraMetadata
5+
import android.hardware.camera2.CaptureRequest
6+
import androidx.camera.camera2.interop.Camera2CameraControl
7+
import androidx.camera.camera2.interop.CaptureRequestOptions
8+
import androidx.camera.camera2.interop.ExperimentalCamera2Interop
9+
import androidx.camera.core.CameraControl
10+
11+
@ExperimentalCamera2Interop
12+
@SuppressLint("RestrictedApi")
13+
suspend fun CameraSession.focusDepth(depth: Double) {
14+
val camera = camera ?: throw CameraNotReadyError()
15+
16+
try {
17+
Camera2CameraControl.from(camera.cameraControl).let {
18+
CaptureRequestOptions.Builder().apply {
19+
val distance = depth.toFloat()
20+
setCaptureRequestOption(CaptureRequest.LENS_FOCUS_DISTANCE, distance)
21+
setCaptureRequestOption(CaptureRequest.CONTROL_AF_MODE, CameraMetadata.CONTROL_AF_MODE_OFF)
22+
}.let { builder ->
23+
it.addCaptureRequestOptions(builder.build())
24+
}
25+
}
26+
} catch (e: CameraControl.OperationCanceledException) {
27+
throw FocusCanceledError()
28+
}
29+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.mrousavy.camera.react
2+
3+
import androidx.camera.camera2.interop.ExperimentalCamera2Interop
4+
import com.mrousavy.camera.core.focusDepth
5+
6+
@ExperimentalCamera2Interop
7+
suspend fun CameraView.focusDepth(distance: Double) {
8+
cameraSession.focusDepth(distance)
9+
}

package/android/src/main/java/com/mrousavy/camera/react/CameraViewModule.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.mrousavy.camera.react
33
import android.Manifest
44
import android.content.pm.PackageManager
55
import android.util.Log
6+
import androidx.camera.camera2.interop.ExperimentalCamera2Interop
67
import androidx.core.content.ContextCompat
78
import com.facebook.react.bridge.Callback
89
import com.facebook.react.bridge.Promise
@@ -188,6 +189,18 @@ class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBase
188189
}
189190
}
190191

192+
@ExperimentalCamera2Interop
193+
@ReactMethod
194+
fun focusDepth(viewTag: Int, distance: Double, promise: Promise) {
195+
backgroundCoroutineScope.launch {
196+
val view = findCameraView(viewTag)
197+
withPromise(promise) {
198+
view.focusDepth(distance)
199+
return@withPromise null
200+
}
201+
}
202+
}
203+
191204
private fun canRequestPermission(permission: String): Boolean {
192205
val activity = reactApplicationContext.currentActivity as? PermissionAwareActivity
193206
return activity?.shouldShowRequestPermissionRationale(permission) ?: false

package/ios/Core/CameraConfiguration.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ final class CameraConfiguration {
153153
case disabled
154154
case enabled(config: T)
155155

156-
public static func == (lhs: OutputConfiguration, rhs: OutputConfiguration) -> Bool {
156+
static func == (lhs: OutputConfiguration, rhs: OutputConfiguration) -> Bool {
157157
switch (lhs, rhs) {
158158
case (.disabled, .disabled):
159159
return true

package/ios/Core/CameraError.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ enum DeviceError {
7676
case microphoneUnavailable
7777
case lowLightBoostNotSupported
7878
case focusNotSupported
79+
case focusDepthNotSupported
7980
case notAvailableOnSimulator
8081
case pixelFormatNotSupported(targetFormats: [FourCharCode], availableFormats: [FourCharCode])
8182

@@ -95,6 +96,8 @@ enum DeviceError {
9596
return "low-light-boost-not-supported"
9697
case .focusNotSupported:
9798
return "focus-not-supported"
99+
case .focusDepthNotSupported:
100+
return "focus-depth-not-supported"
98101
case .notAvailableOnSimulator:
99102
return "camera-not-available-on-simulator"
100103
case .pixelFormatNotSupported:
@@ -116,6 +119,8 @@ enum DeviceError {
116119
return "The currently selected camera device does not support low-light boost! Select a device where `device.supportsLowLightBoost` is true."
117120
case .focusNotSupported:
118121
return "The currently selected camera device does not support focusing!"
122+
case .focusDepthNotSupported:
123+
return "The currently selected camera device does not support manual depth-of-field focusing!"
119124
case .microphoneUnavailable:
120125
return "The microphone was unavailable."
121126
case .notAvailableOnSimulator:
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//
2+
// CameraSession+FocusDepth.swift
3+
// VisionCamera
4+
//
5+
// Created by Hugo Gresse on 12.08.25.
6+
//
7+
8+
import AVFoundation
9+
import Foundation
10+
11+
extension CameraSession {
12+
/**
13+
Focuses the Camera to the specified distance. The distance must be within 0.001f and device.minFocusDistance
14+
*/
15+
func focusDepth(distance: Float) throws {
16+
guard let device = videoDeviceInput?.device else {
17+
throw CameraError.session(SessionError.cameraNotReady)
18+
}
19+
if !device.isLockingFocusWithCustomLensPositionSupported {
20+
throw CameraError.device(DeviceError.focusDepthNotSupported)
21+
}
22+
23+
VisionLogger.log(level: .info, message: "Focusing depth (\(distance))...")
24+
25+
do {
26+
try device.lockForConfiguration()
27+
defer {
28+
device.unlockForConfiguration()
29+
}
30+
31+
// Set Focus depth
32+
device.setFocusModeLocked(lensPosition: distance, completionHandler: nil)
33+
} catch {
34+
throw CameraError.device(DeviceError.configureError)
35+
}
36+
}
37+
}

package/ios/Core/CameraSession.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ final class CameraSession: NSObject, AVCaptureVideoDataOutputSampleBufferDelegat
265265
}
266266
}
267267

268-
public final func captureOutput(_ captureOutput: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
268+
final func captureOutput(_ captureOutput: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
269269
switch captureOutput {
270270
case is AVCaptureVideoDataOutput:
271271
onVideoFrame(sampleBuffer: sampleBuffer, orientation: connection.orientation, isMirrored: connection.isVideoMirrored)

package/ios/Core/Extensions/AVCaptureDevice+toDictionary.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ extension AVCaptureDevice {
2929
"supportsRawCapture": false, // TODO: supportsRawCapture
3030
"supportsLowLightBoost": isLowLightBoostSupported,
3131
"supportsFocus": isFocusPointOfInterestSupported,
32+
"supportsFocusDepth": isLockingFocusWithCustomLensPositionSupported,
3233
"hardwareLevel": HardwareLevel.full.jsValue,
3334
"sensorOrientation": sensorOrientation.jsValue,
3435
"formats": formats.map { $0.toJSValue() },

0 commit comments

Comments
 (0)