feat: Create persistent CaptureSession to avoid any blackscreen issues or errors (#2494)

* feat: Create custom `CaptureSession` wrapper

* Create `PersistentCameraCaptureSession`

* Update VideoStabilizationMode.kt

* Create RepeatingRequest.kt

* Update CaptureSession.kt

* Delete CaptureSession.kt

* Update PersistentCameraCaptureSession.kt

* Update PersistentCameraCaptureSession.kt

* fix: Add `isRepeating`

* Update CameraSession.kt

* Make `SurfaceOutput` not `Closable` anymore

* Update PersistentCameraCaptureSession.kt

* Stub out the rest

* Format

* Set `isRunning` properly

* Close previous outputs

* onError callback

* Format

* Started/Stopped

* Update CameraPage.tsx

* Add `isValid`

* Log `isActive`

* Add `tryAbortCaptures`

* Configure()

* Try?

* Add `didDestroyFromOutside`

* Disable FP for testing

* fix: Call `super.onAttachedToWindow` first

* Hm

* Update CameraSession.kt

* Update PersistentCameraCaptureSession.kt

* Try catch `didDestroyFromOutside`

* Update PersistentCameraCaptureSession.kt

* Session can only be active with a preview

* Update PersistentCameraCaptureSession.kt

* Throw `no-outputs` if needed

* Update logs

* fix: Check for CAMERA permission

* fix: Close session when opening a new device

* perf: Make everything `by lazy` in CameraDeviceDetails

* Update CameraDeviceDetails.kt

* Update PersistentCameraCaptureSession.kt

* Update PersistentCameraCaptureSession.kt

* Move

* Update Podfile.lock

* Implement `capture()`

* Format

* fix: Fix orientation not being applied

* fix: Fix `isMirrored`

* fix: Fix getting size

* fix: Close `Surface` in `VideoPipeline`

* Format

* fix: Fix `VideoPipeline` not properly destroying itself

* Use FP again

* Update CameraConfiguration.kt

* Rename

* Clean up

* Format

* Update CameraConfiguration.kt

* fix: Don't stop repeating request when capturing
This commit is contained in:
Marc Rousavy 2024-02-06 14:19:25 +01:00 committed by GitHub
parent cd5fdd4924
commit 5acc64e031
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 746 additions and 496 deletions

View File

@ -29,10 +29,11 @@ OpenGLRenderer::OpenGLRenderer(std::shared_ptr<OpenGLContext> context, ANativeWi
}
OpenGLRenderer::~OpenGLRenderer() {
__android_log_print(ANDROID_LOG_INFO, TAG, "Destroying OpenGLRenderer...");
destroy();
if (_outputSurface != nullptr) {
ANativeWindow_release(_outputSurface);
}
destroy();
}
void OpenGLRenderer::destroy() {

View File

@ -113,17 +113,17 @@ class CameraView(context: Context) :
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
if (!isMounted) {
isMounted = true
invokeOnViewReady()
}
update()
super.onAttachedToWindow()
}
override fun onDetachedFromWindow() {
update()
super.onDetachedFromWindow()
update()
}
fun destroy() {

View File

@ -67,7 +67,7 @@ data class CameraConfiguration(
}
data class Difference(
// Input Camera (cameraId, isActive)
// Input Camera (cameraId)
val deviceChanged: Boolean,
// Outputs & Session (Photo, Video, CodeScanner, HDR, Format)
val outputsChanged: Boolean,
@ -75,14 +75,17 @@ data class CameraConfiguration(
val sidePropsChanged: Boolean,
// (isActive) changed
val isActiveChanged: Boolean
)
) {
val hasChanges: Boolean
get() = deviceChanged || outputsChanged || sidePropsChanged || isActiveChanged
}
companion object {
fun copyOf(other: CameraConfiguration?): CameraConfiguration = other?.copy() ?: CameraConfiguration()
fun difference(left: CameraConfiguration?, right: CameraConfiguration): Difference {
// input device
val deviceChanged = left?.cameraId != right.cameraId || left?.isActive != right.isActive
val deviceChanged = left?.cameraId != right.cameraId
// outputs
val outputsChanged = deviceChanged ||
@ -101,7 +104,7 @@ data class CameraConfiguration(
left.videoStabilizationMode != right.videoStabilizationMode ||
left.exposure != right.exposure
val isActiveChanged = left?.isActive != right.isActive
val isActiveChanged = sidePropsChanged || left?.isActive != right.isActive
return Difference(
deviceChanged,

View File

@ -1,7 +1,9 @@
package com.mrousavy.camera.core
import android.annotation.SuppressLint
import android.graphics.ImageFormat
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraExtensionCharacteristics
import android.hardware.camera2.CameraManager
import android.hardware.camera2.CameraMetadata
import android.os.Build
@ -23,61 +25,73 @@ import com.mrousavy.camera.types.VideoStabilizationMode
import kotlin.math.atan2
import kotlin.math.sqrt
class CameraDeviceDetails(val cameraManager: CameraManager, val cameraId: String) {
val characteristics = cameraManager.getCameraCharacteristics(cameraId)
val hardwareLevel = HardwareLevel.fromCameraCharacteristics(characteristics)
val capabilities = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES) ?: IntArray(0)
val extensions = getSupportedExtensions()
@SuppressLint("InlinedApi")
class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId: String) {
val characteristics by lazy { cameraManager.getCameraCharacteristics(cameraId) }
val hardwareLevel by lazy { HardwareLevel.fromCameraCharacteristics(characteristics) }
val capabilities by lazy { characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES) ?: IntArray(0) }
val extensions by lazy { getSupportedExtensions() }
// device characteristics
val isMultiCam = capabilities.contains(11) // TODO: CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA
val supportsDepthCapture = capabilities.contains(8) // TODO: CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT
val supportsRawCapture = capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW)
val supportsLowLightBoost = extensions.contains(4) // TODO: CameraExtensionCharacteristics.EXTENSION_NIGHT
val lensFacing = LensFacing.fromCameraCharacteristics(characteristics)
val hasFlash = characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) ?: false
val focalLengths =
characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS)
// 35mm is the film standard sensor size
?: floatArrayOf(35f)
val sensorSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)!!
val sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!!
val minFocusDistance = getMinFocusDistanceCm()
val name = (
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
characteristics.get(CameraCharacteristics.INFO_VERSION)
} else {
null
}
) ?: "$lensFacing ($cameraId)"
val isMultiCam by lazy { capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA) }
val supportsDepthCapture by lazy { capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT) }
val supportsRawCapture by lazy { capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW) }
val supportsLowLightBoost by lazy { extensions.contains(CameraExtensionCharacteristics.EXTENSION_NIGHT) }
val lensFacing by lazy { LensFacing.fromCameraCharacteristics(characteristics) }
val hasFlash by lazy { characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) ?: false }
val focalLengths by lazy {
// 35mm is the film standard sensor size
characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS) ?: floatArrayOf(35f)
}
val sensorSize by lazy { characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)!! }
val sensorOrientation by lazy { 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
return@lazy info ?: "$lensFacing ($cameraId)"
}
// "formats" (all possible configurations for this device)
val zoomRange = (
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val maxDigitalZoom by lazy { characteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM) ?: 1f }
val zoomRange by lazy {
val range = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
characteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE)
} else {
null
}
) ?: Range(1f, characteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM) ?: 1f)
val physicalDevices = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && characteristics.physicalCameraIds.isNotEmpty()) {
characteristics.physicalCameraIds
} else {
setOf(cameraId)
return@lazy range ?: Range(1f, maxDigitalZoom)
}
val minZoom = zoomRange.lower.toDouble()
val maxZoom = zoomRange.upper.toDouble()
val physicalDevices by lazy {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && characteristics.physicalCameraIds.isNotEmpty()) {
characteristics.physicalCameraIds
} else {
setOf(cameraId)
}
}
val minZoom by lazy { zoomRange.lower.toDouble() }
val maxZoom by lazy { zoomRange.upper.toDouble() }
val cameraConfig = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
val isoRange = characteristics.get(CameraCharacteristics.SENSOR_INFO_SENSITIVITY_RANGE) ?: Range(0, 0)
val exposureRange = characteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE) ?: Range(0, 0)
val digitalStabilizationModes =
val cameraConfig by lazy { characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!! }
val isoRange by lazy { characteristics.get(CameraCharacteristics.SENSOR_INFO_SENSITIVITY_RANGE) ?: Range(0, 0) }
val exposureRange by lazy { characteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE) ?: Range(0, 0) }
val digitalStabilizationModes by lazy {
characteristics.get(CameraCharacteristics.CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES) ?: IntArray(0)
val opticalStabilizationModes =
}
val opticalStabilizationModes by lazy {
characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_OPTICAL_STABILIZATION) ?: IntArray(0)
val supportsPhotoHdr = extensions.contains(3) // TODO: CameraExtensionCharacteristics.EXTENSION_HDR
val supportsVideoHdr = getHasVideoHdr()
val autoFocusSystem = getAutoFocusSystemMode()
}
val supportsPhotoHdr by lazy { extensions.contains(CameraExtensionCharacteristics.EXTENSION_HDR) }
val supportsVideoHdr by lazy { getHasVideoHdr() }
val autoFocusSystem by lazy { getAutoFocusSystemMode() }
val supportsYuvProcessing by lazy { capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_YUV_REPROCESSING) }
val supportsPrivateProcessing by lazy { capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_PRIVATE_REPROCESSING) }
val supportsZsl by lazy { supportsYuvProcessing || supportsPrivateProcessing }
val isBackwardsCompatible by lazy { capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE) }
val supportsSnapshotCapture by lazy { supportsSnapshotCapture() }
// TODO: Also add 10-bit YUV here?
val videoFormat = ImageFormat.YUV_420_888
// get extensions (HDR, Night Mode, ..)
@ -107,6 +121,14 @@ class CameraDeviceDetails(val cameraManager: CameraManager, val cameraId: String
return 1.0 / distance * 100.0
}
@Suppress("RedundantIf")
private fun supportsSnapshotCapture(): Boolean {
// As per CameraDevice.TEMPLATE_VIDEO_SNAPSHOT in documentation:
if (hardwareLevel == HardwareLevel.LEGACY) return false
if (supportsDepthCapture && !isBackwardsCompatible) return false
return true
}
private fun createStabilizationModes(): ReadableArray {
val array = Arguments.createArray()
digitalStabilizationModes.forEach { videoStabilizationMode ->
@ -176,8 +198,6 @@ class CameraDeviceDetails(val cameraManager: CameraManager, val cameraId: String
}
}
// TODO: Add high-speed video ranges (high-fps / slow-motion)
return array
}

