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
|
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-08-23 15:39:24 +02:00
|
|
|
import android.view.ScaleGestureDetector
|
2023-08-21 12:50:14 +02:00
|
|
|
import android.view.Surface
|
|
|
|
import android.view.View
|
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
|
|
|
|
import com.mrousavy.camera.extensions.containsAny
|
|
|
|
import com.mrousavy.camera.extensions.installHierarchyFitter
|
2023-07-22 00:15:11 +02:00
|
|
|
import com.mrousavy.camera.frameprocessor.FrameProcessor
|
2023-08-21 12:50:14 +02:00
|
|
|
import com.mrousavy.camera.parsers.Orientation
|
2023-08-23 15:39:24 +02:00
|
|
|
import com.mrousavy.camera.parsers.PixelFormat
|
2023-08-21 12:50:14 +02:00
|
|
|
import com.mrousavy.camera.parsers.PreviewType
|
|
|
|
import com.mrousavy.camera.parsers.Torch
|
|
|
|
import com.mrousavy.camera.parsers.VideoStabilizationMode
|
|
|
|
import com.mrousavy.camera.skia.SkiaPreviewView
|
|
|
|
import com.mrousavy.camera.skia.SkiaRenderer
|
|
|
|
import com.mrousavy.camera.utils.outputs.CameraOutputs
|
|
|
|
import kotlinx.coroutines.CoroutineScope
|
|
|
|
import kotlinx.coroutines.Dispatchers
|
|
|
|
import kotlinx.coroutines.launch
|
|
|
|
import java.io.Closeable
|
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.
|
|
|
|
//
|
|
|
|
// CameraView
|
|
|
|
// TODO: High-speed video recordings (export in CameraViewModule::getAvailableVideoDevices(), and set in CameraView::configurePreview()) (120FPS+)
|
|
|
|
// TODO: configureSession() enableDepthData
|
|
|
|
// TODO: configureSession() enablePortraitEffectsMatteDelivery
|
|
|
|
|
|
|
|
// CameraView+RecordVideo
|
|
|
|
// TODO: Better startRecording()/stopRecording() (promise + callback, wait for TurboModules/JSI)
|
|
|
|
|
|
|
|
// CameraView+TakePhoto
|
|
|
|
// TODO: takePhoto() depth data
|
|
|
|
// TODO: takePhoto() raw capture
|
|
|
|
// TODO: takePhoto() photoCodec ("hevc" | "jpeg" | "raw")
|
|
|
|
// TODO: takePhoto() return with jsi::Value Image reference for faster capture
|
|
|
|
|
2023-08-21 12:50:14 +02:00
|
|
|
@SuppressLint("ClickableViewAccessibility", "ViewConstructor", "MissingPermission")
|
|
|
|
class CameraView(context: Context) : FrameLayout(context) {
|
2021-08-25 11:33:57 +02:00
|
|
|
companion object {
|
|
|
|
const val TAG = "CameraView"
|
|
|
|
|
2023-08-21 12:50:14 +02:00
|
|
|
private val propsThatRequirePreviewReconfiguration = arrayListOf("cameraId", "previewType")
|
|
|
|
private val propsThatRequireSessionReconfiguration = arrayListOf("cameraId", "format", "photo", "video", "enableFrameProcessor", "pixelFormat")
|
|
|
|
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
|
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
|
2021-02-26 10:56:20 +01:00
|
|
|
// props that require format reconfiguring
|
|
|
|
var format: ReadableMap? = null
|
|
|
|
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-08-21 12:50:14 +02:00
|
|
|
var previewType: PreviewType = PreviewType.NONE
|
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
|
|
|
|
|
|
|
// 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
|
|
|
|
private var previewView: View? = null
|
|
|
|
private var previewSurface: Surface? = null
|
2021-12-30 11:39:17 +01:00
|
|
|
|
2023-08-21 12:50:14 +02:00
|
|
|
private var skiaRenderer: SkiaRenderer? = null
|
|
|
|
internal var frameProcessor: FrameProcessor? = null
|
|
|
|
set(value) {
|
|
|
|
field = value
|
2023-08-29 17:52:03 +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
|
|
|
|
|
2021-02-26 10:56:20 +01:00
|
|
|
init {
|
2023-08-21 12:50:14 +02:00
|
|
|
this.installHierarchyFitter()
|
|
|
|
setupPreviewView()
|
|
|
|
cameraSession = CameraSession(context, cameraManager, { invokeOnInitialized() }, { error -> invokeOnError(error) })
|
2021-02-26 10:56:20 +01:00
|
|
|
}
|
|
|
|
|
2021-07-26 11:32:58 +02:00
|
|
|
override fun onConfigurationChanged(newConfig: Configuration?) {
|
|
|
|
super.onConfigurationChanged(newConfig)
|
2023-08-21 12:50:14 +02:00
|
|
|
// TODO: updateOrientation()
|
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-08-21 12:50:14 +02:00
|
|
|
updateLifecycle()
|
2021-02-26 10:56:20 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun onDetachedFromWindow() {
|
|
|
|
super.onDetachedFromWindow()
|
2023-08-21 12:50:14 +02:00
|
|
|
updateLifecycle()
|
2021-02-26 10:56:20 +01:00
|
|
|
}
|
|
|
|
|
2023-08-21 12:50:14 +02:00
|
|
|
private fun setupPreviewView() {
|
|
|
|
this.previewView?.let { previewView ->
|
|
|
|
removeView(previewView)
|
|
|
|
if (previewView is Closeable) previewView.close()
|
|
|
|
}
|
|
|
|
this.previewSurface = null
|
|
|
|
|
|
|
|
when (previewType) {
|
|
|
|
PreviewType.NONE -> {
|
|
|
|
// Do nothing.
|
|
|
|
}
|
|
|
|
PreviewType.NATIVE -> {
|
|
|
|
val cameraId = cameraId ?: throw NoCameraDeviceError()
|
|
|
|
this.previewView = NativePreviewView(context, cameraManager, cameraId) { surface ->
|
|
|
|
previewSurface = surface
|
2021-03-12 10:45:23 +01:00
|
|
|
configureSession()
|
|
|
|
}
|
2023-08-21 12:50:14 +02:00
|
|
|
}
|
|
|
|
PreviewType.SKIA -> {
|
|
|
|
if (skiaRenderer == null) skiaRenderer = SkiaRenderer()
|
|
|
|
this.previewView = SkiaPreviewView(context, skiaRenderer!!)
|
|
|
|
configureSession()
|
2021-02-26 10:56:20 +01:00
|
|
|
}
|
2021-02-19 16:28:14 +01:00
|
|
|
}
|
2023-08-21 12:50:14 +02:00
|
|
|
|
|
|
|
this.previewView?.let { previewView ->
|
|
|
|
previewView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
|
|
|
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")
|
2021-02-26 10:56:20 +01:00
|
|
|
try {
|
2023-08-21 12:50:14 +02:00
|
|
|
val shouldReconfigurePreview = changedProps.containsAny(propsThatRequirePreviewReconfiguration)
|
|
|
|
val shouldReconfigureSession = shouldReconfigurePreview || changedProps.containsAny(propsThatRequireSessionReconfiguration)
|
|
|
|
val shouldReconfigureFormat = shouldReconfigureSession || changedProps.containsAny(propsThatRequireFormatReconfiguration)
|
2023-08-23 15:39:24 +02:00
|
|
|
val shouldReconfigureZoom = shouldReconfigureSession || changedProps.contains("zoom")
|
|
|
|
val shouldReconfigureTorch = shouldReconfigureSession || changedProps.contains("torch")
|
2023-08-21 12:50:14 +02:00
|
|
|
val shouldUpdateOrientation = /* TODO: When should we reconfigure this? */ shouldReconfigureSession || changedProps.contains("orientation")
|
|
|
|
val shouldCheckActive = shouldReconfigureFormat || changedProps.contains("isActive")
|
2023-08-23 15:39:24 +02:00
|
|
|
val shouldReconfigureZoomGesture = changedProps.contains("enableZoomGesture")
|
2023-08-21 12:50:14 +02:00
|
|
|
|
|
|
|
if (shouldReconfigurePreview) {
|
|
|
|
setupPreviewView()
|
|
|
|
}
|
|
|
|
if (shouldReconfigureSession) {
|
|
|
|
configureSession()
|
2021-02-26 10:56:20 +01:00
|
|
|
}
|
2023-08-21 12:50:14 +02:00
|
|
|
if (shouldReconfigureFormat) {
|
|
|
|
configureFormat()
|
|
|
|
}
|
|
|
|
if (shouldCheckActive) {
|
|
|
|
updateLifecycle()
|
2021-02-26 10:56:20 +01:00
|
|
|
}
|
|
|
|
|
2023-08-21 12:50:14 +02:00
|
|
|
if (shouldReconfigureZoom) {
|
|
|
|
updateZoom()
|
|
|
|
}
|
|
|
|
if (shouldReconfigureTorch) {
|
|
|
|
updateTorch()
|
|
|
|
}
|
|
|
|
if (shouldUpdateOrientation) {
|
|
|
|
// TODO: updateOrientation()
|
|
|
|
}
|
2023-08-23 15:39:24 +02:00
|
|
|
if (shouldReconfigureZoomGesture) {
|
|
|
|
updateZoomGesture()
|
|
|
|
}
|
2023-08-21 12:50:14 +02:00
|
|
|
} catch (e: Throwable) {
|
|
|
|
Log.e(TAG, "update() threw: ${e.message}")
|
|
|
|
invokeOnError(e)
|
|
|
|
}
|
|
|
|
}
|
2021-02-26 10:56:20 +01:00
|
|
|
|
2023-08-21 12:50:14 +02:00
|
|
|
private fun configureSession() {
|
|
|
|
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
|
2021-12-30 11:39:17 +01:00
|
|
|
|
2023-08-21 12:50:14 +02:00
|
|
|
if (targetVideoSize != null) skiaRenderer?.setInputSurfaceSize(targetVideoSize.width, targetVideoSize.height)
|
2021-02-26 10:56:20 +01:00
|
|
|
|
2023-08-21 12:50:14 +02:00
|
|
|
val previewOutput = CameraOutputs.PreviewOutput(previewSurface)
|
|
|
|
val photoOutput = if (photo == true) {
|
|
|
|
CameraOutputs.PhotoOutput(targetPhotoSize)
|
|
|
|
} else null
|
|
|
|
val videoOutput = if (video == true || enableFrameProcessor) {
|
|
|
|
CameraOutputs.VideoOutput(targetVideoSize, video == true, enableFrameProcessor, pixelFormat.toImageFormat())
|
|
|
|
} else null
|
2021-12-30 11:39:17 +01:00
|
|
|
|
2023-08-21 12:50:14 +02:00
|
|
|
cameraSession.configureSession(cameraId, previewOutput, photoOutput, videoOutput)
|
|
|
|
} catch (e: Throwable) {
|
|
|
|
Log.e(TAG, "Failed to configure session: ${e.message}", e)
|
|
|
|
invokeOnError(e)
|
|
|
|
}
|
|
|
|
}
|
2021-02-26 10:56:20 +01:00
|
|
|
|
2023-08-21 12:50:14 +02:00
|
|
|
private fun configureFormat() {
|
|
|
|
cameraSession.configureFormat(fps, videoStabilizationMode, hdr, lowLightBoost)
|
|
|
|
}
|
2022-01-04 16:57:40 +01:00
|
|
|
|
2023-08-21 12:50:14 +02:00
|
|
|
private fun updateLifecycle() {
|
|
|
|
cameraSession.setIsActive(isActive && isAttachedToWindow)
|
|
|
|
}
|
2021-06-27 12:37:54 +02:00
|
|
|
|
2023-08-21 12:50:14 +02:00
|
|
|
private fun updateZoom() {
|
|
|
|
cameraSession.setZoom(zoom)
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun updateTorch() {
|
|
|
|
CoroutineScope(Dispatchers.Default).launch {
|
|
|
|
cameraSession.setTorchMode(torch == Torch.ON)
|
2021-02-19 16:28:14 +01:00
|
|
|
}
|
2021-02-26 10:56:20 +01:00
|
|
|
}
|
2023-08-23 15:39:24 +02:00
|
|
|
|
|
|
|
@SuppressLint("ClickableViewAccessibility")
|
|
|
|
private fun updateZoomGesture() {
|
|
|
|
if (enableZoomGesture) {
|
|
|
|
val scaleGestureDetector = ScaleGestureDetector(context, object: ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
|
|
|
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
|
|
|
zoom *= detector.scaleFactor
|
|
|
|
cameraSession.setZoom(zoom)
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
})
|
|
|
|
setOnTouchListener { _, event ->
|
|
|
|
scaleGestureDetector.onTouchEvent(event)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
setOnTouchListener(null)
|
|
|
|
}
|
|
|
|
}
|
2021-02-19 16:28:14 +01:00
|
|
|
}
|