feat: Implement focus() on Android (#2523)

* feat: Implement `focus()` on Android

* Throw if not supported

* Do focus in example

* Format

* fix: Properly convert layer point to camera coordinates

* Fix

* Set trigger back to IDLE

* Fix rotation maybe?

* Rotated by

* fix: Fix display point calculation

* Try other

* Invoke `capture` callback on same thread

* Center metering rectangle

* Reset AF Trigger to IDLE

* Reset it to it's default AF mode again, i dont even know anymore

* Update CameraPage.tsx

* Format

* Apply options to repeating

* Set

* Use scene mode

* Update CameraPage.tsx

* Update CameraDeviceDetails.kt

* It fucking works

* Update PersistentCameraCaptureSession.kt

* Update PersistentCameraCaptureSession.kt

* Update PersistentCameraCaptureSession.kt

* Create CameraCaptureSession+setRepeatingRequestAndWait.kt

* Oh my god it works

* Also focus AE

* Cancel reset request

* Rename to AF

* Format

* Update PersistentCameraCaptureSession.kt
This commit is contained in:
Marc Rousavy 2024-02-08 15:16:58 +01:00 committed by GitHub
parent fce6616964
commit fb1d82ad9a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 288 additions and 33 deletions

View File

@ -44,6 +44,8 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId
characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS) ?: floatArrayOf(35f) characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS) ?: floatArrayOf(35f)
} }
val sensorSize by lazy { characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)!! } val sensorSize by lazy { characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)!! }
val activeSize
get() = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)!!
val sensorOrientation by lazy { characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0 } val sensorOrientation by lazy { characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0 }
val minFocusDistance by lazy { getMinFocusDistanceCm() } val minFocusDistance by lazy { getMinFocusDistanceCm() }
val name by lazy { val name by lazy {
@ -91,6 +93,14 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId
val isBackwardsCompatible by lazy { capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE) } val isBackwardsCompatible by lazy { capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE) }
val supportsSnapshotCapture by lazy { supportsSnapshotCapture() } val supportsSnapshotCapture by lazy { supportsSnapshotCapture() }
val supportsTapToFocus by lazy { (characteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF) ?: 0) > 0 }
val supportsTapToExposure by lazy { (characteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE) ?: 0) > 0 }
val supportsTapToWhiteBalance by lazy { (characteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AWB) ?: 0) > 0 }
val afModes by lazy { characteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES)?.toList() ?: emptyList() }
val aeModes by lazy { characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES)?.toList() ?: emptyList() }
val awbModes by lazy { characteristics.get(CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES)?.toList() ?: emptyList() }
// TODO: Also add 10-bit YUV here? // TODO: Also add 10-bit YUV here?
val videoFormat = ImageFormat.YUV_420_888 val videoFormat = ImageFormat.YUV_420_888
@ -244,7 +254,7 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId
map.putBoolean("isMultiCam", isMultiCam) map.putBoolean("isMultiCam", isMultiCam)
map.putBoolean("supportsRawCapture", supportsRawCapture) map.putBoolean("supportsRawCapture", supportsRawCapture)
map.putBoolean("supportsLowLightBoost", supportsLowLightBoost) map.putBoolean("supportsLowLightBoost", supportsLowLightBoost)
map.putBoolean("supportsFocus", true) // I believe every device here supports focussing map.putBoolean("supportsFocus", supportsTapToFocus)
map.putDouble("minZoom", minZoom) map.putDouble("minZoom", minZoom)
map.putDouble("maxZoom", maxZoom) map.putDouble("maxZoom", maxZoom)
map.putDouble("neutralZoom", 1.0) // Zoom is always relative to 1.0 on Android map.putDouble("neutralZoom", 1.0) // Zoom is always relative to 1.0 on Android

View File

