Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package com.margelo.nitro.camera.hybrids

import android.animation.Animator
import android.animation.ValueAnimator
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.camera.core.Camera
import androidx.camera.core.CameraState
import androidx.camera.core.FocusMeteringAction
Expand Down Expand Up @@ -33,6 +36,7 @@ import com.margelo.nitro.core.Promise
import com.margelo.nitro.core.resolve
import com.margelo.nitro.core.resolved
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicLong

class HybridCameraController(
val camera: Camera,
Expand All @@ -41,6 +45,10 @@ class HybridCameraController(
private val cameraState: CameraState?
get() = camera.cameraInfo.cameraState.value
private var zoomAnimator: ValueAnimator? = null
private val focusResetHandler = Handler(Looper.getMainLooper())
private val focusGeneration = AtomicLong()
private val focusResetLock = Any()
private var pendingFocusReset: Runnable? = null

override val isConnected: Boolean
get() = cameraState?.type == CameraState.Type.OPEN
Expand Down Expand Up @@ -140,7 +148,14 @@ class HybridCameraController(
override fun focusTo(
point: HybridMeteringPointSpec,
options: FocusOptions,
): Promise<Unit> = focusTo(point, options, null)

fun focusTo(
point: HybridMeteringPointSpec,
options: FocusOptions,
onReset: (() -> Unit)?,
): Promise<Unit> {
val generation = beginFocusOperation()
return Promise.async {
val point =
point as? HybridMeteringPoint
Expand All @@ -159,14 +174,68 @@ class HybridCameraController(
SceneAdaptiveness.CONTINUOUS -> action.setLockingMode(0)
}
// Disable auto reset, or set it to a fixed duration (seconds)
autoResetAfter.match(
{ _ -> action.disableAutoCancel() },
{ duration -> action.setAutoCancelDuration(duration.toLong(), TimeUnit.SECONDS) },
)
if (onReset != null) {
action.disableAutoCancel()
} else {
autoResetAfter.match(
{ _ -> action.disableAutoCancel() },
{ duration -> action.setAutoCancelDuration(duration.toLong(), TimeUnit.SECONDS) },
)
}
}
camera.cameraControl
.startFocusAndMetering(focusAction.build())
.await()

if (onReset != null) {
autoResetAfter.match(
{ _ -> },
{ duration -> scheduleFocusReset(generation, duration, onReset) },
)
}
}
}

private fun beginFocusOperation(): Long {
val generation = focusGeneration.incrementAndGet()
synchronized(focusResetLock) {
pendingFocusReset?.let { focusResetHandler.removeCallbacks(it) }
pendingFocusReset = null
}
return generation
}

private fun scheduleFocusReset(
generation: Long,
duration: Double,
onReset: () -> Unit,
) {
val reset =
Runnable {
if (focusGeneration.get() != generation) return@Runnable
val future = camera.cameraControl.cancelFocusAndMetering()
future.addListener(
{
try {
future.get()
if (focusGeneration.get() == generation) {
synchronized(focusResetLock) {
pendingFocusReset = null
}
onReset()
}
} catch (error: Throwable) {
Log.e(TAG, "Failed to automatically reset focus!", error)
}
},
{ command -> focusResetHandler.post(command) },
)
}
synchronized(focusResetLock) {
if (focusGeneration.get() == generation) {
pendingFocusReset = reset
focusResetHandler.postDelayed(reset, (duration * 1_000).toLong())
}
}
}

Expand All @@ -183,6 +252,7 @@ class HybridCameraController(
}

override fun resetFocus(): Promise<Unit> {
beginFocusOperation()
return Promise.async {
camera.cameraControl
.cancelFocusAndMetering()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class HybridTapToFocusGestureController :
private var previewView: PreviewView? = null
private val onTapListeners = mutableSetOf<(HybridMeteringPointSpec) -> Unit>()
private val onFocusCompletedListeners = mutableSetOf<(HybridMeteringPointSpec) -> Unit>()
private val onFocusResetListeners = mutableSetOf<(HybridMeteringPointSpec) -> Unit>()
private val context: Context
get() = NitroModules.applicationContext ?: throw Error("Context not available!")
private val mainHandler = Handler(Looper.getMainLooper())
Expand All @@ -50,8 +51,18 @@ class HybridTapToFocusGestureController :
val density = context.resources.displayMetrics.density
val meteringPoint = HybridMeteringPoint((e.x / density).toDouble(), (e.y / density).toDouble(), null, point)
onTapListeners.toList().forEach { it(meteringPoint) }
controller
.focusTo(meteringPoint, FocusOptions(null, null, null, null))
val focusOptions = FocusOptions(null, null, null, null)
val focusPromise =
if (controller is HybridCameraController) {
controller.focusTo(meteringPoint, focusOptions) {
mainHandler.post {
onFocusResetListeners.toList().forEach { it(meteringPoint) }
}
}
} else {
controller.focusTo(meteringPoint, focusOptions)
}
focusPromise
.then {
mainHandler.post {
onFocusCompletedListeners.toList().forEach { it(meteringPoint) }
Expand Down Expand Up @@ -122,4 +133,11 @@ class HybridTapToFocusGestureController :
onFocusCompletedListeners.remove(onFocusCompleted)
}
}

override fun addOnFocusResetListener(onFocusReset: (HybridMeteringPointSpec) -> Unit): ListenerSubscription {
onFocusResetListeners.add(onFocusReset)
return ListenerSubscription {
onFocusResetListeners.remove(onFocusReset)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ final class HybridTapToFocusGestureController: HybridTapToFocusGestureController
private weak var previewView: (any HybridPreviewViewSpec)? = nil
private var onTapListeners: [UUID: (any HybridMeteringPointSpec) -> Void] = [:]
private var onFocusCompletedListeners: [UUID: (any HybridMeteringPointSpec) -> Void] = [:]
private var onFocusResetListeners: [UUID: (any HybridMeteringPointSpec) -> Void] = [:]

override init() {
super.init()
Expand All @@ -39,7 +40,24 @@ final class HybridTapToFocusGestureController: HybridTapToFocusGestureController
let tapListeners = Array(onTapListeners.values)
tapListeners.forEach { $0(meteringPoint) }

try controller.focusTo(point: meteringPoint, options: FocusOptions())
let onReset = { [weak self] in
DispatchQueue.main.async { [weak self] in
guard let self else { return }
let focusResetListeners = Array(self.onFocusResetListeners.values)
focusResetListeners.forEach { $0(meteringPoint) }
}
}
let focusPromise: Promise<Void>
if let nativeController = controller as? HybridCameraController {
focusPromise = nativeController.focusTo(
point: meteringPoint,
options: FocusOptions(),
onReset: onReset)
} else {
focusPromise = try controller.focusTo(point: meteringPoint, options: FocusOptions())
}

focusPromise
.then { [weak self] _ in
DispatchQueue.main.async { [weak self] in
guard let self else { return }
Expand Down Expand Up @@ -75,6 +93,16 @@ final class HybridTapToFocusGestureController: HybridTapToFocusGestureController
}
}

func addOnFocusResetListener(onFocusReset: @escaping (any HybridMeteringPointSpec) -> Void)
-> ListenerSubscription
{
let id = UUID()
onFocusResetListeners[id] = onFocusReset
return ListenerSubscription { [weak self] in
self?.onFocusResetListeners.removeValue(forKey: id)
}
}

func onAttached(to preview: any HybridPreviewViewSpec) {
self.previewView = preview
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,14 @@ final class HybridCameraController: HybridCameraControllerSpec, NativeCameraCont
func focusTo(
point: any HybridMeteringPointSpec,
options: FocusOptions
) -> Promise<Void> {
return focusTo(point: point, options: options, onReset: nil)
}

func focusTo(
point: any HybridMeteringPointSpec,
options: FocusOptions,
onReset: (() -> Void)?
) -> Promise<Void> {
return captureDevice.withLock(queue) { resolve, reject in
guard let point = point as? HybridMeteringPoint else {
Expand Down Expand Up @@ -214,7 +222,8 @@ final class HybridCameraController: HybridCameraControllerSpec, NativeCameraCont
// Start listening to updates
task.startListening(
onComplete: resolve,
onError: reject
onError: reject,
onReset: onReset
)
self.activeMeteringTask = task

Expand Down
14 changes: 12 additions & 2 deletions packages/react-native-vision-camera/ios/Utils/MeteringTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ final class MeteringTask {
private var isFinished = false
private var onComplete: (() -> Void)? = nil
private var onError: ((Error) -> Void)? = nil
private var onReset: (() -> Void)? = nil

private struct MeteringProgress {
var settledAt: Date? = nil
Expand Down Expand Up @@ -87,10 +88,12 @@ final class MeteringTask {
*/
func startListening(
onComplete: @escaping () -> Void,
onError: @escaping (Error) -> Void
onError: @escaping (Error) -> Void,
onReset: (() -> Void)?
) {
self.onComplete = onComplete
self.onError = onError
self.onReset = onReset
// the Timer periodically polls AE/AF/AWB state - this is how we can ensure the states have
// been stable for 120ms+ and aren't just fluctuating.
let pollTimer = DispatchSource.makeTimerSource(queue: queue)
Expand Down Expand Up @@ -171,7 +174,14 @@ final class MeteringTask {
if case .after(let seconds) = self.autoReset {
self.queue.asyncAfter(deadline: .now() + seconds) { [weak self] in
guard let self else { return }
try? self.resetMeteringValues()
do {
try self.resetMeteringValues()
let onReset = self.onReset
self.onReset = nil
onReset?()
} catch {
logger.error("Failed to automatically reset metering! \(error)")
}
}
}
}
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,17 @@ export interface TapToFocusGestureController extends GestureController {
addOnFocusCompletedListener(
onFocusCompleted: (point: MeteringPoint) => void,
): ListenerSubscription

/**
* Adds a listener that is called after focus triggered by a native tap
* automatically resets.
*
* This is not called if the reset fails or the focus is superseded by a
* newer focus operation. Call
* {@linkcode ListenerSubscription.remove | remove()} on the returned
* subscription to stop receiving updates.
*/
addOnFocusResetListener(
onFocusReset: (point: MeteringPoint) => void,
): ListenerSubscription
}
Loading