View File

@ -71,6 +71,8 @@ class CameraSessionCannotBeConfiguredError(cameraId: String) :
CameraError("session", "cannot-create-session", "Failed to create a Camera Session for Camera #$cameraId!")
class CameraDisconnectedError(cameraId: String, error: CameraDeviceError) :
CameraError("session", "camera-has-been-disconnected", "The given Camera device (id: $cameraId) has been disconnected! Error: $error")
class NoOutputsError :
CameraError("session", "no-outputs", "Cannot create a CameraCaptureSession without any outputs! (PREVIEW, PHOTO, VIDEO, ...)")
class PropRequiresFormatToBeNonNullError(propName: String) :
CameraError("format", "format-required", "The prop \"$propName\" requires a format to be set, but format was null!")

View File

@ -5,49 +5,36 @@ import android.content.Context
import android.content.pm.PackageManager
import android.graphics.ImageFormat
import android.graphics.Point
import android.hardware.camera2.CameraCaptureSession
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CameraManager
import android.hardware.camera2.CameraMetadata
import android.hardware.camera2.CaptureRequest
import android.hardware.camera2.CaptureResult
import android.hardware.camera2.TotalCaptureResult
import android.hardware.camera2.params.MeteringRectangle
import android.media.Image
import android.media.ImageReader
import android.os.Build
import android.util.Log
import android.util.Range
import android.util.Size
import android.view.Surface
import android.view.SurfaceHolder
import androidx.core.content.ContextCompat
import com.google.mlkit.vision.barcode.common.Barcode
import com.mrousavy.camera.core.capture.RepeatingCaptureRequest
import com.mrousavy.camera.core.outputs.BarcodeScannerOutput
import com.mrousavy.camera.core.outputs.PhotoOutput
import com.mrousavy.camera.core.outputs.SurfaceOutput
import com.mrousavy.camera.core.outputs.VideoPipelineOutput
import com.mrousavy.camera.extensions.capture
import com.mrousavy.camera.extensions.closestToOrMax
import com.mrousavy.camera.extensions.createCaptureSession
import com.mrousavy.camera.extensions.createPhotoCaptureRequest
import com.mrousavy.camera.extensions.getPhotoSizes
import com.mrousavy.camera.extensions.getPreviewTargetSize
import com.mrousavy.camera.extensions.getVideoSizes
import com.mrousavy.camera.extensions.openCamera
import com.mrousavy.camera.extensions.setZoom
import com.mrousavy.camera.frameprocessor.Frame
import com.mrousavy.camera.types.Flash
import com.mrousavy.camera.types.LensFacing
import com.mrousavy.camera.types.Orientation
import com.mrousavy.camera.types.QualityPrioritization
import com.mrousavy.camera.types.RecordVideoOptions
import com.mrousavy.camera.types.Torch
import com.mrousavy.camera.types.VideoStabilizationMode
import com.mrousavy.camera.utils.ImageFormatUtils
import java.io.Closeable
import java.lang.IllegalStateException
import java.util.concurrent.CancellationException
import kotlin.coroutines.cancellation.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@ -55,8 +42,8 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
class CameraSession(private val context: Context, private val cameraManager: CameraManager, private val callback: Callback) :
CameraManager.AvailabilityCallback(),
Closeable {
Closeable,
PersistentCameraCaptureSession.Callback {
companion object {
private const val TAG = "CameraSession"
}
@ -65,14 +52,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
private var configuration: CameraConfiguration? = null
// Camera State
private var cameraDevice: CameraDevice? = null
set(value) {
field = value
cameraDeviceDetails = if (value != null) CameraDeviceDetails(cameraManager, value.id) else null
}
private var cameraDeviceDetails: CameraDeviceDetails? = null
private var captureSession: CameraCaptureSession? = null
private var previewRequest: CaptureRequest.Builder? = null
private val captureSession = PersistentCameraCaptureSession(cameraManager, this)
private var photoOutput: PhotoOutput? = null
private var videoOutput: VideoPipelineOutput? = null
private var codeScannerOutput: BarcodeScannerOutput? = null
@ -109,14 +89,9 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
return Orientation.fromRotationDegrees(sensorRotation)
}
init {
cameraManager.registerAvailabilityCallback(this, CameraQueues.cameraQueue.handler)
}
override fun close() {
Log.i(TAG, "Closing CameraSession...")
isDestroyed = true
cameraManager.unregisterAvailabilityCallback(this)
runBlocking {
mutex.withLock {
destroy()
@ -126,18 +101,6 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
Log.i(TAG, "CameraSession closed!")
}
override fun onCameraAvailable(cameraId: String) {
super.onCameraAvailable(cameraId)
if (this.configuration?.cameraId == cameraId && cameraDevice == null && configuration?.isActive == true) {
Log.i(TAG, "Camera #$cameraId is now available again, trying to re-open it now...")
coroutineScope.launch {
configure {
// re-open CameraDevice if needed
}
}
}
}
suspend fun configure(lambda: (configuration: CameraConfiguration) -> Unit) {
Log.i(TAG, "configure { ... }: Waiting for lock...")
@ -146,6 +109,12 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
val config = CameraConfiguration.copyOf(this.configuration)
lambda(config)
val diff = CameraConfiguration.difference(this.configuration, config)
this.configuration = config
if (!diff.hasChanges) {
Log.i(TAG, "Nothing changed, aborting configure { ... }")
return@withLock
}
if (isDestroyed) {
Log.i(TAG, "CameraSession is already destroyed. Skipping configure { ... }")
@ -155,29 +124,11 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
Log.i(TAG, "configure { ... }: Updating CameraSession Configuration... $diff")
try {
val needsRebuild = cameraDevice == null || captureSession == null
if (needsRebuild) {
Log.i(TAG, "Need to rebuild CameraDevice and CameraCaptureSession...")
}
// Since cameraDevice and captureSession are OS resources, we have three possible paths here:
if (needsRebuild) {
if (config.isActive) {
// A: The Camera has been torn down by the OS and we want it to be active - rebuild everything
Log.i(TAG, "Need to rebuild CameraDevice and CameraCaptureSession...")
configureCameraDevice(config)
configureOutputs(config)
configureCaptureRequest(config)
} else {
// B: The Camera has been torn down by the OS but it's currently in the background - ignore this
Log.i(TAG, "CameraDevice and CameraCaptureSession is torn down but Camera is not active, skipping update...")
}
} else {
// C: The Camera has not been torn down and we just want to update some props - update incrementally
captureSession.withConfiguration {
// Build up session or update any props
if (diff.deviceChanged) {
// 1. cameraId changed, open device
configureCameraDevice(config)
configureInput(config)
}
if (diff.outputsChanged) {
// 2. outputs changed, build new session
@ -187,10 +138,18 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
// 3. zoom etc changed, update repeating request
configureCaptureRequest(config)
}
if (diff.isActiveChanged) {
// 4. Either start or stop the session
val isActive = config.isActive && config.preview.isEnabled
captureSession.setIsActive(isActive)
}
}
Log.i(TAG, "Successfully updated CameraSession Configuration! isActive: ${config.isActive}")
this.configuration = config
Log.i(
TAG,
"configure { ... }: Completed CameraSession Configuration! (isActive: ${config.isActive}, isRunning: ${captureSession.isRunning})"
)
isRunning = captureSession.isRunning
// Notify about Camera initialization
if (diff.deviceChanged) {
@ -205,8 +164,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
private fun destroy() {
Log.i(TAG, "Destroying session..")
cameraDevice?.close()
cameraDevice = null
captureSession.close()
photoOutput?.close()
photoOutput = null
@ -262,66 +220,20 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
Log.i(TAG, "Preview Output destroyed!")
}
/**
* Set up the `CameraDevice` (`cameraId`)
*/
private suspend fun configureCameraDevice(configuration: CameraConfiguration) {
if (!configuration.isActive) {
// If isActive=false, we don't care if the device is opened or closed.
// Android OS can close the CameraDevice if it needs it, otherwise we keep it warm.
Log.i(TAG, "isActive is false, skipping CameraDevice configuration.")
return
}
if (cameraDevice != null) {
// Close existing device
Log.i(TAG, "Closing previous Camera #${cameraDevice?.id}...")
cameraDevice?.close()
cameraDevice = null
}
isRunning = false
// Check Camera Permission
val cameraPermission = ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
if (cameraPermission != PackageManager.PERMISSION_GRANTED) throw CameraPermissionError()
// Open new device
private fun configureInput(configuration: CameraConfiguration) {
Log.i(TAG, "Configuring inputs for CameraSession...")
val cameraId = configuration.cameraId ?: throw NoCameraDeviceError()
Log.i(TAG, "Configuring Camera #$cameraId...")
cameraDevice = cameraManager.openCamera(cameraId, { device, error ->
if (cameraDevice != device) {
// a previous device has been disconnected, but we already have a new one.
// this is just normal behavior
return@openCamera
}
this.cameraDevice = null
isRunning = false
if (error != null) {
Log.e(TAG, "Camera #${device.id} has been unexpectedly disconnected!", error)
callback.onError(error)
} else {
Log.i(TAG, "Camera #${device.id} has been gracefully disconnected!")
}
}, CameraQueues.cameraQueue)
Log.i(TAG, "Successfully configured Camera #$cameraId!")
val status = ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
if (status != PackageManager.PERMISSION_GRANTED) throw CameraPermissionError()
isRunning = false
captureSession.setInput(cameraId)
}
/**
* Set up the `CaptureSession` with all outputs (preview, photo, video, codeScanner) and their HDR/Format settings.
*/
private suspend fun configureOutputs(configuration: CameraConfiguration) {
if (!configuration.isActive) {
Log.i(TAG, "isActive is false, skipping CameraCaptureSession configuration.")
return
}
val cameraDevice = cameraDevice
if (cameraDevice == null) {
Log.i(TAG, "CameraSession hasn't configured a CameraDevice, skipping session configuration...")
return
}
private fun configureOutputs(configuration: CameraConfiguration) {
val cameraId = configuration.cameraId ?: throw NoCameraDeviceError()
// Destroy previous outputs
Log.i(TAG, "Destroying previous outputs...")
@ -333,10 +245,10 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
codeScannerOutput = null
isRunning = false
val characteristics = cameraManager.getCameraCharacteristics(cameraDevice.id)
val characteristics = cameraManager.getCameraCharacteristics(cameraId)
val format = configuration.format
Log.i(TAG, "Creating outputs for Camera #${cameraDevice.id}...")
Log.i(TAG, "Creating outputs for Camera #$cameraId...")
val isSelfie = characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT
@ -366,7 +278,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
val video = configuration.video as? CameraConfiguration.Output.Enabled<CameraConfiguration.Video>
if (video != null) {
val imageFormat = video.config.pixelFormat.toImageFormat()
val sizes = characteristics.getVideoSizes(cameraDevice.id, imageFormat)
val sizes = characteristics.getVideoSizes(cameraId, imageFormat)
val size = sizes.closestToOrMax(format?.videoSize)
Log.i(TAG, "Adding ${size.width}x${size.height} Video Output in ${ImageFormatUtils.imageFormatToString(imageFormat)}...")
@ -414,7 +326,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
}
val imageFormat = ImageFormat.YUV_420_888
val sizes = characteristics.getVideoSizes(cameraDevice.id, imageFormat)
val sizes = characteristics.getVideoSizes(cameraId, imageFormat)
val size = sizes.closestToOrMax(Size(1280, 720))
Log.i(TAG, "Adding ${size.width}x${size.height} CodeScanner Output in ${ImageFormatUtils.imageFormatToString(imageFormat)}...")
@ -425,175 +337,63 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
}
// Create session
captureSession = cameraDevice.createCaptureSession(cameraManager, outputs, { session ->
if (this.captureSession != session) {
// a previous session has been closed, but we already have a new one.
// this is just normal behavior
return@createCaptureSession
}
captureSession.setOutputs(outputs)
// onClosed
this.captureSession = null
isRunning = false
Log.i(TAG, "Camera Session $session has been closed.")
}, CameraQueues.cameraQueue)
Log.i(TAG, "Successfully configured Session with ${outputs.size} outputs for Camera #${cameraDevice.id}!")
Log.i(TAG, "Successfully configured Session with ${outputs.size} outputs for Camera #$cameraId!")
// Update Frame Processor and RecordingSession for newly changed output
updateVideoOutputs()
}
private fun createRepeatingRequest(device: CameraDevice, targets: List<Surface>, config: CameraConfiguration): CaptureRequest {
val deviceDetails = cameraDeviceDetails ?: CameraDeviceDetails(cameraManager, device.id)
val template = if (config.video.isEnabled) CameraDevice.TEMPLATE_RECORD else CameraDevice.TEMPLATE_PREVIEW
val captureRequest = device.createCaptureRequest(template)
targets.forEach { t -> captureRequest.addTarget(t) }
val format = config.format
// Set FPS
val fps = config.fps
if (fps != null) {
if (format == null) throw PropRequiresFormatToBeNonNullError("fps")
if (format.maxFps < fps) throw InvalidFpsError(fps)
captureRequest.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, Range(fps, fps))
}
// Set Video Stabilization
if (config.videoStabilizationMode != VideoStabilizationMode.OFF) {
if (format == null) throw PropRequiresFormatToBeNonNullError("videoStabilizationMode")
if (!format.videoStabilizationModes.contains(
config.videoStabilizationMode
)
) {
throw InvalidVideoStabilizationMode(config.videoStabilizationMode)
}
}
when (config.videoStabilizationMode) {
VideoStabilizationMode.OFF -> {
// do nothing
}
VideoStabilizationMode.STANDARD -> {
val mode = if (Build.VERSION.SDK_INT >=
Build.VERSION_CODES.TIRAMISU
) {
CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION
} else {
CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_ON
}
captureRequest.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, mode)
}
VideoStabilizationMode.CINEMATIC, VideoStabilizationMode.CINEMATIC_EXTENDED -> {
captureRequest.set(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE, CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE_ON)
}
}
// Set HDR
val video = config.video as? CameraConfiguration.Output.Enabled<CameraConfiguration.Video>
val videoHdr = video?.config?.enableHdr
if (videoHdr == true) {
if (format == null) throw PropRequiresFormatToBeNonNullError("videoHdr")
if (!format.supportsVideoHdr) throw InvalidVideoHdrError()
captureRequest.set(CaptureRequest.CONTROL_SCENE_MODE, CaptureRequest.CONTROL_SCENE_MODE_HDR)
} else if (config.enableLowLightBoost) {
if (!deviceDetails.supportsLowLightBoost) throw LowLightBoostNotSupportedError()
captureRequest.set(CaptureRequest.CONTROL_SCENE_MODE, CaptureRequest.CONTROL_SCENE_MODE_NIGHT)
}
// Set Exposure Bias
val exposure = config.exposure?.toInt()
if (exposure != null) {
val clamped = deviceDetails.exposureRange.clamp(exposure)
captureRequest.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, clamped)
}
// Set Zoom
// TODO: Cache camera characteristics? Check perf.
val cameraCharacteristics = cameraManager.getCameraCharacteristics(device.id)
captureRequest.setZoom(config.zoom, cameraCharacteristics)
// Set Torch
if (config.torch == Torch.ON) {
if (!deviceDetails.hasFlash) throw FlashUnavailableError()
captureRequest.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH)
}
// Start repeating request if the Camera is active
return captureRequest.build()
}
private fun configureCaptureRequest(config: CameraConfiguration) {
val captureSession = captureSession
val video = config.video as? CameraConfiguration.Output.Enabled<CameraConfiguration.Video>
val enableVideo = video != null
val enableVideoHdr = video?.config?.enableHdr == true
if (!config.isActive) {
isRunning = false
try {
captureSession?.stopRepeating()
} catch (e: IllegalStateException) {
// ignore - captureSession is already closed.
}
return
}
if (captureSession == null) {
Log.i(TAG, "CameraSession hasn't configured the capture session, skipping CaptureRequest...")
return
}
val preview = config.preview as? CameraConfiguration.Output.Enabled<CameraConfiguration.Preview>
val previewSurface = preview?.config?.surface
val targets = listOfNotNull(previewSurface, videoOutput?.surface, codeScannerOutput?.surface)
if (targets.isEmpty()) {
Log.i(TAG, "CameraSession has no repeating outputs (Preview, Video, CodeScanner), skipping CaptureRequest...")
return
}
val request = createRepeatingRequest(captureSession.device, targets, config)
captureSession.setRepeatingRequest(request, null, null)
isRunning = true
captureSession.setRepeatingRequest(
RepeatingCaptureRequest(
enableVideo,
config.torch,
config.fps,
config.videoStabilizationMode,
enableVideoHdr,
config.enableLowLightBoost,
config.exposure,
config.zoom,
config.format
)
)
}
suspend fun takePhoto(
qualityPrioritization: QualityPrioritization,
flashMode: Flash,
flash: Flash,
enableShutterSound: Boolean,
enableRedEyeReduction: Boolean,
enableAutoStabilization: Boolean,
outputOrientation: Orientation
): CapturedPhoto {
val captureSession = captureSession ?: throw CameraNotReadyError()
val photoOutput = photoOutput ?: throw PhotoNotEnabledError()
Log.i(TAG, "Photo capture 0/3 - preparing capture request (${photoOutput.size.width}x${photoOutput.size.height})...")
val zoom = configuration?.zoom ?: 1f
val cameraCharacteristics = cameraManager.getCameraCharacteristics(captureSession.device.id)
val orientation = outputOrientation.toSensorRelativeOrientation(cameraCharacteristics)
val captureRequest = captureSession.device.createPhotoCaptureRequest(
cameraManager,
photoOutput.surface,
zoom,
Log.i(TAG, "Photo capture 1/3 - capturing ${photoOutput.size.width}x${photoOutput.size.height} image...")
val result = captureSession.capture(
qualityPrioritization,
flashMode,
flash,
enableRedEyeReduction,
enableAutoStabilization,
photoOutput.enableHdr,
orientation
outputOrientation,
enableShutterSound
)
Log.i(TAG, "Photo capture 1/3 - starting capture...")
val result = captureSession.capture(captureRequest, enableShutterSound)
val timestamp = result[CaptureResult.SENSOR_TIMESTAMP]!!
Log.i(TAG, "Photo capture 2/3 complete - received metadata with timestamp $timestamp")
try {
val timestamp = result[CaptureResult.SENSOR_TIMESTAMP]!!
Log.i(TAG, "Photo capture 2/3 - waiting for image with timestamp $timestamp now...")
val image = photoOutputSynchronizer.await(timestamp)
val isMirrored = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT
Log.i(TAG, "Photo capture 3/3 complete - received ${image.width} x ${image.height} image.")
Log.i(TAG, "Photo capture 3/3 - received ${image.width} x ${image.height} image, preparing result...")
val deviceDetails = captureSession.getActiveDeviceDetails()
val isMirrored = deviceDetails?.lensFacing == LensFacing.FRONT
return CapturedPhoto(image, result, orientation, isMirrored, image.format)
} catch (e: CancellationException) {
throw CaptureAbortedError(false)
@ -620,13 +420,13 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
mutex.withLock {
if (recording != null) throw RecordingInProgressError()
val videoOutput = videoOutput ?: throw VideoNotEnabledError()
val cameraDevice = cameraDevice ?: throw CameraNotReadyError()
val cameraId = configuration?.cameraId ?: throw NoCameraDeviceError()
val fps = configuration?.fps ?: 30
val recording = RecordingSession(
context,
cameraDevice.id,
cameraId,
videoOutput.size,
enableAudio,
fps,
@ -664,41 +464,13 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
}
}
override fun onError(error: Throwable) {
callback.onError(error)
}
suspend fun focus(x: Int, y: Int): Unit = throw NotImplementedError("focus() is not yet implemented!")
private suspend fun focus(point: Point) {
mutex.withLock {
// TODO: Fix this method
val captureSession = captureSession ?: throw CameraNotReadyError()
val request = previewRequest ?: throw CameraNotReadyError()
val weight = MeteringRectangle.METERING_WEIGHT_MAX - 1
val focusAreaTouch = MeteringRectangle(point, Size(150, 150), weight)
// Quickly pause preview
captureSession.stopRepeating()
request.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL)
request.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF)
captureSession.capture(request.build(), null, null)
// Add AF trigger with focus region
val characteristics = cameraManager.getCameraCharacteristics(captureSession.device.id)
val maxSupportedFocusRegions = characteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE) ?: 0
if (maxSupportedFocusRegions >= 1) {
request.set(CaptureRequest.CONTROL_AF_REGIONS, arrayOf(focusAreaTouch))
}
request.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO)
request.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO)
request.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_START)
captureSession.capture(request.build(), false)
// Resume preview
request.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_IDLE)
captureSession.setRepeatingRequest(request.build(), null, null)
}
}
private suspend fun focus(point: Point): Unit = throw NotImplementedError()
data class CapturedPhoto(
val image: Image,

View File

@ -0,0 +1,266 @@
package com.mrousavy.camera.core
import android.hardware.camera2.CameraAccessException
import android.hardware.camera2.CameraCaptureSession
import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CameraManager
import android.hardware.camera2.TotalCaptureResult
import android.util.Log
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.capture
import com.mrousavy.camera.extensions.createCaptureSession
import com.mrousavy.camera.extensions.isValid
import com.mrousavy.camera.extensions.openCamera
import com.mrousavy.camera.extensions.tryAbortCaptures
import com.mrousavy.camera.types.Flash
import com.mrousavy.camera.types.Orientation
import com.mrousavy.camera.types.QualityPrioritization
import java.io.Closeable
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
/**
* A [CameraCaptureSession] wrapper that safely handles interruptions and remains open whenever available.
*
* This class aims to be similar to Apple's `AVCaptureSession`.
*/
class PersistentCameraCaptureSession(private val cameraManager: CameraManager, private val callback: Callback) : Closeable {
companion object {
private const val TAG = "PersistentCameraCaptureSession"
}
// Inputs/Dependencies
private var cameraId: String? = null
private var outputs: List<SurfaceOutput> = emptyList()
private var repeatingRequest: RepeatingCaptureRequest? = null
private var isActive = false
// State/Dependants
private var device: CameraDevice? = null // depends on [cameraId]
private var session: CameraCaptureSession? = null // depends on [device, surfaceOutputs]
private var cameraDeviceDetails: CameraDeviceDetails? = null // depends on [device]
private val mutex = Mutex()
private var didDestroyFromOutside = false
val isRunning: Boolean
get() = isActive && session != null && device != null && !didDestroyFromOutside
override fun close() {
session?.tryAbortCaptures()
device?.close()
}
private fun assertLocked(method: String) {
if (!mutex.isLocked) {
throw SessionIsNotLockedError("Failed to call $method, session is not locked! Call beginConfiguration() first.")
}
}
suspend fun withConfiguration(block: suspend () -> Unit) {
mutex.withLock {
block()
configure()
}
}
fun setInput(cameraId: String) {
Log.d(TAG, "--> setInput($cameraId)")
assertLocked("setInput")
if (this.cameraId != cameraId || device?.id != cameraId) {
this.cameraId = cameraId
// Abort any captures in the session so we get the onCaptureFailed handler for any outstanding photos
session?.tryAbortCaptures()
session = null
// Closing the device will also close the session above - even faster than manually closing it.
device?.close()
device = null
}
}
fun setOutputs(outputs: List<SurfaceOutput>) {
Log.d(TAG, "--> setOutputs($outputs)")
assertLocked("setOutputs")
if (this.outputs != outputs) {
this.outputs = outputs
if (outputs.isNotEmpty()) {
// Outputs have changed to something else, we don't wanna destroy the session directly
// so the outputs can be kept warm. The session that gets created next will take over the outputs.
session?.tryAbortCaptures()
} else {
// Just stop it, we don't have any outputs
session?.close()
}
session = null
}
}
fun setRepeatingRequest(request: RepeatingCaptureRequest) {
assertLocked("setRepeatingRequest")
Log.d(TAG, "--> setRepeatingRequest(...)")
if (this.repeatingRequest != request) {
this.repeatingRequest = request
}
}
fun setIsActive(isActive: Boolean) {
assertLocked("setIsActive")
Log.d(TAG, "--> setIsActive($isActive)")
if (this.isActive != isActive) {
this.isActive = isActive
}
if (isActive && didDestroyFromOutside) {
didDestroyFromOutside = false
}
}
suspend fun capture(
qualityPrioritization: QualityPrioritization,
flash: Flash,
enableRedEyeReduction: Boolean,
enableAutoStabilization: Boolean,
enablePhotoHdr: Boolean,
orientation: Orientation,
enableShutterSound: Boolean
): TotalCaptureResult {
mutex.withLock {
val session = session ?: throw CameraNotReadyError()
val repeatingRequest = repeatingRequest ?: throw CameraNotReadyError()
val photoRequest = PhotoCaptureRequest(
repeatingRequest,
qualityPrioritization,
flash,
enableRedEyeReduction,
enableAutoStabilization,
enablePhotoHdr,
orientation
)
val device = session.device
val deviceDetails = getOrCreateCameraDeviceDetails(device)
// 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)
}
}
fun getActiveDeviceDetails(): CameraDeviceDetails? {
val device = device ?: return null
return getOrCreateCameraDeviceDetails(device)
}
private suspend fun configure() {
if (didDestroyFromOutside && !isActive) {
Log.d(TAG, "CameraCaptureSession has been destroyed by Android, skipping configuration until isActive is set to `true` again.")
return
}
Log.d(TAG, "Configure() with isActive: $isActive, ID: $cameraId, device: $device, session: $session")
val cameraId = cameraId ?: throw NoCameraDeviceError()
val repeatingRequest = repeatingRequest ?: throw CameraNotReadyError()
val outputs = outputs
try {
didDestroyFromOutside = false
val device = getOrCreateDevice(cameraId)
if (didDestroyFromOutside) return
if (outputs.isEmpty()) return
val session = getOrCreateSession(device, outputs)
if (didDestroyFromOutside) return
if (isActive) {
Log.d(TAG, "Updating repeating request...")
val details = getOrCreateCameraDeviceDetails(device)
val repeatingOutputs = outputs.filter { it.isRepeating }
val builder = repeatingRequest.createCaptureRequest(device, details, repeatingOutputs)
session.setRepeatingRequest(builder.build(), null, null)
} else {
session.stopRepeating()
Log.d(TAG, "Stopping repeating request...")
}
Log.d(TAG, "Configure() done! isActive: $isActive, ID: $cameraId, device: $device, session: $session")
} catch (e: CameraAccessException) {
if (didDestroyFromOutside) {
// Camera device has been destroyed in the meantime, that's fine.
Log.d(TAG, "Configure() canceled, session has been destroyed in the meantime!")
} else {
// Camera should still be active, so not sure what went wrong. Rethrow
throw e
}
}
}
private suspend fun getOrCreateDevice(cameraId: String): CameraDevice {
val currentDevice = device
if (currentDevice?.id == cameraId && currentDevice.isValid) {
return currentDevice
}
this.session?.tryAbortCaptures()
this.device?.close()
this.device = null
this.session = null
Log.i(TAG, "Creating new device...")
val newDevice = cameraManager.openCamera(cameraId, { device, error ->
Log.i(TAG, "Camera $device closed!")
if (this.device == device) {
this.didDestroyFromOutside = true
this.session?.tryAbortCaptures()
this.session = null
this.device = null
this.isActive = false
}
if (error != null) {
callback.onError(error)
}
}, CameraQueues.videoQueue)
this.device = newDevice
return newDevice
}
private suspend fun getOrCreateSession(device: CameraDevice, outputs: List<SurfaceOutput>): CameraCaptureSession {
val currentSession = session
if (currentSession?.device == device) {
return currentSession
}
if (outputs.isEmpty()) throw NoOutputsError()
Log.i(TAG, "Creating new session...")
val newSession = device.createCaptureSession(cameraManager, outputs, { session ->
Log.i(TAG, "Session $session closed!")
if (this.session == session) {
this.didDestroyFromOutside = true
this.session?.tryAbortCaptures()
this.session = null
this.isActive = false
}
}, CameraQueues.videoQueue)
session = newSession
return newSession
}
private fun getOrCreateCameraDeviceDetails(device: CameraDevice): CameraDeviceDetails {
val currentDetails = cameraDeviceDetails
if (currentDetails?.cameraId == device.id) {
return currentDetails
}
val newDetails = CameraDeviceDetails(cameraManager, device.id)
cameraDeviceDetails = newDetails
return newDetails
}
interface Callback {
fun onError(error: Throwable)
}
class SessionIsNotLockedError(message: String) : Error(message)
}

View File

@ -125,8 +125,11 @@ class VideoPipeline(
isActive = false
imageWriter?.close()
imageReader?.close()
removeRecordingSessionOutputSurface()
recordingSession = null
surfaceTexture.setOnFrameAvailableListener(null, null)
surfaceTexture.release()
surface.release()
}
}
@ -170,7 +173,7 @@ class VideoPipeline(
synchronized(this) {
if (recordingSession != null) {
// Configure OpenGL pipeline to stream Frames into the Recording Session's surface
Log.i(TAG, "Setting $width x $height RecordingSession Output...")
Log.i(TAG, "Setting ${recordingSession.size} RecordingSession Output...")
setRecordingSessionOutputSurface(recordingSession.surface)
this.recordingSession = recordingSession
} else {

View File

@ -0,0 +1,86 @@
package com.mrousavy.camera.core.capture
import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CaptureRequest
import com.mrousavy.camera.core.CameraDeviceDetails
import com.mrousavy.camera.core.FlashUnavailableError
import com.mrousavy.camera.core.InvalidVideoHdrError
import com.mrousavy.camera.core.LowLightBoostNotSupportedError
import com.mrousavy.camera.core.PropRequiresFormatToBeNonNullError
import com.mrousavy.camera.core.outputs.SurfaceOutput
import com.mrousavy.camera.extensions.setZoom
import com.mrousavy.camera.types.CameraDeviceFormat
import com.mrousavy.camera.types.Torch
abstract class CameraCaptureRequest(
private val torch: Torch = Torch.OFF,
private val enableVideoHdr: Boolean = false,
val enableLowLightBoost: Boolean = false,
val exposureBias: Double? = null,
val zoom: Float = 1.0f,
val format: CameraDeviceFormat? = null
) {
enum class Template {
RECORD,
PHOTO,
PHOTO_ZSL,
PHOTO_SNAPSHOT,
PREVIEW;
fun toRequestTemplate(): Int =
when (this) {
RECORD -> CameraDevice.TEMPLATE_RECORD
PHOTO -> CameraDevice.TEMPLATE_STILL_CAPTURE
PHOTO_ZSL -> CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG
PHOTO_SNAPSHOT -> CameraDevice.TEMPLATE_VIDEO_SNAPSHOT
PREVIEW -> CameraDevice.TEMPLATE_PREVIEW
}
}
abstract fun createCaptureRequest(
device: CameraDevice,
deviceDetails: CameraDeviceDetails,
outputs: List<SurfaceOutput>
): CaptureRequest.Builder
protected open fun createCaptureRequest(
template: Template,
device: CameraDevice,
deviceDetails: CameraDeviceDetails,
outputs: List<SurfaceOutput>
): CaptureRequest.Builder {
val builder = device.createCaptureRequest(template.toRequestTemplate())
// Add all repeating output surfaces
outputs.forEach { output ->
builder.addTarget(output.surface)
}
// Set HDR
if (enableVideoHdr) {
if (format == null) throw PropRequiresFormatToBeNonNullError("videoHdr")
if (!format.supportsVideoHdr) throw InvalidVideoHdrError()
builder.set(CaptureRequest.CONTROL_SCENE_MODE, CaptureRequest.CONTROL_SCENE_MODE_HDR)
} else if (enableLowLightBoost) {
if (!deviceDetails.supportsLowLightBoost) throw LowLightBoostNotSupportedError()
builder.set(CaptureRequest.CONTROL_SCENE_MODE, CaptureRequest.CONTROL_SCENE_MODE_NIGHT)
}
// Set Exposure Bias
if (exposureBias != null) {
val clamped = deviceDetails.exposureRange.clamp(exposureBias.toInt())
builder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, clamped)
}
// Set Zoom
builder.setZoom(zoom, deviceDetails.characteristics)
// Set Torch
if (torch == Torch.ON) {
if (!deviceDetails.hasFlash) throw FlashUnavailableError()
builder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH)
}
return builder
}
}

View File

@ -0,0 +1,113 @@
package com.mrousavy.camera.core.capture
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CaptureRequest
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.Orientation
import com.mrousavy.camera.types.QualityPrioritization
import com.mrousavy.camera.types.Torch
class PhotoCaptureRequest(
repeatingRequest: RepeatingCaptureRequest,
private val qualityPrioritization: QualityPrioritization,
private val flash: Flash,
private val enableRedEyeReduction: Boolean,
private val enableAutoStabilization: Boolean,
enablePhotoHdr: Boolean,
private val outputOrientation: Orientation
) : CameraCaptureRequest(
Torch.OFF,
enablePhotoHdr,
repeatingRequest.enableLowLightBoost,
repeatingRequest.exposureBias,
repeatingRequest.zoom,
repeatingRequest.format
) {
companion object {
private const val TAG = "PhotoCaptureRequest"
}
override fun createCaptureRequest(
device: CameraDevice,
deviceDetails: CameraDeviceDetails,
outputs: List<SurfaceOutput>
): CaptureRequest.Builder {
val template = when (qualityPrioritization) {
QualityPrioritization.QUALITY -> Template.PHOTO
QualityPrioritization.BALANCED -> {
if (deviceDetails.supportsZsl) {
Template.PHOTO_ZSL
} else {
Template.PHOTO
}
}
QualityPrioritization.SPEED -> {
if (deviceDetails.supportsSnapshotCapture) {
Template.PHOTO_SNAPSHOT
} else if (deviceDetails.supportsZsl) {
Template.PHOTO_ZSL
} else {
Template.PHOTO
}
}
}
Log.i(TAG, "Using CaptureRequest Template $template...")
return this.createCaptureRequest(template, device, deviceDetails, outputs)
}
override fun createCaptureRequest(
template: Template,
device: CameraDevice,
deviceDetails: CameraDeviceDetails,
outputs: List<SurfaceOutput>
): 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
}
builder.set(CaptureRequest.JPEG_QUALITY, jpegQuality.toByte())
// Set JPEG Orientation
val targetOrientation = outputOrientation.toSensorRelativeOrientation(deviceDetails)
builder.set(CaptureRequest.JPEG_ORIENTATION, targetOrientation.toDegrees())
// TODO: Fix flash.
when (flash) {
// Set the Flash Mode
Flash.OFF -> {
builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON)
builder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF)
}
Flash.ON -> {
builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON)
builder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH)
}
Flash.AUTO -> {
if (enableRedEyeReduction) {
builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE)
} else {
builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH)
}
}
}
// Set stabilization for this Frame
if (enableAutoStabilization) {
if (deviceDetails.opticalStabilizationModes.contains(CameraCharacteristics.LENS_OPTICAL_STABILIZATION_MODE_ON)) {
builder.set(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE, CameraCharacteristics.LENS_OPTICAL_STABILIZATION_MODE_ON)
} else if (deviceDetails.digitalStabilizationModes.contains(CameraCharacteristics.CONTROL_VIDEO_STABILIZATION_MODE_ON)) {
builder.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_ON)
}
}
return builder
}
}

