fix: Validate input props (fps, hdr, torch, ...) instead of silently crashing (#2354)

* fix: Fix Blackscreen by deterministically destroying session if `isActive=false`

* Re-open Camera if session died

* Simplify Camera

* Disconnect is optional, block when resetting state

* fix: Log in `configure { ... }`

* fix: Make concurrent configure safe

* fix: Don't resize preview

* fix: Use current `CameraConfiguration`

* Don't start if no outputs are available

* Only mount with preview outputs

* Update CameraSession.kt

* Update PreviewView.kt

* Better logging

* Update CameraSession.kt

* Extract

* fix: Rebuild entire session if `isActive` changed

* isActive safe

* Start session at 1

* Create ActiveCameraDevice.kt

* interrupts

* chore: Freeze `frame` in `useFrameProcessor`

* Revert "chore: Freeze `frame` in `useFrameProcessor`"

This reverts commit dff93d506e29a791d8dea8842b880ab5c892211e.

* chore: Better logging

* fix: Move HDR to `video`/`photo` config

* fix: Fix hdr usage

* fix: Ignore any updates after destroying Camera

* fix: Fix video HDR

* chore: Format code

* fix: Check Camera permission

* Remove unneeded error

* Update CameraSession.kt

* Update CameraPage.tsx

* Delete OutputConfiguration.toDebugString.kt

* Update CameraSession.kt

* fix: Perform sanity checks to make sure props are valid

* format
This commit is contained in:
Marc Rousavy 2024-01-08 12:13:05 +01:00 committed by GitHub
parent 0d21bc3a57
commit cc60ad296a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 94 additions and 39 deletions

View File

@ -23,26 +23,26 @@ import com.mrousavy.camera.types.VideoStabilizationMode
import kotlin.math.atan2 import kotlin.math.atan2
import kotlin.math.sqrt import kotlin.math.sqrt
class CameraDeviceDetails(private val cameraManager: CameraManager, private val cameraId: String) { class CameraDeviceDetails(val cameraManager: CameraManager, val cameraId: String) {
private val characteristics = cameraManager.getCameraCharacteristics(cameraId) val characteristics = cameraManager.getCameraCharacteristics(cameraId)
private val hardwareLevel = HardwareLevel.fromCameraCharacteristics(characteristics) val hardwareLevel = HardwareLevel.fromCameraCharacteristics(characteristics)
private val capabilities = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES) ?: IntArray(0) val capabilities = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES) ?: IntArray(0)
private val extensions = getSupportedExtensions() val extensions = getSupportedExtensions()
// device characteristics // device characteristics
private val isMultiCam = capabilities.contains(11) // TODO: CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA val isMultiCam = capabilities.contains(11) // TODO: CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA
private val supportsDepthCapture = capabilities.contains(8) // TODO: CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT val supportsDepthCapture = capabilities.contains(8) // TODO: CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT
private val supportsRawCapture = capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW) val supportsRawCapture = capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW)
private val supportsLowLightBoost = extensions.contains(4) // TODO: CameraExtensionCharacteristics.EXTENSION_NIGHT val supportsLowLightBoost = extensions.contains(4) // TODO: CameraExtensionCharacteristics.EXTENSION_NIGHT
private val lensFacing = LensFacing.fromCameraCharacteristics(characteristics) val lensFacing = LensFacing.fromCameraCharacteristics(characteristics)
private val hasFlash = characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) ?: false val hasFlash = characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) ?: false
private val focalLengths = val focalLengths =
characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS) characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS)
// 35mm is the film standard sensor size // 35mm is the film standard sensor size
?: floatArrayOf(35f) ?: floatArrayOf(35f)
private val sensorSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)!! val sensorSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)!!
private val sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!! val sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!!
private val name = ( val name = (
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
characteristics.get(CameraCharacteristics.INFO_VERSION) characteristics.get(CameraCharacteristics.INFO_VERSION)
} else { } else {
@ -51,32 +51,32 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, private val
) ?: "$lensFacing ($cameraId)" ) ?: "$lensFacing ($cameraId)"
// "formats" (all possible configurations for this device) // "formats" (all possible configurations for this device)
private val zoomRange = ( val zoomRange = (
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 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) ) ?: Range(1f, characteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM) ?: 1f)
private val physicalDevices = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && characteristics.physicalCameraIds.isNotEmpty()) { val physicalDevices = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && characteristics.physicalCameraIds.isNotEmpty()) {
characteristics.physicalCameraIds characteristics.physicalCameraIds
} else { } else {
setOf(cameraId) setOf(cameraId)
} }
private val minZoom = zoomRange.lower.toDouble() val minZoom = zoomRange.lower.toDouble()
private val maxZoom = zoomRange.upper.toDouble() val maxZoom = zoomRange.upper.toDouble()
private val cameraConfig = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!! val cameraConfig = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
private val isoRange = characteristics.get(CameraCharacteristics.SENSOR_INFO_SENSITIVITY_RANGE) ?: Range(0, 0) val isoRange = characteristics.get(CameraCharacteristics.SENSOR_INFO_SENSITIVITY_RANGE) ?: Range(0, 0)
private val exposureRange = characteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE) ?: Range(0, 0) val exposureRange = characteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE) ?: Range(0, 0)
private val digitalStabilizationModes = val digitalStabilizationModes =
characteristics.get(CameraCharacteristics.CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES) ?: IntArray(0) characteristics.get(CameraCharacteristics.CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES) ?: IntArray(0)
private val opticalStabilizationModes = val opticalStabilizationModes =
characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_OPTICAL_STABILIZATION) ?: IntArray(0) characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_OPTICAL_STABILIZATION) ?: IntArray(0)
private val supportsPhotoHdr = extensions.contains(3) // TODO: CameraExtensionCharacteristics.EXTENSION_HDR val supportsPhotoHdr = extensions.contains(3) // TODO: CameraExtensionCharacteristics.EXTENSION_HDR
private val supportsVideoHdr = getHasVideoHdr() val supportsVideoHdr = getHasVideoHdr()
private val videoFormat = ImageFormat.YUV_420_888 val videoFormat = ImageFormat.YUV_420_888
// get extensions (HDR, Night Mode, ..) // get extensions (HDR, Night Mode, ..)
private fun getSupportedExtensions(): List<Int> = private fun getSupportedExtensions(): List<Int> =

