2021-02-19 12:41:49 -07:00
package com.mrousavy.camera
2021-02-19 08:28:14 -07:00
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager
import android.os.Build
import android.util.Log
import androidx.camera.core.CameraSelector
2021-07-07 04:57:28 -06:00
import androidx.camera.extensions.ExtensionMode
2021-03-12 02:45:23 -07:00
import androidx.camera.extensions.ExtensionsManager
import androidx.camera.lifecycle.ProcessCameraProvider
2021-02-19 08:28:14 -07:00
import androidx.core.content.ContextCompat
import com.facebook.react.bridge.*
import com.facebook.react.modules.core.PermissionAwareActivity
import com.facebook.react.modules.core.PermissionListener
2021-06-27 04:37:54 -06:00
import com.mrousavy.camera.frameprocessor.FrameProcessorRuntimeManager
2021-02-26 02:56:20 -07:00
import com.mrousavy.camera.parsers.*
import com.mrousavy.camera.utils.*
2021-06-27 04:37:54 -06:00
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
2021-03-12 02:45:23 -07:00
import kotlinx.coroutines.*
import kotlinx.coroutines.guava.await
2021-02-19 08:28:14 -07:00
class CameraViewModule ( reactContext : ReactApplicationContext ) : ReactContextBaseJavaModule ( reactContext ) {
2021-02-26 02:56:20 -07:00
companion object {
2021-07-07 07:00:32 -06:00
const val TAG = " CameraView "
2021-02-26 02:56:20 -07:00
var RequestCode = 10
2021-06-27 04:37:54 -06:00
val FrameProcessorThread : ExecutorService = Executors . newSingleThreadExecutor ( )
2021-02-26 02:56:20 -07:00
fun parsePermissionStatus ( status : Int ) : String {
return when ( status ) {
PackageManager . PERMISSION _DENIED -> " denied "
PackageManager . PERMISSION _GRANTED -> " authorized "
else -> " not-determined "
}
2021-02-19 08:28:14 -07:00
}
2021-02-26 02:56:20 -07:00
}
2021-02-19 08:28:14 -07:00
2021-06-27 04:37:54 -06:00
private var frameProcessorManager : FrameProcessorRuntimeManager ? = null
override fun initialize ( ) {
super . initialize ( )
2021-07-07 07:00:32 -06:00
if ( frameProcessorManager == null ) {
FrameProcessorThread . execute {
frameProcessorManager = FrameProcessorRuntimeManager ( reactApplicationContext )
reactApplicationContext . runOnJSQueueThread {
frameProcessorManager !! . installJSIBindings ( )
}
2021-06-27 04:37:54 -06:00
}
}
}
override fun onCatalystInstanceDestroy ( ) {
super . onCatalystInstanceDestroy ( )
frameProcessorManager ?. destroy ( )
2021-07-07 07:00:32 -06:00
frameProcessorManager = null
2021-06-27 04:37:54 -06:00
}
2021-02-26 02:56:20 -07:00
override fun getName ( ) : String {
2021-07-07 07:00:32 -06:00
return TAG
2021-02-26 02:56:20 -07:00
}
2021-02-19 08:28:14 -07:00
2021-02-26 02:56:20 -07:00
private fun findCameraView ( id : Int ) : CameraView = reactApplicationContext . currentActivity ?. findViewById ( id ) ?: throw ViewNotFoundError ( id )
2021-02-19 08:28:14 -07:00
2021-02-26 02:56:20 -07:00
@ReactMethod
fun takePhoto ( viewTag : Int , options : ReadableMap , promise : Promise ) {
GlobalScope . launch ( Dispatchers . Main ) {
withPromise ( promise ) {
val view = findCameraView ( viewTag )
view . takePhoto ( options )
}
2021-02-19 08:28:14 -07:00
}
2021-02-26 02:56:20 -07:00
}
@ReactMethod
fun takeSnapshot ( viewTag : Int , options : ReadableMap , promise : Promise ) {
GlobalScope . launch ( Dispatchers . Main ) {
withPromise ( promise ) {
val view = findCameraView ( viewTag )
view . takeSnapshot ( options )
}
2021-02-19 08:28:14 -07:00
}
2021-02-26 02:56:20 -07:00
}
// TODO: startRecording() cannot be awaited, because I can't have a Promise and a onRecordedCallback in the same function. Hopefully TurboModules allows that
2021-06-07 05:08:40 -06:00
@ReactMethod
2021-02-26 02:56:20 -07:00
fun startRecording ( viewTag : Int , options : ReadableMap , onRecordCallback : Callback ) {
GlobalScope . launch ( Dispatchers . Main ) {
val view = findCameraView ( viewTag )
2021-06-07 05:08:40 -06:00
try {
view . startRecording ( options , onRecordCallback )
} catch ( error : CameraError ) {
val map = makeErrorMap ( " ${error.domain} / ${error.id} " , error . message , error )
onRecordCallback ( null , map )
} catch ( error : Throwable ) {
2021-06-27 04:37:54 -06:00
val map = makeErrorMap ( " capture/unknown " , " An unknown error occurred while trying to start a video recording! " , error )
2021-06-07 05:08:40 -06:00
onRecordCallback ( null , map )
}
2021-02-19 08:28:14 -07:00
}
2021-02-26 02:56:20 -07:00
}
@ReactMethod
fun stopRecording ( viewTag : Int , promise : Promise ) {
withPromise ( promise ) {
val view = findCameraView ( viewTag )
view . stopRecording ( )
return @withPromise null
2021-02-19 08:28:14 -07:00
}
2021-02-26 02:56:20 -07:00
}
@ReactMethod
fun focus ( viewTag : Int , point : ReadableMap , promise : Promise ) {
GlobalScope . launch ( Dispatchers . Main ) {
withPromise ( promise ) {
val view = findCameraView ( viewTag )
view . focus ( point )
return @withPromise null
}
}
}
2021-02-19 08:28:14 -07:00
2021-02-26 02:56:20 -07:00
// TODO: This uses the Camera2 API to list all characteristics of a camera device and therefore doesn't work with Camera1. Find a way to use CameraX for this
2021-03-12 02:45:23 -07:00
// https://issuetracker.google.com/issues/179925896
2021-02-26 02:56:20 -07:00
@ReactMethod
fun getAvailableCameraDevices ( promise : Promise ) {
2021-03-12 02:45:23 -07:00
val startTime = System . currentTimeMillis ( )
GlobalScope . launch ( Dispatchers . Main ) {
withPromise ( promise ) {
2021-07-07 04:57:28 -06:00
val extensionsManager = ExtensionsManager . getInstance ( reactApplicationContext ) . await ( )
val cameraProvider = ProcessCameraProvider . getInstance ( reactApplicationContext ) . await ( )
2021-04-13 05:01:24 -06:00
ProcessCameraProvider . getInstance ( reactApplicationContext ) . await ( )
2021-03-12 02:45:23 -07:00
val manager = reactApplicationContext . getSystemService ( Context . CAMERA _SERVICE ) as ? CameraManager
?: throw CameraManagerUnavailableError ( )
val cameraDevices : WritableArray = Arguments . createArray ( )
manager . cameraIdList . forEach loop @ { id ->
val cameraSelector = CameraSelector . Builder ( ) . byID ( id ) . build ( )
val characteristics = manager . getCameraCharacteristics ( id )
val hardwareLevel = characteristics . get ( CameraCharacteristics . INFO _SUPPORTED _HARDWARE _LEVEL ) !!
val capabilities = characteristics . get ( CameraCharacteristics . REQUEST _AVAILABLE _CAPABILITIES ) !!
val isMultiCam = Build . VERSION . SDK _INT >= Build . VERSION_CODES . P &&
capabilities . contains ( CameraCharacteristics . REQUEST _AVAILABLE _CAPABILITIES _LOGICAL _MULTI _CAMERA )
val deviceTypes = characteristics . getDeviceTypes ( )
val cameraConfig = characteristics . get ( CameraCharacteristics . SCALER _STREAM _CONFIGURATION _MAP ) !!
val lensFacing = characteristics . get ( CameraCharacteristics . LENS _FACING ) !!
val hasFlash = characteristics . get ( CameraCharacteristics . FLASH _INFO _AVAILABLE ) !!
val maxScalerZoom = characteristics . get ( CameraCharacteristics . SCALER _AVAILABLE _MAX _DIGITAL _ZOOM ) !!
val supportsDepthCapture = Build . VERSION . SDK _INT >= Build . VERSION_CODES . M &&
capabilities . contains ( CameraCharacteristics . REQUEST _AVAILABLE _CAPABILITIES _DEPTH _OUTPUT )
val supportsRawCapture = capabilities . contains ( CameraCharacteristics . REQUEST _AVAILABLE _CAPABILITIES _RAW )
val isoRange = characteristics . get ( CameraCharacteristics . SENSOR _INFO _SENSITIVITY _RANGE )
val stabilizationModes = characteristics . get ( CameraCharacteristics . CONTROL _AVAILABLE _VIDEO _STABILIZATION _MODES ) !! // only digital, no optical
val zoomRange = if ( Build . VERSION . SDK _INT >= Build . VERSION_CODES . R )
characteristics . get ( CameraCharacteristics . CONTROL _ZOOM _RATIO _RANGE )
else null
val name = if ( Build . VERSION . SDK _INT >= Build . VERSION_CODES . P )
characteristics . get ( CameraCharacteristics . INFO _VERSION )
else null
val fpsRanges = characteristics . get ( CameraCharacteristics . CONTROL _AE _AVAILABLE _TARGET _FPS _RANGES ) !!
2021-02-26 02:56:20 -07:00
2021-07-07 04:57:28 -06:00
val supportsHdr = extensionsManager . isExtensionAvailable ( cameraProvider , cameraSelector , ExtensionMode . HDR )
val supportsLowLightBoost = extensionsManager . isExtensionAvailable ( cameraProvider , cameraSelector , ExtensionMode . NIGHT )
2021-06-27 04:37:54 -06:00
// see https://developer.android.com/reference/android/hardware/camera2/CameraDevice#regular-capture
val supportsParallelVideoProcessing = hardwareLevel != CameraCharacteristics . INFO _SUPPORTED _HARDWARE _LEVEL _LEGACY && hardwareLevel != CameraCharacteristics . INFO _SUPPORTED _HARDWARE _LEVEL _LIMITED
2021-03-12 02:45:23 -07:00
val fieldOfView = characteristics . getFieldOfView ( )
val map = Arguments . createMap ( )
map . putString ( " id " , id )
map . putArray ( " devices " , deviceTypes )
map . putString ( " position " , parseLensFacing ( lensFacing ) )
map . putString ( " name " , name ?: " ${parseLensFacing(lensFacing)} ( $id ) " )
map . putBoolean ( " hasFlash " , hasFlash )
map . putBoolean ( " hasTorch " , hasFlash )
map . putBoolean ( " isMultiCam " , isMultiCam )
2021-06-27 04:37:54 -06:00
map . putBoolean ( " supportsParallelVideoProcessing " , supportsParallelVideoProcessing )
2021-03-12 02:45:23 -07:00
map . putBoolean ( " supportsRawCapture " , supportsRawCapture )
map . putBoolean ( " supportsDepthCapture " , supportsDepthCapture )
map . putBoolean ( " supportsLowLightBoost " , supportsLowLightBoost )
2021-03-17 11:07:05 -06:00
map . putBoolean ( " supportsFocus " , true ) // I believe every device here supports focussing
2021-03-12 02:45:23 -07:00
if ( zoomRange != null ) {
map . putDouble ( " minZoom " , zoomRange . lower . toDouble ( ) )
map . putDouble ( " maxZoom " , zoomRange . upper . toDouble ( ) )
} else {
map . putDouble ( " minZoom " , 1.0 )
map . putDouble ( " maxZoom " , maxScalerZoom . toDouble ( ) )
2021-02-26 02:56:20 -07:00
}
2021-06-07 02:46:53 -06:00
map . putDouble ( " neutralZoom " , 1.0 )
2021-03-12 02:45:23 -07:00
2021-04-13 05:01:24 -06:00
// TODO: Optimize?
val maxImageOutputSize = cameraConfig . outputFormats
. flatMap { cameraConfig . getOutputSizes ( it ) . toList ( ) }
. maxByOrNull { it . width * it . height } !!
2021-03-12 02:45:23 -07:00
2021-04-13 05:01:24 -06:00
val formats = Arguments . createArray ( )
2021-03-12 02:45:23 -07:00
2021-04-13 05:01:24 -06:00
cameraConfig . outputFormats . forEach { formatId ->
val formatName = parseImageFormat ( formatId )
cameraConfig . getOutputSizes ( formatId ) . forEach { size ->
val isHighestPhotoQualitySupported = areUltimatelyEqual ( size , maxImageOutputSize )
// Get the number of seconds that each frame will take to process
val secondsPerFrame = try {
cameraConfig . getOutputMinFrameDuration ( formatId , size ) / 1 _000 _000 _000 . 0
} catch ( error : Throwable ) {
2021-07-07 07:00:32 -06:00
Log . e ( TAG , " Minimum Frame Duration for MediaRecorder Output cannot be calculated, format \" $formatName \" is not supported. " )
2021-04-13 05:01:24 -06:00
null
}
val frameRateRanges = Arguments . createArray ( )
if ( secondsPerFrame != null && secondsPerFrame > 0 ) {
val fps = ( 1.0 / secondsPerFrame ) . toInt ( )
val frameRateRange = Arguments . createMap ( )
frameRateRange . putInt ( " minFrameRate " , 1 )
frameRateRange . putInt ( " maxFrameRate " , fps )
frameRateRanges . pushMap ( frameRateRange )
}
fpsRanges . forEach { range ->
val frameRateRange = Arguments . createMap ( )
frameRateRange . putInt ( " minFrameRate " , range . lower )
frameRateRange . putInt ( " maxFrameRate " , range . upper )
frameRateRanges . pushMap ( frameRateRange )
}
val colorSpaces = Arguments . createArray ( )
colorSpaces . pushString ( formatName )
val videoStabilizationModes = Arguments . createArray ( )
if ( stabilizationModes . contains ( CameraCharacteristics . CONTROL _VIDEO _STABILIZATION _MODE _OFF ) ) {
videoStabilizationModes . pushString ( " off " )
}
if ( stabilizationModes . contains ( CameraCharacteristics . CONTROL _VIDEO _STABILIZATION _MODE _ON ) ) {
videoStabilizationModes . pushString ( " auto " )
videoStabilizationModes . pushString ( " standard " )
}
val format = Arguments . createMap ( )
format . putDouble ( " photoHeight " , size . height . toDouble ( ) )
format . putDouble ( " photoWidth " , size . width . toDouble ( ) )
format . putDouble ( " videoHeight " , size . height . toDouble ( ) ) // TODO: Revisit getAvailableCameraDevices (videoHeight == photoHeight?)
format . putDouble ( " videoWidth " , size . width . toDouble ( ) ) // TODO: Revisit getAvailableCameraDevices (videoWidth == photoWidth?)
format . putBoolean ( " isHighestPhotoQualitySupported " , isHighestPhotoQualitySupported )
format . putInt ( " maxISO " , isoRange ?. upper )
format . putInt ( " minISO " , isoRange ?. lower )
format . putDouble ( " fieldOfView " , fieldOfView ) // TODO: Revisit getAvailableCameraDevices (is fieldOfView accurate?)
format . putDouble ( " maxZoom " , ( zoomRange ?. upper ?: maxScalerZoom ) . toDouble ( ) )
format . putArray ( " colorSpaces " , colorSpaces )
format . putBoolean ( " supportsVideoHDR " , false ) // TODO: supportsVideoHDR
format . putBoolean ( " supportsPhotoHDR " , supportsHdr )
format . putArray ( " frameRateRanges " , frameRateRanges )
format . putString ( " autoFocusSystem " , " none " ) // TODO: Revisit getAvailableCameraDevices (autoFocusSystem) (CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES or CameraCharacteristics.LENS_INFO_FOCUS_DISTANCE_CALIBRATION)
format . putArray ( " videoStabilizationModes " , videoStabilizationModes )
formats . pushMap ( format )
2021-03-17 12:29:03 -06:00
}
2021-02-26 02:56:20 -07:00
}
2021-03-12 02:45:23 -07:00
map . putArray ( " formats " , formats )
cameraDevices . pushMap ( map )
2021-02-19 08:28:14 -07:00
}
2021-03-12 02:45:23 -07:00
val difference = System . currentTimeMillis ( ) - startTime
2021-07-07 07:00:32 -06:00
Log . w ( TAG , " CameraViewModule::getAvailableCameraDevices took: $difference ms " )
2021-03-12 02:45:23 -07:00
return @withPromise cameraDevices
2021-02-26 02:56:20 -07:00
}
2021-02-19 08:28:14 -07:00
}
2021-02-26 02:56:20 -07:00
}
@ReactMethod
fun getCameraPermissionStatus ( promise : Promise ) {
val status = ContextCompat . checkSelfPermission ( reactApplicationContext , Manifest . permission . CAMERA )
promise . resolve ( parsePermissionStatus ( status ) )
}
@ReactMethod
fun getMicrophonePermissionStatus ( promise : Promise ) {
val status = ContextCompat . checkSelfPermission ( reactApplicationContext , Manifest . permission . RECORD _AUDIO )
promise . resolve ( parsePermissionStatus ( status ) )
}
@ReactMethod
fun requestCameraPermission ( promise : Promise ) {
2021-08-09 03:43:56 -06:00
if ( Build . VERSION . SDK _INT < Build . VERSION_CODES . M ) {
// API 21 and below always grants permission on app install
return promise . resolve ( " authorized " )
}
2021-02-26 02:56:20 -07:00
val activity = reactApplicationContext . currentActivity
if ( activity is PermissionAwareActivity ) {
2021-04-20 02:21:59 -06:00
val currentRequestCode = RequestCode ++
2021-02-26 02:56:20 -07:00
val listener = PermissionListener { requestCode : Int , _ : Array < String > , grantResults : IntArray ->
if ( requestCode == currentRequestCode ) {
2021-04-26 07:39:20 -06:00
val permissionStatus = if ( grantResults . isNotEmpty ( ) ) grantResults [ 0 ] else PackageManager . PERMISSION _DENIED
2021-02-26 02:56:20 -07:00
promise . resolve ( parsePermissionStatus ( permissionStatus ) )
return @PermissionListener true
2021-02-19 08:28:14 -07:00
}
2021-02-26 02:56:20 -07:00
return @PermissionListener false
}
activity . requestPermissions ( arrayOf ( Manifest . permission . CAMERA ) , currentRequestCode , listener )
} else {
promise . reject ( " NO_ACTIVITY " , " No PermissionAwareActivity was found! Make sure the app has launched before calling this function. " )
2021-02-19 08:28:14 -07:00
}
2021-02-26 02:56:20 -07:00
}
@ReactMethod
fun requestMicrophonePermission ( promise : Promise ) {
2021-08-09 03:43:56 -06:00
if ( Build . VERSION . SDK _INT < Build . VERSION_CODES . M ) {
// API 21 and below always grants permission on app install
return promise . resolve ( " authorized " )
}
2021-02-26 02:56:20 -07:00
val activity = reactApplicationContext . currentActivity
if ( activity is PermissionAwareActivity ) {
2021-04-20 02:21:59 -06:00
val currentRequestCode = RequestCode ++
2021-02-26 02:56:20 -07:00
val listener = PermissionListener { requestCode : Int , _ : Array < String > , grantResults : IntArray ->
if ( requestCode == currentRequestCode ) {
2021-04-26 07:39:20 -06:00
val permissionStatus = if ( grantResults . isNotEmpty ( ) ) grantResults [ 0 ] else PackageManager . PERMISSION _DENIED
2021-02-26 02:56:20 -07:00
promise . resolve ( parsePermissionStatus ( permissionStatus ) )
return @PermissionListener true
2021-02-19 08:28:14 -07:00
}
2021-02-26 02:56:20 -07:00
return @PermissionListener false
}
activity . requestPermissions ( arrayOf ( Manifest . permission . RECORD _AUDIO ) , currentRequestCode , listener )
} else {
promise . reject ( " NO_ACTIVITY " , " No PermissionAwareActivity was found! Make sure the app has launched before calling this function. " )
2021-02-19 08:28:14 -07:00
}
2021-02-26 02:56:20 -07:00
}
2021-02-19 08:28:14 -07:00
}