@ -62,6 +62,8 @@ class FlashUnavailableError :
"flash-unavailable", "flash-unavailable",
"The Camera Device does not have a flash unit! Make sure you select a device where `device.hasFlash`/`device.hasTorch` is true." "The Camera Device does not have a flash unit! Make sure you select a device where `device.hasFlash`/`device.hasTorch` is true."
) )
class FocusNotSupportedError :
CameraError("device", "focus-not-supported", "The currently selected camera device does not support focusing!")
class CameraNotReadyError : class CameraNotReadyError :
CameraError("session", "camera-not-ready", "The Camera is not ready yet! Wait for the onInitialized() callback!") CameraError("session", "camera-not-ready", "The Camera is not ready yet! Wait for the onInitialized() callback!")

View File

@ -468,9 +468,13 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
callback.onError(error) callback.onError(error)
} }
suspend fun focus(x: Int, y: Int): Unit = throw NotImplementedError("focus() is not yet implemented!") suspend fun focus(x: Int, y: Int) {
val previewView = previewView ?: throw CameraNotReadyError()
val deviceDetails = captureSession.getActiveDeviceDetails() ?: throw CameraNotReadyError()
private suspend fun focus(point: Point): Unit = throw NotImplementedError() val cameraPoint = previewView.convertLayerPointToCameraCoordinates(Point(x, y), deviceDetails)
captureSession.focus(cameraPoint)
}
data class CapturedPhoto( data class CapturedPhoto(
val image: Image, val image: Image,

View File

@ -1,11 +1,15 @@
package com.mrousavy.camera.core package com.mrousavy.camera.core
import android.graphics.Point
import android.hardware.camera2.CameraAccessException import android.hardware.camera2.CameraAccessException
import android.hardware.camera2.CameraCaptureSession import android.hardware.camera2.CameraCaptureSession
import android.hardware.camera2.CameraDevice import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CameraManager import android.hardware.camera2.CameraManager
import android.hardware.camera2.CaptureRequest
import android.hardware.camera2.TotalCaptureResult import android.hardware.camera2.TotalCaptureResult
import android.hardware.camera2.params.MeteringRectangle
import android.util.Log import android.util.Log
import android.util.Size
import com.mrousavy.camera.core.capture.PhotoCaptureRequest import com.mrousavy.camera.core.capture.PhotoCaptureRequest
import com.mrousavy.camera.core.capture.RepeatingCaptureRequest import com.mrousavy.camera.core.capture.RepeatingCaptureRequest
import com.mrousavy.camera.core.outputs.SurfaceOutput import com.mrousavy.camera.core.outputs.SurfaceOutput
@ -13,11 +17,18 @@ import com.mrousavy.camera.extensions.capture
import com.mrousavy.camera.extensions.createCaptureSession import com.mrousavy.camera.extensions.createCaptureSession
import com.mrousavy.camera.extensions.isValid import com.mrousavy.camera.extensions.isValid
import com.mrousavy.camera.extensions.openCamera import com.mrousavy.camera.extensions.openCamera
import com.mrousavy.camera.extensions.setRepeatingRequestAndWaitForAF
import com.mrousavy.camera.extensions.tryAbortCaptures import com.mrousavy.camera.extensions.tryAbortCaptures
import com.mrousavy.camera.types.Flash import com.mrousavy.camera.types.Flash
import com.mrousavy.camera.types.Orientation import com.mrousavy.camera.types.Orientation
import com.mrousavy.camera.types.QualityPrioritization import com.mrousavy.camera.types.QualityPrioritization
import java.io.Closeable import java.io.Closeable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
@ -29,6 +40,8 @@ import kotlinx.coroutines.sync.withLock
class PersistentCameraCaptureSession(private val cameraManager: CameraManager, private val callback: Callback) : Closeable { class PersistentCameraCaptureSession(private val cameraManager: CameraManager, private val callback: Callback) : Closeable {
companion object { companion object {
private const val TAG = "PersistentCameraCaptureSession" private const val TAG = "PersistentCameraCaptureSession"
private val DEFAULT_METERING_SIZE = Size(100, 100)
private const val FOCUS_RESET_TIMEOUT = 3000L
} }
// Inputs/Dependencies // Inputs/Dependencies
@ -44,6 +57,8 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p
private val mutex = Mutex() private val mutex = Mutex()
private var didDestroyFromOutside = false private var didDestroyFromOutside = false
private var focusResetJob: Job? = null
private val coroutineScope = CoroutineScope(CameraQueues.cameraQueue.coroutineDispatcher)
val isRunning: Boolean val isRunning: Boolean
get() = isActive && session != null && device != null && !didDestroyFromOutside get() = isActive && session != null && device != null && !didDestroyFromOutside
@ -128,6 +143,7 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p
enableShutterSound: Boolean enableShutterSound: Boolean
): TotalCaptureResult { ): TotalCaptureResult {
mutex.withLock { mutex.withLock {
Log.i(TAG, "Capturing photo...")
val session = session ?: throw CameraNotReadyError() val session = session ?: throw CameraNotReadyError()
val repeatingRequest = repeatingRequest ?: throw CameraNotReadyError() val repeatingRequest = repeatingRequest ?: throw CameraNotReadyError()
val photoRequest = PhotoCaptureRequest( val photoRequest = PhotoCaptureRequest(
@ -149,6 +165,88 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p
} }
} }
suspend fun focus(point: Point) {
mutex.withLock {
Log.i(TAG, "Focusing to $point...")
val session = session ?: throw CameraNotReadyError()
val repeatingRequest = repeatingRequest ?: throw CameraNotReadyError()
val device = session.device
val deviceDetails = getOrCreateCameraDeviceDetails(device)
if (!deviceDetails.supportsTapToFocus) {
throw FocusNotSupportedError()
}
val outputs = outputs.filter { it.isRepeating }
val meteringRectangle = MeteringRectangle(point, DEFAULT_METERING_SIZE, MeteringRectangle.METERING_WEIGHT_MAX - 1)
// 0. Cancel the 3 second focus reset task
focusResetJob?.cancelAndJoin()
focusResetJob = null
// 1. Cancel any ongoing AF/AE/AWB request
repeatingRequest.createCaptureRequest(device, deviceDetails, outputs).also { request ->
if (deviceDetails.supportsTapToFocus) {
request.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
}
if (deviceDetails.supportsTapToExposure) {
request.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL)
}
session.capture(request.build(), null, null)
}
// 2. After previous AF/AE/AWB requests have been canceled, start a new AF/AE/AWB request
repeatingRequest.createCaptureRequest(device, deviceDetails, outputs).also { request ->
request.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO)
if (deviceDetails.supportsTapToFocus) {
request.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO)
request.set(CaptureRequest.CONTROL_AF_REGIONS, arrayOf(meteringRectangle))
request.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START)
}
if (deviceDetails.supportsTapToExposure) {
request.set(CaptureRequest.CONTROL_AE_REGIONS, arrayOf(meteringRectangle))
request.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START)
}
if (deviceDetails.supportsTapToWhiteBalance) {
request.set(CaptureRequest.CONTROL_AWB_REGIONS, arrayOf(meteringRectangle))
}
session.capture(request.build(), null, null)
// 3. Start a repeating request without the trigger and wait until AF/AE/AWB locks
request.set(CaptureRequest.CONTROL_AF_TRIGGER, null)
request.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, null)
session.setRepeatingRequestAndWaitForAF(request.build())
}
// 4. After the Camera has successfully found the AF/AE/AWB lock-point, we set it to idle and keep the point metered
repeatingRequest.createCaptureRequest(device, deviceDetails, outputs).also { request ->
request.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO)
if (deviceDetails.supportsTapToFocus) {
request.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO)
request.set(CaptureRequest.CONTROL_AF_REGIONS, arrayOf(meteringRectangle))
request.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_IDLE)
}
if (deviceDetails.supportsTapToExposure) {
request.set(CaptureRequest.CONTROL_AE_REGIONS, arrayOf(meteringRectangle))
request.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE)
}
session.setRepeatingRequest(request.build(), null, null)
}
// 5. Wait 3 seconds
focusResetJob = coroutineScope.launch {
delay(FOCUS_RESET_TIMEOUT)
if (!this.isActive) {
// this job got canceled from the outside
return@launch
}
Log.i(TAG, "Resetting focus to auto-focus...")
// 6. Reset AF/AE/AWB to continuous auto-focus again, which is the default here.
repeatingRequest.createCaptureRequest(device, deviceDetails, outputs).also { request ->
session.setRepeatingRequest(request.build(), null, null)
}
}
}
}
fun getActiveDeviceDetails(): CameraDeviceDetails? { fun getActiveDeviceDetails(): CameraDeviceDetails? {
val device = device ?: return null val device = device ?: return null
return getOrCreateCameraDeviceDetails(device) return getOrCreateCameraDeviceDetails(device)

View File

@ -2,6 +2,7 @@ package com.mrousavy.camera.core
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.graphics.Point
import android.util.Log import android.util.Log
import android.util.Size import android.util.Size
import android.view.Gravity import android.view.Gravity
@ -11,6 +12,7 @@ import android.widget.FrameLayout
import com.facebook.react.bridge.UiThreadUtil import com.facebook.react.bridge.UiThreadUtil
import com.mrousavy.camera.extensions.getMaximumPreviewSize import com.mrousavy.camera.extensions.getMaximumPreviewSize
import com.mrousavy.camera.extensions.resize import com.mrousavy.camera.extensions.resize
import com.mrousavy.camera.types.Orientation
import com.mrousavy.camera.types.ResizeMode import com.mrousavy.camera.types.ResizeMode
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -51,6 +53,24 @@ class PreviewView(context: Context, callback: SurfaceHolder.Callback) : SurfaceV
} }
} }
private val viewSize: Size
get() {
val displayMetrics = context.resources.displayMetrics
val dpX = width / displayMetrics.density
val dpY = height / displayMetrics.density
return Size(dpX.toInt(), dpY.toInt())
}
fun convertLayerPointToCameraCoordinates(point: Point, cameraDeviceDetails: CameraDeviceDetails): Point {
val sensorOrientation = Orientation.fromRotationDegrees(cameraDeviceDetails.sensorOrientation)
val cameraSize = Size(cameraDeviceDetails.activeSize.width(), cameraDeviceDetails.activeSize.height())
val viewOrientation = Orientation.PORTRAIT
val rotated = Orientation.rotatePoint(point, viewSize, cameraSize, viewOrientation, sensorOrientation)
Log.i(TAG, "$point -> $sensorOrientation (in $cameraSize -> $viewSize) -> $rotated")
return rotated
}
private fun getSize(contentSize: Size, containerSize: Size, resizeMode: ResizeMode): Size { private fun getSize(contentSize: Size, containerSize: Size, resizeMode: ResizeMode): Size {
// TODO: Take sensor orientation into account here // TODO: Take sensor orientation into account here
val contentAspectRatio = contentSize.height.toDouble() / contentSize.width val contentAspectRatio = contentSize.height.toDouble() / contentSize.width

View File

@ -61,9 +61,11 @@ abstract class CameraCaptureRequest(
if (format == null) throw PropRequiresFormatToBeNonNullError("videoHdr") if (format == null) throw PropRequiresFormatToBeNonNullError("videoHdr")
if (!format.supportsVideoHdr) throw InvalidVideoHdrError() if (!format.supportsVideoHdr) throw InvalidVideoHdrError()
builder.set(CaptureRequest.CONTROL_SCENE_MODE, CaptureRequest.CONTROL_SCENE_MODE_HDR) builder.set(CaptureRequest.CONTROL_SCENE_MODE, CaptureRequest.CONTROL_SCENE_MODE_HDR)
builder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_USE_SCENE_MODE)
} else if (enableLowLightBoost) { } else if (enableLowLightBoost) {
if (!deviceDetails.supportsLowLightBoost) throw LowLightBoostNotSupportedError() if (!deviceDetails.supportsLowLightBoost) throw LowLightBoostNotSupportedError()
builder.set(CaptureRequest.CONTROL_SCENE_MODE, CaptureRequest.CONTROL_SCENE_MODE_NIGHT) builder.set(CaptureRequest.CONTROL_SCENE_MODE, CaptureRequest.CONTROL_SCENE_MODE_NIGHT)
builder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_USE_SCENE_MODE)
} }
// Set Exposure Bias // Set Exposure Bias