View File

@ -1,6 +1,7 @@
package com.mrousavy.camera.core package com.mrousavy.camera.core
import com.mrousavy.camera.types.CameraDeviceError import com.mrousavy.camera.types.CameraDeviceError
import com.mrousavy.camera.types.VideoStabilizationMode
abstract class CameraError( abstract class CameraError(
/** /**
@ -49,6 +50,18 @@ class NoCameraDeviceError :
) )
class PixelFormatNotSupportedError(format: String) : class PixelFormatNotSupportedError(format: String) :
CameraError("device", "pixel-format-not-supported", "The pixelFormat $format is not supported on the given Camera Device!") CameraError("device", "pixel-format-not-supported", "The pixelFormat $format is not supported on the given Camera Device!")
class LowLightBoostNotSupportedError :
CameraError(
"device",
"low-light-boost-not-supported",
"The currently selected camera device does not support low-light boost! Select a device where `device.supportsLowLightBoost` is true."
)
class FlashUnavailableError :
CameraError(
"device",
"flash-unavailable",
"The Camera Device does not have a flash unit! Make sure you select a device where `device.hasFlash`/`device.hasTorch` is true."
)
class CameraNotReadyError : class CameraNotReadyError :
CameraError("session", "camera-not-ready", "The Camera is not ready yet! Wait for the onInitialized() callback!") CameraError("session", "camera-not-ready", "The Camera is not ready yet! Wait for the onInitialized() callback!")
@ -59,6 +72,28 @@ class CameraSessionCannotBeConfiguredError(cameraId: String) :
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 PropRequiresFormatToBeNonNullError(propName: String) :
CameraError("format", "format-required", "The prop \"$propName\" requires a format to be set, but format was null!")
class InvalidFpsError(fps: Int) :
CameraError(
"format",
"invalid-fps",
"The given format cannot run at $fps FPS! Make sure your FPS is lower than `format.maxFps` but higher than `format.minFps`."
)
class InvalidVideoStabilizationMode(mode: VideoStabilizationMode) :
CameraError(
"format",
"invalid-video-stabilization-mode",
"The given format does not support the videoStabilizationMode \"${mode.unionValue}\"! " +
"Select a format that contains ${mode.unionValue} in `format.supportedVideoStabilizationModes`."
)
class InvalidVideoHdrError :
CameraError(
"format",
"invalid-video-hdr",
"The given format does not support videoHdr! Select a format where `format.supportsVideoHdr` is true."
)
class VideoNotEnabledError : class VideoNotEnabledError :
CameraError("capture", "video-not-enabled", "Video capture is disabled! Pass `video={true}` to enable video recordings.") CameraError("capture", "video-not-enabled", "Video capture is disabled! Pass `video={true}` to enable video recordings.")
class PhotoNotEnabledError : class PhotoNotEnabledError :

View File

@ -68,8 +68,13 @@ 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 captureSession: CameraCaptureSession? = null
private var cameraDevice: CameraDevice? = null 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 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
@ -430,27 +435,38 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
} }
private fun createRepeatingRequest(device: CameraDevice, targets: List<Surface>, config: CameraConfiguration): CaptureRequest { private fun createRepeatingRequest(device: CameraDevice, targets: List<Surface>, config: CameraConfiguration): CaptureRequest {
val cameraCharacteristics = cameraManager.getCameraCharacteristics(device.id) val deviceDetails = cameraDeviceDetails ?: CameraDeviceDetails(cameraManager, device.id)
val template = if (config.video.isEnabled) CameraDevice.TEMPLATE_RECORD else CameraDevice.TEMPLATE_PREVIEW val template = if (config.video.isEnabled) CameraDevice.TEMPLATE_RECORD else CameraDevice.TEMPLATE_PREVIEW
val captureRequest = device.createCaptureRequest(template) val captureRequest = device.createCaptureRequest(template)
targets.forEach { t -> captureRequest.addTarget(t) } targets.forEach { t -> captureRequest.addTarget(t) }
val format = config.format
// Set FPS // Set FPS
// TODO: Check if the FPS range is actually supported in the current configuration.
val fps = config.fps val fps = config.fps
if (fps != null) { 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)) captureRequest.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, Range(fps, fps))
} }
// Set Video Stabilization // 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) { when (config.videoStabilizationMode) {
VideoStabilizationMode.OFF -> { VideoStabilizationMode.OFF -> {
// do nothing // do nothing
} }
VideoStabilizationMode.STANDARD -> { VideoStabilizationMode.STANDARD -> {
// TODO: Check if that stabilization mode is even supported
val mode = if (Build.VERSION.SDK_INT >= val mode = if (Build.VERSION.SDK_INT >=
Build.VERSION_CODES.TIRAMISU Build.VERSION_CODES.TIRAMISU
) { ) {
@ -461,35 +477,37 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
captureRequest.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, mode) captureRequest.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, mode)
} }
VideoStabilizationMode.CINEMATIC, VideoStabilizationMode.CINEMATIC_EXTENDED -> { VideoStabilizationMode.CINEMATIC, VideoStabilizationMode.CINEMATIC_EXTENDED -> {
// TODO: Check if that stabilization mode is even supported
captureRequest.set(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE, CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE_ON) captureRequest.set(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE, CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE_ON)
} }
} }
// Set HDR // Set HDR
// TODO: Check if that value is even supported
val video = config.video as? CameraConfiguration.Output.Enabled<CameraConfiguration.Video> val video = config.video as? CameraConfiguration.Output.Enabled<CameraConfiguration.Video>
val videoHdr = video?.config?.enableHdr val videoHdr = video?.config?.enableHdr
if (videoHdr == true) { 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) captureRequest.set(CaptureRequest.CONTROL_SCENE_MODE, CaptureRequest.CONTROL_SCENE_MODE_HDR)
} else if (config.enableLowLightBoost) { } else if (config.enableLowLightBoost) {
if (!deviceDetails.supportsLowLightBoost) throw LowLightBoostNotSupportedError()
captureRequest.set(CaptureRequest.CONTROL_SCENE_MODE, CaptureRequest.CONTROL_SCENE_MODE_NIGHT) captureRequest.set(CaptureRequest.CONTROL_SCENE_MODE, CaptureRequest.CONTROL_SCENE_MODE_NIGHT)
} }
// Set Exposure Bias // Set Exposure Bias
// TODO: Check if that exposure value is even supported
val exposure = config.exposure?.toInt() val exposure = config.exposure?.toInt()
if (exposure != null) { if (exposure != null) {
captureRequest.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, exposure) val clamped = deviceDetails.exposureRange.clamp(exposure)
captureRequest.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, clamped)
} }
// Set Zoom // Set Zoom
// TODO: Check if that zoom value is even supported // TODO: Cache camera characteristics? Check perf.
val cameraCharacteristics = cameraManager.getCameraCharacteristics(device.id)
captureRequest.setZoom(config.zoom, cameraCharacteristics) captureRequest.setZoom(config.zoom, cameraCharacteristics)
// Set Torch // Set Torch
// TODO: Check if torch is even supported
if (config.torch == Torch.ON) { if (config.torch == Torch.ON) {
if (!deviceDetails.hasFlash) throw FlashUnavailableError()
captureRequest.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH) captureRequest.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH)
} }

View File

@ -89,9 +89,9 @@ enum DeviceError: String {
case .invalid: case .invalid:
return "The given Camera device was invalid. Use `useCameraDevice(..)` or `Camera.getAvailableCameraDevices()` to select a suitable Camera device." return "The given Camera device was invalid. Use `useCameraDevice(..)` or `Camera.getAvailableCameraDevices()` to select a suitable Camera device."
case .flashUnavailable: case .flashUnavailable:
return "The Camera Device does not have a flash unit! Make sure you select a device where `hasFlash`/`hasTorch` is true!" return "The Camera Device does not have a flash unit! Select a device where `device.hasFlash`/`device.hasTorch` is true."
case .lowLightBoostNotSupported: case .lowLightBoostNotSupported:
return "The currently selected camera device does not support low-light boost! Make sure you select a device where `supportsLowLightBoost` is true!" return "The currently selected camera device does not support low-light boost! Select a device where `device.supportsLowLightBoost` is true."
case .focusNotSupported: case .focusNotSupported:
return "The currently selected camera device does not support focussing!" return "The currently selected camera device does not support focussing!"
case .microphoneUnavailable: case .microphoneUnavailable:

View File

@ -16,8 +16,10 @@ export type DeviceError =
export type FormatError = export type FormatError =
| 'format/invalid-fps' | 'format/invalid-fps'
| 'format/invalid-video-hdr' | 'format/invalid-video-hdr'
| 'format/invalid-video-stabilization-mode'
| 'format/incompatible-pixel-format-with-hdr-setting' | 'format/incompatible-pixel-format-with-hdr-setting'
| 'format/invalid-format' | 'format/invalid-format'
| 'format/format-required'
export type SessionError = export type SessionError =
| 'session/camera-not-ready' | 'session/camera-not-ready'
| 'session/camera-cannot-be-opened' | 'session/camera-cannot-be-opened'