View File

@ -0,0 +1,79 @@
package com.mrousavy.camera.core.capture
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CaptureRequest
import android.util.Range
import com.mrousavy.camera.core.CameraDeviceDetails
import com.mrousavy.camera.core.InvalidFpsError
import com.mrousavy.camera.core.InvalidVideoStabilizationMode
import com.mrousavy.camera.core.PropRequiresFormatToBeNonNullError
import com.mrousavy.camera.core.outputs.SurfaceOutput
import com.mrousavy.camera.types.CameraDeviceFormat
import com.mrousavy.camera.types.Torch
import com.mrousavy.camera.types.VideoStabilizationMode
class RepeatingCaptureRequest(
private val enableVideoPipeline: Boolean,
torch: Torch = Torch.OFF,
private val fps: Int? = null,
private val videoStabilizationMode: VideoStabilizationMode = VideoStabilizationMode.OFF,
enableVideoHdr: Boolean = false,
enableLowLightBoost: Boolean = false,
exposureBias: Double? = null,
zoom: Float = 1.0f,
format: CameraDeviceFormat? = null
) : CameraCaptureRequest(torch, enableVideoHdr, enableLowLightBoost, exposureBias, zoom, format) {
override fun createCaptureRequest(
device: CameraDevice,
deviceDetails: CameraDeviceDetails,
outputs: List<SurfaceOutput>
): CaptureRequest.Builder {
val template = if (enableVideoPipeline) Template.RECORD else Template.PREVIEW
return this.createCaptureRequest(template, device, deviceDetails, outputs)
}
private fun getBestDigitalStabilizationMode(deviceDetails: CameraDeviceDetails): Int {
if (deviceDetails.digitalStabilizationModes.contains(CameraCharacteristics.CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION)) {
return CameraCharacteristics.CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION
}
return CameraCharacteristics.CONTROL_VIDEO_STABILIZATION_MODE_ON
}
override fun createCaptureRequest(
template: Template,
device: CameraDevice,
deviceDetails: CameraDeviceDetails,
outputs: List<SurfaceOutput>
): CaptureRequest.Builder {
val builder = super.createCaptureRequest(template, device, deviceDetails, outputs)
// Set FPS
if (fps != null) {
if (format == null) throw PropRequiresFormatToBeNonNullError("fps")
if (format.maxFps < fps) throw InvalidFpsError(fps)
builder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, Range(fps, fps))
}
// Set Video Stabilization
if (videoStabilizationMode != VideoStabilizationMode.OFF) {
if (format == null) throw PropRequiresFormatToBeNonNullError("videoStabilizationMode")
if (!format.videoStabilizationModes.contains(videoStabilizationMode)) {
throw InvalidVideoStabilizationMode(videoStabilizationMode)
}
}
when (videoStabilizationMode) {
VideoStabilizationMode.OFF -> {
// do nothing
}
VideoStabilizationMode.STANDARD -> {
builder.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, getBestDigitalStabilizationMode(deviceDetails))
}
VideoStabilizationMode.CINEMATIC, VideoStabilizationMode.CINEMATIC_EXTENDED -> {
builder.set(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE, CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE_ON)
}
}
return builder
}
}

