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:
parent
cd5fdd4924
commit
5acc64e031
@ -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() {
|
||||
|
@ -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() {
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
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
|
||||
?: 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
|
||||
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)"
|
||||
}
|
||||
) ?: "$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()) {
|
||||
return@lazy range ?: Range(1f, maxDigitalZoom)
|
||||
}
|
||||
val physicalDevices by lazy {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && characteristics.physicalCameraIds.isNotEmpty()) {
|
||||
characteristics.physicalCameraIds
|
||||
} else {
|
||||
setOf(cameraId)
|
||||
}
|
||||
val minZoom = zoomRange.lower.toDouble()
|
||||
val maxZoom = zoomRange.upper.toDouble()
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -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!")
|
||||
|
@ -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
|
||||
val status = ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
|
||||
if (status != PackageManager.PERMISSION_GRANTED) throw CameraPermissionError()
|
||||
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!")
|
||||
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,
|
||||
|
@ -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)
|
||||
}
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -0,0 +1,9 @@
|
||||
package com.mrousavy.camera.extensions
|
||||
|
||||
import android.hardware.camera2.CameraCaptureSession
|
||||
|
||||
fun CameraCaptureSession.tryAbortCaptures() {
|
||||
try {
|
||||
abortCaptures()
|
||||
} catch (_: Throwable) {}
|
||||
}
|
@ -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()
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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'
|
||||
|
Loading…
Reference in New Issue
Block a user