feat: Flash with AE Pre-capture trigger for Android (#2558)
### Flash (`flash`) Adds `flash` functionality with a fully custom pre-capture AE/AF/AWB trigger sequence for Android. 🎉 ```ts camera.current.takePhoto({ flash: 'on' // or 'auto' }) ``` ### Better photos (`qualityPrioritization`) We now also run the AE/AF/AWB precapture sequence on every photo (unless `qualityPrioritization` is `speed`), meaning photos are now less blurry, properly exposed, and properly white-balanced - so in short: **photo quality is now better!**. The fast path still exists when using `qualityPrioritization: speed`, as that will skip the precapture sequence and metering actions and just grab an Image from the Camera as quickly as possible. Additionally, `qualityPrioritization` now controls these options: - [COLOR_CORRECTION_MODE](https://developer.android.com/reference/android/hardware/camera2/CaptureRequest#COLOR_CORRECTION_MODE) - [EDGE_MODE](https://developer.android.com/reference/android/hardware/camera2/CaptureRequest#EDGE_MODE) - [COLOR_CORRECTION_ABERRATION_MODE](https://developer.android.com/reference/android/hardware/camera2/CaptureRequest#COLOR_CORRECTION_ABERRATION_MODE) - [HOT_PIXEL_MODE](https://developer.android.com/reference/android/hardware/camera2/CaptureRequest#HOT_PIXEL_MODE) - [DISTORTION_CORRECTION_MODE](https://developer.android.com/reference/android/hardware/camera2/CaptureRequest#DISTORTION_CORRECTION_MODE) - [NOISE_REDUCTION_MODE](https://developer.android.com/reference/android/hardware/camera2/CaptureRequest#NOISE_REDUCTION_MODE) - [SHADING_MODE](https://developer.android.com/reference/android/hardware/camera2/CaptureRequest#SHADING_MODE) - [TONEMAP_MODE](https://developer.android.com/reference/android/hardware/camera2/CaptureRequest#TONEMAP_MODE) ..by setting them to `_FAST` or `_HIGH_QUALITY`, which was previously left untouched. This now means: - `takePhoto({ qualityPrioritization: 'speed' })` got FASTER 🚀 - `takePhoto({ qualityPrioritization: 'quality' })` got BETTER QUALITY 📸 - `takePhoto({ qualityPrioritization: 'balanced' })` is left unchanged ✅
This commit is contained in:
parent
61b2f7dd4a
commit
37398cc909
@ -50,7 +50,7 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId
|
|||||||
val sensorSize by lazy { characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)!! }
|
val sensorSize by lazy { characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)!! }
|
||||||
val activeSize
|
val activeSize
|
||||||
get() = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)!!
|
get() = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)!!
|
||||||
val sensorOrientation by lazy { characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0 }
|
val sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0
|
||||||
val minFocusDistance by lazy { getMinFocusDistanceCm() }
|
val minFocusDistance by lazy { getMinFocusDistanceCm() }
|
||||||
val name by lazy {
|
val name by lazy {
|
||||||
val info = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) characteristics.get(CameraCharacteristics.INFO_VERSION) else null
|
val info = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) characteristics.get(CameraCharacteristics.INFO_VERSION) else null
|
||||||
@ -97,14 +97,28 @@ 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 supportsFocusRegions 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 supportsExposureRegions 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 supportsWhiteBalanceRegions 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 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 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() }
|
val awbModes by lazy { characteristics.get(CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES)?.toList() ?: emptyList() }
|
||||||
|
|
||||||
|
val availableAberrationModes by lazy {
|
||||||
|
characteristics.get(CameraCharacteristics.COLOR_CORRECTION_AVAILABLE_ABERRATION_MODES)
|
||||||
|
?: intArrayOf()
|
||||||
|
}
|
||||||
|
val availableHotPixelModes by lazy { characteristics.get(CameraCharacteristics.HOT_PIXEL_AVAILABLE_HOT_PIXEL_MODES) ?: intArrayOf() }
|
||||||
|
val availableEdgeModes by lazy { characteristics.get(CameraCharacteristics.EDGE_AVAILABLE_EDGE_MODES) ?: intArrayOf() }
|
||||||
|
val availableDistortionCorrectionModes by lazy { getAvailableDistortionCorrectionModesOrEmptyArray() }
|
||||||
|
val availableShadingModes by lazy { characteristics.get(CameraCharacteristics.SHADING_AVAILABLE_MODES) ?: intArrayOf() }
|
||||||
|
val availableToneMapModes by lazy { characteristics.get(CameraCharacteristics.TONEMAP_AVAILABLE_TONE_MAP_MODES) ?: intArrayOf() }
|
||||||
|
val availableNoiseReductionModes by lazy {
|
||||||
|
characteristics.get(CameraCharacteristics.NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES)
|
||||||
|
?: intArrayOf()
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
|
||||||
@ -117,6 +131,13 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId
|
|||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getAvailableDistortionCorrectionModesOrEmptyArray(): IntArray =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
characteristics.get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES) ?: intArrayOf()
|
||||||
|
} else {
|
||||||
|
intArrayOf()
|
||||||
|
}
|
||||||
|
|
||||||
private fun getHasVideoHdr(): Boolean {
|
private fun getHasVideoHdr(): Boolean {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
if (capabilities.contains(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT)) {
|
if (capabilities.contains(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT)) {
|
||||||
@ -266,7 +287,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", supportsTapToFocus)
|
map.putBoolean("supportsFocus", supportsFocusRegions)
|
||||||
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
|
||||||
|
@ -104,6 +104,7 @@ class PhotoNotEnabledError :
|
|||||||
CameraError("capture", "photo-not-enabled", "Photo capture is disabled! Pass `photo={true}` to enable photo capture.")
|
CameraError("capture", "photo-not-enabled", "Photo capture is disabled! Pass `photo={true}` to enable photo capture.")
|
||||||
class CaptureAbortedError(wasImageCaptured: Boolean) :
|
class CaptureAbortedError(wasImageCaptured: Boolean) :
|
||||||
CameraError("capture", "aborted", "The image capture was aborted! Was Image captured: $wasImageCaptured")
|
CameraError("capture", "aborted", "The image capture was aborted! Was Image captured: $wasImageCaptured")
|
||||||
|
class CaptureTimedOutError : CameraError("capture", "timed-out", "The image capture was aborted because it timed out.")
|
||||||
class UnknownCaptureError(wasImageCaptured: Boolean) :
|
class UnknownCaptureError(wasImageCaptured: Boolean) :
|
||||||
CameraError("capture", "unknown", "An unknown error occurred while trying to capture an Image! Was Image captured: $wasImageCaptured")
|
CameraError("capture", "unknown", "An unknown error occurred while trying to capture an Image! Was Image captured: $wasImageCaptured")
|
||||||
class RecorderError(name: String, extra: Int) :
|
class RecorderError(name: String, extra: Int) :
|
||||||
|
@ -7,17 +7,17 @@ import android.hardware.camera2.CameraDevice
|
|||||||
import android.hardware.camera2.CameraManager
|
import android.hardware.camera2.CameraManager
|
||||||
import android.hardware.camera2.CaptureRequest
|
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
|
||||||
|
import com.mrousavy.camera.extensions.PrecaptureOptions
|
||||||
|
import com.mrousavy.camera.extensions.PrecaptureTrigger
|
||||||
import com.mrousavy.camera.extensions.capture
|
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.precapture
|
||||||
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
|
||||||
@ -40,7 +40,6 @@ 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
|
private const val FOCUS_RESET_TIMEOUT = 3000L
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,8 +159,34 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p
|
|||||||
|
|
||||||
// Submit a single high-res capture to photo output as well as all preview outputs
|
// Submit a single high-res capture to photo output as well as all preview outputs
|
||||||
val outputs = outputs
|
val outputs = outputs
|
||||||
val request = photoRequest.createCaptureRequest(device, deviceDetails, outputs)
|
val repeatingOutputs = outputs.filter { it.isRepeating }
|
||||||
return session.capture(request.build(), enableShutterSound)
|
|
||||||
|
if (qualityPrioritization == QualityPrioritization.SPEED && flash == Flash.OFF) {
|
||||||
|
// 0. We want to take a picture as fast as possible, so skip any precapture sequence and just capture one Frame.
|
||||||
|
Log.i(TAG, "Using fast capture path without pre-capture sequence...")
|
||||||
|
val singleRequest = photoRequest.createCaptureRequest(device, deviceDetails, outputs)
|
||||||
|
return session.capture(singleRequest.build(), enableShutterSound)
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "Locking AF/AE/AWB...")
|
||||||
|
|
||||||
|
// 1. Run precapture sequence
|
||||||
|
val precaptureRequest = repeatingRequest.createCaptureRequest(device, deviceDetails, repeatingOutputs)
|
||||||
|
val options = PrecaptureOptions(listOf(PrecaptureTrigger.AF, PrecaptureTrigger.AE, PrecaptureTrigger.AWB), flash, emptyList())
|
||||||
|
val result = session.precapture(precaptureRequest, deviceDetails, options)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 2. Once precapture AF/AE/AWB successfully locked, capture the actual photo
|
||||||
|
val singleRequest = photoRequest.createCaptureRequest(device, deviceDetails, outputs)
|
||||||
|
if (result.needsFlash) {
|
||||||
|
singleRequest.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_SINGLE)
|
||||||
|
}
|
||||||
|
return session.capture(singleRequest.build(), enableShutterSound)
|
||||||
|
} finally {
|
||||||
|
// 5. After taking a photo we set the repeating request back to idle to remove the AE/AF/AWB locks again
|
||||||
|
val idleRequest = repeatingRequest.createCaptureRequest(device, deviceDetails, repeatingOutputs)
|
||||||
|
session.setRepeatingRequest(idleRequest.build(), null, null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,66 +197,21 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p
|
|||||||
val repeatingRequest = repeatingRequest ?: throw CameraNotReadyError()
|
val repeatingRequest = repeatingRequest ?: throw CameraNotReadyError()
|
||||||
val device = session.device
|
val device = session.device
|
||||||
val deviceDetails = getOrCreateCameraDeviceDetails(device)
|
val deviceDetails = getOrCreateCameraDeviceDetails(device)
|
||||||
if (!deviceDetails.supportsTapToFocus) {
|
if (!deviceDetails.supportsFocusRegions) {
|
||||||
throw FocusNotSupportedError()
|
throw FocusNotSupportedError()
|
||||||
}
|
}
|
||||||
val outputs = outputs.filter { it.isRepeating }
|
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
|
// 0. Cancel the 3 second focus reset task
|
||||||
focusResetJob?.cancelAndJoin()
|
focusResetJob?.cancelAndJoin()
|
||||||
focusResetJob = null
|
focusResetJob = null
|
||||||
|
|
||||||
// 1. Cancel any ongoing AF/AE/AWB request
|
// 1. Run a precapture sequence for AF, AE and AWB.
|
||||||
repeatingRequest.createCaptureRequest(device, deviceDetails, outputs).also { request ->
|
val request = repeatingRequest.createCaptureRequest(device, deviceDetails, outputs)
|
||||||
if (deviceDetails.supportsTapToFocus) {
|
val options = PrecaptureOptions(listOf(PrecaptureTrigger.AF, PrecaptureTrigger.AE), Flash.OFF, listOf(point))
|
||||||
request.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
|
session.precapture(request, deviceDetails, options)
|
||||||
}
|
|
||||||
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
|
// 2. Wait 3 seconds
|
||||||
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 {
|
focusResetJob = coroutineScope.launch {
|
||||||
delay(FOCUS_RESET_TIMEOUT)
|
delay(FOCUS_RESET_TIMEOUT)
|
||||||
if (!this.isActive) {
|
if (!this.isActive) {
|
||||||
@ -243,7 +223,7 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p
|
|||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
Log.i(TAG, "Resetting focus to auto-focus...")
|
Log.i(TAG, "Resetting focus to auto-focus...")
|
||||||
// 6. Reset AF/AE/AWB to continuous auto-focus again, which is the default here.
|
// 3. Reset AF/AE/AWB to continuous auto-focus again, which is the default here.
|
||||||
repeatingRequest.createCaptureRequest(device, deviceDetails, outputs).also { request ->
|
repeatingRequest.createCaptureRequest(device, deviceDetails, outputs).also { request ->
|
||||||
session.setRepeatingRequest(request.build(), null, null)
|
session.setRepeatingRequest(request.build(), null, null)
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,12 @@ package com.mrousavy.camera.core.capture
|
|||||||
import android.hardware.camera2.CameraCharacteristics
|
import android.hardware.camera2.CameraCharacteristics
|
||||||
import android.hardware.camera2.CameraDevice
|
import android.hardware.camera2.CameraDevice
|
||||||
import android.hardware.camera2.CaptureRequest
|
import android.hardware.camera2.CaptureRequest
|
||||||
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.mrousavy.camera.core.CameraDeviceDetails
|
import com.mrousavy.camera.core.CameraDeviceDetails
|
||||||
import com.mrousavy.camera.core.outputs.SurfaceOutput
|
import com.mrousavy.camera.core.outputs.SurfaceOutput
|
||||||
import com.mrousavy.camera.types.Flash
|
import com.mrousavy.camera.types.Flash
|
||||||
|
import com.mrousavy.camera.types.HardwareLevel
|
||||||
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 com.mrousavy.camera.types.Torch
|
import com.mrousavy.camera.types.Torch
|
||||||
@ -67,13 +69,70 @@ class PhotoCaptureRequest(
|
|||||||
): CaptureRequest.Builder {
|
): CaptureRequest.Builder {
|
||||||
val builder = super.createCaptureRequest(template, device, deviceDetails, outputs)
|
val builder = super.createCaptureRequest(template, device, deviceDetails, outputs)
|
||||||
|
|
||||||
// Set JPEG quality
|
// Set various speed vs quality optimization flags
|
||||||
val jpegQuality = when (qualityPrioritization) {
|
when (qualityPrioritization) {
|
||||||
QualityPrioritization.SPEED -> 85
|
QualityPrioritization.SPEED -> {
|
||||||
QualityPrioritization.BALANCED -> 92
|
if (deviceDetails.hardwareLevel.isAtLeast(HardwareLevel.FULL)) {
|
||||||
QualityPrioritization.QUALITY -> 100
|
builder.set(CaptureRequest.COLOR_CORRECTION_MODE, CaptureRequest.COLOR_CORRECTION_MODE_FAST)
|
||||||
|
if (deviceDetails.availableEdgeModes.contains(CaptureRequest.EDGE_MODE_FAST)) {
|
||||||
|
builder.set(CaptureRequest.EDGE_MODE, CaptureRequest.EDGE_MODE_FAST)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (deviceDetails.availableAberrationModes.contains(CaptureRequest.COLOR_CORRECTION_ABERRATION_MODE_FAST)) {
|
||||||
|
builder.set(CaptureRequest.COLOR_CORRECTION_ABERRATION_MODE, CaptureRequest.COLOR_CORRECTION_ABERRATION_MODE_FAST)
|
||||||
|
}
|
||||||
|
if (deviceDetails.availableHotPixelModes.contains(CaptureRequest.HOT_PIXEL_MODE_FAST)) {
|
||||||
|
builder.set(CaptureRequest.HOT_PIXEL_MODE, CaptureRequest.HOT_PIXEL_MODE_FAST)
|
||||||
|
}
|
||||||
|
if (deviceDetails.availableDistortionCorrectionModes.contains(CaptureRequest.DISTORTION_CORRECTION_MODE_FAST) &&
|
||||||
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
|
||||||
|
) {
|
||||||
|
builder.set(CaptureRequest.DISTORTION_CORRECTION_MODE, CaptureRequest.DISTORTION_CORRECTION_MODE_FAST)
|
||||||
|
}
|
||||||
|
if (deviceDetails.availableNoiseReductionModes.contains(CaptureRequest.NOISE_REDUCTION_MODE_FAST)) {
|
||||||
|
builder.set(CaptureRequest.NOISE_REDUCTION_MODE, CaptureRequest.NOISE_REDUCTION_MODE_FAST)
|
||||||
|
}
|
||||||
|
if (deviceDetails.availableShadingModes.contains(CaptureRequest.SHADING_MODE_FAST)) {
|
||||||
|
builder.set(CaptureRequest.SHADING_MODE, CaptureRequest.SHADING_MODE_FAST)
|
||||||
|
}
|
||||||
|
if (deviceDetails.availableToneMapModes.contains(CaptureRequest.TONEMAP_MODE_FAST)) {
|
||||||
|
builder.set(CaptureRequest.TONEMAP_MODE, CaptureRequest.TONEMAP_MODE_FAST)
|
||||||
|
}
|
||||||
|
builder.set(CaptureRequest.JPEG_QUALITY, 85)
|
||||||
|
}
|
||||||
|
QualityPrioritization.BALANCED -> {
|
||||||
|
builder.set(CaptureRequest.JPEG_QUALITY, 92)
|
||||||
|
}
|
||||||
|
QualityPrioritization.QUALITY -> {
|
||||||
|
if (deviceDetails.hardwareLevel.isAtLeast(HardwareLevel.FULL)) {
|
||||||
|
builder.set(CaptureRequest.COLOR_CORRECTION_MODE, CaptureRequest.COLOR_CORRECTION_MODE_HIGH_QUALITY)
|
||||||
|
if (deviceDetails.availableEdgeModes.contains(CaptureRequest.EDGE_MODE_HIGH_QUALITY)) {
|
||||||
|
builder.set(CaptureRequest.EDGE_MODE, CaptureRequest.EDGE_MODE_HIGH_QUALITY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (deviceDetails.availableAberrationModes.contains(CaptureRequest.COLOR_CORRECTION_ABERRATION_MODE_HIGH_QUALITY)) {
|
||||||
|
builder.set(CaptureRequest.COLOR_CORRECTION_ABERRATION_MODE, CaptureRequest.COLOR_CORRECTION_ABERRATION_MODE_HIGH_QUALITY)
|
||||||
|
}
|
||||||
|
if (deviceDetails.availableHotPixelModes.contains(CaptureRequest.HOT_PIXEL_MODE_HIGH_QUALITY)) {
|
||||||
|
builder.set(CaptureRequest.HOT_PIXEL_MODE, CaptureRequest.HOT_PIXEL_MODE_HIGH_QUALITY)
|
||||||
|
}
|
||||||
|
if (deviceDetails.availableDistortionCorrectionModes.contains(CaptureRequest.DISTORTION_CORRECTION_MODE_HIGH_QUALITY) &&
|
||||||
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
|
||||||
|
) {
|
||||||
|
builder.set(CaptureRequest.DISTORTION_CORRECTION_MODE, CaptureRequest.DISTORTION_CORRECTION_MODE_HIGH_QUALITY)
|
||||||
|
}
|
||||||
|
if (deviceDetails.availableNoiseReductionModes.contains(CaptureRequest.NOISE_REDUCTION_MODE_HIGH_QUALITY)) {
|
||||||
|
builder.set(CaptureRequest.NOISE_REDUCTION_MODE, CaptureRequest.NOISE_REDUCTION_MODE_HIGH_QUALITY)
|
||||||
|
}
|
||||||
|
if (deviceDetails.availableShadingModes.contains(CaptureRequest.SHADING_MODE_HIGH_QUALITY)) {
|
||||||
|
builder.set(CaptureRequest.SHADING_MODE, CaptureRequest.SHADING_MODE_HIGH_QUALITY)
|
||||||
|
}
|
||||||
|
if (deviceDetails.availableToneMapModes.contains(CaptureRequest.TONEMAP_MODE_HIGH_QUALITY)) {
|
||||||
|
builder.set(CaptureRequest.TONEMAP_MODE, CaptureRequest.TONEMAP_MODE_HIGH_QUALITY)
|
||||||
|
}
|
||||||
|
builder.set(CaptureRequest.JPEG_QUALITY, 100)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
builder.set(CaptureRequest.JPEG_QUALITY, jpegQuality.toByte())
|
|
||||||
|
|
||||||
// Set JPEG Orientation
|
// Set JPEG Orientation
|
||||||
val targetOrientation = outputOrientation.toSensorRelativeOrientation(deviceDetails)
|
val targetOrientation = outputOrientation.toSensorRelativeOrientation(deviceDetails)
|
||||||
|
@ -5,17 +5,34 @@ 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 android.util.Log
|
||||||
import com.mrousavy.camera.core.CaptureAbortedError
|
import com.mrousavy.camera.core.CaptureAbortedError
|
||||||
|
import com.mrousavy.camera.core.CaptureTimedOutError
|
||||||
import com.mrousavy.camera.core.UnknownCaptureError
|
import com.mrousavy.camera.core.UnknownCaptureError
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.resumeWithException
|
import kotlin.coroutines.resumeWithException
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
|
||||||
|
private const val TAG = "CameraCaptureSession"
|
||||||
|
|
||||||
suspend fun CameraCaptureSession.capture(captureRequest: CaptureRequest, enableShutterSound: Boolean): TotalCaptureResult =
|
suspend fun CameraCaptureSession.capture(captureRequest: CaptureRequest, enableShutterSound: Boolean): TotalCaptureResult =
|
||||||
suspendCoroutine { continuation ->
|
suspendCancellableCoroutine { continuation ->
|
||||||
val shutterSound = if (enableShutterSound) MediaActionSound() else null
|
val shutterSound = if (enableShutterSound) MediaActionSound() else null
|
||||||
shutterSound?.load(MediaActionSound.SHUTTER_CLICK)
|
shutterSound?.load(MediaActionSound.SHUTTER_CLICK)
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
|
delay(5000) // after 5s, cancel capture
|
||||||
|
if (continuation.isActive) {
|
||||||
|
Log.e(TAG, "Capture timed out after 5 seconds!")
|
||||||
|
continuation.resumeWithException(CaptureTimedOutError())
|
||||||
|
tryAbortCaptures()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.capture(
|
this.capture(
|
||||||
captureRequest,
|
captureRequest,
|
||||||
object : CameraCaptureSession.CaptureCallback() {
|
object : CameraCaptureSession.CaptureCallback() {
|
||||||
|
@ -0,0 +1,99 @@
|
|||||||
|
package com.mrousavy.camera.extensions
|
||||||
|
|
||||||
|
import android.graphics.Point
|
||||||
|
import android.hardware.camera2.CameraCaptureSession
|
||||||
|
import android.hardware.camera2.CaptureRequest
|
||||||
|
import android.hardware.camera2.CaptureResult
|
||||||
|
import android.hardware.camera2.params.MeteringRectangle
|
||||||
|
import android.util.Log
|
||||||
|
import android.util.Size
|
||||||
|
import com.mrousavy.camera.core.CameraDeviceDetails
|
||||||
|
import com.mrousavy.camera.types.Flash
|
||||||
|
import com.mrousavy.camera.types.HardwareLevel
|
||||||
|
|
||||||
|
data class PrecaptureOptions(val modes: List<PrecaptureTrigger>, val flash: Flash = Flash.OFF, val pointsOfInterest: List<Point>)
|
||||||
|
|
||||||
|
data class PrecaptureResult(val needsFlash: Boolean)
|
||||||
|
|
||||||
|
private const val TAG = "Precapture"
|
||||||
|
private val DEFAULT_METERING_SIZE = Size(100, 100)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a precapture sequence to trigger an AF, AE or AWB scan and lock to the optimal values.
|
||||||
|
* After this function completes, you can capture high quality photos as AF/AE/AWB are in focused state.
|
||||||
|
*
|
||||||
|
* To reset to auto-focus again, create a new `RepeatingRequest` with a fresh set of CONTROL_MODEs set.
|
||||||
|
*/
|
||||||
|
suspend fun CameraCaptureSession.precapture(
|
||||||
|
request: CaptureRequest.Builder,
|
||||||
|
deviceDetails: CameraDeviceDetails,
|
||||||
|
options: PrecaptureOptions
|
||||||
|
): PrecaptureResult {
|
||||||
|
Log.i(TAG, "Running precapture sequence... ($options)")
|
||||||
|
request.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO)
|
||||||
|
|
||||||
|
var enableFlash = options.flash == Flash.ON
|
||||||
|
|
||||||
|
// 1. Cancel any ongoing precapture sequences
|
||||||
|
request.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
|
||||||
|
request.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL)
|
||||||
|
if (options.flash == Flash.AUTO) {
|
||||||
|
// we want Auto-Flash, so check the current lighting conditions if we need it.
|
||||||
|
val result = this.capture(request.build(), false)
|
||||||
|
val aeState = result.get(CaptureResult.CONTROL_AE_STATE)
|
||||||
|
if (aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED) {
|
||||||
|
Log.i(TAG, "Auto-Flash: Flash is required for photo capture, enabling flash...")
|
||||||
|
enableFlash = true
|
||||||
|
} else {
|
||||||
|
Log.i(TAG, "Auto-Flash: Flash is not required for photo capture.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// we either want Flash ON or OFF, so we don't care about lighting conditions - do a fast capture.
|
||||||
|
this.capture(request.build(), null, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
val meteringWeight = MeteringRectangle.METERING_WEIGHT_MAX - 1
|
||||||
|
val meteringRectangles = options.pointsOfInterest.map { point ->
|
||||||
|
MeteringRectangle(point, DEFAULT_METERING_SIZE, meteringWeight)
|
||||||
|
}.toTypedArray()
|
||||||
|
|
||||||
|
// 2. Submit a precapture start sequence
|
||||||
|
if (enableFlash && deviceDetails.hasFlash) {
|
||||||
|
request.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH)
|
||||||
|
}
|
||||||
|
if (options.modes.contains(PrecaptureTrigger.AF)) {
|
||||||
|
// AF Precapture
|
||||||
|
if (deviceDetails.afModes.contains(CaptureRequest.CONTROL_AF_MODE_AUTO)) {
|
||||||
|
request.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO)
|
||||||
|
}
|
||||||
|
if (meteringRectangles.isNotEmpty() && deviceDetails.supportsFocusRegions) {
|
||||||
|
request.set(CaptureRequest.CONTROL_AF_REGIONS, meteringRectangles)
|
||||||
|
}
|
||||||
|
request.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START)
|
||||||
|
}
|
||||||
|
if (options.modes.contains(PrecaptureTrigger.AE) && deviceDetails.hardwareLevel.isAtLeast(HardwareLevel.LIMITED)) {
|
||||||
|
// AE Precapture
|
||||||
|
if (meteringRectangles.isNotEmpty() && deviceDetails.supportsExposureRegions) {
|
||||||
|
request.set(CaptureRequest.CONTROL_AE_REGIONS, meteringRectangles)
|
||||||
|
}
|
||||||
|
request.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START)
|
||||||
|
}
|
||||||
|
if (options.modes.contains(PrecaptureTrigger.AWB)) {
|
||||||
|
// AWB Precapture
|
||||||
|
if (meteringRectangles.isNotEmpty() && deviceDetails.supportsWhiteBalanceRegions) {
|
||||||
|
request.set(CaptureRequest.CONTROL_AWB_REGIONS, meteringRectangles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.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)
|
||||||
|
val result = this.setRepeatingRequestAndWaitForPrecapture(request.build(), *options.modes.toTypedArray())
|
||||||
|
|
||||||
|
Log.i(TAG, "AF/AE/AWB successfully locked!")
|
||||||
|
// TODO: Set to idle again?
|
||||||
|
|
||||||
|
val needsFlash = result.exposureState == ExposureState.FlashRequired
|
||||||
|
return PrecaptureResult(needsFlash)
|
||||||
|
}
|
@ -1,47 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
@ -0,0 +1,178 @@
|
|||||||
|
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 com.mrousavy.camera.core.CaptureTimedOutError
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
|
||||||
|
private const val TAG = "CameraCaptureSession"
|
||||||
|
|
||||||
|
enum class PrecaptureTrigger {
|
||||||
|
AE,
|
||||||
|
AF,
|
||||||
|
AWB
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class FocusState {
|
||||||
|
Inactive,
|
||||||
|
Scanning,
|
||||||
|
Focused,
|
||||||
|
Unfocused,
|
||||||
|
PassiveScanning,
|
||||||
|
PassiveFocused,
|
||||||
|
PassiveUnfocused;
|
||||||
|
|
||||||
|
val isCompleted: Boolean
|
||||||
|
get() = this == Focused || this == Unfocused
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromAFState(afState: Int): FocusState =
|
||||||
|
when (afState) {
|
||||||
|
CaptureResult.CONTROL_AF_STATE_INACTIVE -> Inactive
|
||||||
|
CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN -> Scanning
|
||||||
|
CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED -> Focused
|
||||||
|
CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED -> Unfocused
|
||||||
|
CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN -> PassiveScanning
|
||||||
|
CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED -> PassiveFocused
|
||||||
|
CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED -> PassiveUnfocused
|
||||||
|
else -> throw Error("Invalid CONTROL_AF_STATE! $afState")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
enum class ExposureState {
|
||||||
|
Locked,
|
||||||
|
Inactive,
|
||||||
|
Precapture,
|
||||||
|
Searching,
|
||||||
|
Converged,
|
||||||
|
FlashRequired;
|
||||||
|
|
||||||
|
val isCompleted: Boolean
|
||||||
|
get() = this == Converged || this == FlashRequired
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromAEState(aeState: Int): ExposureState =
|
||||||
|
when (aeState) {
|
||||||
|
CaptureResult.CONTROL_AE_STATE_INACTIVE -> Inactive
|
||||||
|
CaptureResult.CONTROL_AE_STATE_SEARCHING -> Searching
|
||||||
|
CaptureResult.CONTROL_AE_STATE_PRECAPTURE -> Precapture
|
||||||
|
CaptureResult.CONTROL_AE_STATE_CONVERGED -> Converged
|
||||||
|
CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED -> FlashRequired
|
||||||
|
CaptureResult.CONTROL_AE_STATE_LOCKED -> Locked
|
||||||
|
else -> throw Error("Invalid CONTROL_AE_STATE! $aeState")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class WhiteBalanceState {
|
||||||
|
Inactive,
|
||||||
|
Locked,
|
||||||
|
Searching,
|
||||||
|
Converged;
|
||||||
|
|
||||||
|
val isCompleted: Boolean
|
||||||
|
get() = this == Converged
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromAWBState(awbState: Int): WhiteBalanceState =
|
||||||
|
when (awbState) {
|
||||||
|
CaptureResult.CONTROL_AWB_STATE_INACTIVE -> Inactive
|
||||||
|
CaptureResult.CONTROL_AWB_STATE_SEARCHING -> Searching
|
||||||
|
CaptureResult.CONTROL_AWB_STATE_CONVERGED -> Converged
|
||||||
|
CaptureResult.CONTROL_AWB_STATE_LOCKED -> Locked
|
||||||
|
else -> throw Error("Invalid CONTROL_AWB_STATE! $awbState")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ResultState(val focusState: FocusState, val exposureState: ExposureState, val whiteBalanceState: WhiteBalanceState)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a new repeating request for the [CameraCaptureSession] that contains a precapture trigger, and wait until the given precaptures have locked.
|
||||||
|
*/
|
||||||
|
suspend fun CameraCaptureSession.setRepeatingRequestAndWaitForPrecapture(
|
||||||
|
request: CaptureRequest,
|
||||||
|
vararg precaptureTriggers: PrecaptureTrigger
|
||||||
|
): ResultState =
|
||||||
|
suspendCancellableCoroutine { continuation ->
|
||||||
|
// Map<PrecaptureTrigger, Boolean> of all completed precaptures
|
||||||
|
val completed = precaptureTriggers.associateWith { false }.toMutableMap()
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
|
delay(5000) // after 5s, cancel capture
|
||||||
|
if (continuation.isActive) {
|
||||||
|
Log.e(TAG, "Precapture timed out after 5 seconds!")
|
||||||
|
continuation.resumeWithException(CaptureTimedOutError())
|
||||||
|
try {
|
||||||
|
setRepeatingRequest(request, null, null)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
// session might have already been closed
|
||||||
|
Log.e(TAG, "Error resetting session repeating request..", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setRepeatingRequest(
|
||||||
|
request,
|
||||||
|
object : CameraCaptureSession.CaptureCallback() {
|
||||||
|
override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) {
|
||||||
|
super.onCaptureCompleted(session, request, result)
|
||||||
|
|
||||||
|
if (continuation.isActive) {
|
||||||
|
// AF Precapture
|
||||||
|
val afState = FocusState.fromAFState(result.get(CaptureResult.CONTROL_AF_STATE) ?: CaptureResult.CONTROL_AF_STATE_INACTIVE)
|
||||||
|
val aeState = ExposureState.fromAEState(result.get(CaptureResult.CONTROL_AE_STATE) ?: CaptureResult.CONTROL_AE_STATE_INACTIVE)
|
||||||
|
val awbState = WhiteBalanceState.fromAWBState(
|
||||||
|
result.get(CaptureResult.CONTROL_AWB_STATE) ?: CaptureResult.CONTROL_AWB_STATE_INACTIVE
|
||||||
|
)
|
||||||
|
|
||||||
|
if (precaptureTriggers.contains(PrecaptureTrigger.AF)) {
|
||||||
|
Log.i(TAG, "AF State: $afState (isCompleted: ${afState.isCompleted})")
|
||||||
|
completed[PrecaptureTrigger.AF] = afState.isCompleted
|
||||||
|
}
|
||||||
|
// AE Precapture
|
||||||
|
if (precaptureTriggers.contains(PrecaptureTrigger.AE)) {
|
||||||
|
Log.i(TAG, "AE State: $aeState (isCompleted: ${aeState.isCompleted})")
|
||||||
|
completed[PrecaptureTrigger.AE] = aeState.isCompleted
|
||||||
|
}
|
||||||
|
// AWB Precapture
|
||||||
|
if (precaptureTriggers.contains(PrecaptureTrigger.AWB)) {
|
||||||
|
Log.i(TAG, "AWB State: $awbState (isCompleted: ${awbState.isCompleted})")
|
||||||
|
completed[PrecaptureTrigger.AWB] = awbState.isCompleted
|
||||||
|
}
|
||||||
|
|
||||||
|
if (completed.values.all { it == true }) {
|
||||||
|
// All precaptures did complete!
|
||||||
|
continuation.resume(ResultState(afState, aeState, awbState))
|
||||||
|
session.setRepeatingRequest(request, null, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun onCaptureFailed(session: CameraCaptureSession, request: CaptureRequest, failure: CaptureFailure) {
|
||||||
|
super.onCaptureFailed(session, request, failure)
|
||||||
|
|
||||||
|
if (continuation.isActive) {
|
||||||
|
// Capture failed or session closed.
|
||||||
|
continuation.resumeWithException(CaptureAbortedError(failure.wasImageCaptured()))
|
||||||
|
try {
|
||||||
|
session.setRepeatingRequest(request, null, null)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.e(TAG, "Failed to continue repeating request!", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
@ -9,6 +9,19 @@ enum class HardwareLevel(override val unionValue: String) : JSUnionValue {
|
|||||||
FULL("full"),
|
FULL("full"),
|
||||||
LEVEL_3("full");
|
LEVEL_3("full");
|
||||||
|
|
||||||
|
private val rank: Int
|
||||||
|
get() {
|
||||||
|
return when (this) {
|
||||||
|
LEGACY -> 0
|
||||||
|
LIMITED -> 1
|
||||||
|
EXTERNAL -> 1
|
||||||
|
FULL -> 2
|
||||||
|
LEVEL_3 -> 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isAtLeast(level: HardwareLevel): Boolean = this.rank >= level.rank
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromCameraCharacteristics(cameraCharacteristics: CameraCharacteristics): HardwareLevel =
|
fun fromCameraCharacteristics(cameraCharacteristics: CameraCharacteristics): HardwareLevel =
|
||||||
when (cameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)) {
|
when (cameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)) {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useMemo, useRef } from 'react'
|
import React, { useCallback, useRef } from 'react'
|
||||||
import { StyleSheet, View, ViewProps } from 'react-native'
|
import { StyleSheet, View, ViewProps } from 'react-native'
|
||||||
import {
|
import {
|
||||||
PanGestureHandler,
|
PanGestureHandler,
|
||||||
@ -19,7 +19,7 @@ import Reanimated, {
|
|||||||
useSharedValue,
|
useSharedValue,
|
||||||
withRepeat,
|
withRepeat,
|
||||||
} from 'react-native-reanimated'
|
} from 'react-native-reanimated'
|
||||||
import type { Camera, PhotoFile, TakePhotoOptions, VideoFile } from 'react-native-vision-camera'
|
import type { Camera, PhotoFile, VideoFile } from 'react-native-vision-camera'
|
||||||
import { CAPTURE_BUTTON_SIZE, SCREEN_HEIGHT, SCREEN_WIDTH } from './../Constants'
|
import { CAPTURE_BUTTON_SIZE, SCREEN_HEIGHT, SCREEN_WIDTH } from './../Constants'
|
||||||
|
|
||||||
const PAN_GESTURE_HANDLER_FAIL_X = [-SCREEN_WIDTH, SCREEN_WIDTH]
|
const PAN_GESTURE_HANDLER_FAIL_X = [-SCREEN_WIDTH, SCREEN_WIDTH]
|
||||||
@ -58,15 +58,6 @@ const _CaptureButton: React.FC<Props> = ({
|
|||||||
const pressDownDate = useRef<Date | undefined>(undefined)
|
const pressDownDate = useRef<Date | undefined>(undefined)
|
||||||
const isRecording = useRef(false)
|
const isRecording = useRef(false)
|
||||||
const recordingProgress = useSharedValue(0)
|
const recordingProgress = useSharedValue(0)
|
||||||
const takePhotoOptions = useMemo<TakePhotoOptions>(
|
|
||||||
() => ({
|
|
||||||
qualityPrioritization: 'speed',
|
|
||||||
flash: flash,
|
|
||||||
quality: 90,
|
|
||||||
enableShutterSound: false,
|
|
||||||
}),
|
|
||||||
[flash],
|
|
||||||
)
|
|
||||||
const isPressingButton = useSharedValue(false)
|
const isPressingButton = useSharedValue(false)
|
||||||
|
|
||||||
//#region Camera Capture
|
//#region Camera Capture
|
||||||
@ -75,12 +66,16 @@ const _CaptureButton: React.FC<Props> = ({
|
|||||||
if (camera.current == null) throw new Error('Camera ref is null!')
|
if (camera.current == null) throw new Error('Camera ref is null!')
|
||||||
|
|
||||||
console.log('Taking photo...')
|
console.log('Taking photo...')
|
||||||
const photo = await camera.current.takePhoto(takePhotoOptions)
|
const photo = await camera.current.takePhoto({
|
||||||
|
qualityPrioritization: 'quality',
|
||||||
|
flash: flash,
|
||||||
|
enableShutterSound: false,
|
||||||
|
})
|
||||||
onMediaCaptured(photo, 'photo')
|
onMediaCaptured(photo, 'photo')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to take photo!', e)
|
console.error('Failed to take photo!', e)
|
||||||
}
|
}
|
||||||
}, [camera, onMediaCaptured, takePhotoOptions])
|
}, [camera, flash, onMediaCaptured])
|
||||||
|
|
||||||
const onStoppedRecording = useCallback(() => {
|
const onStoppedRecording = useCallback(() => {
|
||||||
isRecording.current = false
|
isRecording.current = false
|
||||||
|
@ -43,6 +43,7 @@ export type CaptureError =
|
|||||||
| 'capture/photo-not-enabled'
|
| 'capture/photo-not-enabled'
|
||||||
| 'capture/frame-invalid'
|
| 'capture/frame-invalid'
|
||||||
| 'capture/aborted'
|
| 'capture/aborted'
|
||||||
|
| 'capture/timed-out'
|
||||||
| 'capture/unknown'
|
| 'capture/unknown'
|
||||||
export type SystemError =
|
export type SystemError =
|
||||||
| 'system/camera-module-not-found'
|
| 'system/camera-module-not-found'
|
||||||
|
Loading…
Reference in New Issue
Block a user