View File

@ -14,7 +14,7 @@ import com.mrousavy.camera.types.Torch
import com.mrousavy.camera.types.VideoStabilizationMode import com.mrousavy.camera.types.VideoStabilizationMode
class RepeatingCaptureRequest( class RepeatingCaptureRequest(
private val enableVideoPipeline: Boolean, val enableVideoPipeline: Boolean,
torch: Torch = Torch.OFF, torch: Torch = Torch.OFF,
private val fps: Int? = null, private val fps: Int? = null,
private val videoStabilizationMode: VideoStabilizationMode = VideoStabilizationMode.OFF, private val videoStabilizationMode: VideoStabilizationMode = VideoStabilizationMode.OFF,
@ -48,6 +48,32 @@ class RepeatingCaptureRequest(
): CaptureRequest.Builder { ): CaptureRequest.Builder {
val builder = super.createCaptureRequest(template, device, deviceDetails, outputs) val builder = super.createCaptureRequest(template, device, deviceDetails, outputs)
builder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO)
// Set AF
if (enableVideoPipeline && deviceDetails.afModes.contains(CameraCharacteristics.CONTROL_AF_MODE_CONTINUOUS_VIDEO)) {
builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO)
} else if (deviceDetails.afModes.contains(CameraCharacteristics.CONTROL_AF_MODE_CONTINUOUS_PICTURE)) {
builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)
} else if (deviceDetails.afModes.contains(CameraCharacteristics.CONTROL_AF_MODE_AUTO)) {
builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO)
} else if (deviceDetails.afModes.contains(CameraCharacteristics.CONTROL_AF_MODE_OFF)) {
builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF)
builder.set(CaptureRequest.LENS_FOCUS_DISTANCE, 0f)
}
// Set AE
if (deviceDetails.aeModes.contains(CameraCharacteristics.CONTROL_AE_MODE_ON)) {
builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON)
} else if (deviceDetails.aeModes.contains(CameraCharacteristics.CONTROL_AE_MODE_OFF)) {
builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF)
}
// Set AWB
if (deviceDetails.awbModes.contains(CameraCharacteristics.CONTROL_AWB_MODE_AUTO)) {
builder.set(CaptureRequest.CONTROL_AWB_MODE, CaptureRequest.CONTROL_AWB_MODE_AUTO)
}
// Set FPS // Set FPS
if (fps != null) { if (fps != null) {
if (format == null) throw PropRequiresFormatToBeNonNullError("fps") if (format == null) throw PropRequiresFormatToBeNonNullError("fps")

View File

@ -5,7 +5,6 @@ import android.hardware.camera2.CaptureFailure
import android.hardware.camera2.CaptureRequest import android.hardware.camera2.CaptureRequest
import android.hardware.camera2.TotalCaptureResult import android.hardware.camera2.TotalCaptureResult
import android.media.MediaActionSound import android.media.MediaActionSound
import com.mrousavy.camera.core.CameraQueues
import com.mrousavy.camera.core.CaptureAbortedError import com.mrousavy.camera.core.CaptureAbortedError
import com.mrousavy.camera.core.UnknownCaptureError import com.mrousavy.camera.core.UnknownCaptureError
import kotlin.coroutines.resume import kotlin.coroutines.resume
@ -23,29 +22,36 @@ suspend fun CameraCaptureSession.capture(captureRequest: CaptureRequest, enableS
override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) { override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) {
super.onCaptureCompleted(session, request, result) super.onCaptureCompleted(session, request, result)
continuation.resume(result) if (request == captureRequest) {
shutterSound?.release() continuation.resume(result)
shutterSound?.release()
}
} }
override fun onCaptureStarted(session: CameraCaptureSession, request: CaptureRequest, timestamp: Long, frameNumber: Long) { override fun onCaptureStarted(session: CameraCaptureSession, request: CaptureRequest, timestamp: Long, frameNumber: Long) {
super.onCaptureStarted(session, request, timestamp, frameNumber) super.onCaptureStarted(session, request, timestamp, frameNumber)
if (enableShutterSound) { if (request == captureRequest) {
shutterSound?.play(MediaActionSound.SHUTTER_CLICK) if (enableShutterSound) {
shutterSound?.play(MediaActionSound.SHUTTER_CLICK)
}
} }
} }
override fun onCaptureFailed(session: CameraCaptureSession, request: CaptureRequest, failure: CaptureFailure) { override fun onCaptureFailed(session: CameraCaptureSession, request: CaptureRequest, failure: CaptureFailure) {
super.onCaptureFailed(session, request, failure) super.onCaptureFailed(session, request, failure)
val wasImageCaptured = failure.wasImageCaptured()
val error = when (failure.reason) { if (request == captureRequest) {
CaptureFailure.REASON_ERROR -> UnknownCaptureError(wasImageCaptured) val wasImageCaptured = failure.wasImageCaptured()
CaptureFailure.REASON_FLUSHED -> CaptureAbortedError(wasImageCaptured) val error = when (failure.reason) {
else -> UnknownCaptureError(wasImageCaptured) CaptureFailure.REASON_ERROR -> UnknownCaptureError(wasImageCaptured)
CaptureFailure.REASON_FLUSHED -> CaptureAbortedError(wasImageCaptured)
else -> UnknownCaptureError(wasImageCaptured)
}
continuation.resumeWithException(error)
} }
continuation.resumeWithException(error)
} }
}, },
CameraQueues.cameraQueue.handler null
) )
} }