View File

@ -10,13 +10,7 @@ import android.view.Surface
import androidx.annotation.RequiresApi
import java.io.Closeable
open class SurfaceOutput(
val surface: Surface,
val size: Size,
val outputType: OutputType,
val enableHdr: Boolean = false,
private val closeSurfaceOnEnd: Boolean = false
) : Closeable {
open class SurfaceOutput(val surface: Surface, val size: Size, val outputType: OutputType, val enableHdr: Boolean = false) : Closeable {
companion object {
const val TAG = "SurfaceOutput"
@ -52,12 +46,18 @@ open class SurfaceOutput(
return result
}
val isRepeating: Boolean
get() {
return when (outputType) {
OutputType.VIDEO, OutputType.PREVIEW, OutputType.VIDEO_AND_PREVIEW -> true
OutputType.PHOTO -> false
}
}
override fun toString(): String = "$outputType (${size.width} x ${size.height})"
override fun close() {
if (closeSurfaceOnEnd) {
surface.release()
}
// close() does nothing by default
}
enum class OutputType {

View File

@ -0,0 +1,9 @@
package com.mrousavy.camera.extensions
import android.hardware.camera2.CameraCaptureSession
fun CameraCaptureSession.tryAbortCaptures() {
try {
abortCaptures()
} catch (_: Throwable) {}
}

View File

@ -1,104 +0,0 @@
package com.mrousavy.camera.extensions
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CameraManager
import android.hardware.camera2.CaptureRequest
import android.view.Surface
import com.mrousavy.camera.types.Flash
import com.mrousavy.camera.types.Orientation
import com.mrousavy.camera.types.QualityPrioritization
private fun supportsSnapshotCapture(cameraCharacteristics: CameraCharacteristics): Boolean {
// As per CameraDevice.TEMPLATE_VIDEO_SNAPSHOT in documentation:
val hardwareLevel = cameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)!!
if (hardwareLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) return false
val capabilities = cameraCharacteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)!!
val hasDepth = capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT)
val isBackwardsCompatible = !capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE)
if (hasDepth && !isBackwardsCompatible) return false
return true
}
fun CameraDevice.createPhotoCaptureRequest(
cameraManager: CameraManager,
surface: Surface,
zoom: Float,
qualityPrioritization: QualityPrioritization,
flashMode: Flash,
enableRedEyeReduction: Boolean,
enableAutoStabilization: Boolean,
enableHdr: Boolean,
orientation: Orientation
): CaptureRequest {
val cameraCharacteristics = cameraManager.getCameraCharacteristics(this.id)
val template = if (qualityPrioritization == QualityPrioritization.SPEED && supportsSnapshotCapture(cameraCharacteristics)) {
CameraDevice.TEMPLATE_VIDEO_SNAPSHOT
} else {
CameraDevice.TEMPLATE_STILL_CAPTURE
}
val captureRequest = this.createCaptureRequest(template)
captureRequest.addTarget(surface)
// TODO: Maybe we can even expose that prop directly?
val jpegQuality = when (qualityPrioritization) {
QualityPrioritization.SPEED -> 85
QualityPrioritization.BALANCED -> 92
QualityPrioritization.QUALITY -> 100
}
captureRequest.set(CaptureRequest.JPEG_QUALITY, jpegQuality.toByte())
captureRequest.set(CaptureRequest.JPEG_ORIENTATION, orientation.toDegrees())
// TODO: Use the same options as from the preview request. This is duplicate code!
when (flashMode) {
// Set the Flash Mode
Flash.OFF -> {
captureRequest.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON)
captureRequest.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF)
}
Flash.ON -> {
captureRequest.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON)
captureRequest.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH)
}
Flash.AUTO -> {
if (enableRedEyeReduction) {
captureRequest.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE)
} else {
captureRequest.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH)
}
}
}
if (enableAutoStabilization) {
// Enable optical or digital image stabilization
val digitalStabilization = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES)
val hasDigitalStabilization = digitalStabilization?.contains(CameraCharacteristics.CONTROL_VIDEO_STABILIZATION_MODE_ON) ?: false
val opticalStabilization = cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_OPTICAL_STABILIZATION)
val hasOpticalStabilization = opticalStabilization?.contains(CameraCharacteristics.LENS_OPTICAL_STABILIZATION_MODE_ON) ?: false
if (hasOpticalStabilization) {
captureRequest.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_OFF)
captureRequest.set(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE, CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE_ON)
} else if (hasDigitalStabilization) {
captureRequest.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_ON)
} else {
// no stabilization is supported. ignore it
}
}
// TODO: Check if that zoom value is even supported.
captureRequest.setZoom(zoom, cameraCharacteristics)
// Set HDR
// TODO: Check if that value is even supported
if (enableHdr) {
captureRequest.set(CaptureRequest.CONTROL_SCENE_MODE, CaptureRequest.CONTROL_SCENE_MODE_HDR)
}
return captureRequest.build()
}

