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.*
|
|
|
|
import com.facebook.react.uimanager.events.RCTEventEmitter
|
2021-09-06 16:27:16 +02:00
|
|
|
import com.mrousavy.camera.frameprocessor.FrameProcessorPerformanceDataCollector
|
2022-01-02 17:35:26 +01:00
|
|
|
import com.mrousavy.camera.frameprocessor.FrameProcessorRuntimeManager
|
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
|
2021-09-06 16:27:16 +02:00
|
|
|
import kotlin.math.floor
|
2021-02-19 16:28:14 +01:00
|
|
|
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"
|
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-06-27 12:37:54 +02:00
|
|
|
var frameProcessorFps = 1.0
|
2021-09-06 16:27:16 +02:00
|
|
|
set(value) {
|
|
|
|
field = value
|
|
|
|
actualFrameProcessorFps = if (value == -1.0) 30.0 else value
|
|
|
|
lastFrameProcessorPerformanceEvaluation = System.currentTimeMillis()
|
|
|
|
frameProcessorPerformanceDataCollector.clear()
|
|
|
|
}
|
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()
|
2021-08-25 11:33:57 +02:00
|
|
|
private var coroutineScope = CoroutineScope(Dispatchers.Main)
|
2021-02-26 10:56:20 +01:00
|
|
|
|
|
|
|
internal var camera: Camera? = null
|
|
|
|
internal var imageCapture: ImageCapture? = null
|
2021-12-30 11:39:17 +01:00
|
|
|
internal var videoCapture: Recorder? = null
|
2021-07-12 15:16:03 +02:00
|
|
|
private var imageAnalysis: ImageAnalysis? = null
|
2021-07-26 11:32:58 +02:00
|
|
|
private var preview: Preview? = null
|
2021-07-12 15:16:03 +02:00
|
|
|
|
2021-12-30 11:39:17 +01:00
|
|
|
internal var activeVideoRecording: Recording? = null
|
|
|
|
|
2021-07-12 15:16:03 +02:00
|
|
|
private var lastFrameProcessorCall = System.currentTimeMillis()
|
|
|
|
|
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
|
|
|
|
|
2021-08-25 11:16:07 +02:00
|
|
|
private val rotation: Int
|
|
|
|
get() {
|
|
|
|
return context.displayRotation
|
|
|
|
}
|
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
|
|
|
|
|
2021-09-06 16:27:16 +02:00
|
|
|
private var actualFrameProcessorFps = 30.0
|
|
|
|
private val frameProcessorPerformanceDataCollector = FrameProcessorPerformanceDataCollector()
|
|
|
|
private var lastSuggestedFrameProcessorFps = 0.0
|
|
|
|
private var lastFrameProcessorPerformanceEvaluation = System.currentTimeMillis()
|
|
|
|
private val isReadyForNewEvaluation: Boolean
|
|
|
|
get() {
|
|
|
|
val lastPerformanceEvaluationElapsedTime = System.currentTimeMillis() - lastFrameProcessorPerformanceEvaluation
|
|
|
|
return lastPerformanceEvaluationElapsedTime > 1000
|
|
|
|
}
|
|
|
|
|
2021-06-27 12:37:54 +02:00
|
|
|
@DoNotStrip
|
2022-01-02 17:35:26 +01:00
|
|
|
private var mHybridData: HybridData? = null
|
2021-06-27 12:37:54 +02:00
|
|
|
|
|
|
|
@Suppress("LiftReturnOrAssignment", "RedundantIf")
|
|
|
|
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 {
|
2022-01-02 17:35:26 +01:00
|
|
|
if (FrameProcessorRuntimeManager.enableFrameProcessors) {
|
|
|
|
mHybridData = initHybrid()
|
|
|
|
}
|
2021-06-27 12:37:54 +02:00
|
|
|
|
2021-02-26 10:56:20 +01:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
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
|
|
|
@SuppressLint("RestrictedApi")
|
|
|
|
override fun onConfigurationChanged(newConfig: Configuration?) {
|
|
|
|
super.onConfigurationChanged(newConfig)
|
|
|
|
|
|
|
|
if (preview?.targetRotation != rotation) {
|
|
|
|
preview?.targetRotation = rotation
|
|
|
|
imageCapture?.targetRotation = rotation
|
|
|
|
imageAnalysis?.targetRotation = rotation
|
2021-12-30 11:39:17 +01:00
|
|
|
// TODO: videoCapture?.setTargetRotation(rotation)
|
2021-07-26 11:32:58 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-27 12:37:54 +02:00
|
|
|
private external fun initHybrid(): HybridData
|
|
|
|
private external fun frameProcessorCallback(frame: ImageProxy)
|
|
|
|
|
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")
|
|
|
|
|
|
|
|
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")
|
|
|
|
}
|
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.
|
|
|
|
*/
|
2021-04-26 12:56:36 +02:00
|
|
|
@SuppressLint("RestrictedApi")
|
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()
|
|
|
|
.setTargetRotation(rotation)
|
2021-12-30 11:39:17 +01:00
|
|
|
|
2021-02-26 10:56:20 +01:00
|
|
|
val imageCaptureBuilder = ImageCapture.Builder()
|
|
|
|
.setTargetRotation(rotation)
|
|
|
|
.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()
|
|
|
|
.setTargetRotation(rotation)
|
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")
|
2021-08-05 15:54:01 +02:00
|
|
|
previewBuilder.setTargetResolution(format.videoSize)
|
|
|
|
imageCaptureBuilder.setTargetResolution(format.photoSize)
|
2021-09-24 12:01:45 +02:00
|
|
|
imageAnalysisBuilder.setTargetResolution(format.videoSize)
|
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))
|
|
|
|
in 480..720 -> videoRecorderBuilder.setQualitySelector(QualitySelector.from(Quality.HD))
|
|
|
|
in 720..1080 -> videoRecorderBuilder.setQualitySelector(QualitySelector.from(Quality.FHD))
|
|
|
|
in 1080..2160 -> videoRecorderBuilder.setQualitySelector(QualitySelector.from(Quality.UHD))
|
|
|
|
}
|
|
|
|
|
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
|
|
|
val videoRecorder = videoRecorderBuilder.build()
|
|
|
|
val videoCapture = VideoCapture.withOutput(videoRecorder)
|
|
|
|
videoCapture.targetRotation = rotation
|
|
|
|
|
2021-02-26 10:56:20 +01:00
|
|
|
// Unbind use cases before rebinding
|
2021-12-30 11:39:17 +01:00
|
|
|
this.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...")
|
2021-12-30 11:39:17 +01:00
|
|
|
this.videoCapture = videoRecorder
|
|
|
|
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 {
|
|
|
|
setAnalyzer(cameraExecutor, { image ->
|
|
|
|
val now = System.currentTimeMillis()
|
2021-09-06 16:27:16 +02:00
|
|
|
val intervalMs = (1.0 / actualFrameProcessorFps) * 1000.0
|
2021-07-12 15:16:03 +02:00
|
|
|
if (now - lastFrameProcessorCall > intervalMs) {
|
|
|
|
lastFrameProcessorCall = now
|
2021-09-06 16:27:16 +02:00
|
|
|
|
|
|
|
val perfSample = frameProcessorPerformanceDataCollector.beginPerformanceSampleCollection()
|
2021-06-27 12:37:54 +02:00
|
|
|
frameProcessorCallback(image)
|
2021-09-06 16:27:16 +02:00
|
|
|
perfSample.endPerformanceSampleCollection()
|
2021-06-27 12:37:54 +02:00
|
|
|
}
|
|
|
|
image.close()
|
2021-09-06 16:27:16 +02:00
|
|
|
|
|
|
|
if (isReadyForNewEvaluation) {
|
|
|
|
// last evaluation was more than a second ago, evaluate again
|
|
|
|
evaluateNewPerformanceSamples()
|
|
|
|
}
|
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-09-06 16:27:16 +02:00
|
|
|
private fun evaluateNewPerformanceSamples() {
|
|
|
|
lastFrameProcessorPerformanceEvaluation = System.currentTimeMillis()
|
|
|
|
val maxFrameProcessorFps = 30 // TODO: Get maxFrameProcessorFps from ImageAnalyser
|
|
|
|
val averageFps = 1.0 / frameProcessorPerformanceDataCollector.averageExecutionTimeSeconds
|
|
|
|
val suggestedFrameProcessorFps = floor(min(averageFps, maxFrameProcessorFps.toDouble()))
|
2021-06-29 10:36:39 +02:00
|
|
|
|
2021-09-06 16:27:16 +02:00
|
|
|
if (frameProcessorFps == -1.0) {
|
|
|
|
// frameProcessorFps="auto"
|
|
|
|
actualFrameProcessorFps = suggestedFrameProcessorFps
|
|
|
|
} else {
|
|
|
|
// frameProcessorFps={someCustomFpsValue}
|
|
|
|
if (suggestedFrameProcessorFps != lastSuggestedFrameProcessorFps && suggestedFrameProcessorFps != frameProcessorFps) {
|
|
|
|
invokeOnFrameProcessorPerformanceSuggestionAvailable(frameProcessorFps, suggestedFrameProcessorFps)
|
|
|
|
lastSuggestedFrameProcessorFps = suggestedFrameProcessorFps
|
|
|
|
}
|
2021-02-19 16:28:14 +01:00
|
|
|
}
|
2021-02-26 10:56:20 +01:00
|
|
|
}
|
2021-02-19 16:28:14 +01:00
|
|
|
}
|