View File

@ -0,0 +1,47 @@
package com.mrousavy.camera.extensions
import android.hardware.camera2.CameraCaptureSession
import android.hardware.camera2.CaptureFailure
import android.hardware.camera2.CaptureRequest
import android.hardware.camera2.CaptureResult
import android.hardware.camera2.TotalCaptureResult
import android.util.Log
import com.mrousavy.camera.core.CaptureAbortedError
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlinx.coroutines.suspendCancellableCoroutine
private const val TAG = "CameraCaptureSession"
/**
* Set a new repeating request for the [CameraCaptureSession] that contains an AF trigger, and wait until AF has locked.
*/
suspend fun CameraCaptureSession.setRepeatingRequestAndWaitForAF(request: CaptureRequest) =
suspendCancellableCoroutine { continuation ->
this.setRepeatingRequest(
request,
object : CameraCaptureSession.CaptureCallback() {
override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) {
super.onCaptureCompleted(session, request, result)
if (continuation.isActive) {
val afState = result.get(CaptureResult.CONTROL_AF_STATE)
Log.i(TAG, "AF State: $afState")
if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED || afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) {
continuation.resume(Unit)
session.setRepeatingRequest(request, null, null)
}
}
}
override fun onCaptureFailed(session: CameraCaptureSession, request: CaptureRequest, failure: CaptureFailure) {
super.onCaptureFailed(session, request, failure)
if (continuation.isActive) {
continuation.resumeWithException(CaptureAbortedError(failure.wasImageCaptured()))
session.setRepeatingRequest(request, null, null)
}
}
},
null
)
}

