Skip to content

Commit ebca62b

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

File tree

12 files changed

+152
-0
lines changed

12 files changed

+152
-0
lines changed

docs/docs/guides/FOCUSING.mdx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,15 @@ export function App() {
5858
}
5959
```
6060

61+
### Focus depth (focus on a fixed manual distance)
62+
63+
You can programmatically set the distance of the focus, or the depth.
64+
For example, if the user wants to set the closest distance from the camera to the object (macro), you will set `device.minFocusDistance + 0.1` on Android or `0.0` on iOS.
65+
66+
```ts
67+
await camera.current.focusDepth(0.5)
68+
```
69+
6170
<br />
6271

6372
#### 🚀 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
@@ -4,6 +4,7 @@ import android.Manifest
44
import android.content.pm.PackageManager
55
import android.util.Log
66
import androidx.core.content.ContextCompat
7+
import androidx.camera.camera2.interop.ExperimentalCamera2Interop
78
import com.facebook.react.bridge.Callback
89
import com.facebook.react.bridge.Promise
910
import com.facebook.react.bridge.ReactApplicationContext
@@ -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/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/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() },
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
//
2+
// CameraView+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 CameraView {
12+
func focusDepth(distance: Float, promise: Promise) {
13+
withPromise(promise) {
14+
try cameraSession.focusDepth(distance: distance)
15+
return nil
16+
}
17+
}
18+
}

package/ios/React/CameraViewManager.m

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,5 +88,7 @@ @interface RCT_EXTERN_REMAP_MODULE (CameraView, CameraViewManager, RCTViewManage
8888
resolve reject : (RCTPromiseRejectBlock)reject);
8989
RCT_EXTERN_METHOD(focus : (nonnull NSNumber*)node point : (NSDictionary*)point resolve : (RCTPromiseResolveBlock)
9090
resolve reject : (RCTPromiseRejectBlock)reject);
91+
RCT_EXTERN_METHOD(focusDepth : (nonnull NSNumber*)node distance : (NSNumber*)distance resolve : (RCTPromiseResolveBlock)
92+
resolve reject : (RCTPromiseRejectBlock)reject);
9193

9294
@end

package/ios/React/CameraViewManager.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,13 @@ final class CameraViewManager: RCTViewManager {
9696
component.focus(point: CGPoint(x: x.doubleValue, y: y.doubleValue), promise: promise)
9797
}
9898

99+
@objc
100+
final func focusDepth(_ node: NSNumber, distance: NSNumber, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
101+
let promise = Promise(resolver: resolve, rejecter: reject)
102+
let component = getCameraView(withTag: node)
103+
component.focusDepth(distance: distance.floatValue, promise: promise)
104+
}
105+
99106
@objc
100107
final func getCameraPermissionStatus() -> String {
101108
let status = AVCaptureDevice.authorizationStatus(for: .video)

0 commit comments

Comments
 (0)