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() {
|
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() {
|
||||||
|
@ -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() {
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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!")
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
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 {
|
||||||
|
@ -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 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 {
|
||||||
|
@ -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
|
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)
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
|
@ -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}
|
||||||
|
@ -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'
|
||||||
|
Loading…
Reference in New Issue
Block a user