View File

@ -2,7 +2,7 @@ package com.mrousavy.camera.extensions
import android.util.Size import android.util.Size
import android.util.SizeF import android.util.SizeF
import android.view.Surface import com.mrousavy.camera.types.Orientation
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@ -14,13 +14,10 @@ fun List<Size>.closestToOrMax(size: Size?): Size =
this.maxBy { it.width * it.height } this.maxBy { it.width * it.height }
} }
fun Size.rotated(surfaceRotation: Int): Size = fun Size.rotatedBy(orientation: Orientation): Size =
when (surfaceRotation) { when (orientation) {
Surface.ROTATION_0 -> Size(width, height) Orientation.PORTRAIT, Orientation.PORTRAIT_UPSIDE_DOWN -> this
Surface.ROTATION_90 -> Size(height, width) Orientation.LANDSCAPE_LEFT, Orientation.LANDSCAPE_RIGHT -> Size(height, width)
Surface.ROTATION_180 -> Size(width, height)
Surface.ROTATION_270 -> Size(height, width)
else -> Size(width, height)
} }
val Size.bigger: Int val Size.bigger: Int

View File

@ -1,5 +1,6 @@
package com.mrousavy.camera.extensions package com.mrousavy.camera.extensions
import android.util.Log
import android.view.SurfaceHolder import android.view.SurfaceHolder
import androidx.annotation.UiThread import androidx.annotation.UiThread
import kotlin.coroutines.resume import kotlin.coroutines.resume
@ -15,14 +16,18 @@ suspend fun SurfaceHolder.resize(width: Int, height: Int) {
return@suspendCancellableCoroutine return@suspendCancellableCoroutine
} }
Log.i("SurfaceHolder", "Resizing SurfaceHolder to $width x $height...")
val callback = object : SurfaceHolder.Callback { val callback = object : SurfaceHolder.Callback {
override fun surfaceCreated(holder: SurfaceHolder) = Unit override fun surfaceCreated(holder: SurfaceHolder) = Unit
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
holder.removeCallback(this) holder.removeCallback(this)
Log.i("SurfaceHolder", "Resized SurfaceHolder to $width x $height!")
continuation.resume(Unit) continuation.resume(Unit)
} }
override fun surfaceDestroyed(holder: SurfaceHolder) { override fun surfaceDestroyed(holder: SurfaceHolder) {
holder.removeCallback(this) holder.removeCallback(this)
Log.e("SurfaceHolder", "Failed to resize SurfaceHolder to $width x $height!")
continuation.cancel(Error("Tried to resize SurfaceView, but Surface has been destroyed!")) continuation.cancel(Error("Tried to resize SurfaceView, but Surface has been destroyed!"))
} }
} }