View File

@ -0,0 +1,13 @@
package com.mrousavy.camera.extensions
import android.hardware.camera2.CameraDevice
val CameraDevice.isValid: Boolean
get() {
try {
this.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
return true
} catch (e: Throwable) {
return false
}
}

View File

@ -1,6 +1,6 @@
package com.mrousavy.camera.types
import android.hardware.camera2.CameraCharacteristics
import com.mrousavy.camera.core.CameraDeviceDetails
enum class Orientation(override val unionValue: String) : JSUnionValue {
PORTRAIT("portrait"),
@ -16,18 +16,17 @@ enum class Orientation(override val unionValue: String) : JSUnionValue {
LANDSCAPE_LEFT -> 270
}
fun toSensorRelativeOrientation(cameraCharacteristics: CameraCharacteristics): Orientation {
val sensorOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!!
fun toSensorRelativeOrientation(deviceDetails: CameraDeviceDetails): Orientation {
// Convert target orientation to rotation degrees (0, 90, 180, 270)
var rotationDegrees = this.toDegrees()
// Reverse device orientation for front-facing cameras
val facingFront = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT
if (facingFront) rotationDegrees = -rotationDegrees
if (deviceDetails.lensFacing == LensFacing.FRONT) {
rotationDegrees = -rotationDegrees
}
// Rotate sensor rotation by target rotation
val newRotationDegrees = (sensorOrientation + rotationDegrees + 360) % 360
val newRotationDegrees = (deviceDetails.sensorOrientation + rotationDegrees + 360) % 360
return fromRotationDegrees(newRotationDegrees)
}

View File

@ -13,21 +13,6 @@ enum class VideoStabilizationMode(override val unionValue: String) : JSUnionValu
CINEMATIC("cinematic"),
CINEMATIC_EXTENDED("cinematic-extended");
fun toDigitalStabilizationMode(): Int =
when (this) {
OFF -> CONTROL_VIDEO_STABILIZATION_MODE_OFF
STANDARD -> CONTROL_VIDEO_STABILIZATION_MODE_ON
CINEMATIC -> 2 // TODO: CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION
else -> CONTROL_VIDEO_STABILIZATION_MODE_OFF
}
fun toOpticalStabilizationMode(): Int =
when (this) {
OFF -> LENS_OPTICAL_STABILIZATION_MODE_OFF
CINEMATIC_EXTENDED -> LENS_OPTICAL_STABILIZATION_MODE_ON
else -> LENS_OPTICAL_STABILIZATION_MODE_OFF
}
companion object : JSUnionValue.Companion<VideoStabilizationMode> {
override fun fromUnionValue(unionValue: String?): VideoStabilizationMode =
when (unionValue) {

View File

@ -484,7 +484,7 @@ PODS:
- libwebp (~> 1.0)
- SDWebImage/Core (~> 5.10)
- SocketRocket (0.6.1)
- VisionCamera (3.8.2):
- VisionCamera (3.9.0-beta.0):
- React
- React-callinvoker
- React-Core
@ -724,7 +724,7 @@ SPEC CHECKSUMS:
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
VisionCamera: edbcd00e27a438b2228f67823e2b8d15a189065f
VisionCamera: f2f2fa58be438670ef5d5aa88846ffe59a78f7a8
Yoga: 4c3aa327e4a6a23eeacd71f61c81df1bcdf677d5
PODFILE CHECKSUM: 27f53791141a3303d814e09b55770336416ff4eb

View File

@ -172,17 +172,19 @@ export function CameraPage({ navigation }: Props): React.ReactElement {
<Reanimated.View style={StyleSheet.absoluteFill}>
<TapGestureHandler onEnded={onDoubleTap} numberOfTaps={2}>
<ReanimatedCamera
ref={camera}
style={StyleSheet.absoluteFill}
device={device}
isActive={isActive}
ref={camera}
onInitialized={onInitialized}
onError={onError}
onStarted={() => 'Camera started!'}
onStopped={() => 'Camera stopped!'}
format={format}
fps={fps}
photoHdr={enableHdr}
videoHdr={enableHdr}
lowLightBoost={device.supportsLowLightBoost && enableNightMode}
isActive={isActive}
onInitialized={onInitialized}
onError={onError}
enableZoomGesture={false}
animatedProps={cameraAnimatedProps}
exposure={0}

View File

@ -25,6 +25,7 @@ export type SessionError =
| 'session/camera-cannot-be-opened'
| 'session/camera-has-been-disconnected'
| 'session/audio-in-use-by-other-app'
| 'session/no-outputs'
| 'session/audio-session-failed-to-activate'
export type CodeScannerError =
| 'code-scanner/not-compatible-with-outputs'