2021-02-19 20:41:49 +01:00
|
|
|
package com.mrousavy.camera
|
2021-02-19 16:28:14 +01:00
|
|
|
|
|
|
|
import android.Manifest
|
|
|
|
import android.annotation.SuppressLint
|
|
|
|
import android.content.Context
|
|
|
|
import android.content.pm.PackageManager
|
2021-07-26 11:32:58 +02:00
|
|
|
import android.content.res.Configuration
|
2021-02-19 16:28:14 +01:00
|
|
|
import android.hardware.camera2.*
|
|
|
|
import android.util.Log
|
|
|
|
import android.util.Range
|
|
|
|
import android.view.*
|
|
|
|
import android.view.View.OnTouchListener
|
|
|
|
import android.widget.FrameLayout
|
|
|
|
import androidx.camera.camera2.interop.Camera2Interop
|
|
|
|
import androidx.camera.core.*
|
|
|
|
import androidx.camera.core.impl.*
|
2021-07-07 12:57:28 +02:00
|
|
|
import androidx.camera.extensions.*
|
2021-02-26 17:34:28 +01:00
|
|
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
2021-12-30 11:39:17 +01:00
|
|
|
import androidx.camera.video.*
|
|
|
|
import androidx.camera.video.VideoCapture
|
2021-02-19 16:28:14 +01:00
|
|
|
import androidx.camera.view.PreviewView
|
|
|
|
import androidx.core.content.ContextCompat
|
|
|
|
import androidx.lifecycle.*
|
2021-06-27 12:37:54 +02:00
|
|
|
import com.facebook.jni.HybridData
|
|
|
|
import com.facebook.proguard.annotations.DoNotStrip
|
2021-02-19 16:28:14 +01:00
|
|
|
import com.facebook.react.bridge.*
|
2023-07-22 00:15:11 +02:00
|
|
|
import com.mrousavy.camera.frameprocessor.Frame
|
|
|
|
import com.mrousavy.camera.frameprocessor.FrameProcessor
|
|
|
|
import com.mrousavy.camera.frameprocessor.FrameProcessorPlugin
|
|
|
|
import com.mrousavy.camera.frameprocessor.FrameProcessorPluginRegistry
|
2021-02-26 10:56:20 +01:00
|
|
|
import com.mrousavy.camera.utils.*
|
2021-02-19 16:28:14 +01:00
|
|
|
import kotlinx.coroutines.*
|
2021-02-26 17:34:28 +01:00
|
|
|
import kotlinx.coroutines.guava.await
|
2021-02-19 16:28:14 +01:00
|
|
|
import java.lang.IllegalArgumentException
|
2021-08-25 11:33:57 +02:00
|
|
|
import java.util.concurrent.ExecutorService
|
2021-02-19 16:28:14 +01:00
|
|
|
import java.util.concurrent.Executors
|
|
|
|
import kotlin.math.max
|
|
|
|
import kotlin.math.min
|
|
|
|
|
|
|
|
//
|
|
|
|
// TODOs for the CameraView which are currently too hard to implement either because of CameraX' limitations, or my brain capacity.
|
|
|
|
//
|
|
|
|
// CameraView
|
2021-03-17 19:29:03 +01:00
|
|
|
// TODO: Actually use correct sizes for video and photo (currently it's both the video size)
|
2021-02-19 16:28:14 +01:00
|
|
|
// TODO: Configurable FPS higher than 30
|
|
|
|
// TODO: High-speed video recordings (export in CameraViewModule::getAvailableVideoDevices(), and set in CameraView::configurePreview()) (120FPS+)
|
|
|
|
// TODO: configureSession() enableDepthData
|
2021-06-10 13:49:34 +02:00
|
|
|
// TODO: configureSession() enableHighQualityPhotos
|
2021-02-19 16:28:14 +01:00
|
|
|
// TODO: configureSession() enablePortraitEffectsMatteDelivery
|
|
|
|
// TODO: configureSession() colorSpace
|
|
|
|
|
|
|
|
// CameraView+RecordVideo
|
|
|
|
// TODO: Better startRecording()/stopRecording() (promise + callback, wait for TurboModules/JSI)
|
|
|
|
// TODO: videoStabilizationMode
|
2021-03-17 19:29:03 +01:00
|
|
|
// TODO: Return Video size/duration
|
2021-02-19 16:28:14 +01:00
|
|
|
|
|
|
|
// CameraView+TakePhoto
|
2021-03-17 19:29:03 +01:00
|
|
|
// TODO: Mirror selfie images
|
2021-02-19 16:28:14 +01:00
|
|
|
// TODO: takePhoto() depth data
|
|
|
|
// TODO: takePhoto() raw capture
|
|
|
|
// TODO: takePhoto() photoCodec ("hevc" | "jpeg" | "raw")
|
|
|
|
// TODO: takePhoto() qualityPrioritization
|
|
|
|
// TODO: takePhoto() enableAutoRedEyeReduction
|
|
|
|
// TODO: takePhoto() enableAutoStabilization
|
|
|
|
// TODO: takePhoto() enableAutoDistortionCorrection
|
|
|
|
// TODO: takePhoto() return with jsi::Value Image reference for faster capture
|
|
|
|
|
2021-07-07 13:15:32 +02:00
|
|
|
@Suppress("KotlinJniMissingFunction") // I use fbjni, Android Studio is not smart enough to realize that.
|
2021-08-25 11:33:57 +02:00
|
|
|
@SuppressLint("ClickableViewAccessibility", "ViewConstructor")
|
|
|
|
class CameraView(context: Context, private val frameProcessorThread: ExecutorService) : FrameLayout(context), LifecycleOwner {
|
|
|
|
companion object {
|
|
|
|
const val TAG = "CameraView"
|
|
|
|
const val TAG_PERF = "CameraView.performance"
|
|
|
|
|
|
|
|
private val propsThatRequireSessionReconfiguration = arrayListOf("cameraId", "format", "fps", "hdr", "lowLightBoost", "photo", "video", "enableFrameProcessor")
|
|
|
|
private val arrayListOfZoom = arrayListOf("zoom")
|
|
|
|
}
|
|
|
|
|
2021-02-26 10:56:20 +01:00
|
|
|
// react properties
|
|
|
|
// props that require reconfiguring
|
|
|
|
var cameraId: String? = null // this is actually not a react prop directly, but the result of setting device={}
|
|
|
|
var enableDepthData = false
|
2021-06-10 13:49:34 +02:00
|
|
|
var enableHighQualityPhotos: Boolean? = null
|
2021-02-26 10:56:20 +01:00
|
|
|
var enablePortraitEffectsMatteDelivery = false
|
2021-06-07 13:08:40 +02:00
|
|
|
// use-cases
|
|
|
|
var photo: Boolean? = null
|
|
|
|
var video: Boolean? = null
|
|
|
|
var audio: Boolean? = null
|
2021-07-12 15:16:03 +02:00
|
|
|
var enableFrameProcessor = false
|
2021-02-26 10:56:20 +01:00
|
|
|
// props that require format reconfiguring
|
|
|
|
var format: ReadableMap? = null
|
|
|
|
var fps: Int? = null
|
|
|
|
var hdr: Boolean? = null // nullable bool
|
|
|
|
var colorSpace: String? = null
|
|
|
|
var lowLightBoost: Boolean? = null // nullable bool
|
|
|
|
// other props
|
|
|
|
var isActive = false
|
|
|
|
var torch = "off"
|
2021-07-29 11:44:22 +02:00
|
|
|
var zoom: Float = 1f // in "factor"
|
2022-01-04 16:57:40 +01:00
|
|
|
var orientation: String? = null
|
2021-02-26 10:56:20 +01:00
|
|
|
var enableZoomGesture = false
|
2021-09-06 16:27:16 +02:00
|
|
|
set(value) {
|
|
|
|
field = value
|
|
|
|
setOnTouchListener(if (value) touchEventListener else null)
|
|
|
|
}
|
2021-02-26 10:56:20 +01:00
|
|
|
|
|
|
|
// private properties
|
2021-10-11 18:27:23 +02:00
|
|
|
private var isMounted = false
|
2021-02-26 10:56:20 +01:00
|
|
|
private val reactContext: ReactContext
|
|
|
|
get() = context as ReactContext
|
|
|
|
|
2021-03-12 10:45:23 +01:00
|
|
|
@Suppress("JoinDeclarationAndAssignment")
|
2021-02-26 10:56:20 +01:00
|
|
|
internal val previewView: PreviewView
|
|
|
|
private val cameraExecutor = Executors.newSingleThreadExecutor()
|
|
|
|
internal val takePhotoExecutor = Executors.newSingleThreadExecutor()
|
|
|
|
internal val recordVideoExecutor = Executors.newSingleThreadExecutor()
|
2022-03-31 17:01:21 +01:00
|
|
|
internal var coroutineScope = CoroutineScope(Dispatchers.Main)
|
2021-02-26 10:56:20 +01:00
|
|
|
|
|
|
|
internal var camera: Camera? = null
|
|
|
|
internal var imageCapture: ImageCapture? = null
|
2022-01-04 16:57:40 +01:00
|
|
|
internal var videoCapture: VideoCapture<Recorder>? = null
|
2023-07-22 00:15:11 +02:00
|
|
|
public var frameProcessor: FrameProcessor? = null
|
2021-07-26 11:32:58 +02:00
|
|
|
private var preview: Preview? = null
|
2023-07-22 00:15:11 +02:00
|
|
|
private var imageAnalysis: ImageAnalysis? = null
|
2021-07-12 15:16:03 +02:00
|
|
|
|
2021-12-30 11:39:17 +01:00
|
|
|
internal var activeVideoRecording: Recording? = null
|
|
|
|
|
2021-07-07 12:57:28 +02:00
|
|
|
private var extensionsManager: ExtensionsManager? = null
|
2021-02-26 10:56:20 +01:00
|
|
|
|
|
|
|
private val scaleGestureListener: ScaleGestureDetector.SimpleOnScaleGestureListener
|
|
|
|
private val scaleGestureDetector: ScaleGestureDetector
|
|
|
|
private val touchEventListener: OnTouchListener
|
|
|
|
|
|
|
|
private val lifecycleRegistry: LifecycleRegistry
|
|
|
|
private var hostLifecycleState: Lifecycle.State
|
|
|
|
|
2022-01-04 16:57:40 +01:00
|
|
|
private val inputRotation: Int
|
2021-08-25 11:16:07 +02:00
|
|
|
get() {
|
|
|
|
return context.displayRotation
|
|
|
|
}
|
2022-01-04 16:57:40 +01:00
|
|
|
private val outputRotation: Int
|
|
|
|
get() {
|
|
|
|
if (orientation != null) {
|
|
|
|
// user is overriding output orientation
|
|
|
|
return when (orientation!!) {
|
|
|
|
"portrait" -> Surface.ROTATION_0
|
|
|
|
"landscapeRight" -> Surface.ROTATION_90
|
|
|
|
"portraitUpsideDown" -> Surface.ROTATION_180
|
|
|
|
"landscapeLeft" -> Surface.ROTATION_270
|
|
|
|
else -> throw InvalidTypeScriptUnionError("orientation", orientation!!)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// use same as input rotation
|
|
|
|
return inputRotation
|
|
|
|
}
|
|
|
|
}
|
2021-07-26 11:32:58 +02:00
|
|
|
|
2021-02-26 10:56:20 +01:00
|
|
|
private var minZoom: Float = 1f
|
|
|
|
private var maxZoom: Float = 1f
|
|
|
|
|
2023-07-22 00:15:11 +02:00
|
|
|
@Suppress("RedundantIf")
|
2021-06-27 12:37:54 +02:00
|
|
|
internal val fallbackToSnapshot: Boolean
|
|
|
|
@SuppressLint("UnsafeOptInUsageError")
|
|
|
|
get() {
|
|
|
|
if (video != true && !enableFrameProcessor) {
|
|
|
|
// Both use-cases are disabled, so `photo` is the only use-case anyways. Don't need to fallback here.
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
cameraId?.let { cameraId ->
|
|
|
|
val cameraManger = reactContext.getSystemService(Context.CAMERA_SERVICE) as? CameraManager
|
|
|
|
cameraManger?.let {
|
|
|
|
val characteristics = cameraManger.getCameraCharacteristics(cameraId)
|
|
|
|
val hardwareLevel = characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)
|
|
|
|
if (hardwareLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
|
|
|
|
// Camera only supports a single use-case at a time
|
|
|
|
return true
|
|
|
|
} else {
|
|
|
|
if (video == true && enableFrameProcessor) {
|
|
|
|
// Camera supports max. 2 use-cases, but both are occupied by `frameProcessor` and `video`
|
|
|
|
return true
|
|
|
|
} else {
|
|
|
|
// Camera supports max. 2 use-cases and only one is occupied (either `frameProcessor` or `video`), so we can add `photo`
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2021-02-26 10:56:20 +01:00
|
|
|
init {
|
|
|
|
previewView = PreviewView(context)
|
|
|
|
previewView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
|
|
|
previewView.installHierarchyFitter() // If this is not called correctly, view finder will be black/blank
|
|
|
|
addView(previewView)
|
|
|
|
|
|
|
|
scaleGestureListener = object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
|
|
|
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
2021-07-29 11:44:22 +02:00
|
|
|
zoom = max(min((zoom * detector.scaleFactor), maxZoom), minZoom)
|
2021-02-26 17:34:28 +01:00
|
|
|
update(arrayListOfZoom)
|
2021-02-26 10:56:20 +01:00
|
|
|
return true
|
|
|
|
}
|
2021-02-19 16:28:14 +01:00
|
|
|
}
|
2021-02-26 10:56:20 +01:00
|
|
|
scaleGestureDetector = ScaleGestureDetector(context, scaleGestureListener)
|
|
|
|
touchEventListener = OnTouchListener { _, event -> return@OnTouchListener scaleGestureDetector.onTouchEvent(event) }
|
2021-08-25 11:16:07 +02:00
|
|
|
|
2021-02-26 10:56:20 +01:00
|
|
|
hostLifecycleState = Lifecycle.State.INITIALIZED
|
|
|
|
lifecycleRegistry = LifecycleRegistry(this)
|
|
|
|
reactContext.addLifecycleEventListener(object : LifecycleEventListener {
|
|
|
|
override fun onHostResume() {
|
|
|
|
hostLifecycleState = Lifecycle.State.RESUMED
|
|
|
|
updateLifecycleState()
|
2022-06-14 02:54:33 -06:00
|
|
|
// workaround for https://issuetracker.google.com/issues/147354615, preview must be bound on resume
|
|
|
|
update(propsThatRequireSessionReconfiguration)
|
2021-02-26 10:56:20 +01:00
|
|
|
}
|
|
|
|
override fun onHostPause() {
|
|
|
|
hostLifecycleState = Lifecycle.State.CREATED
|
2021-02-19 16:28:14 +01:00
|
|
|
updateLifecycleState()
|
2021-02-26 10:56:20 +01:00
|
|
|
}
|
|
|
|
override fun onHostDestroy() {
|
|
|
|
hostLifecycleState = Lifecycle.State.DESTROYED
|
|
|
|
updateLifecycleState()
|
|
|
|
cameraExecutor.shutdown()
|
|
|
|
takePhotoExecutor.shutdown()
|
|
|
|
recordVideoExecutor.shutdown()
|
2022-01-03 09:30:40 +01:00
|
|
|
reactContext.removeLifecycleEventListener(this)
|
2021-02-26 10:56:20 +01:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-07-26 11:32:58 +02:00
|
|
|
override fun onConfigurationChanged(newConfig: Configuration?) {
|
|
|
|
super.onConfigurationChanged(newConfig)
|
2022-01-04 16:57:40 +01:00
|
|
|
updateOrientation()
|
|
|
|
}
|
2021-07-26 11:32:58 +02:00
|
|
|
|
2022-01-04 16:57:40 +01:00
|
|
|
@SuppressLint("RestrictedApi")
|
|
|
|
private fun updateOrientation() {
|
|
|
|
preview?.targetRotation = inputRotation
|
|
|
|
imageCapture?.targetRotation = outputRotation
|
|
|
|
videoCapture?.targetRotation = outputRotation
|
|
|
|
imageAnalysis?.targetRotation = outputRotation
|
2021-07-26 11:32:58 +02:00
|
|
|
}
|
|
|
|
|
2021-02-26 10:56:20 +01:00
|
|
|
override fun getLifecycle(): Lifecycle {
|
|
|
|
return lifecycleRegistry
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Updates the custom Lifecycle to match the host activity's lifecycle, and if it's active we narrow it down to the [isActive] and [isAttachedToWindow] fields.
|
|
|
|
*/
|
|
|
|
private fun updateLifecycleState() {
|
|
|
|
val lifecycleBefore = lifecycleRegistry.currentState
|
|
|
|
if (hostLifecycleState == Lifecycle.State.RESUMED) {
|
|
|
|
// Host Lifecycle (Activity) is currently active (RESUMED), so we narrow it down to the view's lifecycle
|
|
|
|
if (isActive && isAttachedToWindow) {
|
|
|
|
lifecycleRegistry.currentState = Lifecycle.State.RESUMED
|
|
|
|
} else {
|
|
|
|
lifecycleRegistry.currentState = Lifecycle.State.CREATED
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Host Lifecycle (Activity) is currently inactive (STARTED or DESTROYED), so that overrules our view's lifecycle
|
|
|
|
lifecycleRegistry.currentState = hostLifecycleState
|
2021-02-19 16:28:14 +01:00
|
|
|
}
|
2021-05-03 19:14:19 +02:00
|
|
|
Log.d(TAG, "Lifecycle went from ${lifecycleBefore.name} -> ${lifecycleRegistry.currentState.name} (isActive: $isActive | isAttachedToWindow: $isAttachedToWindow)")
|
2021-02-26 10:56:20 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun onAttachedToWindow() {
|
|
|
|
super.onAttachedToWindow()
|
|
|
|
updateLifecycleState()
|
2021-10-11 18:27:23 +02:00
|
|
|
if (!isMounted) {
|
|
|
|
isMounted = true
|
|
|
|
invokeOnViewReady()
|
|
|
|
}
|
2021-02-26 10:56:20 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun onDetachedFromWindow() {
|
|
|
|
super.onDetachedFromWindow()
|
|
|
|
updateLifecycleState()
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Invalidate all React Props and reconfigure the device
|
|
|
|
*/
|
2021-03-12 10:45:23 +01:00
|
|
|
fun update(changedProps: ArrayList<String>) = previewView.post {
|
|
|
|
// TODO: Does this introduce too much overhead?
|
|
|
|
// I need to .post on the previewView because it might've not been initialized yet
|
2021-08-25 11:33:57 +02:00
|
|
|
// I need to use CoroutineScope.launch because of the suspend fun [configureSession]
|
|
|
|
coroutineScope.launch {
|
2021-03-12 10:45:23 +01:00
|
|
|
try {
|
|
|
|
val shouldReconfigureSession = changedProps.containsAny(propsThatRequireSessionReconfiguration)
|
|
|
|
val shouldReconfigureZoom = shouldReconfigureSession || changedProps.contains("zoom")
|
|
|
|
val shouldReconfigureTorch = shouldReconfigureSession || changedProps.contains("torch")
|
2022-01-04 16:57:40 +01:00
|
|
|
val shouldUpdateOrientation = shouldReconfigureSession || changedProps.contains("orientation")
|
2021-03-12 10:45:23 +01:00
|
|
|
|
|
|
|
if (changedProps.contains("isActive")) {
|
|
|
|
updateLifecycleState()
|
|
|
|
}
|
|
|
|
if (shouldReconfigureSession) {
|
|
|
|
configureSession()
|
|
|
|
}
|
|
|
|
if (shouldReconfigureZoom) {
|
2021-08-25 11:16:07 +02:00
|
|
|
val zoomClamped = max(min(zoom, maxZoom), minZoom)
|
2021-07-29 11:44:22 +02:00
|
|
|
camera!!.cameraControl.setZoomRatio(zoomClamped)
|
2021-03-12 10:45:23 +01:00
|
|
|
}
|
|
|
|
if (shouldReconfigureTorch) {
|
|
|
|
camera!!.cameraControl.enableTorch(torch == "on")
|
|
|
|
}
|
2022-01-04 16:57:40 +01:00
|
|
|
if (shouldUpdateOrientation) {
|
|
|
|
updateOrientation()
|
|
|
|
}
|
2021-06-29 10:14:33 +02:00
|
|
|
} catch (e: Throwable) {
|
2021-06-29 10:18:39 +02:00
|
|
|
Log.e(TAG, "update() threw: ${e.message}")
|
2021-06-29 10:34:48 +02:00
|
|
|
invokeOnError(e)
|
2021-02-26 10:56:20 +01:00
|
|
|
}
|
2021-02-19 16:28:14 +01:00
|
|
|
}
|
2021-02-26 10:56:20 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Configures the camera capture session. This should only be called when the camera device changes.
|
|
|
|
*/
|
2023-02-21 15:00:48 +01:00
|
|
|
@SuppressLint("RestrictedApi", "UnsafeOptInUsageError")
|
2021-02-26 10:56:20 +01:00
|
|
|
private suspend fun configureSession() {
|
|
|
|
try {
|
2021-03-12 10:45:23 +01:00
|
|
|
val startTime = System.currentTimeMillis()
|
2021-05-03 19:14:19 +02:00
|
|
|
Log.i(TAG, "Configuring session...")
|
2021-02-26 10:56:20 +01:00
|
|
|
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
|
2021-06-03 20:07:52 +03:00
|
|
|
throw CameraPermissionError()
|
2021-02-26 10:56:20 +01:00
|
|
|
}
|
|
|
|
if (cameraId == null) {
|
|
|
|
throw NoCameraDeviceError()
|
|
|
|
}
|
|
|
|
if (format != null)
|
2021-05-03 19:14:19 +02:00
|
|
|
Log.i(TAG, "Configuring session with Camera ID $cameraId and custom format...")
|
2021-02-26 10:56:20 +01:00
|
|
|
else
|
2021-05-03 19:14:19 +02:00
|
|
|
Log.i(TAG, "Configuring session with Camera ID $cameraId and default format options...")
|
2021-02-26 10:56:20 +01:00
|
|
|
|
|
|
|
// Used to bind the lifecycle of cameras to the lifecycle owner
|
2021-03-12 10:45:23 +01:00
|
|
|
val cameraProvider = ProcessCameraProvider.getInstance(reactContext).await()
|
2021-02-26 10:56:20 +01:00
|
|
|
|
2021-07-07 12:57:28 +02:00
|
|
|
var cameraSelector = CameraSelector.Builder().byID(cameraId!!).build()
|
|
|
|
|
|
|
|
val tryEnableExtension: (suspend (extension: Int) -> Unit) = lambda@ { extension ->
|
|
|
|
if (extensionsManager == null) {
|
|
|
|
Log.i(TAG, "Initializing ExtensionsManager...")
|
2021-12-30 11:39:17 +01:00
|
|
|
extensionsManager = ExtensionsManager.getInstanceAsync(context, cameraProvider).await()
|
2021-07-07 12:57:28 +02:00
|
|
|
}
|
2021-12-30 11:39:17 +01:00
|
|
|
if (extensionsManager!!.isExtensionAvailable(cameraSelector, extension)) {
|
2021-07-07 12:57:28 +02:00
|
|
|
Log.i(TAG, "Enabling extension $extension...")
|
2021-12-30 11:39:17 +01:00
|
|
|
cameraSelector = extensionsManager!!.getExtensionEnabledCameraSelector(cameraSelector, extension)
|
2021-07-07 12:57:28 +02:00
|
|
|
} else {
|
|
|
|
Log.e(TAG, "Extension $extension is not available for the given Camera!")
|
2021-07-07 13:14:36 +02:00
|
|
|
throw when (extension) {
|
|
|
|
ExtensionMode.HDR -> HdrNotContainedInFormatError()
|
|
|
|
ExtensionMode.NIGHT -> LowLightBoostNotContainedInFormatError()
|
|
|
|
else -> Error("Invalid extension supplied! Extension $extension is not available.")
|
|
|
|
}
|
2021-07-07 12:57:28 +02:00
|
|
|
}
|
|
|
|
}
|
2021-02-26 10:56:20 +01:00
|
|
|
|
|
|
|
val previewBuilder = Preview.Builder()
|
2022-01-04 16:57:40 +01:00
|
|
|
.setTargetRotation(inputRotation)
|
2021-12-30 11:39:17 +01:00
|
|
|
|
2021-02-26 10:56:20 +01:00
|
|
|
val imageCaptureBuilder = ImageCapture.Builder()
|
2022-01-04 16:57:40 +01:00
|
|
|
.setTargetRotation(outputRotation)
|
2021-02-26 10:56:20 +01:00
|
|
|
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
|
2021-12-30 11:39:17 +01:00
|
|
|
|
|
|
|
val videoRecorderBuilder = Recorder.Builder()
|
|
|
|
.setExecutor(cameraExecutor)
|
|
|
|
|
2021-06-27 12:37:54 +02:00
|
|
|
val imageAnalysisBuilder = ImageAnalysis.Builder()
|
2022-01-04 16:57:40 +01:00
|
|
|
.setTargetRotation(outputRotation)
|
2021-07-26 11:32:58 +02:00
|
|
|
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
2021-08-25 11:33:57 +02:00
|
|
|
.setBackgroundExecutor(frameProcessorThread)
|
2021-02-26 10:56:20 +01:00
|
|
|
|
2021-02-26 17:34:28 +01:00
|
|
|
if (format == null) {
|
|
|
|
// let CameraX automatically find best resolution for the target aspect ratio
|
2021-05-03 19:14:19 +02:00
|
|
|
Log.i(TAG, "No custom format has been set, CameraX will automatically determine best configuration...")
|
2021-06-07 13:08:40 +02:00
|
|
|
val aspectRatio = aspectRatio(previewView.height, previewView.width) // flipped because it's in sensor orientation.
|
2021-02-26 17:34:28 +01:00
|
|
|
previewBuilder.setTargetAspectRatio(aspectRatio)
|
|
|
|
imageCaptureBuilder.setTargetAspectRatio(aspectRatio)
|
2021-12-30 11:39:17 +01:00
|
|
|
// TODO: Aspect Ratio for Video Recorder?
|
2021-09-24 12:01:45 +02:00
|
|
|
imageAnalysisBuilder.setTargetAspectRatio(aspectRatio)
|
2021-02-26 17:34:28 +01:00
|
|
|
} else {
|
2021-02-26 10:56:20 +01:00
|
|
|
// User has selected a custom format={}. Use that
|
|
|
|
val format = DeviceFormat(format!!)
|
2021-05-03 19:14:19 +02:00
|
|
|
Log.i(TAG, "Using custom format - photo: ${format.photoSize}, video: ${format.videoSize} @ $fps FPS")
|
2022-06-14 02:54:33 -06:00
|
|
|
if (video == true) {
|
|
|
|
previewBuilder.setTargetResolution(format.videoSize)
|
|
|
|
} else {
|
|
|
|
previewBuilder.setTargetResolution(format.photoSize)
|
|
|
|
}
|
2021-08-05 15:54:01 +02:00
|
|
|
imageCaptureBuilder.setTargetResolution(format.photoSize)
|
2022-06-14 02:54:33 -06:00
|
|
|
imageAnalysisBuilder.setTargetResolution(format.photoSize)
|
2021-02-26 10:56:20 +01:00
|
|
|
|
2021-12-30 11:39:17 +01:00
|
|
|
// TODO: Ability to select resolution exactly depending on format? Just like on iOS...
|
|
|
|
when (min(format.videoSize.height, format.videoSize.width)) {
|
|
|
|
in 0..480 -> videoRecorderBuilder.setQualitySelector(QualitySelector.from(Quality.SD))
|
2022-06-14 02:54:33 -06:00
|
|
|
in 480..720 -> videoRecorderBuilder.setQualitySelector(QualitySelector.from(Quality.HD, FallbackStrategy.lowerQualityThan(Quality.HD)))
|
|
|
|
in 720..1080 -> videoRecorderBuilder.setQualitySelector(QualitySelector.from(Quality.FHD, FallbackStrategy.lowerQualityThan(Quality.FHD)))
|
|
|
|
in 1080..2160 -> videoRecorderBuilder.setQualitySelector(QualitySelector.from(Quality.UHD, FallbackStrategy.lowerQualityThan(Quality.UHD)))
|
|
|
|
in 2160..4320 -> videoRecorderBuilder.setQualitySelector(QualitySelector.from(Quality.HIGHEST, FallbackStrategy.lowerQualityThan(Quality.HIGHEST)))
|
2021-12-30 11:39:17 +01:00
|
|
|
}
|
|
|
|
|
2021-02-26 10:56:20 +01:00
|
|
|
fps?.let { fps ->
|
|
|
|
if (format.frameRateRanges.any { it.contains(fps) }) {
|
|
|
|
// Camera supports the given FPS (frame rate range)
|
|
|
|
val frameDuration = (1.0 / fps.toDouble()).toLong() * 1_000_000_000
|
|
|
|
|
2021-05-03 19:14:19 +02:00
|
|
|
Log.i(TAG, "Setting AE_TARGET_FPS_RANGE to $fps-$fps, and SENSOR_FRAME_DURATION to $frameDuration")
|
2021-02-26 10:56:20 +01:00
|
|
|
Camera2Interop.Extender(previewBuilder)
|
|
|
|
.setCaptureRequestOption(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, Range(fps, fps))
|
|
|
|
.setCaptureRequestOption(CaptureRequest.SENSOR_FRAME_DURATION, frameDuration)
|
2021-12-30 11:39:17 +01:00
|
|
|
// TODO: Frame Rate/FPS for Video Recorder?
|
2021-02-26 10:56:20 +01:00
|
|
|
} else {
|
|
|
|
throw FpsNotContainedInFormatError(fps)
|
|
|
|
}
|
2021-02-19 16:28:14 +01:00
|
|
|
}
|
2021-07-07 12:57:28 +02:00
|
|
|
if (hdr == true) {
|
|
|
|
tryEnableExtension(ExtensionMode.HDR)
|
2021-02-26 10:56:20 +01:00
|
|
|
}
|
2021-07-07 12:57:28 +02:00
|
|
|
if (lowLightBoost == true) {
|
|
|
|
tryEnableExtension(ExtensionMode.NIGHT)
|
2021-02-19 16:28:14 +01:00
|
|
|
}
|
2021-02-26 10:56:20 +01:00
|
|
|
}
|
|
|
|
|
2021-12-30 11:39:17 +01:00
|
|
|
|
2021-02-26 10:56:20 +01:00
|
|
|
// Unbind use cases before rebinding
|
2022-01-04 16:57:40 +01:00
|
|
|
videoCapture = null
|
2021-06-07 13:08:40 +02:00
|
|
|
imageCapture = null
|
2021-06-27 12:37:54 +02:00
|
|
|
imageAnalysis = null
|
2021-02-26 10:56:20 +01:00
|
|
|
cameraProvider.unbindAll()
|
|
|
|
|
|
|
|
// Bind use cases to camera
|
2021-06-07 13:08:40 +02:00
|
|
|
val useCases = ArrayList<UseCase>()
|
|
|
|
if (video == true) {
|
2021-07-12 15:16:03 +02:00
|
|
|
Log.i(TAG, "Adding VideoCapture use-case...")
|
2022-01-04 16:57:40 +01:00
|
|
|
|
|
|
|
val videoRecorder = videoRecorderBuilder.build()
|
|
|
|
videoCapture = VideoCapture.withOutput(videoRecorder)
|
|
|
|
videoCapture!!.targetRotation = outputRotation
|
|
|
|
useCases.add(videoCapture!!)
|
2021-06-07 13:08:40 +02:00
|
|
|
}
|
|
|
|
if (photo == true) {
|
2021-06-27 12:37:54 +02:00
|
|
|
if (fallbackToSnapshot) {
|
|
|
|
Log.i(TAG, "Tried to add photo use-case (`photo={true}`) but the Camera device only supports " +
|
|
|
|
"a single use-case at a time. Falling back to Snapshot capture.")
|
|
|
|
} else {
|
2021-07-12 15:16:03 +02:00
|
|
|
Log.i(TAG, "Adding ImageCapture use-case...")
|
2021-06-27 12:37:54 +02:00
|
|
|
imageCapture = imageCaptureBuilder.build()
|
|
|
|
useCases.add(imageCapture!!)
|
|
|
|
}
|
2021-06-07 13:08:40 +02:00
|
|
|
}
|
2021-06-27 12:37:54 +02:00
|
|
|
if (enableFrameProcessor) {
|
2021-07-12 15:16:03 +02:00
|
|
|
Log.i(TAG, "Adding ImageAnalysis use-case...")
|
2021-06-27 12:37:54 +02:00
|
|
|
imageAnalysis = imageAnalysisBuilder.build().apply {
|
2023-02-21 15:00:48 +01:00
|
|
|
setAnalyzer(cameraExecutor) { image ->
|
feat: Sync Frame Processors (plus `runAsync` and `runAtTargetFps`) (#1472)
Before, Frame Processors ran on a separate Thread.
After, Frame Processors run fully synchronous and always at the same FPS as the Camera.
Two new functions have been introduced:
* `runAtTargetFps(fps: number, func: () => void)`: Runs the given code as often as the given `fps`, effectively throttling it's calls.
* `runAsync(frame: Frame, func: () => void)`: Runs the given function on a separate Thread for Frame Processing. A strong reference to the Frame is held as long as the function takes to execute.
You can use `runAtTargetFps` to throttle calls to a specific API (e.g. if your Camera is running at 60 FPS, but you only want to run face detection at ~25 FPS, use `runAtTargetFps(25, ...)`.)
You can use `runAsync` to run a heavy algorithm asynchronous, so that the Camera is not blocked while your algorithm runs. This is useful if your main sync processor draws something, and your async processor is doing some image analysis on the side.
You can also combine both functions.
Examples:
```js
const frameProcessor = useFrameProcessor((frame) => {
'worklet'
console.log("I'm running at 60 FPS!")
}, [])
```
```js
const frameProcessor = useFrameProcessor((frame) => {
'worklet'
console.log("I'm running at 60 FPS!")
runAtTargetFps(10, () => {
'worklet'
console.log("I'm running at 10 FPS!")
})
}, [])
```
```js
const frameProcessor = useFrameProcessor((frame) => {
'worklet'
console.log("I'm running at 60 FPS!")
runAsync(frame, () => {
'worklet'
console.log("I'm running on another Thread, I can block for longer!")
})
}, [])
```
```js
const frameProcessor = useFrameProcessor((frame) => {
'worklet'
console.log("I'm running at 60 FPS!")
runAtTargetFps(10, () => {
'worklet'
runAsync(frame, () => {
'worklet'
console.log("I'm running on another Thread at 10 FPS, I can block for longer!")
})
})
}, [])
```
2023-02-15 16:47:09 +01:00
|
|
|
// Call JS Frame Processor
|
2023-07-22 00:15:11 +02:00
|
|
|
val frame = Frame(image)
|
|
|
|
frameProcessor?.call(frame)
|
|
|
|
// ...frame gets closed in FrameHostObject implementation via JS ref counting
|
2023-02-21 15:00:48 +01:00
|
|
|
}
|
2021-06-27 12:37:54 +02:00
|
|
|
}
|
|
|
|
useCases.add(imageAnalysis!!)
|
|
|
|
}
|
|
|
|
|
2021-07-26 11:32:58 +02:00
|
|
|
preview = previewBuilder.build()
|
2021-06-29 10:36:39 +02:00
|
|
|
Log.i(TAG, "Attaching ${useCases.size} use-cases...")
|
2021-06-07 13:08:40 +02:00
|
|
|
camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview, *useCases.toTypedArray())
|
2021-07-26 11:32:58 +02:00
|
|
|
preview!!.setSurfaceProvider(previewView.surfaceProvider)
|
2021-02-26 10:56:20 +01:00
|
|
|
|
|
|
|
minZoom = camera!!.cameraInfo.zoomState.value?.minZoomRatio ?: 1f
|
|
|
|
maxZoom = camera!!.cameraInfo.zoomState.value?.maxZoomRatio ?: 1f
|
|
|
|
|
2021-03-12 10:45:23 +01:00
|
|
|
val duration = System.currentTimeMillis() - startTime
|
2021-05-03 19:14:19 +02:00
|
|
|
Log.i(TAG_PERF, "Session configured in $duration ms! Camera: ${camera!!}")
|
2021-02-26 10:56:20 +01:00
|
|
|
invokeOnInitialized()
|
|
|
|
} catch (exc: Throwable) {
|
2021-06-29 10:18:39 +02:00
|
|
|
Log.e(TAG, "Failed to configure session: ${exc.message}")
|
2021-06-29 10:16:38 +02:00
|
|
|
throw when (exc) {
|
2021-02-26 10:56:20 +01:00
|
|
|
is CameraError -> exc
|
2021-06-27 12:37:54 +02:00
|
|
|
is IllegalArgumentException -> {
|
|
|
|
if (exc.message?.contains("too many use cases") == true) {
|
|
|
|
ParallelVideoProcessingNotSupportedError(exc)
|
|
|
|
} else {
|
|
|
|
InvalidCameraDeviceError(exc)
|
|
|
|
}
|
|
|
|
}
|
2021-02-26 10:56:20 +01:00
|
|
|
else -> UnknownCameraError(exc)
|
|
|
|
}
|
2021-02-19 16:28:14 +01:00
|
|
|
}
|
2021-02-26 10:56:20 +01:00
|
|
|
}
|
2021-02-19 16:28:14 +01:00
|
|
|
}
|