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

View File

@ -1,6 +1,7 @@
package com.mrousavy.camera.core
import com.mrousavy.camera.types.CameraDeviceError
import com.mrousavy.camera.types.VideoStabilizationMode
abstract class CameraError(
/**
@ -49,6 +50,18 @@ class NoCameraDeviceError :
)
class PixelFormatNotSupportedError(format: String) :
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 :
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) :
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 :
CameraError("capture", "video-not-enabled", "Video capture is disabled! Pass `video={true}` to enable video recordings.")
class PhotoNotEnabledError :

View File

@ -68,8 +68,13 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
private var configuration: CameraConfiguration? = null
// Camera State
private var captureSession: CameraCaptureSession? = 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 photoOutput: PhotoOutput? = 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 {
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 captureRequest = device.createCaptureRequest(template)
targets.forEach { t -> captureRequest.addTarget(t) }
val format = config.format
// Set FPS
// TODO: Check if the FPS range is actually supported in the current configuration.
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 -> {
// TODO: Check if that stabilization mode is even supported
val mode = if (Build.VERSION.SDK_INT >=
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)
}
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)
}
}
// Set HDR
// TODO: Check if that value is even supported
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
// TODO: Check if that exposure value is even supported
val exposure = config.exposure?.toInt()
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
// 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)
// Set Torch
// TODO: Check if torch is even supported
if (config.torch == Torch.ON) {
if (!deviceDetails.hasFlash) throw FlashUnavailableError()
captureRequest.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH)
}

View File

@ -89,9 +89,9 @@ enum DeviceError: String {
case .invalid:
return "The given Camera device was invalid. Use `useCameraDevice(..)` or `Camera.getAvailableCameraDevices()` to select a suitable Camera device."
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:
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:
return "The currently selected camera device does not support focussing!"
case .microphoneUnavailable:

View File

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