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() { OpenGLRenderer::~OpenGLRenderer() {
__android_log_print(ANDROID_LOG_INFO, TAG, "Destroying OpenGLRenderer...");
destroy();
if (_outputSurface != nullptr) { if (_outputSurface != nullptr) {
ANativeWindow_release(_outputSurface); ANativeWindow_release(_outputSurface);
} }
destroy();
} }
void OpenGLRenderer::destroy() { void OpenGLRenderer::destroy() {

View File

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

View File

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

View File

@ -1,7 +1,9 @@
package com.mrousavy.camera.core package com.mrousavy.camera.core
import android.annotation.SuppressLint
import android.graphics.ImageFormat import android.graphics.ImageFormat
import android.hardware.camera2.CameraCharacteristics import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraExtensionCharacteristics
import android.hardware.camera2.CameraManager import android.hardware.camera2.CameraManager
import android.hardware.camera2.CameraMetadata import android.hardware.camera2.CameraMetadata
import android.os.Build import android.os.Build
@ -23,61 +25,73 @@ import com.mrousavy.camera.types.VideoStabilizationMode
import kotlin.math.atan2 import kotlin.math.atan2
import kotlin.math.sqrt import kotlin.math.sqrt
class CameraDeviceDetails(val cameraManager: CameraManager, val cameraId: String) { @SuppressLint("InlinedApi")
val characteristics = cameraManager.getCameraCharacteristics(cameraId) class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId: String) {
val hardwareLevel = HardwareLevel.fromCameraCharacteristics(characteristics) val characteristics by lazy { cameraManager.getCameraCharacteristics(cameraId) }
val capabilities = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES) ?: IntArray(0) val hardwareLevel by lazy { HardwareLevel.fromCameraCharacteristics(characteristics) }
val extensions = getSupportedExtensions() val capabilities by lazy { characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES) ?: IntArray(0) }
val extensions by lazy { getSupportedExtensions() }
// device characteristics // device characteristics
val isMultiCam = capabilities.contains(11) // TODO: CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA val isMultiCam by lazy { capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA) }
val supportsDepthCapture = capabilities.contains(8) // TODO: CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT val supportsDepthCapture by lazy { capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT) }
val supportsRawCapture = capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW) val supportsRawCapture by lazy { capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW) }
val supportsLowLightBoost = extensions.contains(4) // TODO: CameraExtensionCharacteristics.EXTENSION_NIGHT val supportsLowLightBoost by lazy { extensions.contains(CameraExtensionCharacteristics.EXTENSION_NIGHT) }
val lensFacing = LensFacing.fromCameraCharacteristics(characteristics) val lensFacing by lazy { LensFacing.fromCameraCharacteristics(characteristics) }
val hasFlash = characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) ?: false val hasFlash by lazy { characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) ?: false }
val focalLengths = val focalLengths by lazy {
characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS) // 35mm is the film standard sensor size
// 35mm is the film standard sensor size characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS) ?: floatArrayOf(35f)
?: floatArrayOf(35f) }
val sensorSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)!! val sensorSize by lazy { characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)!! }
val sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!! val sensorOrientation by lazy { characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0 }
val minFocusDistance = getMinFocusDistanceCm() val minFocusDistance by lazy { getMinFocusDistanceCm() }
val name = ( val name by lazy {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { val info = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) characteristics.get(CameraCharacteristics.INFO_VERSION) else null
characteristics.get(CameraCharacteristics.INFO_VERSION) return@lazy info ?: "$lensFacing ($cameraId)"
} else { }
null
}
) ?: "$lensFacing ($cameraId)"
// "formats" (all possible configurations for this device) // "formats" (all possible configurations for this device)
val zoomRange = ( val maxDigitalZoom by lazy { characteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM) ?: 1f }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { val zoomRange by lazy {
val range = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
characteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE) characteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE)
} else { } else {
null null
} }
) ?: Range(1f, characteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM) ?: 1f) return@lazy range ?: Range(1f, maxDigitalZoom)
val physicalDevices = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && characteristics.physicalCameraIds.isNotEmpty()) {
characteristics.physicalCameraIds
} else {
setOf(cameraId)
} }
val minZoom = zoomRange.lower.toDouble() val physicalDevices by lazy {
val maxZoom = zoomRange.upper.toDouble() 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 cameraConfig by lazy { characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!! }
val isoRange = characteristics.get(CameraCharacteristics.SENSOR_INFO_SENSITIVITY_RANGE) ?: Range(0, 0) val isoRange by lazy { characteristics.get(CameraCharacteristics.SENSOR_INFO_SENSITIVITY_RANGE) ?: Range(0, 0) }
val exposureRange = characteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE) ?: Range(0, 0) val exposureRange by lazy { characteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE) ?: Range(0, 0) }
val digitalStabilizationModes = val digitalStabilizationModes by lazy {
characteristics.get(CameraCharacteristics.CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES) ?: IntArray(0) 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) characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_OPTICAL_STABILIZATION) ?: IntArray(0)
val supportsPhotoHdr = extensions.contains(3) // TODO: CameraExtensionCharacteristics.EXTENSION_HDR }
val supportsVideoHdr = getHasVideoHdr() val supportsPhotoHdr by lazy { extensions.contains(CameraExtensionCharacteristics.EXTENSION_HDR) }
val autoFocusSystem = getAutoFocusSystemMode() 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 val videoFormat = ImageFormat.YUV_420_888
// get extensions (HDR, Night Mode, ..) // get extensions (HDR, Night Mode, ..)
@ -107,6 +121,14 @@ class CameraDeviceDetails(val cameraManager: CameraManager, val cameraId: String
return 1.0 / distance * 100.0 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 { private fun createStabilizationModes(): ReadableArray {
val array = Arguments.createArray() val array = Arguments.createArray()
digitalStabilizationModes.forEach { videoStabilizationMode -> 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 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!") CameraError("session", "cannot-create-session", "Failed to create a Camera Session for Camera #$cameraId!")
class CameraDisconnectedError(cameraId: String, error: CameraDeviceError) : class CameraDisconnectedError(cameraId: String, error: CameraDeviceError) :
CameraError("session", "camera-has-been-disconnected", "The given Camera device (id: $cameraId) has been disconnected! Error: $error") 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) : class PropRequiresFormatToBeNonNullError(propName: String) :
CameraError("format", "format-required", "The prop \"$propName\" requires a format to be set, but format was null!") 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.content.pm.PackageManager
import android.graphics.ImageFormat import android.graphics.ImageFormat
import android.graphics.Point import android.graphics.Point
import android.hardware.camera2.CameraCaptureSession
import android.hardware.camera2.CameraCharacteristics import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CameraManager import android.hardware.camera2.CameraManager
import android.hardware.camera2.CameraMetadata
import android.hardware.camera2.CaptureRequest
import android.hardware.camera2.CaptureResult import android.hardware.camera2.CaptureResult
import android.hardware.camera2.TotalCaptureResult import android.hardware.camera2.TotalCaptureResult
import android.hardware.camera2.params.MeteringRectangle
import android.media.Image import android.media.Image
import android.media.ImageReader import android.media.ImageReader
import android.os.Build
import android.util.Log import android.util.Log
import android.util.Range
import android.util.Size import android.util.Size
import android.view.Surface import android.view.Surface
import android.view.SurfaceHolder import android.view.SurfaceHolder
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.google.mlkit.vision.barcode.common.Barcode 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.BarcodeScannerOutput
import com.mrousavy.camera.core.outputs.PhotoOutput import com.mrousavy.camera.core.outputs.PhotoOutput
import com.mrousavy.camera.core.outputs.SurfaceOutput import com.mrousavy.camera.core.outputs.SurfaceOutput
import com.mrousavy.camera.core.outputs.VideoPipelineOutput import com.mrousavy.camera.core.outputs.VideoPipelineOutput
import com.mrousavy.camera.extensions.capture
import com.mrousavy.camera.extensions.closestToOrMax 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.getPhotoSizes
import com.mrousavy.camera.extensions.getPreviewTargetSize import com.mrousavy.camera.extensions.getPreviewTargetSize
import com.mrousavy.camera.extensions.getVideoSizes 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.frameprocessor.Frame
import com.mrousavy.camera.types.Flash import com.mrousavy.camera.types.Flash
import com.mrousavy.camera.types.LensFacing
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.RecordVideoOptions 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 com.mrousavy.camera.utils.ImageFormatUtils
import java.io.Closeable import java.io.Closeable
import java.lang.IllegalStateException import kotlin.coroutines.cancellation.CancellationException
import java.util.concurrent.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -55,8 +42,8 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
class CameraSession(private val context: Context, private val cameraManager: CameraManager, private val callback: Callback) : class CameraSession(private val context: Context, private val cameraManager: CameraManager, private val callback: Callback) :
CameraManager.AvailabilityCallback(), Closeable,
Closeable { PersistentCameraCaptureSession.Callback {
companion object { companion object {
private const val TAG = "CameraSession" private const val TAG = "CameraSession"
} }
@ -65,14 +52,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
private var configuration: CameraConfiguration? = null private var configuration: CameraConfiguration? = null
// Camera State // Camera State
private var cameraDevice: CameraDevice? = null private val captureSession = PersistentCameraCaptureSession(cameraManager, this)
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 var photoOutput: PhotoOutput? = null private var photoOutput: PhotoOutput? = null
private var videoOutput: VideoPipelineOutput? = null private var videoOutput: VideoPipelineOutput? = null
private var codeScannerOutput: BarcodeScannerOutput? = null private var codeScannerOutput: BarcodeScannerOutput? = null
@ -109,14 +89,9 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
return Orientation.fromRotationDegrees(sensorRotation) return Orientation.fromRotationDegrees(sensorRotation)
} }
init {
cameraManager.registerAvailabilityCallback(this, CameraQueues.cameraQueue.handler)
}
override fun close() { override fun close() {
Log.i(TAG, "Closing CameraSession...") Log.i(TAG, "Closing CameraSession...")
isDestroyed = true isDestroyed = true
cameraManager.unregisterAvailabilityCallback(this)
runBlocking { runBlocking {
mutex.withLock { mutex.withLock {
destroy() destroy()
@ -126,18 +101,6 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
Log.i(TAG, "CameraSession closed!") 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) { suspend fun configure(lambda: (configuration: CameraConfiguration) -> Unit) {
Log.i(TAG, "configure { ... }: Waiting for lock...") 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) val config = CameraConfiguration.copyOf(this.configuration)
lambda(config) lambda(config)
val diff = CameraConfiguration.difference(this.configuration, 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) { if (isDestroyed) {
Log.i(TAG, "CameraSession is already destroyed. Skipping configure { ... }") 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") Log.i(TAG, "configure { ... }: Updating CameraSession Configuration... $diff")
try { try {
val needsRebuild = cameraDevice == null || captureSession == null captureSession.withConfiguration {
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
// Build up session or update any props // Build up session or update any props
if (diff.deviceChanged) { if (diff.deviceChanged) {
// 1. cameraId changed, open device // 1. cameraId changed, open device
configureCameraDevice(config) configureInput(config)
} }
if (diff.outputsChanged) { if (diff.outputsChanged) {
// 2. outputs changed, build new session // 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 // 3. zoom etc changed, update repeating request
configureCaptureRequest(config) 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}") Log.i(
this.configuration = config TAG,
"configure { ... }: Completed CameraSession Configuration! (isActive: ${config.isActive}, isRunning: ${captureSession.isRunning})"
)
isRunning = captureSession.isRunning
// Notify about Camera initialization // Notify about Camera initialization
if (diff.deviceChanged) { if (diff.deviceChanged) {
@ -205,8 +164,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
private fun destroy() { private fun destroy() {
Log.i(TAG, "Destroying session..") Log.i(TAG, "Destroying session..")
cameraDevice?.close() captureSession.close()
cameraDevice = null
photoOutput?.close() photoOutput?.close()
photoOutput = null photoOutput = null
@ -262,66 +220,20 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
Log.i(TAG, "Preview Output destroyed!") Log.i(TAG, "Preview Output destroyed!")
} }
/** private fun configureInput(configuration: CameraConfiguration) {
* Set up the `CameraDevice` (`cameraId`) Log.i(TAG, "Configuring inputs for CameraSession...")
*/
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
val cameraId = configuration.cameraId ?: throw NoCameraDeviceError() val cameraId = configuration.cameraId ?: throw NoCameraDeviceError()
Log.i(TAG, "Configuring Camera #$cameraId...") val status = ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
cameraDevice = cameraManager.openCamera(cameraId, { device, error -> if (status != PackageManager.PERMISSION_GRANTED) throw CameraPermissionError()
if (cameraDevice != device) { isRunning = false
// a previous device has been disconnected, but we already have a new one. captureSession.setInput(cameraId)
// 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!")
} }
/** /**
* Set up the `CaptureSession` with all outputs (preview, photo, video, codeScanner) and their HDR/Format settings. * Set up the `CaptureSession` with all outputs (preview, photo, video, codeScanner) and their HDR/Format settings.
*/ */
private suspend fun configureOutputs(configuration: CameraConfiguration) { private fun configureOutputs(configuration: CameraConfiguration) {
if (!configuration.isActive) { val cameraId = configuration.cameraId ?: throw NoCameraDeviceError()
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
}
// Destroy previous outputs // Destroy previous outputs
Log.i(TAG, "Destroying previous outputs...") Log.i(TAG, "Destroying previous outputs...")
@ -333,10 +245,10 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
codeScannerOutput = null codeScannerOutput = null
isRunning = false isRunning = false
val characteristics = cameraManager.getCameraCharacteristics(cameraDevice.id) val characteristics = cameraManager.getCameraCharacteristics(cameraId)
val format = configuration.format 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 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> val video = configuration.video as? CameraConfiguration.Output.Enabled<CameraConfiguration.Video>
if (video != null) { if (video != null) {
val imageFormat = video.config.pixelFormat.toImageFormat() 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) val size = sizes.closestToOrMax(format?.videoSize)
Log.i(TAG, "Adding ${size.width}x${size.height} Video Output in ${ImageFormatUtils.imageFormatToString(imageFormat)}...") 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 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)) val size = sizes.closestToOrMax(Size(1280, 720))
Log.i(TAG, "Adding ${size.width}x${size.height} CodeScanner Output in ${ImageFormatUtils.imageFormatToString(imageFormat)}...") 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 // Create session
captureSession = cameraDevice.createCaptureSession(cameraManager, outputs, { session -> captureSession.setOutputs(outputs)
if (this.captureSession != session) {
// a previous session has been closed, but we already have a new one.
// this is just normal behavior
return@createCaptureSession
}
// onClosed Log.i(TAG, "Successfully configured Session with ${outputs.size} outputs for Camera #$cameraId!")
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}!")
// Update Frame Processor and RecordingSession for newly changed output // Update Frame Processor and RecordingSession for newly changed output
updateVideoOutputs() 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) { 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) { captureSession.setRepeatingRequest(
isRunning = false RepeatingCaptureRequest(
try { enableVideo,
captureSession?.stopRepeating() config.torch,
} catch (e: IllegalStateException) { config.fps,
// ignore - captureSession is already closed. config.videoStabilizationMode,
} enableVideoHdr,
return config.enableLowLightBoost,
} config.exposure,
if (captureSession == null) { config.zoom,
Log.i(TAG, "CameraSession hasn't configured the capture session, skipping CaptureRequest...") config.format
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
} }
suspend fun takePhoto( suspend fun takePhoto(
qualityPrioritization: QualityPrioritization, qualityPrioritization: QualityPrioritization,
flashMode: Flash, flash: Flash,
enableShutterSound: Boolean, enableShutterSound: Boolean,
enableRedEyeReduction: Boolean, enableRedEyeReduction: Boolean,
enableAutoStabilization: Boolean, enableAutoStabilization: Boolean,
outputOrientation: Orientation outputOrientation: Orientation
): CapturedPhoto { ): CapturedPhoto {
val captureSession = captureSession ?: throw CameraNotReadyError()
val photoOutput = photoOutput ?: throw PhotoNotEnabledError() val photoOutput = photoOutput ?: throw PhotoNotEnabledError()
Log.i(TAG, "Photo capture 0/3 - preparing capture request (${photoOutput.size.width}x${photoOutput.size.height})...") Log.i(TAG, "Photo capture 1/3 - capturing ${photoOutput.size.width}x${photoOutput.size.height} image...")
val result = captureSession.capture(
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,
qualityPrioritization, qualityPrioritization,
flashMode, flash,
enableRedEyeReduction, enableRedEyeReduction,
enableAutoStabilization, enableAutoStabilization,
photoOutput.enableHdr, 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 { 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 image = photoOutputSynchronizer.await(timestamp)
val isMirrored = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT Log.i(TAG, "Photo capture 3/3 - received ${image.width} x ${image.height} image, preparing result...")
val deviceDetails = captureSession.getActiveDeviceDetails()
Log.i(TAG, "Photo capture 3/3 complete - received ${image.width} x ${image.height} image.") val isMirrored = deviceDetails?.lensFacing == LensFacing.FRONT
return CapturedPhoto(image, result, orientation, isMirrored, image.format) return CapturedPhoto(image, result, orientation, isMirrored, image.format)
} catch (e: CancellationException) { } catch (e: CancellationException) {
throw CaptureAbortedError(false) throw CaptureAbortedError(false)
@ -620,13 +420,13 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
mutex.withLock { mutex.withLock {
if (recording != null) throw RecordingInProgressError() if (recording != null) throw RecordingInProgressError()
val videoOutput = videoOutput ?: throw VideoNotEnabledError() val videoOutput = videoOutput ?: throw VideoNotEnabledError()
val cameraDevice = cameraDevice ?: throw CameraNotReadyError() val cameraId = configuration?.cameraId ?: throw NoCameraDeviceError()
val fps = configuration?.fps ?: 30 val fps = configuration?.fps ?: 30
val recording = RecordingSession( val recording = RecordingSession(
context, context,
cameraDevice.id, cameraId,
videoOutput.size, videoOutput.size,
enableAudio, enableAudio,
fps, 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!") suspend fun focus(x: Int, y: Int): Unit = throw NotImplementedError("focus() is not yet implemented!")
private suspend fun focus(point: Point) { private suspend fun focus(point: Point): Unit = throw NotImplementedError()
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)
}
}
data class CapturedPhoto( data class CapturedPhoto(
val image: Image, 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 isActive = false
imageWriter?.close() imageWriter?.close()
imageReader?.close() imageReader?.close()
removeRecordingSessionOutputSurface()
recordingSession = null recordingSession = null
surfaceTexture.setOnFrameAvailableListener(null, null)
surfaceTexture.release() surfaceTexture.release()
surface.release()
} }
} }
@ -170,7 +173,7 @@ class VideoPipeline(
synchronized(this) { synchronized(this) {
if (recordingSession != null) { if (recordingSession != null) {
// Configure OpenGL pipeline to stream Frames into the Recording Session's surface // 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) setRecordingSessionOutputSurface(recordingSession.surface)
this.recordingSession = recordingSession this.recordingSession = recordingSession
} else { } 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 androidx.annotation.RequiresApi
import java.io.Closeable import java.io.Closeable
open class SurfaceOutput( open class SurfaceOutput(val surface: Surface, val size: Size, val outputType: OutputType, val enableHdr: Boolean = false) : Closeable {
val surface: Surface,
val size: Size,
val outputType: OutputType,
val enableHdr: Boolean = false,
private val closeSurfaceOnEnd: Boolean = false
) : Closeable {
companion object { companion object {
const val TAG = "SurfaceOutput" const val TAG = "SurfaceOutput"
@ -52,12 +46,18 @@ open class SurfaceOutput(
return result 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 toString(): String = "$outputType (${size.width} x ${size.height})"
override fun close() { override fun close() {
if (closeSurfaceOnEnd) { // close() does nothing by default
surface.release()
}
} }
enum class OutputType { 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 package com.mrousavy.camera.types
import android.hardware.camera2.CameraCharacteristics import com.mrousavy.camera.core.CameraDeviceDetails
enum class Orientation(override val unionValue: String) : JSUnionValue { enum class Orientation(override val unionValue: String) : JSUnionValue {
PORTRAIT("portrait"), PORTRAIT("portrait"),
@ -16,18 +16,17 @@ enum class Orientation(override val unionValue: String) : JSUnionValue {
LANDSCAPE_LEFT -> 270 LANDSCAPE_LEFT -> 270
} }
fun toSensorRelativeOrientation(cameraCharacteristics: CameraCharacteristics): Orientation { fun toSensorRelativeOrientation(deviceDetails: CameraDeviceDetails): Orientation {
val sensorOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!!
// Convert target orientation to rotation degrees (0, 90, 180, 270) // Convert target orientation to rotation degrees (0, 90, 180, 270)
var rotationDegrees = this.toDegrees() var rotationDegrees = this.toDegrees()
// Reverse device orientation for front-facing cameras // Reverse device orientation for front-facing cameras
val facingFront = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT if (deviceDetails.lensFacing == LensFacing.FRONT) {
if (facingFront) rotationDegrees = -rotationDegrees rotationDegrees = -rotationDegrees
}
// Rotate sensor rotation by target rotation // Rotate sensor rotation by target rotation
val newRotationDegrees = (sensorOrientation + rotationDegrees + 360) % 360 val newRotationDegrees = (deviceDetails.sensorOrientation + rotationDegrees + 360) % 360
return fromRotationDegrees(newRotationDegrees) return fromRotationDegrees(newRotationDegrees)
} }

View File

@ -13,21 +13,6 @@ enum class VideoStabilizationMode(override val unionValue: String) : JSUnionValu
CINEMATIC("cinematic"), CINEMATIC("cinematic"),
CINEMATIC_EXTENDED("cinematic-extended"); 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> { companion object : JSUnionValue.Companion<VideoStabilizationMode> {
override fun fromUnionValue(unionValue: String?): VideoStabilizationMode = override fun fromUnionValue(unionValue: String?): VideoStabilizationMode =
when (unionValue) { when (unionValue) {

View File

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

View File

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

View File

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