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
|
2023-08-21 12:50:14 +02:00
|
|
|
import android.hardware.camera2.CameraManager
|
2021-02-19 16:28:14 +01:00
|
|
|
import android.util.Log
|
2023-08-21 12:50:14 +02:00
|
|
|
import android.util.Size
|
2023-09-22 17:32:34 +02:00
|
|
|
import android.view.Gravity
|
2023-08-23 15:39:24 +02:00
|
|
|
import android.view.ScaleGestureDetector
|
2023-08-21 12:50:14 +02:00
|
|
|
import android.view.Surface
|
2021-02-19 16:28:14 +01:00
|
|
|
import android.widget.FrameLayout
|
|
|
|
import androidx.core.content.ContextCompat
|
2023-08-21 12:50:14 +02:00
|
|
|
import com.facebook.react.bridge.ReadableMap
|
2023-10-14 13:17:05 +02:00
|
|
|
import com.facebook.react.bridge.UiThreadUtil
|
2023-10-13 18:33:20 +02:00
|
|
|
import com.mrousavy.camera.core.CameraPermissionError
|
|
|
|
import com.mrousavy.camera.core.CameraQueues
|
2023-09-01 13:08:33 +02:00
|
|
|
import com.mrousavy.camera.core.CameraSession
|
2023-10-13 18:33:20 +02:00
|
|
|
import com.mrousavy.camera.core.NoCameraDeviceError
|
2023-09-01 13:08:33 +02:00
|
|
|
import com.mrousavy.camera.core.PreviewView
|
2023-09-21 11:20:33 +02:00
|
|
|
import com.mrousavy.camera.core.outputs.CameraOutputs
|
2023-09-23 10:14:27 +02:00
|
|
|
import com.mrousavy.camera.extensions.bigger
|
2023-08-21 12:50:14 +02:00
|
|
|
import com.mrousavy.camera.extensions.containsAny
|
2023-09-23 10:14:27 +02:00
|
|
|
import com.mrousavy.camera.extensions.getPreviewTargetSize
|
2023-08-21 12:50:14 +02:00
|
|
|
import com.mrousavy.camera.extensions.installHierarchyFitter
|
2023-09-23 10:14:27 +02:00
|
|
|
import com.mrousavy.camera.extensions.smaller
|
2023-07-22 00:15:11 +02:00
|
|
|
import com.mrousavy.camera.frameprocessor.FrameProcessor
|
2023-10-17 11:49:04 +02:00
|
|
|
import com.mrousavy.camera.types.CodeScannerOptions
|
|
|
|
import com.mrousavy.camera.types.Orientation
|
|
|
|
import com.mrousavy.camera.types.PixelFormat
|
|
|
|
import com.mrousavy.camera.types.ResizeMode
|
|
|
|
import com.mrousavy.camera.types.Torch
|
|
|
|
import com.mrousavy.camera.types.VideoStabilizationMode
|
2023-10-10 18:51:46 +02:00
|
|
|
import kotlin.coroutines.CoroutineContext
|
2023-08-21 12:50:14 +02:00
|
|
|
import kotlinx.coroutines.CoroutineScope
|
|
|
|
import kotlinx.coroutines.launch
|
2021-02-19 16:28:14 +01:00
|
|
|
|
|
|
|
//
|
|
|
|
// TODOs for the CameraView which are currently too hard to implement either because of CameraX' limitations, or my brain capacity.
|
|
|
|
//
|
|
|
|
// TODO: High-speed video recordings (export in CameraViewModule::getAvailableVideoDevices(), and set in CameraView::configurePreview()) (120FPS+)
|
|
|
|
// TODO: Better startRecording()/stopRecording() (promise + callback, wait for TurboModules/JSI)
|
|
|
|
// TODO: takePhoto() depth data
|
|
|
|
// TODO: takePhoto() raw capture
|
|
|
|
// TODO: takePhoto() return with jsi::Value Image reference for faster capture
|
|
|
|
|
2023-08-21 12:50:14 +02:00
|
|
|
@SuppressLint("ClickableViewAccessibility", "ViewConstructor", "MissingPermission")
|
2023-10-10 18:51:46 +02:00
|
|
|
class CameraView(context: Context) :
|
|
|
|
FrameLayout(context),
|
|
|
|
CoroutineScope {
|
2021-08-25 11:33:57 +02:00
|
|
|
companion object {
|
|
|
|
const val TAG = "CameraView"
|
|
|
|
|
2023-09-22 17:32:34 +02:00
|
|
|
private val propsThatRequirePreviewReconfiguration = arrayListOf("cameraId", "format", "resizeMode")
|
2023-09-21 11:20:33 +02:00
|
|
|
private val propsThatRequireSessionReconfiguration =
|
2023-10-04 12:53:52 +02:00
|
|
|
arrayListOf("cameraId", "format", "photo", "video", "enableFrameProcessor", "codeScannerOptions", "pixelFormat")
|
2023-08-21 12:50:14 +02:00
|
|
|
private val propsThatRequireFormatReconfiguration = arrayListOf("fps", "hdr", "videoStabilizationMode", "lowLightBoost")
|
2021-08-25 11:33:57 +02:00
|
|
|
}
|
|
|
|
|
2021-02-26 10:56:20 +01:00
|
|
|
// react properties
|
|
|
|
// props that require reconfiguring
|
2023-08-21 12:50:14 +02:00
|
|
|
var cameraId: String? = null
|
2021-02-26 10:56:20 +01:00
|
|
|
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
|
2023-09-21 11:20:33 +02:00
|
|
|
|
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
|
2023-08-21 12:50:14 +02:00
|
|
|
var pixelFormat: PixelFormat = PixelFormat.NATIVE
|
2023-09-21 11:20:33 +02:00
|
|
|
|
2021-02-26 10:56:20 +01:00
|
|
|
// props that require format reconfiguring
|
|
|
|
var format: ReadableMap? = null
|
2023-09-22 17:32:34 +02:00
|
|
|
var resizeMode: ResizeMode = ResizeMode.COVER
|
2021-02-26 10:56:20 +01:00
|
|
|
var fps: Int? = null
|
2023-08-21 12:50:14 +02:00
|
|
|
var videoStabilizationMode: VideoStabilizationMode? = null
|
2021-02-26 10:56:20 +01:00
|
|
|
var hdr: Boolean? = null // nullable bool
|
|
|
|
var lowLightBoost: Boolean? = null // nullable bool
|
2023-09-21 11:20:33 +02:00
|
|
|
|
2021-02-26 10:56:20 +01:00
|
|
|
// other props
|
|
|
|
var isActive = false
|
2023-08-21 12:50:14 +02:00
|
|
|
var torch: Torch = Torch.OFF
|
2021-07-29 11:44:22 +02:00
|
|
|
var zoom: Float = 1f // in "factor"
|
2023-08-21 12:50:14 +02:00
|
|
|
var orientation: Orientation? = null
|
2023-08-23 15:39:24 +02:00
|
|
|
var enableZoomGesture: Boolean = false
|
2021-02-26 10:56:20 +01:00
|
|
|
|
2023-10-04 12:53:52 +02:00
|
|
|
// code scanner
|
2023-10-16 16:56:39 +02:00
|
|
|
var codeScannerOptions: CodeScannerOptions? = null
|
2023-10-04 12:53:52 +02:00
|
|
|
|
2021-02-26 10:56:20 +01:00
|
|
|
// private properties
|
2021-10-11 18:27:23 +02:00
|
|
|
private var isMounted = false
|
2023-08-21 12:50:14 +02:00
|
|
|
internal val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
|
2021-07-12 15:16:03 +02:00
|
|
|
|
2023-08-21 12:50:14 +02:00
|
|
|
// session
|
|
|
|
internal val cameraSession: CameraSession
|
2023-09-22 17:32:34 +02:00
|
|
|
private var previewView: PreviewView? = null
|
2023-08-21 12:50:14 +02:00
|
|
|
private var previewSurface: Surface? = null
|
2021-12-30 11:39:17 +01:00
|
|
|
|
2023-08-21 12:50:14 +02:00
|
|
|
internal var frameProcessor: FrameProcessor? = null
|
|
|
|
set(value) {
|
|
|
|
field = value
|
2023-09-01 12:20:17 +02:00
|
|
|
cameraSession.frameProcessor = frameProcessor
|
2022-01-04 16:57:40 +01:00
|
|
|
}
|
2021-07-26 11:32:58 +02:00
|
|
|
|
2023-08-21 12:50:14 +02:00
|
|
|
private val inputOrientation: Orientation
|
|
|
|
get() = cameraSession.orientation
|
|
|
|
internal val outputOrientation: Orientation
|
|
|
|
get() = orientation ?: inputOrientation
|
|
|
|
|
2023-10-10 18:51:46 +02:00
|
|
|
override val coroutineContext: CoroutineContext = CameraQueues.cameraQueue.coroutineDispatcher
|
|
|
|
|
2021-02-26 10:56:20 +01:00
|
|
|
init {
|
2023-08-21 12:50:14 +02:00
|
|
|
this.installHierarchyFitter()
|
2023-10-14 13:17:05 +02:00
|
|
|
clipToOutline = true
|
2023-08-21 12:50:14 +02:00
|
|
|
setupPreviewView()
|
|
|
|
cameraSession = CameraSession(context, cameraManager, { invokeOnInitialized() }, { error -> invokeOnError(error) })
|
2021-02-26 10:56:20 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun onAttachedToWindow() {
|
|
|
|
super.onAttachedToWindow()
|
2021-10-11 18:27:23 +02:00
|
|
|
if (!isMounted) {
|
|
|
|
isMounted = true
|
|
|
|
invokeOnViewReady()
|
|
|
|
}
|
2023-10-10 18:51:46 +02:00
|
|
|
launch { updateLifecycle() }
|
2021-02-26 10:56:20 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun onDetachedFromWindow() {
|
|
|
|
super.onDetachedFromWindow()
|
2023-10-10 18:51:46 +02:00
|
|
|
launch { updateLifecycle() }
|
2021-02-26 10:56:20 +01:00
|
|
|
}
|
|
|
|
|
2023-09-22 17:32:34 +02:00
|
|
|
private fun getPreviewTargetSize(): Size {
|
|
|
|
val cameraId = cameraId ?: throw NoCameraDeviceError()
|
|
|
|
|
|
|
|
val format = format
|
|
|
|
val targetPreviewSize = if (format != null) Size(format.getInt("videoWidth"), format.getInt("videoHeight")) else null
|
|
|
|
val formatAspectRatio = if (targetPreviewSize != null) targetPreviewSize.bigger.toDouble() / targetPreviewSize.smaller else null
|
|
|
|
|
|
|
|
return this.cameraManager.getCameraCharacteristics(cameraId).getPreviewTargetSize(formatAspectRatio)
|
|
|
|
}
|
|
|
|
|
2023-08-21 12:50:14 +02:00
|
|
|
private fun setupPreviewView() {
|
2023-09-01 10:43:19 +02:00
|
|
|
removeView(previewView)
|
2023-08-21 12:50:14 +02:00
|
|
|
this.previewSurface = null
|
|
|
|
|
2023-09-22 17:32:34 +02:00
|
|
|
if (cameraId == null) return
|
|
|
|
|
|
|
|
val previewView = PreviewView(context, this.getPreviewTargetSize(), resizeMode) { surface ->
|
2023-09-01 10:43:19 +02:00
|
|
|
previewSurface = surface
|
2023-10-10 18:51:46 +02:00
|
|
|
launch { configureSession() }
|
2023-08-21 12:50:14 +02:00
|
|
|
}
|
2023-09-22 17:32:34 +02:00
|
|
|
previewView.layoutParams = LayoutParams(
|
2023-09-23 10:14:27 +02:00
|
|
|
LayoutParams.MATCH_PARENT,
|
|
|
|
LayoutParams.MATCH_PARENT,
|
|
|
|
Gravity.CENTER
|
|
|
|
)
|
2023-09-01 10:43:19 +02:00
|
|
|
this.previewView = previewView
|
2023-10-14 13:17:05 +02:00
|
|
|
UiThreadUtil.runOnUiThread {
|
|
|
|
addView(previewView)
|
|
|
|
}
|
2021-02-26 10:56:20 +01:00
|
|
|
}
|
|
|
|
|
2023-08-21 12:50:14 +02:00
|
|
|
fun update(changedProps: ArrayList<String>) {
|
|
|
|
Log.i(TAG, "Props changed: $changedProps")
|
2023-10-10 19:18:54 +02:00
|
|
|
val shouldReconfigurePreview = changedProps.containsAny(propsThatRequirePreviewReconfiguration)
|
|
|
|
val shouldReconfigureSession = shouldReconfigurePreview || changedProps.containsAny(propsThatRequireSessionReconfiguration)
|
|
|
|
val shouldReconfigureFormat = shouldReconfigureSession || changedProps.containsAny(propsThatRequireFormatReconfiguration)
|
|
|
|
val shouldReconfigureZoom = shouldReconfigureSession || changedProps.contains("zoom")
|
|
|
|
val shouldReconfigureTorch = shouldReconfigureSession || changedProps.contains("torch")
|
|
|
|
val shouldCheckActive = shouldReconfigureFormat || changedProps.contains("isActive")
|
|
|
|
val shouldReconfigureZoomGesture = changedProps.contains("enableZoomGesture")
|
|
|
|
|
|
|
|
launch {
|
|
|
|
try {
|
2023-10-10 18:51:46 +02:00
|
|
|
// Expensive Calls
|
|
|
|
if (shouldReconfigurePreview) {
|
|
|
|
setupPreviewView()
|
|
|
|
}
|
|
|
|
if (shouldReconfigureSession) {
|
|
|
|
configureSession()
|
|
|
|
}
|
|
|
|
if (shouldReconfigureFormat) {
|
|
|
|
configureFormat()
|
|
|
|
}
|
|
|
|
if (shouldCheckActive) {
|
|
|
|
updateLifecycle()
|
|
|
|
}
|
|
|
|
// Fast Calls
|
|
|
|
if (shouldReconfigureZoom) {
|
|
|
|
updateZoom()
|
|
|
|
}
|
|
|
|
if (shouldReconfigureTorch) {
|
|
|
|
updateTorch()
|
|
|
|
}
|
|
|
|
if (shouldReconfigureZoomGesture) {
|
|
|
|
updateZoomGesture()
|
|
|
|
}
|
2023-10-10 19:18:54 +02:00
|
|
|
} catch (e: Throwable) {
|
|
|
|
Log.e(TAG, "update() threw: ${e.message}")
|
|
|
|
invokeOnError(e)
|
2023-08-23 15:39:24 +02:00
|
|
|
}
|
2023-08-21 12:50:14 +02:00
|
|
|
}
|
|
|
|
}
|
2021-02-26 10:56:20 +01:00
|
|
|
|
2023-10-10 18:51:46 +02:00
|
|
|
private suspend fun configureSession() {
|
2023-08-21 12:50:14 +02:00
|
|
|
try {
|
|
|
|
Log.i(TAG, "Configuring Camera Device...")
|
2021-07-07 12:57:28 +02:00
|
|
|
|
2023-08-21 12:50:14 +02:00
|
|
|
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
|
|
|
|
throw CameraPermissionError()
|
2021-07-07 12:57:28 +02:00
|
|
|
}
|
2023-08-21 12:50:14 +02:00
|
|
|
val cameraId = cameraId ?: throw NoCameraDeviceError()
|
2021-02-26 10:56:20 +01:00
|
|
|
|
2023-08-21 12:50:14 +02:00
|
|
|
val format = format
|
|
|
|
val targetVideoSize = if (format != null) Size(format.getInt("videoWidth"), format.getInt("videoHeight")) else null
|
|
|
|
val targetPhotoSize = if (format != null) Size(format.getInt("photoWidth"), format.getInt("photoHeight")) else null
|
|
|
|
// TODO: Allow previewSurface to be null/none
|
|
|
|
val previewSurface = previewSurface ?: return
|
2023-10-04 12:53:52 +02:00
|
|
|
val codeScannerOptions = codeScannerOptions
|
2021-12-30 11:39:17 +01:00
|
|
|
|
2023-09-22 17:32:34 +02:00
|
|
|
val previewOutput = CameraOutputs.PreviewOutput(previewSurface, previewView?.targetSize)
|
2023-08-21 12:50:14 +02:00
|
|
|
val photoOutput = if (photo == true) {
|
|
|
|
CameraOutputs.PhotoOutput(targetPhotoSize)
|
2023-09-21 11:20:33 +02:00
|
|
|
} else {
|
|
|
|
null
|
|
|
|
}
|
2023-08-21 12:50:14 +02:00
|
|
|
val videoOutput = if (video == true || enableFrameProcessor) {
|
2023-09-29 21:52:19 +02:00
|
|
|
CameraOutputs.VideoOutput(targetVideoSize, video == true, enableFrameProcessor, pixelFormat)
|
2023-09-21 11:20:33 +02:00
|
|
|
} else {
|
|
|
|
null
|
|
|
|
}
|
2023-10-04 12:53:52 +02:00
|
|
|
val codeScanner = if (codeScannerOptions != null) {
|
|
|
|
CameraOutputs.CodeScannerOutput(
|
|
|
|
codeScannerOptions,
|
|
|
|
{ codes -> invokeOnCodeScanned(codes) },
|
|
|
|
{ error -> invokeOnError(error) }
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
null
|
|
|
|
}
|
2021-12-30 11:39:17 +01:00
|
|
|
|
2023-10-04 12:53:52 +02:00
|
|
|
cameraSession.configureSession(cameraId, previewOutput, photoOutput, videoOutput, codeScanner)
|
2023-08-21 12:50:14 +02:00
|
|
|
} catch (e: Throwable) {
|
|
|
|
Log.e(TAG, "Failed to configure session: ${e.message}", e)
|
|
|
|
invokeOnError(e)
|
|
|
|
}
|
|
|
|
}
|
2021-02-26 10:56:20 +01:00
|
|
|
|
2023-10-10 18:51:46 +02:00
|
|
|
private suspend fun configureFormat() {
|
2023-08-21 12:50:14 +02:00
|
|
|
cameraSession.configureFormat(fps, videoStabilizationMode, hdr, lowLightBoost)
|
|
|
|
}
|
2022-01-04 16:57:40 +01:00
|
|
|
|
2023-10-10 18:51:46 +02:00
|
|
|
private suspend fun updateLifecycle() {
|
2023-08-21 12:50:14 +02:00
|
|
|
cameraSession.setIsActive(isActive && isAttachedToWindow)
|
|
|
|
}
|
2021-06-27 12:37:54 +02:00
|
|
|
|
2023-10-10 18:51:46 +02:00
|
|
|
private suspend fun updateZoom() {
|
2023-08-21 12:50:14 +02:00
|
|
|
cameraSession.setZoom(zoom)
|
|
|
|
}
|
|
|
|
|
2023-10-10 18:51:46 +02:00
|
|
|
private suspend fun updateTorch() {
|
|
|
|
cameraSession.setTorchMode(torch == Torch.ON)
|
2021-02-26 10:56:20 +01:00
|
|
|
}
|
2023-08-23 15:39:24 +02:00
|
|
|
|
|
|
|
@SuppressLint("ClickableViewAccessibility")
|
|
|
|
private fun updateZoomGesture() {
|
|
|
|
if (enableZoomGesture) {
|
2023-09-21 11:20:33 +02:00
|
|
|
val scaleGestureDetector = ScaleGestureDetector(
|
|
|
|
context,
|
|
|
|
object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
|
|
|
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
|
|
|
zoom *= detector.scaleFactor
|
2023-10-10 18:51:46 +02:00
|
|
|
launch { updateZoom() }
|
2023-09-21 11:20:33 +02:00
|
|
|
return true
|
|
|
|
}
|
2023-08-23 15:39:24 +02:00
|
|
|
}
|
2023-09-21 11:20:33 +02:00
|
|
|
)
|
2023-08-23 15:39:24 +02:00
|
|
|
setOnTouchListener { _, event ->
|
|
|
|
scaleGestureDetector.onTouchEvent(event)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
setOnTouchListener(null)
|
|
|
|
}
|
|
|
|
}
|
2021-02-19 16:28:14 +01:00
|
|
|
}
|