Skip to content

Commit

Permalink
feat: Flash with AE Pre-capture trigger for Android (mrousavy#2558)
Browse files Browse the repository at this point in the history
### 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 ✅
  • Loading branch information
mrousavy authored Feb 14, 2024
1 parent 406fc81 commit ba851c2
Show file tree
Hide file tree
Showing 11 changed files with 448 additions and 131 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId
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 = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0
val minFocusDistance by lazy { getMinFocusDistanceCm() }
val name by lazy {
val info = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) characteristics.get(CameraCharacteristics.INFO_VERSION) else null
Expand Down Expand Up @@ -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 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 supportsFocusRegions by lazy { (characteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF) ?: 0) > 0 }
val supportsExposureRegions by lazy { (characteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE) ?: 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 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 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?
val videoFormat = ImageFormat.YUV_420_888

Expand All @@ -117,6 +131,13 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId
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 {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (capabilities.contains(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT)) {
Expand Down Expand Up @@ -266,7 +287,7 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId
map.putBoolean("isMultiCam", isMultiCam)
map.putBoolean("supportsRawCapture", supportsRawCapture)
map.putBoolean("supportsLowLightBoost", supportsLowLightBoost)
map.putBoolean("supportsFocus", supportsTapToFocus)
map.putBoolean("supportsFocus", supportsFocusRegions)
map.putDouble("minZoom", minZoom)
map.putDouble("maxZoom", maxZoom)
map.putDouble("neutralZoom", 1.0) // Zoom is always relative to 1.0 on Android
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ class PhotoNotEnabledError :
CameraError("capture", "photo-not-enabled", "Photo capture is disabled! Pass `photo={true}` to enable photo capture.")
class CaptureAbortedError(wasImageCaptured: Boolean) :
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) :
CameraError("capture", "unknown", "An unknown error occurred while trying to capture an Image! Was Image captured: $wasImageCaptured")
class RecorderError(name: String, extra: Int) :
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@ import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CameraManager
import android.hardware.camera2.CaptureRequest
import android.hardware.camera2.TotalCaptureResult
import android.hardware.camera2.params.MeteringRectangle
import android.util.Log
import android.util.Size
import com.mrousavy.camera.core.capture.PhotoCaptureRequest
import com.mrousavy.camera.core.capture.RepeatingCaptureRequest
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.createCaptureSession
import com.mrousavy.camera.extensions.isValid
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.types.Flash
import com.mrousavy.camera.types.Orientation
Expand All @@ -40,7 +40,6 @@ import kotlinx.coroutines.sync.withLock
class PersistentCameraCaptureSession(private val cameraManager: CameraManager, private val callback: Callback) : Closeable {
companion object {
private const val TAG = "PersistentCameraCaptureSession"
private val DEFAULT_METERING_SIZE = Size(100, 100)
private const val FOCUS_RESET_TIMEOUT = 3000L
}

Expand Down Expand Up @@ -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
val outputs = outputs
val request = photoRequest.createCaptureRequest(device, deviceDetails, outputs)
return session.capture(request.build(), enableShutterSound)
val repeatingOutputs = outputs.filter { it.isRepeating }

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)
}
}
}

Expand All @@ -172,66 +197,21 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p
val repeatingRequest = repeatingRequest ?: throw CameraNotReadyError()
val device = session.device
val deviceDetails = getOrCreateCameraDeviceDetails(device)
if (!deviceDetails.supportsTapToFocus) {
if (!deviceDetails.supportsFocusRegions) {
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)
}
// 1. Run a precapture sequence for AF, AE and AWB.
val request = repeatingRequest.createCaptureRequest(device, deviceDetails, outputs)
val options = PrecaptureOptions(listOf(PrecaptureTrigger.AF, PrecaptureTrigger.AE), Flash.OFF, listOf(point))
session.precapture(request, deviceDetails, options)

// 5. Wait 3 seconds
// 2. Wait 3 seconds
focusResetJob = coroutineScope.launch {
delay(FOCUS_RESET_TIMEOUT)
if (!this.isActive) {
Expand All @@ -243,7 +223,7 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p
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.
// 3. 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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package com.mrousavy.camera.core.capture
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CaptureRequest
import android.os.Build
import android.util.Log
import com.mrousavy.camera.core.CameraDeviceDetails
import com.mrousavy.camera.core.outputs.SurfaceOutput
import com.mrousavy.camera.types.Flash
import com.mrousavy.camera.types.HardwareLevel
import com.mrousavy.camera.types.Orientation
import com.mrousavy.camera.types.QualityPrioritization
import com.mrousavy.camera.types.Torch
Expand Down Expand Up @@ -67,13 +69,70 @@ class PhotoCaptureRequest(
): CaptureRequest.Builder {
val builder = super.createCaptureRequest(template, device, deviceDetails, outputs)

// Set JPEG quality
val jpegQuality = when (qualityPrioritization) {
QualityPrioritization.SPEED -> 85
QualityPrioritization.BALANCED -> 92
QualityPrioritization.QUALITY -> 100
// Set various speed vs quality optimization flags
when (qualityPrioritization) {
QualityPrioritization.SPEED -> {
if (deviceDetails.hardwareLevel.isAtLeast(HardwareLevel.FULL)) {
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
val targetOrientation = outputOrientation.toSensorRelativeOrientation(deviceDetails)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,34 @@ import android.hardware.camera2.CaptureFailure
import android.hardware.camera2.CaptureRequest
import android.hardware.camera2.TotalCaptureResult
import android.media.MediaActionSound
import android.util.Log
import com.mrousavy.camera.core.CaptureAbortedError
import com.mrousavy.camera.core.CaptureTimedOutError
import com.mrousavy.camera.core.UnknownCaptureError
import kotlin.coroutines.resume
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 =
suspendCoroutine { continuation ->
suspendCancellableCoroutine { continuation ->
val shutterSound = if (enableShutterSound) MediaActionSound() else null
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(
captureRequest,
object : CameraCaptureSession.CaptureCallback() {
Expand Down
Loading

0 comments on commit ba851c2

Please sign in to comment.