View File

@ -1,5 +1,9 @@
package com.mrousavy.camera.types package com.mrousavy.camera.types
import android.graphics.Point
import android.graphics.PointF
import android.util.Log
import android.util.Size
import com.mrousavy.camera.core.CameraDeviceDetails import com.mrousavy.camera.core.CameraDeviceDetails
enum class Orientation(override val unionValue: String) : JSUnionValue { enum class Orientation(override val unionValue: String) : JSUnionValue {
@ -11,9 +15,9 @@ enum class Orientation(override val unionValue: String) : JSUnionValue {
fun toDegrees(): Int = fun toDegrees(): Int =
when (this) { when (this) {
PORTRAIT -> 0 PORTRAIT -> 0
LANDSCAPE_RIGHT -> 90 LANDSCAPE_LEFT -> 90
PORTRAIT_UPSIDE_DOWN -> 180 PORTRAIT_UPSIDE_DOWN -> 180
LANDSCAPE_LEFT -> 270 LANDSCAPE_RIGHT -> 270
} }
fun toSensorRelativeOrientation(deviceDetails: CameraDeviceDetails): Orientation { fun toSensorRelativeOrientation(deviceDetails: CameraDeviceDetails): Orientation {
@ -43,10 +47,34 @@ enum class Orientation(override val unionValue: String) : JSUnionValue {
fun fromRotationDegrees(rotationDegrees: Int): Orientation = fun fromRotationDegrees(rotationDegrees: Int): Orientation =
when (rotationDegrees) { when (rotationDegrees) {
in 45..135 -> LANDSCAPE_RIGHT in 45..135 -> LANDSCAPE_LEFT
in 135..225 -> PORTRAIT_UPSIDE_DOWN in 135..225 -> PORTRAIT_UPSIDE_DOWN
in 225..315 -> LANDSCAPE_LEFT in 225..315 -> LANDSCAPE_RIGHT
else -> PORTRAIT else -> PORTRAIT
} }
fun rotatePoint(
point: Point,
fromSize: Size,
toSize: Size,
fromOrientation: Orientation,
toOrientation: Orientation
): Point {
val differenceDegrees = (fromOrientation.toDegrees() + toOrientation.toDegrees()) % 360
val difference = Orientation.fromRotationDegrees(differenceDegrees)
val normalizedPoint = PointF(point.x / fromSize.width.toFloat(), point.y / fromSize.height.toFloat())
val rotatedNormalizedPoint = when (difference) {
PORTRAIT -> normalizedPoint
PORTRAIT_UPSIDE_DOWN -> PointF(1 - normalizedPoint.x, 1 - normalizedPoint.y)
LANDSCAPE_LEFT -> PointF(normalizedPoint.y, 1 - normalizedPoint.x)
LANDSCAPE_RIGHT -> PointF(1 - normalizedPoint.y, normalizedPoint.x)
}
val rotatedX = rotatedNormalizedPoint.x * toSize.width
val rotatedY = rotatedNormalizedPoint.y * toSize.height
Log.i("ROTATE", "$point -> $normalizedPoint -> $difference -> $rotatedX, $rotatedY")
return Point(rotatedX.toInt(), rotatedY.toInt())
}
} }
} }

View File

@ -1,6 +1,6 @@
import * as React from 'react' import * as React from 'react'
import { useRef, useState, useCallback, useMemo } from 'react' import { useRef, useState, useCallback, useMemo } from 'react'
import { StyleSheet, Text, View } from 'react-native' import { GestureResponderEvent, StyleSheet, Text, View } from 'react-native'
import { PinchGestureHandler, PinchGestureHandlerGestureEvent, TapGestureHandler } from 'react-native-gesture-handler' import { PinchGestureHandler, PinchGestureHandlerGestureEvent, TapGestureHandler } from 'react-native-gesture-handler'
import { import {
CameraProps, CameraProps,
@ -125,6 +125,16 @@ export function CameraPage({ navigation }: Props): React.ReactElement {
//#endregion //#endregion
//#region Tap Gesture //#region Tap Gesture
const onFocusTap = useCallback(
({ nativeEvent: event }: GestureResponderEvent) => {
if (!device?.supportsFocus) return
camera.current?.focus({
x: event.locationX,
y: event.locationY,
})
},
[device?.supportsFocus],
)
const onDoubleTap = useCallback(() => { const onDoubleTap = useCallback(() => {
onFlipCameraPressed() onFlipCameraPressed()
}, [onFlipCameraPressed]) }, [onFlipCameraPressed])
@ -173,7 +183,7 @@ export function CameraPage({ navigation }: Props): React.ReactElement {
<View style={styles.container}> <View style={styles.container}>
{device != null && ( {device != null && (
<PinchGestureHandler onGestureEvent={onPinchGesture} enabled={isActive}> <PinchGestureHandler onGestureEvent={onPinchGesture} enabled={isActive}>
<Reanimated.View style={StyleSheet.absoluteFill}> <Reanimated.View onTouchEnd={onFocusTap} style={StyleSheet.absoluteFill}>
<TapGestureHandler onEnded={onDoubleTap} numberOfTaps={2}> <TapGestureHandler onEnded={onDoubleTap} numberOfTaps={2}>
<ReanimatedCamera <ReanimatedCamera
style={StyleSheet.absoluteFill} style={StyleSheet.absoluteFill}
@ -186,8 +196,8 @@ export function CameraPage({ navigation }: Props): React.ReactElement {
onStopped={() => 'Camera stopped!'} onStopped={() => 'Camera stopped!'}
format={format} format={format}
fps={fps} fps={fps}
photoHdr={enableHdr} photoHdr={format?.supportsPhotoHdr && enableHdr}
videoHdr={enableHdr} videoHdr={format?.supportsVideoHdr && enableHdr}
lowLightBoost={device.supportsLowLightBoost && enableNightMode} lowLightBoost={device.supportsLowLightBoost && enableNightMode}
enableZoomGesture={false} enableZoomGesture={false}
animatedProps={cameraAnimatedProps} animatedProps={cameraAnimatedProps}

View File

@ -93,7 +93,7 @@ enum DeviceError: String {
case .lowLightBoostNotSupported: case .lowLightBoostNotSupported:
return "The currently selected camera device does not support low-light boost! Select a device where `device.supportsLowLightBoost` is true." return "The currently selected camera device does not support low-light boost! Select a device where `device.supportsLowLightBoost` is true."
case .focusNotSupported: case .focusNotSupported:
return "The currently selected camera device does not support focussing!" return "The currently selected camera device does not support focusing!"
case .microphoneUnavailable: case .microphoneUnavailable:
return "The microphone was unavailable." return "The microphone was unavailable."
case .notAvailableOnSimulator: case .notAvailableOnSimulator: