Devops: KTLint to lint Kotlin code (#6)
* Adds KTLint as a GitHub action * Adds KTLint to the gradle project for IDE integration * Adds .editorconfig to configure KTLint (android/)
This commit is contained in:
parent
2e60110070
commit
03b9246afe
11
.github/workflows/validate-android.yml
vendored
11
.github/workflows/validate-android.yml
vendored
@ -7,10 +7,12 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- '.github/workflows/validate-android.yml'
|
- '.github/workflows/validate-android.yml'
|
||||||
- 'android/**'
|
- 'android/**'
|
||||||
|
- '.editorconfig'
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- '.github/workflows/validate-android.yml'
|
- '.github/workflows/validate-android.yml'
|
||||||
- 'android/**'
|
- 'android/**'
|
||||||
|
- '.editorconfig'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
@ -32,3 +34,12 @@ jobs:
|
|||||||
- uses: yutailang0119/action-android-lint@v1.0.2
|
- uses: yutailang0119/action-android-lint@v1.0.2
|
||||||
with:
|
with:
|
||||||
xml_path: android/build/reports/lint-results.xml
|
xml_path: android/build/reports/lint-results.xml
|
||||||
|
ktlint:
|
||||||
|
name: Kotlin Lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Run KTLint
|
||||||
|
uses: mrousavy/action-ktlint@v1.6
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.github_token }}
|
||||||
|
5
android/.editorconfig
Normal file
5
android/.editorconfig
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
[*.{kt,kts}]
|
||||||
|
indent_size=2
|
||||||
|
insert_final_newline=true
|
||||||
|
max_line_length=off
|
||||||
|
disabled_rules=no-wildcard-imports
|
16
android/README.md
Normal file
16
android/README.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# android
|
||||||
|
|
||||||
|
This folder contains the Android-platform-specific code for react-native-vision-camera.
|
||||||
|
|
||||||
|
## Prerequesites
|
||||||
|
|
||||||
|
1. Install ktlint
|
||||||
|
```sh
|
||||||
|
brew install ktlint
|
||||||
|
```
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
It is recommended that you work on the code using the Example project (`example/android/`), since that always includes the React Native header files, plus you can easily test changes that way.
|
||||||
|
|
||||||
|
You can however still edit the library project here by opening this folder with Android Studio.
|
@ -15,12 +15,15 @@ buildscript {
|
|||||||
// noinspection DifferentKotlinGradleVersion
|
// noinspection DifferentKotlinGradleVersion
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version"
|
||||||
|
// ktlint
|
||||||
|
classpath "org.jlleitschuh.gradle:ktlint-gradle:10.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
apply plugin: 'com.android.library'
|
apply plugin: 'com.android.library'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'kotlin-android-extensions'
|
apply plugin: 'kotlin-android-extensions'
|
||||||
|
apply plugin: 'org.jlleitschuh.gradle.ktlint'
|
||||||
|
|
||||||
def getExtOrDefault(name) {
|
def getExtOrDefault(name) {
|
||||||
return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['VisionCamera_' + name]
|
return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['VisionCamera_' + name]
|
||||||
|
@ -6,11 +6,11 @@ import com.facebook.react.bridge.ReactApplicationContext
|
|||||||
import com.facebook.react.uimanager.ViewManager
|
import com.facebook.react.uimanager.ViewManager
|
||||||
|
|
||||||
class CameraPackage : ReactPackage {
|
class CameraPackage : ReactPackage {
|
||||||
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
||||||
return listOf(CameraViewModule(reactContext))
|
return listOf(CameraViewModule(reactContext))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
||||||
return listOf(CameraViewManager())
|
return listOf(CameraViewManager())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,20 +7,20 @@ import kotlinx.coroutines.guava.await
|
|||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
suspend fun CameraView.focus(pointMap: ReadableMap) {
|
suspend fun CameraView.focus(pointMap: ReadableMap) {
|
||||||
val cameraControl = camera?.cameraControl ?: throw CameraNotReadyError()
|
val cameraControl = camera?.cameraControl ?: throw CameraNotReadyError()
|
||||||
if (!pointMap.hasKey("x") || !pointMap.hasKey("y")) {
|
if (!pointMap.hasKey("x") || !pointMap.hasKey("y")) {
|
||||||
throw InvalidTypeScriptUnionError("point", pointMap.toString())
|
throw InvalidTypeScriptUnionError("point", pointMap.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
val dpi = resources.displayMetrics.density
|
val dpi = resources.displayMetrics.density
|
||||||
val x = pointMap.getDouble("x") * dpi
|
val x = pointMap.getDouble("x") * dpi
|
||||||
val y = pointMap.getDouble("y") * dpi
|
val y = pointMap.getDouble("y") * dpi
|
||||||
|
|
||||||
val factory = SurfaceOrientedMeteringPointFactory(this.width.toFloat(), this.height.toFloat())
|
val factory = SurfaceOrientedMeteringPointFactory(this.width.toFloat(), this.height.toFloat())
|
||||||
val point = factory.createPoint(x.toFloat(), y.toFloat())
|
val point = factory.createPoint(x.toFloat(), y.toFloat())
|
||||||
val action = FocusMeteringAction.Builder(point, FocusMeteringAction.FLAG_AF or FocusMeteringAction.FLAG_AE)
|
val action = FocusMeteringAction.Builder(point, FocusMeteringAction.FLAG_AF or FocusMeteringAction.FLAG_AE)
|
||||||
.setAutoCancelDuration(5, TimeUnit.SECONDS) // auto-reset after 5 seconds
|
.setAutoCancelDuration(5, TimeUnit.SECONDS) // auto-reset after 5 seconds
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
cameraControl.startFocusAndMetering(action).await()
|
cameraControl.startFocusAndMetering(action).await()
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,8 @@ package com.mrousavy.camera
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import androidx.camera.core.VideoCapture
|
import androidx.camera.core.VideoCapture
|
||||||
import com.mrousavy.camera.utils.makeErrorMap
|
|
||||||
import com.facebook.react.bridge.*
|
import com.facebook.react.bridge.*
|
||||||
|
import com.mrousavy.camera.utils.makeErrorMap
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@ -11,60 +11,62 @@ data class TemporaryFile(val path: String)
|
|||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
suspend fun CameraView.startRecording(options: ReadableMap, onRecordCallback: Callback): TemporaryFile {
|
suspend fun CameraView.startRecording(options: ReadableMap, onRecordCallback: Callback): TemporaryFile {
|
||||||
if (videoCapture == null) {
|
if (videoCapture == null) {
|
||||||
throw CameraNotReadyError()
|
throw CameraNotReadyError()
|
||||||
}
|
}
|
||||||
if (options.hasKey("flash")) {
|
if (options.hasKey("flash")) {
|
||||||
val enableFlash = options.getString("flash") == "on"
|
val enableFlash = options.getString("flash") == "on"
|
||||||
// overrides current torch mode value to enable flash while recording
|
// overrides current torch mode value to enable flash while recording
|
||||||
camera!!.cameraControl.enableTorch(enableFlash)
|
camera!!.cameraControl.enableTorch(enableFlash)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext") // in withContext we are not blocking. False positive.
|
@Suppress("BlockingMethodInNonBlockingContext") // in withContext we are not blocking. False positive.
|
||||||
val videoFile = withContext(Dispatchers.IO) {
|
val videoFile = withContext(Dispatchers.IO) {
|
||||||
File.createTempFile("video", ".mp4", context.cacheDir).apply { deleteOnExit() }
|
File.createTempFile("video", ".mp4", context.cacheDir).apply { deleteOnExit() }
|
||||||
}
|
}
|
||||||
val videoFileOptions = VideoCapture.OutputFileOptions.Builder(videoFile)
|
val videoFileOptions = VideoCapture.OutputFileOptions.Builder(videoFile)
|
||||||
|
|
||||||
videoCapture!!.startRecording(videoFileOptions.build(), recordVideoExecutor, object : VideoCapture.OnVideoSavedCallback {
|
videoCapture!!.startRecording(
|
||||||
override fun onVideoSaved(outputFileResults: VideoCapture.OutputFileResults) {
|
videoFileOptions.build(), recordVideoExecutor,
|
||||||
val map = Arguments.createMap()
|
object : VideoCapture.OnVideoSavedCallback {
|
||||||
map.putString("path", videoFile.absolutePath)
|
override fun onVideoSaved(outputFileResults: VideoCapture.OutputFileResults) {
|
||||||
// TODO: duration and size
|
val map = Arguments.createMap()
|
||||||
onRecordCallback(map, null)
|
map.putString("path", videoFile.absolutePath)
|
||||||
|
// TODO: duration and size
|
||||||
|
onRecordCallback(map, null)
|
||||||
|
|
||||||
// reset the torch mode
|
// reset the torch mode
|
||||||
camera!!.cameraControl.enableTorch(torch == "on")
|
camera!!.cameraControl.enableTorch(torch == "on")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(videoCaptureError: Int, message: String, cause: Throwable?) {
|
||||||
|
val error = when (videoCaptureError) {
|
||||||
|
VideoCapture.ERROR_ENCODER -> VideoEncoderError(message, cause)
|
||||||
|
VideoCapture.ERROR_FILE_IO -> FileIOError(message, cause)
|
||||||
|
VideoCapture.ERROR_INVALID_CAMERA -> InvalidCameraError(message, cause)
|
||||||
|
VideoCapture.ERROR_MUXER -> VideoMuxerError(message, cause)
|
||||||
|
VideoCapture.ERROR_RECORDING_IN_PROGRESS -> RecordingInProgressError(message, cause)
|
||||||
|
else -> UnknownCameraError(Error(message, cause))
|
||||||
}
|
}
|
||||||
|
val map = makeErrorMap("${error.domain}/${error.id}", error.message, error)
|
||||||
|
onRecordCallback(null, map)
|
||||||
|
|
||||||
override fun onError(videoCaptureError: Int, message: String, cause: Throwable?) {
|
// reset the torch mode
|
||||||
val error = when (videoCaptureError) {
|
camera!!.cameraControl.enableTorch(torch == "on")
|
||||||
VideoCapture.ERROR_ENCODER -> VideoEncoderError(message, cause)
|
}
|
||||||
VideoCapture.ERROR_FILE_IO -> FileIOError(message, cause)
|
}
|
||||||
VideoCapture.ERROR_INVALID_CAMERA -> InvalidCameraError(message, cause)
|
)
|
||||||
VideoCapture.ERROR_MUXER -> VideoMuxerError(message, cause)
|
|
||||||
VideoCapture.ERROR_RECORDING_IN_PROGRESS -> RecordingInProgressError(message, cause)
|
|
||||||
else -> UnknownCameraError(Error(message, cause))
|
|
||||||
}
|
|
||||||
val map = makeErrorMap("${error.domain}/${error.id}", error.message, error)
|
|
||||||
onRecordCallback(null, map)
|
|
||||||
|
|
||||||
// reset the torch mode
|
return TemporaryFile(videoFile.absolutePath)
|
||||||
camera!!.cameraControl.enableTorch(torch == "on")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return TemporaryFile(videoFile.absolutePath)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
fun CameraView.stopRecording() {
|
fun CameraView.stopRecording() {
|
||||||
if (videoCapture == null) {
|
if (videoCapture == null) {
|
||||||
throw CameraNotReadyError()
|
throw CameraNotReadyError()
|
||||||
}
|
}
|
||||||
|
|
||||||
videoCapture!!.stopRecording()
|
videoCapture!!.stopRecording()
|
||||||
// reset torch mode to original value
|
// reset torch mode to original value
|
||||||
camera!!.cameraControl.enableTorch(torch == "on")
|
camera!!.cameraControl.enableTorch(torch == "on")
|
||||||
}
|
}
|
||||||
|
@ -7,10 +7,10 @@ import androidx.camera.camera2.interop.Camera2CameraInfo
|
|||||||
import androidx.camera.core.ImageCapture
|
import androidx.camera.core.ImageCapture
|
||||||
import androidx.camera.core.ImageProxy
|
import androidx.camera.core.ImageProxy
|
||||||
import androidx.exifinterface.media.ExifInterface
|
import androidx.exifinterface.media.ExifInterface
|
||||||
import com.mrousavy.camera.utils.*
|
|
||||||
import com.facebook.react.bridge.Arguments
|
import com.facebook.react.bridge.Arguments
|
||||||
import com.facebook.react.bridge.ReadableMap
|
import com.facebook.react.bridge.ReadableMap
|
||||||
import com.facebook.react.bridge.WritableMap
|
import com.facebook.react.bridge.WritableMap
|
||||||
|
import com.mrousavy.camera.utils.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@ -18,88 +18,89 @@ private const val TAG = "CameraView.performance"
|
|||||||
|
|
||||||
@SuppressLint("UnsafeExperimentalUsageError")
|
@SuppressLint("UnsafeExperimentalUsageError")
|
||||||
suspend fun CameraView.takePhoto(options: ReadableMap): WritableMap = coroutineScope {
|
suspend fun CameraView.takePhoto(options: ReadableMap): WritableMap = coroutineScope {
|
||||||
val startFunc = System.nanoTime()
|
val startFunc = System.nanoTime()
|
||||||
Log.d(CameraView.REACT_CLASS, "takePhoto() called")
|
Log.d(CameraView.REACT_CLASS, "takePhoto() called")
|
||||||
val imageCapture = imageCapture ?: throw CameraNotReadyError()
|
val imageCapture = imageCapture ?: throw CameraNotReadyError()
|
||||||
|
|
||||||
if (options.hasKey("photoCodec")) {
|
if (options.hasKey("photoCodec")) {
|
||||||
// TODO photoCodec
|
// TODO photoCodec
|
||||||
|
}
|
||||||
|
if (options.hasKey("qualityPrioritization")) {
|
||||||
|
// TODO qualityPrioritization
|
||||||
|
}
|
||||||
|
if (options.hasKey("flash")) {
|
||||||
|
val flashMode = options.getString("flash")
|
||||||
|
imageCapture.flashMode = when (flashMode) {
|
||||||
|
"on" -> ImageCapture.FLASH_MODE_ON
|
||||||
|
"off" -> ImageCapture.FLASH_MODE_OFF
|
||||||
|
"auto" -> ImageCapture.FLASH_MODE_AUTO
|
||||||
|
else -> throw InvalidTypeScriptUnionError("flash", flashMode ?: "(null)")
|
||||||
}
|
}
|
||||||
if (options.hasKey("qualityPrioritization")) {
|
}
|
||||||
// TODO qualityPrioritization
|
if (options.hasKey("enableAutoRedEyeReduction")) {
|
||||||
}
|
// TODO enableAutoRedEyeReduction
|
||||||
if (options.hasKey("flash")) {
|
}
|
||||||
val flashMode = options.getString("flash")
|
if (options.hasKey("enableDualCameraFusion")) {
|
||||||
imageCapture.flashMode = when (flashMode) {
|
// TODO enableDualCameraFusion
|
||||||
"on" -> ImageCapture.FLASH_MODE_ON
|
}
|
||||||
"off" -> ImageCapture.FLASH_MODE_OFF
|
if (options.hasKey("enableVirtualDeviceFusion")) {
|
||||||
"auto" -> ImageCapture.FLASH_MODE_AUTO
|
// TODO enableVirtualDeviceFusion
|
||||||
else -> throw InvalidTypeScriptUnionError("flash", flashMode ?: "(null)")
|
}
|
||||||
}
|
if (options.hasKey("enableAutoStabilization")) {
|
||||||
}
|
// TODO enableAutoStabilization
|
||||||
if (options.hasKey("enableAutoRedEyeReduction")) {
|
}
|
||||||
// TODO enableAutoRedEyeReduction
|
if (options.hasKey("enableAutoDistortionCorrection")) {
|
||||||
}
|
// TODO enableAutoDistortionCorrection
|
||||||
if (options.hasKey("enableDualCameraFusion")) {
|
}
|
||||||
// TODO enableDualCameraFusion
|
val skipMetadata = if (options.hasKey("skipMetadata")) options.getBoolean("skipMetadata") else false
|
||||||
}
|
|
||||||
if (options.hasKey("enableVirtualDeviceFusion")) {
|
|
||||||
// TODO enableVirtualDeviceFusion
|
|
||||||
}
|
|
||||||
if (options.hasKey("enableAutoStabilization")) {
|
|
||||||
// TODO enableAutoStabilization
|
|
||||||
}
|
|
||||||
if (options.hasKey("enableAutoDistortionCorrection")) {
|
|
||||||
// TODO enableAutoDistortionCorrection
|
|
||||||
}
|
|
||||||
val skipMetadata = if (options.hasKey("skipMetadata")) options.getBoolean("skipMetadata") else false
|
|
||||||
|
|
||||||
val camera2Info = Camera2CameraInfo.from(camera!!.cameraInfo)
|
val camera2Info = Camera2CameraInfo.from(camera!!.cameraInfo)
|
||||||
val lensFacing = camera2Info.getCameraCharacteristic(CameraCharacteristics.LENS_FACING)
|
val lensFacing = camera2Info.getCameraCharacteristic(CameraCharacteristics.LENS_FACING)
|
||||||
// TODO: Flip image if lens is front side
|
// TODO: Flip image if lens is front side
|
||||||
|
|
||||||
val results = awaitAll(
|
val results = awaitAll(
|
||||||
async(coroutineContext) {
|
async(coroutineContext) {
|
||||||
Log.d(CameraView.REACT_CLASS, "Taking picture...")
|
Log.d(CameraView.REACT_CLASS, "Taking picture...")
|
||||||
val startCapture = System.nanoTime()
|
val startCapture = System.nanoTime()
|
||||||
val pic = imageCapture.takePicture(takePhotoExecutor)
|
val pic = imageCapture.takePicture(takePhotoExecutor)
|
||||||
val endCapture = System.nanoTime()
|
val endCapture = System.nanoTime()
|
||||||
Log.d(TAG, "Finished image capture in ${(endCapture - startCapture) / 1_000_000}ms")
|
Log.d(TAG, "Finished image capture in ${(endCapture - startCapture) / 1_000_000}ms")
|
||||||
pic
|
pic
|
||||||
},
|
},
|
||||||
async(Dispatchers.IO) {
|
async(Dispatchers.IO) {
|
||||||
Log.d(CameraView.REACT_CLASS, "Creating temp file...")
|
Log.d(CameraView.REACT_CLASS, "Creating temp file...")
|
||||||
File.createTempFile("mrousavy", ".jpg", context.cacheDir).apply { deleteOnExit() }
|
File.createTempFile("mrousavy", ".jpg", context.cacheDir).apply { deleteOnExit() }
|
||||||
})
|
|
||||||
val photo = results.first { it is ImageProxy } as ImageProxy
|
|
||||||
val file = results.first { it is File } as File
|
|
||||||
|
|
||||||
val exif: ExifInterface?
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
Log.d(CameraView.REACT_CLASS, "Saving picture to ${file.absolutePath}...")
|
|
||||||
val startSave = System.nanoTime()
|
|
||||||
photo.save(file, lensFacing == CameraCharacteristics.LENS_FACING_FRONT)
|
|
||||||
val endSave = System.nanoTime()
|
|
||||||
Log.d(TAG, "Finished image saving in ${(endSave - startSave) / 1_000_000}ms")
|
|
||||||
// TODO: Read Exif from existing in-memory photo buffer instead of file?
|
|
||||||
exif = if (skipMetadata) null else ExifInterface(file)
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
val photo = results.first { it is ImageProxy } as ImageProxy
|
||||||
|
val file = results.first { it is File } as File
|
||||||
|
|
||||||
val map = Arguments.createMap()
|
val exif: ExifInterface?
|
||||||
map.putString("path", file.absolutePath)
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
map.putInt("width", photo.width)
|
withContext(Dispatchers.IO) {
|
||||||
map.putInt("height", photo.height)
|
Log.d(CameraView.REACT_CLASS, "Saving picture to ${file.absolutePath}...")
|
||||||
map.putBoolean("isRawPhoto", photo.isRaw)
|
val startSave = System.nanoTime()
|
||||||
|
photo.save(file, lensFacing == CameraCharacteristics.LENS_FACING_FRONT)
|
||||||
|
val endSave = System.nanoTime()
|
||||||
|
Log.d(TAG, "Finished image saving in ${(endSave - startSave) / 1_000_000}ms")
|
||||||
|
// TODO: Read Exif from existing in-memory photo buffer instead of file?
|
||||||
|
exif = if (skipMetadata) null else ExifInterface(file)
|
||||||
|
}
|
||||||
|
|
||||||
val metadata = exif?.buildMetadataMap()
|
val map = Arguments.createMap()
|
||||||
map.putMap("metadata", metadata)
|
map.putString("path", file.absolutePath)
|
||||||
|
map.putInt("width", photo.width)
|
||||||
|
map.putInt("height", photo.height)
|
||||||
|
map.putBoolean("isRawPhoto", photo.isRaw)
|
||||||
|
|
||||||
photo.close()
|
val metadata = exif?.buildMetadataMap()
|
||||||
|
map.putMap("metadata", metadata)
|
||||||
|
|
||||||
Log.d(CameraView.REACT_CLASS, "Finished taking photo!")
|
photo.close()
|
||||||
|
|
||||||
val endFunc = System.nanoTime()
|
Log.d(CameraView.REACT_CLASS, "Finished taking photo!")
|
||||||
Log.d(TAG, "Finished function execution in ${(endFunc - startFunc) / 1_000_000}ms")
|
|
||||||
return@coroutineScope map
|
val endFunc = System.nanoTime()
|
||||||
|
Log.d(TAG, "Finished function execution in ${(endFunc - startFunc) / 1_000_000}ms")
|
||||||
|
return@coroutineScope map
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,10 @@ package com.mrousavy.camera
|
|||||||
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import androidx.exifinterface.media.ExifInterface
|
import androidx.exifinterface.media.ExifInterface
|
||||||
import com.mrousavy.camera.utils.buildMetadataMap
|
|
||||||
import com.facebook.react.bridge.Arguments
|
import com.facebook.react.bridge.Arguments
|
||||||
import com.facebook.react.bridge.ReadableMap
|
import com.facebook.react.bridge.ReadableMap
|
||||||
import com.facebook.react.bridge.WritableMap
|
import com.facebook.react.bridge.WritableMap
|
||||||
|
import com.mrousavy.camera.utils.buildMetadataMap
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@ -13,30 +13,30 @@ import java.io.File
|
|||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
|
|
||||||
suspend fun CameraView.takeSnapshot(options: ReadableMap): WritableMap = coroutineScope {
|
suspend fun CameraView.takeSnapshot(options: ReadableMap): WritableMap = coroutineScope {
|
||||||
val bitmap = this@takeSnapshot.previewView.bitmap ?: throw CameraNotReadyError()
|
val bitmap = this@takeSnapshot.previewView.bitmap ?: throw CameraNotReadyError()
|
||||||
|
|
||||||
val quality = if (options.hasKey("quality")) options.getInt("quality") else 100
|
val quality = if (options.hasKey("quality")) options.getInt("quality") else 100
|
||||||
|
|
||||||
val file: File
|
val file: File
|
||||||
val exif: ExifInterface
|
val exif: ExifInterface
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
file = File.createTempFile("mrousavy", ".jpg", context.cacheDir).apply { deleteOnExit() }
|
file = File.createTempFile("mrousavy", ".jpg", context.cacheDir).apply { deleteOnExit() }
|
||||||
FileOutputStream(file).use { stream ->
|
FileOutputStream(file).use { stream ->
|
||||||
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, stream)
|
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, stream)
|
||||||
}
|
|
||||||
exif = ExifInterface(file)
|
|
||||||
}
|
}
|
||||||
|
exif = ExifInterface(file)
|
||||||
|
}
|
||||||
|
|
||||||
val map = Arguments.createMap()
|
val map = Arguments.createMap()
|
||||||
map.putString("path", file.absolutePath)
|
map.putString("path", file.absolutePath)
|
||||||
map.putInt("width", bitmap.width)
|
map.putInt("width", bitmap.width)
|
||||||
map.putInt("height", bitmap.height)
|
map.putInt("height", bitmap.height)
|
||||||
map.putBoolean("isRawPhoto", false)
|
map.putBoolean("isRawPhoto", false)
|
||||||
|
|
||||||
val skipMetadata = if (options.hasKey("skipMetadata")) options.getBoolean("skipMetadata") else false
|
val skipMetadata = if (options.hasKey("skipMetadata")) options.getBoolean("skipMetadata") else false
|
||||||
val metadata = if (skipMetadata) null else exif.buildMetadataMap()
|
val metadata = if (skipMetadata) null else exif.buildMetadataMap()
|
||||||
map.putMap("metadata", metadata)
|
map.putMap("metadata", metadata)
|
||||||
|
|
||||||
return@coroutineScope map
|
return@coroutineScope map
|
||||||
}
|
}
|
||||||
|
@ -20,9 +20,9 @@ import androidx.camera.extensions.NightPreviewExtender
|
|||||||
import androidx.camera.view.PreviewView
|
import androidx.camera.view.PreviewView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.*
|
import androidx.lifecycle.*
|
||||||
import com.mrousavy.camera.utils.*
|
|
||||||
import com.facebook.react.bridge.*
|
import com.facebook.react.bridge.*
|
||||||
import com.facebook.react.uimanager.events.RCTEventEmitter
|
import com.facebook.react.uimanager.events.RCTEventEmitter
|
||||||
|
import com.mrousavy.camera.utils.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import java.lang.IllegalArgumentException
|
import java.lang.IllegalArgumentException
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
@ -75,326 +75,326 @@ import kotlin.math.min
|
|||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility") // suppresses the warning that the pinch to zoom gesture is not accessible
|
@SuppressLint("ClickableViewAccessibility") // suppresses the warning that the pinch to zoom gesture is not accessible
|
||||||
class CameraView(context: Context) : FrameLayout(context), LifecycleOwner {
|
class CameraView(context: Context) : FrameLayout(context), LifecycleOwner {
|
||||||
// react properties
|
// react properties
|
||||||
// props that require reconfiguring
|
// props that require reconfiguring
|
||||||
var cameraId: String? = null // this is actually not a react prop directly, but the result of setting device={}
|
var cameraId: String? = null // this is actually not a react prop directly, but the result of setting device={}
|
||||||
var enableDepthData = false
|
var enableDepthData = false
|
||||||
var enableHighResolutionCapture: Boolean? = null
|
var enableHighResolutionCapture: Boolean? = null
|
||||||
var enablePortraitEffectsMatteDelivery = false
|
var enablePortraitEffectsMatteDelivery = false
|
||||||
var scannableCodes: ReadableArray? = null
|
var scannableCodes: ReadableArray? = null
|
||||||
// props that require format reconfiguring
|
// props that require format reconfiguring
|
||||||
var format: ReadableMap? = null
|
var format: ReadableMap? = null
|
||||||
var fps: Int? = null
|
var fps: Int? = null
|
||||||
var hdr: Boolean? = null // nullable bool
|
var hdr: Boolean? = null // nullable bool
|
||||||
var colorSpace: String? = null
|
var colorSpace: String? = null
|
||||||
var lowLightBoost: Boolean? = null // nullable bool
|
var lowLightBoost: Boolean? = null // nullable bool
|
||||||
// other props
|
// other props
|
||||||
var isActive = false
|
var isActive = false
|
||||||
var torch = "off"
|
var torch = "off"
|
||||||
var zoom = 0.0 // in percent
|
var zoom = 0.0 // in percent
|
||||||
var enableZoomGesture = false
|
var enableZoomGesture = false
|
||||||
|
|
||||||
// private properties
|
// private properties
|
||||||
private val reactContext: ReactContext
|
private val reactContext: ReactContext
|
||||||
get() = context as ReactContext
|
get() = context as ReactContext
|
||||||
|
|
||||||
internal val previewView: PreviewView
|
internal val previewView: PreviewView
|
||||||
private val cameraExecutor = Executors.newSingleThreadExecutor()
|
private val cameraExecutor = Executors.newSingleThreadExecutor()
|
||||||
internal val takePhotoExecutor = Executors.newSingleThreadExecutor()
|
internal val takePhotoExecutor = Executors.newSingleThreadExecutor()
|
||||||
internal val recordVideoExecutor = Executors.newSingleThreadExecutor()
|
internal val recordVideoExecutor = Executors.newSingleThreadExecutor()
|
||||||
|
|
||||||
internal var camera: Camera? = null
|
internal var camera: Camera? = null
|
||||||
internal var imageCapture: ImageCapture? = null
|
internal var imageCapture: ImageCapture? = null
|
||||||
internal var videoCapture: VideoCapture? = null
|
internal var videoCapture: VideoCapture? = null
|
||||||
|
|
||||||
private val scaleGestureListener: ScaleGestureDetector.SimpleOnScaleGestureListener
|
private val scaleGestureListener: ScaleGestureDetector.SimpleOnScaleGestureListener
|
||||||
private val scaleGestureDetector: ScaleGestureDetector
|
private val scaleGestureDetector: ScaleGestureDetector
|
||||||
private val touchEventListener: OnTouchListener
|
private val touchEventListener: OnTouchListener
|
||||||
|
|
||||||
private val lifecycleRegistry: LifecycleRegistry
|
private val lifecycleRegistry: LifecycleRegistry
|
||||||
private var hostLifecycleState: Lifecycle.State
|
private var hostLifecycleState: Lifecycle.State
|
||||||
|
|
||||||
private var minZoom: Float = 1f
|
private var minZoom: Float = 1f
|
||||||
private var maxZoom: Float = 1f
|
private var maxZoom: Float = 1f
|
||||||
|
|
||||||
init {
|
init {
|
||||||
previewView = PreviewView(context)
|
previewView = PreviewView(context)
|
||||||
previewView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
previewView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
||||||
previewView.installHierarchyFitter() // If this is not called correctly, view finder will be black/blank
|
previewView.installHierarchyFitter() // If this is not called correctly, view finder will be black/blank
|
||||||
addView(previewView)
|
addView(previewView)
|
||||||
|
|
||||||
scaleGestureListener = object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
scaleGestureListener = object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
||||||
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
||||||
zoom = min(max(((zoom + 1) * detector.scaleFactor) - 1, 0.0), 1.0)
|
zoom = min(max(((zoom + 1) * detector.scaleFactor) - 1, 0.0), 1.0)
|
||||||
update(arrayListOf("zoom"))
|
update(arrayListOf("zoom"))
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
scaleGestureDetector = ScaleGestureDetector(context, scaleGestureListener)
|
||||||
|
touchEventListener = OnTouchListener { _, event -> return@OnTouchListener scaleGestureDetector.onTouchEvent(event) }
|
||||||
|
|
||||||
|
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
|
||||||
|
updateLifecycleState()
|
||||||
|
}
|
||||||
|
override fun onHostDestroy() {
|
||||||
|
hostLifecycleState = Lifecycle.State.DESTROYED
|
||||||
|
updateLifecycleState()
|
||||||
|
cameraExecutor.shutdown()
|
||||||
|
takePhotoExecutor.shutdown()
|
||||||
|
recordVideoExecutor.shutdown()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
Log.d(REACT_CLASS, "Lifecycle went from ${lifecycleBefore.name} -> ${lifecycleRegistry.currentState.name} (isActive: $isActive | isAttachedToWindow: $isAttachedToWindow)")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttachedToWindow() {
|
||||||
|
super.onAttachedToWindow()
|
||||||
|
updateLifecycleState()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromWindow() {
|
||||||
|
super.onDetachedFromWindow()
|
||||||
|
updateLifecycleState()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate all React Props and reconfigure the device
|
||||||
|
*/
|
||||||
|
fun update(changedProps: ArrayList<String>) = GlobalScope.launch(Dispatchers.Main) {
|
||||||
|
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) {
|
||||||
|
val scaled = (zoom.toFloat() * (maxZoom - minZoom)) + minZoom
|
||||||
|
camera!!.cameraControl.setZoomRatio(scaled)
|
||||||
|
}
|
||||||
|
if (shouldReconfigureTorch) {
|
||||||
|
camera!!.cameraControl.enableTorch(torch == "on")
|
||||||
|
}
|
||||||
|
if (changedProps.contains("enableZoomGesture")) {
|
||||||
|
setOnTouchListener(if (enableZoomGesture) touchEventListener else null)
|
||||||
|
}
|
||||||
|
} catch (e: CameraError) {
|
||||||
|
invokeOnError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures the camera capture session. This should only be called when the camera device changes.
|
||||||
|
*/
|
||||||
|
@SuppressLint("UnsafeExperimentalUsageError", "RestrictedApi")
|
||||||
|
private suspend fun configureSession() {
|
||||||
|
try {
|
||||||
|
Log.d(REACT_CLASS, "Configuring session...")
|
||||||
|
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
throw MicrophonePermissionError()
|
||||||
|
}
|
||||||
|
if (ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
throw CameraPermissionError()
|
||||||
|
}
|
||||||
|
if (cameraId == null) {
|
||||||
|
throw NoCameraDeviceError()
|
||||||
|
}
|
||||||
|
if (format != null)
|
||||||
|
Log.d(REACT_CLASS, "Configuring session with Camera ID $cameraId and custom format...")
|
||||||
|
else
|
||||||
|
Log.d(REACT_CLASS, "Configuring session with Camera ID $cameraId and default format options...")
|
||||||
|
|
||||||
|
// Used to bind the lifecycle of cameras to the lifecycle owner
|
||||||
|
val cameraProvider = getCameraProvider(context)
|
||||||
|
|
||||||
|
val cameraSelector = CameraSelector.Builder().byID(cameraId!!).build()
|
||||||
|
|
||||||
|
val rotation = previewView.display.rotation
|
||||||
|
val aspectRatio = aspectRatio(previewView.width, previewView.height)
|
||||||
|
|
||||||
|
val previewBuilder = Preview.Builder()
|
||||||
|
.setTargetAspectRatio(aspectRatio)
|
||||||
|
.setTargetRotation(rotation)
|
||||||
|
val imageCaptureBuilder = ImageCapture.Builder()
|
||||||
|
.setTargetAspectRatio(aspectRatio)
|
||||||
|
.setTargetRotation(rotation)
|
||||||
|
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
|
||||||
|
val videoCaptureBuilder = VideoCapture.Builder()
|
||||||
|
.setTargetAspectRatio(aspectRatio)
|
||||||
|
.setTargetRotation(rotation)
|
||||||
|
|
||||||
|
if (format != null) {
|
||||||
|
// User has selected a custom format={}. Use that
|
||||||
|
val format = DeviceFormat(format!!)
|
||||||
|
|
||||||
|
// The format (exported in CameraViewModule) specifies the resolution in ROTATION_90 (horizontal)
|
||||||
|
val rotationRelativeToFormat = rotation - 1 // subtract one, so that ROTATION_90 becomes ROTATION_0 and so on
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
Log.d(REACT_CLASS, "Setting AE_TARGET_FPS_RANGE to $fps-$fps, and SENSOR_FRAME_DURATION to $frameDuration")
|
||||||
|
Camera2Interop.Extender(previewBuilder)
|
||||||
|
.setCaptureRequestOption(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, Range(fps, fps))
|
||||||
|
.setCaptureRequestOption(CaptureRequest.SENSOR_FRAME_DURATION, frameDuration)
|
||||||
|
Camera2Interop.Extender(videoCaptureBuilder)
|
||||||
|
.setCaptureRequestOption(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, Range(fps, fps))
|
||||||
|
.setCaptureRequestOption(CaptureRequest.SENSOR_FRAME_DURATION, frameDuration)
|
||||||
|
} else {
|
||||||
|
throw FpsNotContainedInFormatError(fps)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
scaleGestureDetector = ScaleGestureDetector(context, scaleGestureListener)
|
hdr?.let { hdr ->
|
||||||
touchEventListener = OnTouchListener { _, event -> return@OnTouchListener scaleGestureDetector.onTouchEvent(event) }
|
// Enable HDR scene mode if set
|
||||||
|
if (hdr) {
|
||||||
hostLifecycleState = Lifecycle.State.INITIALIZED
|
val imageExtension = HdrImageCaptureExtender.create(imageCaptureBuilder)
|
||||||
lifecycleRegistry = LifecycleRegistry(this)
|
val previewExtension = HdrPreviewExtender.create(previewBuilder)
|
||||||
reactContext.addLifecycleEventListener(object : LifecycleEventListener {
|
val isExtensionAvailable = imageExtension.isExtensionAvailable(cameraSelector) &&
|
||||||
override fun onHostResume() {
|
previewExtension.isExtensionAvailable(cameraSelector)
|
||||||
hostLifecycleState = Lifecycle.State.RESUMED
|
if (isExtensionAvailable) {
|
||||||
updateLifecycleState()
|
Log.i(REACT_CLASS, "Enabling native HDR extension...")
|
||||||
}
|
imageExtension.enableExtension(cameraSelector)
|
||||||
override fun onHostPause() {
|
previewExtension.enableExtension(cameraSelector)
|
||||||
hostLifecycleState = Lifecycle.State.CREATED
|
|
||||||
updateLifecycleState()
|
|
||||||
}
|
|
||||||
override fun onHostDestroy() {
|
|
||||||
hostLifecycleState = Lifecycle.State.DESTROYED
|
|
||||||
updateLifecycleState()
|
|
||||||
cameraExecutor.shutdown()
|
|
||||||
takePhotoExecutor.shutdown()
|
|
||||||
recordVideoExecutor.shutdown()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
} else {
|
||||||
lifecycleRegistry.currentState = Lifecycle.State.CREATED
|
Log.e(REACT_CLASS, "Native HDR vendor extension not available!")
|
||||||
|
throw HdrNotContainedInFormatError()
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
// Host Lifecycle (Activity) is currently inactive (STARTED or DESTROYED), so that overrules our view's lifecycle
|
|
||||||
lifecycleRegistry.currentState = hostLifecycleState
|
|
||||||
}
|
}
|
||||||
Log.d(REACT_CLASS, "Lifecycle went from ${lifecycleBefore.name} -> ${lifecycleRegistry.currentState.name} (isActive: $isActive | isAttachedToWindow: $isAttachedToWindow)")
|
lowLightBoost?.let { lowLightBoost ->
|
||||||
}
|
if (lowLightBoost) {
|
||||||
|
val imageExtension = NightImageCaptureExtender.create(imageCaptureBuilder)
|
||||||
override fun onAttachedToWindow() {
|
val previewExtension = NightPreviewExtender.create(previewBuilder)
|
||||||
super.onAttachedToWindow()
|
val isExtensionAvailable = imageExtension.isExtensionAvailable(cameraSelector) &&
|
||||||
updateLifecycleState()
|
previewExtension.isExtensionAvailable(cameraSelector)
|
||||||
}
|
if (isExtensionAvailable) {
|
||||||
|
Log.i(REACT_CLASS, "Enabling native night-mode extension...")
|
||||||
override fun onDetachedFromWindow() {
|
imageExtension.enableExtension(cameraSelector)
|
||||||
super.onDetachedFromWindow()
|
previewExtension.enableExtension(cameraSelector)
|
||||||
updateLifecycleState()
|
} else {
|
||||||
}
|
Log.e(REACT_CLASS, "Native night-mode vendor extension not available!")
|
||||||
|
throw LowLightBoostNotContainedInFormatError()
|
||||||
/**
|
|
||||||
* Invalidate all React Props and reconfigure the device
|
|
||||||
*/
|
|
||||||
fun update(changedProps: ArrayList<String>) = GlobalScope.launch(Dispatchers.Main) {
|
|
||||||
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) {
|
|
||||||
val scaled = (zoom.toFloat() * (maxZoom - minZoom)) + minZoom
|
|
||||||
camera!!.cameraControl.setZoomRatio(scaled)
|
|
||||||
}
|
|
||||||
if (shouldReconfigureTorch) {
|
|
||||||
camera!!.cameraControl.enableTorch(torch == "on")
|
|
||||||
}
|
|
||||||
if (changedProps.contains("enableZoomGesture")) {
|
|
||||||
setOnTouchListener(if (enableZoomGesture) touchEventListener else null)
|
|
||||||
}
|
|
||||||
} catch (e: CameraError) {
|
|
||||||
invokeOnError(e)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// TODO: qualityPrioritization for ImageCapture
|
||||||
* Configures the camera capture session. This should only be called when the camera device changes.
|
imageCaptureBuilder.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
|
||||||
*/
|
val photoResolution = format.photoSize.rotated(rotationRelativeToFormat)
|
||||||
@SuppressLint("UnsafeExperimentalUsageError", "RestrictedApi")
|
// TODO: imageCaptureBuilder.setTargetResolution(photoResolution)
|
||||||
private suspend fun configureSession() {
|
Log.d(REACT_CLASS, "Using Photo Capture resolution $photoResolution")
|
||||||
try {
|
|
||||||
Log.d(REACT_CLASS, "Configuring session...")
|
|
||||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
|
|
||||||
throw MicrophonePermissionError()
|
|
||||||
}
|
|
||||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
|
|
||||||
throw CameraPermissionError()
|
|
||||||
}
|
|
||||||
if (cameraId == null) {
|
|
||||||
throw NoCameraDeviceError()
|
|
||||||
}
|
|
||||||
if (format != null)
|
|
||||||
Log.d(REACT_CLASS, "Configuring session with Camera ID $cameraId and custom format...")
|
|
||||||
else
|
|
||||||
Log.d(REACT_CLASS, "Configuring session with Camera ID $cameraId and default format options...")
|
|
||||||
|
|
||||||
// Used to bind the lifecycle of cameras to the lifecycle owner
|
fps?.let { fps ->
|
||||||
val cameraProvider = getCameraProvider(context)
|
Log.d(REACT_CLASS, "Setting video recording FPS to $fps")
|
||||||
|
videoCaptureBuilder.setVideoFrameRate(fps)
|
||||||
val cameraSelector = CameraSelector.Builder().byID(cameraId!!).build()
|
|
||||||
|
|
||||||
val rotation = previewView.display.rotation
|
|
||||||
val aspectRatio = aspectRatio(previewView.width, previewView.height)
|
|
||||||
|
|
||||||
val previewBuilder = Preview.Builder()
|
|
||||||
.setTargetAspectRatio(aspectRatio)
|
|
||||||
.setTargetRotation(rotation)
|
|
||||||
val imageCaptureBuilder = ImageCapture.Builder()
|
|
||||||
.setTargetAspectRatio(aspectRatio)
|
|
||||||
.setTargetRotation(rotation)
|
|
||||||
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
|
|
||||||
val videoCaptureBuilder = VideoCapture.Builder()
|
|
||||||
.setTargetAspectRatio(aspectRatio)
|
|
||||||
.setTargetRotation(rotation)
|
|
||||||
|
|
||||||
if (format != null) {
|
|
||||||
// User has selected a custom format={}. Use that
|
|
||||||
val format = DeviceFormat(format!!)
|
|
||||||
|
|
||||||
// The format (exported in CameraViewModule) specifies the resolution in ROTATION_90 (horizontal)
|
|
||||||
val rotationRelativeToFormat = rotation - 1 // subtract one, so that ROTATION_90 becomes ROTATION_0 and so on
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
Log.d(REACT_CLASS, "Setting AE_TARGET_FPS_RANGE to $fps-$fps, and SENSOR_FRAME_DURATION to $frameDuration")
|
|
||||||
Camera2Interop.Extender(previewBuilder)
|
|
||||||
.setCaptureRequestOption(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, Range(fps, fps))
|
|
||||||
.setCaptureRequestOption(CaptureRequest.SENSOR_FRAME_DURATION, frameDuration)
|
|
||||||
Camera2Interop.Extender(videoCaptureBuilder)
|
|
||||||
.setCaptureRequestOption(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, Range(fps, fps))
|
|
||||||
.setCaptureRequestOption(CaptureRequest.SENSOR_FRAME_DURATION, frameDuration)
|
|
||||||
} else {
|
|
||||||
throw FpsNotContainedInFormatError(fps)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hdr?.let { hdr ->
|
|
||||||
// Enable HDR scene mode if set
|
|
||||||
if (hdr) {
|
|
||||||
val imageExtension = HdrImageCaptureExtender.create(imageCaptureBuilder)
|
|
||||||
val previewExtension = HdrPreviewExtender.create(previewBuilder)
|
|
||||||
val isExtensionAvailable = imageExtension.isExtensionAvailable(cameraSelector) &&
|
|
||||||
previewExtension.isExtensionAvailable(cameraSelector)
|
|
||||||
if (isExtensionAvailable) {
|
|
||||||
Log.i(REACT_CLASS, "Enabling native HDR extension...")
|
|
||||||
imageExtension.enableExtension(cameraSelector)
|
|
||||||
previewExtension.enableExtension(cameraSelector)
|
|
||||||
} else {
|
|
||||||
Log.e(REACT_CLASS, "Native HDR vendor extension not available!")
|
|
||||||
throw HdrNotContainedInFormatError()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lowLightBoost?.let { lowLightBoost ->
|
|
||||||
if (lowLightBoost) {
|
|
||||||
val imageExtension = NightImageCaptureExtender.create(imageCaptureBuilder)
|
|
||||||
val previewExtension = NightPreviewExtender.create(previewBuilder)
|
|
||||||
val isExtensionAvailable = imageExtension.isExtensionAvailable(cameraSelector) &&
|
|
||||||
previewExtension.isExtensionAvailable(cameraSelector)
|
|
||||||
if (isExtensionAvailable) {
|
|
||||||
Log.i(REACT_CLASS, "Enabling native night-mode extension...")
|
|
||||||
imageExtension.enableExtension(cameraSelector)
|
|
||||||
previewExtension.enableExtension(cameraSelector)
|
|
||||||
} else {
|
|
||||||
Log.e(REACT_CLASS, "Native night-mode vendor extension not available!")
|
|
||||||
throw LowLightBoostNotContainedInFormatError()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: qualityPrioritization for ImageCapture
|
|
||||||
imageCaptureBuilder.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
|
|
||||||
val photoResolution = format.photoSize.rotated(rotationRelativeToFormat)
|
|
||||||
// TODO: imageCaptureBuilder.setTargetResolution(photoResolution)
|
|
||||||
Log.d(REACT_CLASS, "Using Photo Capture resolution $photoResolution")
|
|
||||||
|
|
||||||
fps?.let { fps ->
|
|
||||||
Log.d(REACT_CLASS, "Setting video recording FPS to $fps")
|
|
||||||
videoCaptureBuilder.setVideoFrameRate(fps)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val preview = previewBuilder.build()
|
|
||||||
imageCapture = imageCaptureBuilder.build()
|
|
||||||
videoCapture = videoCaptureBuilder.build()
|
|
||||||
|
|
||||||
// Unbind use cases before rebinding
|
|
||||||
cameraProvider.unbindAll()
|
|
||||||
|
|
||||||
// Bind use cases to camera
|
|
||||||
camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture!!, videoCapture!!)
|
|
||||||
preview.setSurfaceProvider(previewView.surfaceProvider)
|
|
||||||
|
|
||||||
minZoom = camera!!.cameraInfo.zoomState.value?.minZoomRatio ?: 1f
|
|
||||||
maxZoom = camera!!.cameraInfo.zoomState.value?.maxZoomRatio ?: 1f
|
|
||||||
|
|
||||||
Log.d(REACT_CLASS, "Session configured! Camera: ${camera!!}")
|
|
||||||
invokeOnInitialized()
|
|
||||||
} catch(exc: Throwable) {
|
|
||||||
throw when (exc) {
|
|
||||||
is CameraError -> exc
|
|
||||||
is IllegalArgumentException -> InvalidCameraDeviceError(exc)
|
|
||||||
else -> UnknownCameraError(exc)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAvailablePhotoCodecs(): WritableArray {
|
val preview = previewBuilder.build()
|
||||||
// TODO
|
imageCapture = imageCaptureBuilder.build()
|
||||||
return Arguments.createArray()
|
videoCapture = videoCaptureBuilder.build()
|
||||||
}
|
|
||||||
|
|
||||||
fun getAvailableVideoCodecs(): WritableArray {
|
// Unbind use cases before rebinding
|
||||||
// TODO
|
cameraProvider.unbindAll()
|
||||||
return Arguments.createArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
// Bind use cases to camera
|
||||||
super.onLayout(changed, left, top, right, bottom)
|
camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture!!, videoCapture!!)
|
||||||
Log.i(REACT_CLASS, "onLayout($changed, $left, $top, $right, $bottom) was called! (Width: $width, Height: $height)")
|
preview.setSurfaceProvider(previewView.surfaceProvider)
|
||||||
}
|
|
||||||
|
|
||||||
private fun invokeOnInitialized() {
|
minZoom = camera!!.cameraInfo.zoomState.value?.minZoomRatio ?: 1f
|
||||||
val reactContext = context as ReactContext
|
maxZoom = camera!!.cameraInfo.zoomState.value?.maxZoomRatio ?: 1f
|
||||||
reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, "cameraInitialized", null)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun invokeOnError(error: CameraError) {
|
Log.d(REACT_CLASS, "Session configured! Camera: ${camera!!}")
|
||||||
val event = Arguments.createMap()
|
invokeOnInitialized()
|
||||||
event.putString("code", error.code)
|
} catch (exc: Throwable) {
|
||||||
event.putString("message", error.message)
|
throw when (exc) {
|
||||||
error.cause?.let { cause ->
|
is CameraError -> exc
|
||||||
event.putMap("cause", errorToMap(cause))
|
is IllegalArgumentException -> InvalidCameraDeviceError(exc)
|
||||||
}
|
else -> UnknownCameraError(exc)
|
||||||
val reactContext = context as ReactContext
|
}
|
||||||
reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, "cameraError", event)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun errorToMap(error: Throwable): WritableMap {
|
fun getAvailablePhotoCodecs(): WritableArray {
|
||||||
val map = Arguments.createMap()
|
// TODO
|
||||||
map.putString("message", error.message)
|
return Arguments.createArray()
|
||||||
map.putString("stacktrace", error.stackTraceToString())
|
}
|
||||||
error.cause?.let { cause ->
|
|
||||||
map.putMap("cause", errorToMap(cause))
|
fun getAvailableVideoCodecs(): WritableArray {
|
||||||
}
|
// TODO
|
||||||
return map
|
return Arguments.createArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
||||||
|
super.onLayout(changed, left, top, right, bottom)
|
||||||
|
Log.i(REACT_CLASS, "onLayout($changed, $left, $top, $right, $bottom) was called! (Width: $width, Height: $height)")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun invokeOnInitialized() {
|
||||||
|
val reactContext = context as ReactContext
|
||||||
|
reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, "cameraInitialized", null)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun invokeOnError(error: CameraError) {
|
||||||
|
val event = Arguments.createMap()
|
||||||
|
event.putString("code", error.code)
|
||||||
|
event.putString("message", error.message)
|
||||||
|
error.cause?.let { cause ->
|
||||||
|
event.putMap("cause", errorToMap(cause))
|
||||||
}
|
}
|
||||||
|
val reactContext = context as ReactContext
|
||||||
|
reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, "cameraError", event)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
private fun errorToMap(error: Throwable): WritableMap {
|
||||||
const val REACT_CLASS = "CameraView"
|
val map = Arguments.createMap()
|
||||||
|
map.putString("message", error.message)
|
||||||
private val propsThatRequireSessionReconfiguration = arrayListOf("cameraId", "format", "fps", "hdr", "lowLightBoost")
|
map.putString("stacktrace", error.stackTraceToString())
|
||||||
|
error.cause?.let { cause ->
|
||||||
|
map.putMap("cause", errorToMap(cause))
|
||||||
}
|
}
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val REACT_CLASS = "CameraView"
|
||||||
|
|
||||||
|
private val propsThatRequireSessionReconfiguration = arrayListOf("cameraId", "format", "fps", "hdr", "lowLightBoost")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,156 +1,154 @@
|
|||||||
package com.mrousavy.camera
|
package com.mrousavy.camera
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.facebook.react.bridge.ReactContext
|
|
||||||
import com.facebook.react.bridge.ReadableArray
|
import com.facebook.react.bridge.ReadableArray
|
||||||
import com.facebook.react.bridge.ReadableMap
|
import com.facebook.react.bridge.ReadableMap
|
||||||
import com.facebook.react.common.MapBuilder
|
import com.facebook.react.common.MapBuilder
|
||||||
import com.facebook.react.uimanager.SimpleViewManager
|
import com.facebook.react.uimanager.SimpleViewManager
|
||||||
import com.facebook.react.uimanager.ThemedReactContext
|
import com.facebook.react.uimanager.ThemedReactContext
|
||||||
import com.facebook.react.uimanager.annotations.ReactProp
|
import com.facebook.react.uimanager.annotations.ReactProp
|
||||||
import java.lang.ref.WeakReference
|
|
||||||
|
|
||||||
class CameraViewManager : SimpleViewManager<CameraView>() {
|
class CameraViewManager : SimpleViewManager<CameraView>() {
|
||||||
private fun addChangedPropToTransaction(view: CameraView, changedProp: String) {
|
private fun addChangedPropToTransaction(view: CameraView, changedProp: String) {
|
||||||
if (cameraViewTransactions[view] == null) {
|
if (cameraViewTransactions[view] == null) {
|
||||||
cameraViewTransactions[view] = ArrayList()
|
cameraViewTransactions[view] = ArrayList()
|
||||||
}
|
|
||||||
cameraViewTransactions[view]!!.add(changedProp)
|
|
||||||
}
|
}
|
||||||
|
cameraViewTransactions[view]!!.add(changedProp)
|
||||||
|
}
|
||||||
|
|
||||||
@ReactProp(name = "cameraId")
|
@ReactProp(name = "cameraId")
|
||||||
fun setCameraId(view: CameraView, cameraId: String) {
|
fun setCameraId(view: CameraView, cameraId: String) {
|
||||||
if (view.cameraId != cameraId)
|
if (view.cameraId != cameraId)
|
||||||
addChangedPropToTransaction(view, "cameraId")
|
addChangedPropToTransaction(view, "cameraId")
|
||||||
view.cameraId = cameraId
|
view.cameraId = cameraId
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactProp(name = "enableDepthData")
|
@ReactProp(name = "enableDepthData")
|
||||||
fun setEnableDepthData(view: CameraView, enableDepthData: Boolean) {
|
fun setEnableDepthData(view: CameraView, enableDepthData: Boolean) {
|
||||||
if (view.enableDepthData != enableDepthData)
|
if (view.enableDepthData != enableDepthData)
|
||||||
addChangedPropToTransaction(view, "enableDepthData")
|
addChangedPropToTransaction(view, "enableDepthData")
|
||||||
view.enableDepthData = enableDepthData
|
view.enableDepthData = enableDepthData
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactProp(name = "enableHighResolutionCapture")
|
@ReactProp(name = "enableHighResolutionCapture")
|
||||||
fun setEnableHighResolutionCapture(view: CameraView, enableHighResolutionCapture: Boolean?) {
|
fun setEnableHighResolutionCapture(view: CameraView, enableHighResolutionCapture: Boolean?) {
|
||||||
if (view.enableHighResolutionCapture != enableHighResolutionCapture)
|
if (view.enableHighResolutionCapture != enableHighResolutionCapture)
|
||||||
addChangedPropToTransaction(view, "enableHighResolutionCapture")
|
addChangedPropToTransaction(view, "enableHighResolutionCapture")
|
||||||
view.enableHighResolutionCapture = enableHighResolutionCapture
|
view.enableHighResolutionCapture = enableHighResolutionCapture
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactProp(name = "enablePortraitEffectsMatteDelivery")
|
@ReactProp(name = "enablePortraitEffectsMatteDelivery")
|
||||||
fun setEnablePortraitEffectsMatteDelivery(view: CameraView, enablePortraitEffectsMatteDelivery: Boolean) {
|
fun setEnablePortraitEffectsMatteDelivery(view: CameraView, enablePortraitEffectsMatteDelivery: Boolean) {
|
||||||
if (view.enablePortraitEffectsMatteDelivery != enablePortraitEffectsMatteDelivery)
|
if (view.enablePortraitEffectsMatteDelivery != enablePortraitEffectsMatteDelivery)
|
||||||
addChangedPropToTransaction(view, "enablePortraitEffectsMatteDelivery")
|
addChangedPropToTransaction(view, "enablePortraitEffectsMatteDelivery")
|
||||||
view.enablePortraitEffectsMatteDelivery = enablePortraitEffectsMatteDelivery
|
view.enablePortraitEffectsMatteDelivery = enablePortraitEffectsMatteDelivery
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactProp(name = "scannableCodes")
|
@ReactProp(name = "scannableCodes")
|
||||||
fun setScannableCodes(view: CameraView, scannableCodes: ReadableArray?) {
|
fun setScannableCodes(view: CameraView, scannableCodes: ReadableArray?) {
|
||||||
if (view.scannableCodes != scannableCodes)
|
if (view.scannableCodes != scannableCodes)
|
||||||
addChangedPropToTransaction(view, "scannableCodes")
|
addChangedPropToTransaction(view, "scannableCodes")
|
||||||
view.scannableCodes = scannableCodes
|
view.scannableCodes = scannableCodes
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactProp(name = "format")
|
@ReactProp(name = "format")
|
||||||
fun setFormat(view: CameraView, format: ReadableMap?) {
|
fun setFormat(view: CameraView, format: ReadableMap?) {
|
||||||
if (view.format != format)
|
if (view.format != format)
|
||||||
addChangedPropToTransaction(view, "format")
|
addChangedPropToTransaction(view, "format")
|
||||||
view.format = format
|
view.format = format
|
||||||
}
|
}
|
||||||
|
|
||||||
// We're treating -1 as "null" here, because when I make the fps parameter
|
// We're treating -1 as "null" here, because when I make the fps parameter
|
||||||
// of type "Int?" the react bridge throws an error.
|
// of type "Int?" the react bridge throws an error.
|
||||||
@ReactProp(name = "fps", defaultInt = -1)
|
@ReactProp(name = "fps", defaultInt = -1)
|
||||||
fun setFps(view: CameraView, fps: Int) {
|
fun setFps(view: CameraView, fps: Int) {
|
||||||
if (view.fps != fps)
|
if (view.fps != fps)
|
||||||
addChangedPropToTransaction(view, "fps")
|
addChangedPropToTransaction(view, "fps")
|
||||||
view.fps = if (fps > 0) fps else null
|
view.fps = if (fps > 0) fps else null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactProp(name = "hdr")
|
@ReactProp(name = "hdr")
|
||||||
fun setHdr(view: CameraView, hdr: Boolean?) {
|
fun setHdr(view: CameraView, hdr: Boolean?) {
|
||||||
if (view.hdr != hdr)
|
if (view.hdr != hdr)
|
||||||
addChangedPropToTransaction(view, "hdr")
|
addChangedPropToTransaction(view, "hdr")
|
||||||
view.hdr = hdr
|
view.hdr = hdr
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactProp(name = "lowLightBoost")
|
@ReactProp(name = "lowLightBoost")
|
||||||
fun setLowLightBoost(view: CameraView, lowLightBoost: Boolean?) {
|
fun setLowLightBoost(view: CameraView, lowLightBoost: Boolean?) {
|
||||||
if (view.lowLightBoost != lowLightBoost)
|
if (view.lowLightBoost != lowLightBoost)
|
||||||
addChangedPropToTransaction(view, "lowLightBoost")
|
addChangedPropToTransaction(view, "lowLightBoost")
|
||||||
view.lowLightBoost = lowLightBoost
|
view.lowLightBoost = lowLightBoost
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactProp(name = "colorSpace")
|
@ReactProp(name = "colorSpace")
|
||||||
fun setColorSpace(view: CameraView, colorSpace: String?) {
|
fun setColorSpace(view: CameraView, colorSpace: String?) {
|
||||||
if (view.colorSpace != colorSpace)
|
if (view.colorSpace != colorSpace)
|
||||||
addChangedPropToTransaction(view, "colorSpace")
|
addChangedPropToTransaction(view, "colorSpace")
|
||||||
view.colorSpace = colorSpace
|
view.colorSpace = colorSpace
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactProp(name = "isActive")
|
@ReactProp(name = "isActive")
|
||||||
fun setIsActive(view: CameraView, isActive: Boolean) {
|
fun setIsActive(view: CameraView, isActive: Boolean) {
|
||||||
if (view.isActive != isActive)
|
if (view.isActive != isActive)
|
||||||
addChangedPropToTransaction(view, "isActive")
|
addChangedPropToTransaction(view, "isActive")
|
||||||
view.isActive = isActive
|
view.isActive = isActive
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactProp(name = "torch")
|
@ReactProp(name = "torch")
|
||||||
fun setTorch(view: CameraView, torch: String) {
|
fun setTorch(view: CameraView, torch: String) {
|
||||||
if (view.torch != torch)
|
if (view.torch != torch)
|
||||||
addChangedPropToTransaction(view, "torch")
|
addChangedPropToTransaction(view, "torch")
|
||||||
// TODO: why THE FUCK is this not being called?
|
// TODO: why THE FUCK is this not being called?
|
||||||
view.torch = torch
|
view.torch = torch
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactProp(name = "zoom")
|
@ReactProp(name = "zoom")
|
||||||
fun setZoom(view: CameraView, zoom: Double) {
|
fun setZoom(view: CameraView, zoom: Double) {
|
||||||
if (view.zoom != zoom)
|
if (view.zoom != zoom)
|
||||||
addChangedPropToTransaction(view, "zoom")
|
addChangedPropToTransaction(view, "zoom")
|
||||||
// TODO: why THE FUCK is this not being called?
|
// TODO: why THE FUCK is this not being called?
|
||||||
view.zoom = zoom
|
view.zoom = zoom
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactProp(name = "enableZoomGesture")
|
@ReactProp(name = "enableZoomGesture")
|
||||||
fun setEnableZoomGesture(view: CameraView, enableZoomGesture: Boolean) {
|
fun setEnableZoomGesture(view: CameraView, enableZoomGesture: Boolean) {
|
||||||
if (view.enableZoomGesture != enableZoomGesture)
|
if (view.enableZoomGesture != enableZoomGesture)
|
||||||
addChangedPropToTransaction(view, "enableZoomGesture")
|
addChangedPropToTransaction(view, "enableZoomGesture")
|
||||||
view.enableZoomGesture = enableZoomGesture
|
view.enableZoomGesture = enableZoomGesture
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAfterUpdateTransaction(view: CameraView) {
|
override fun onAfterUpdateTransaction(view: CameraView) {
|
||||||
super.onAfterUpdateTransaction(view)
|
super.onAfterUpdateTransaction(view)
|
||||||
val changedProps = cameraViewTransactions[view] ?: ArrayList()
|
val changedProps = cameraViewTransactions[view] ?: ArrayList()
|
||||||
view.update(changedProps)
|
view.update(changedProps)
|
||||||
cameraViewTransactions.remove(view)
|
cameraViewTransactions.remove(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
public override fun createViewInstance(context: ThemedReactContext): CameraView {
|
public override fun createViewInstance(context: ThemedReactContext): CameraView {
|
||||||
return CameraView(context)
|
return CameraView(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any>? {
|
override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any>? {
|
||||||
return MapBuilder.builder<String, Any>()
|
return MapBuilder.builder<String, Any>()
|
||||||
.put("cameraInitialized", MapBuilder.of("registrationName", "onInitialized"))
|
.put("cameraInitialized", MapBuilder.of("registrationName", "onInitialized"))
|
||||||
.put("cameraError", MapBuilder.of("registrationName", "onError"))
|
.put("cameraError", MapBuilder.of("registrationName", "onError"))
|
||||||
.put("cameraCodeScanned", MapBuilder.of("registrationName", "onCodeScanned"))
|
.put("cameraCodeScanned", MapBuilder.of("registrationName", "onCodeScanned"))
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDropViewInstance(view: CameraView) {
|
override fun onDropViewInstance(view: CameraView) {
|
||||||
Log.d(REACT_CLASS, "onDropViewInstance() called!")
|
Log.d(REACT_CLASS, "onDropViewInstance() called!")
|
||||||
super.onDropViewInstance(view)
|
super.onDropViewInstance(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getName(): String {
|
override fun getName(): String {
|
||||||
return REACT_CLASS
|
return REACT_CLASS
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val REACT_CLASS = "CameraView"
|
const val REACT_CLASS = "CameraView"
|
||||||
|
|
||||||
val cameraViewTransactions: HashMap<CameraView, ArrayList<String>> = HashMap()
|
val cameraViewTransactions: HashMap<CameraView, ArrayList<String>> = HashMap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,296 +14,299 @@ import androidx.camera.core.ImageCapture
|
|||||||
import androidx.camera.extensions.HdrImageCaptureExtender
|
import androidx.camera.extensions.HdrImageCaptureExtender
|
||||||
import androidx.camera.extensions.NightImageCaptureExtender
|
import androidx.camera.extensions.NightImageCaptureExtender
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.mrousavy.camera.parsers.*
|
|
||||||
import com.mrousavy.camera.utils.*
|
|
||||||
import com.facebook.react.bridge.*
|
import com.facebook.react.bridge.*
|
||||||
import com.facebook.react.modules.core.PermissionAwareActivity
|
import com.facebook.react.modules.core.PermissionAwareActivity
|
||||||
import com.facebook.react.modules.core.PermissionListener
|
import com.facebook.react.modules.core.PermissionListener
|
||||||
|
import com.mrousavy.camera.parsers.*
|
||||||
|
import com.mrousavy.camera.utils.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
|
class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
|
||||||
companion object {
|
companion object {
|
||||||
const val REACT_CLASS = "CameraView"
|
const val REACT_CLASS = "CameraView"
|
||||||
var RequestCode = 10
|
var RequestCode = 10
|
||||||
|
|
||||||
fun parsePermissionStatus(status: Int): String {
|
fun parsePermissionStatus(status: Int): String {
|
||||||
return when(status) {
|
return when (status) {
|
||||||
PackageManager.PERMISSION_DENIED -> "denied"
|
PackageManager.PERMISSION_DENIED -> "denied"
|
||||||
PackageManager.PERMISSION_GRANTED -> "authorized"
|
PackageManager.PERMISSION_GRANTED -> "authorized"
|
||||||
else -> "not-determined"
|
else -> "not-determined"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getName(): String {
|
||||||
|
return REACT_CLASS
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findCameraView(id: Int): CameraView = reactApplicationContext.currentActivity?.findViewById(id) ?: throw ViewNotFoundError(id)
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
fun takePhoto(viewTag: Int, options: ReadableMap, promise: Promise) {
|
||||||
|
GlobalScope.launch(Dispatchers.Main) {
|
||||||
|
withPromise(promise) {
|
||||||
|
val view = findCameraView(viewTag)
|
||||||
|
view.takePhoto(options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
fun takeSnapshot(viewTag: Int, options: ReadableMap, promise: Promise) {
|
||||||
|
GlobalScope.launch(Dispatchers.Main) {
|
||||||
|
withPromise(promise) {
|
||||||
|
val view = findCameraView(viewTag)
|
||||||
|
view.takeSnapshot(options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: startRecording() cannot be awaited, because I can't have a Promise and a onRecordedCallback in the same function. Hopefully TurboModules allows that
|
||||||
|
@ReactMethod(isBlockingSynchronousMethod = true)
|
||||||
|
fun startRecording(viewTag: Int, options: ReadableMap, onRecordCallback: Callback) {
|
||||||
|
GlobalScope.launch(Dispatchers.Main) {
|
||||||
|
val view = findCameraView(viewTag)
|
||||||
|
view.startRecording(options, onRecordCallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
fun stopRecording(viewTag: Int, promise: Promise) {
|
||||||
|
withPromise(promise) {
|
||||||
|
val view = findCameraView(viewTag)
|
||||||
|
view.stopRecording()
|
||||||
|
return@withPromise null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
fun getAvailableVideoCodecs(viewTag: Int, promise: Promise) {
|
||||||
|
withPromise(promise) {
|
||||||
|
val view = findCameraView(viewTag)
|
||||||
|
view.getAvailableVideoCodecs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
fun getAvailablePhotoCodecs(viewTag: Int, promise: Promise) {
|
||||||
|
withPromise(promise) {
|
||||||
|
val view = findCameraView(viewTag)
|
||||||
|
view.getAvailablePhotoCodecs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
@ReactMethod
|
||||||
|
fun getAvailableCameraDevices(promise: Promise) {
|
||||||
|
withPromise(promise) {
|
||||||
|
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()
|
||||||
|
// TODO: ImageCapture.Builder - I'm not setting the target resolution, does that matter?
|
||||||
|
val imageCaptureBuilder = ImageCapture.Builder()
|
||||||
|
|
||||||
|
val characteristics = manager.getCameraCharacteristics(id)
|
||||||
|
val hardwareLevel = characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)!!
|
||||||
|
|
||||||
|
// Filters out cameras that are LEGACY hardware level. Those don't support Preview + Photo Capture + Video Capture at the same time.
|
||||||
|
if (hardwareLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
|
||||||
|
Log.i(
|
||||||
|
REACT_CLASS,
|
||||||
|
"Skipping Camera #$id because it does not meet the minimum requirements for react-native-vision-camera. " +
|
||||||
|
"See the tables at https://developer.android.com/reference/android/hardware/camera2/CameraDevice#regular-capture for more information."
|
||||||
|
)
|
||||||
|
return@loop
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun getName(): String {
|
val capabilities = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)!!
|
||||||
return REACT_CLASS
|
val isMultiCam = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P &&
|
||||||
}
|
capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA)
|
||||||
|
val deviceTypes = characteristics.getDeviceTypes()
|
||||||
|
|
||||||
private fun findCameraView(id: Int): CameraView = reactApplicationContext.currentActivity?.findViewById(id) ?: throw ViewNotFoundError(id)
|
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)!!
|
||||||
|
|
||||||
@ReactMethod
|
var supportsHdr = false
|
||||||
fun takePhoto(viewTag: Int, options: ReadableMap, promise: Promise) {
|
var supportsLowLightBoost = false
|
||||||
GlobalScope.launch(Dispatchers.Main) {
|
try {
|
||||||
withPromise(promise) {
|
val hdrExtension = HdrImageCaptureExtender.create(imageCaptureBuilder)
|
||||||
val view = findCameraView(viewTag)
|
supportsHdr = hdrExtension.isExtensionAvailable(cameraSelector)
|
||||||
view.takePhoto(options)
|
|
||||||
}
|
val nightExtension = NightImageCaptureExtender.create(imageCaptureBuilder)
|
||||||
|
supportsLowLightBoost = nightExtension.isExtensionAvailable(cameraSelector)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
// error on checking availability. falls back to "false"
|
||||||
|
Log.e(REACT_CLASS, "Failed to check HDR/Night Mode extension availability.", e)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@ReactMethod
|
val fieldOfView = characteristics.getFieldOfView()
|
||||||
fun takeSnapshot(viewTag: Int, options: ReadableMap, promise: Promise) {
|
|
||||||
GlobalScope.launch(Dispatchers.Main) {
|
|
||||||
withPromise(promise) {
|
|
||||||
val view = findCameraView(viewTag)
|
|
||||||
view.takeSnapshot(options)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: startRecording() cannot be awaited, because I can't have a Promise and a onRecordedCallback in the same function. Hopefully TurboModules allows that
|
val map = Arguments.createMap()
|
||||||
@ReactMethod(isBlockingSynchronousMethod = true)
|
val formats = Arguments.createArray()
|
||||||
fun startRecording(viewTag: Int, options: ReadableMap, onRecordCallback: Callback) {
|
map.putString("id", id)
|
||||||
GlobalScope.launch(Dispatchers.Main) {
|
map.putArray("devices", deviceTypes)
|
||||||
val view = findCameraView(viewTag)
|
map.putString("position", parseLensFacing(lensFacing))
|
||||||
view.startRecording(options, onRecordCallback)
|
map.putString("name", name ?: "${parseLensFacing(lensFacing)} ($id)")
|
||||||
}
|
map.putBoolean("hasFlash", hasFlash)
|
||||||
}
|
map.putBoolean("hasTorch", hasFlash)
|
||||||
|
map.putBoolean("isMultiCam", isMultiCam)
|
||||||
@ReactMethod
|
map.putBoolean("supportsRawCapture", supportsRawCapture)
|
||||||
fun stopRecording(viewTag: Int, promise: Promise) {
|
map.putBoolean("supportsDepthCapture", supportsDepthCapture)
|
||||||
withPromise(promise) {
|
map.putBoolean("supportsLowLightBoost", supportsLowLightBoost)
|
||||||
val view = findCameraView(viewTag)
|
if (zoomRange != null) {
|
||||||
view.stopRecording()
|
map.putDouble("minZoom", zoomRange.lower.toDouble())
|
||||||
return@withPromise null
|
map.putDouble("maxZoom", zoomRange.upper.toDouble())
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ReactMethod
|
|
||||||
fun getAvailableVideoCodecs(viewTag: Int, promise: Promise) {
|
|
||||||
withPromise(promise) {
|
|
||||||
val view = findCameraView(viewTag)
|
|
||||||
view.getAvailableVideoCodecs()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ReactMethod
|
|
||||||
fun getAvailablePhotoCodecs(viewTag: Int, promise: Promise) {
|
|
||||||
withPromise(promise) {
|
|
||||||
val view = findCameraView(viewTag)
|
|
||||||
view.getAvailablePhotoCodecs()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
@ReactMethod
|
|
||||||
fun getAvailableCameraDevices(promise: Promise) {
|
|
||||||
withPromise(promise) {
|
|
||||||
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()
|
|
||||||
// TODO: ImageCapture.Builder - I'm not setting the target resolution, does that matter?
|
|
||||||
val imageCaptureBuilder = ImageCapture.Builder()
|
|
||||||
|
|
||||||
val characteristics = manager.getCameraCharacteristics(id)
|
|
||||||
val hardwareLevel = characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)!!
|
|
||||||
|
|
||||||
// Filters out cameras that are LEGACY hardware level. Those don't support Preview + Photo Capture + Video Capture at the same time.
|
|
||||||
if (hardwareLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
|
|
||||||
Log.i(REACT_CLASS, "Skipping Camera #${id} because it does not meet the minimum requirements for react-native-vision-camera. " +
|
|
||||||
"See the tables at https://developer.android.com/reference/android/hardware/camera2/CameraDevice#regular-capture for more information.")
|
|
||||||
return@loop
|
|
||||||
}
|
|
||||||
|
|
||||||
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)!!
|
|
||||||
|
|
||||||
var supportsHdr = false
|
|
||||||
var supportsLowLightBoost = false
|
|
||||||
try {
|
|
||||||
val hdrExtension = HdrImageCaptureExtender.create(imageCaptureBuilder)
|
|
||||||
supportsHdr = hdrExtension.isExtensionAvailable(cameraSelector)
|
|
||||||
|
|
||||||
val nightExtension = NightImageCaptureExtender.create(imageCaptureBuilder)
|
|
||||||
supportsLowLightBoost = nightExtension.isExtensionAvailable(cameraSelector)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
// error on checking availability. falls back to "false"
|
|
||||||
Log.e(REACT_CLASS, "Failed to check HDR/Night Mode extension availability.", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
val fieldOfView = characteristics.getFieldOfView()
|
|
||||||
|
|
||||||
val map = Arguments.createMap()
|
|
||||||
val formats = Arguments.createArray()
|
|
||||||
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)
|
|
||||||
map.putBoolean("supportsRawCapture", supportsRawCapture)
|
|
||||||
map.putBoolean("supportsDepthCapture", supportsDepthCapture)
|
|
||||||
map.putBoolean("supportsLowLightBoost", supportsLowLightBoost)
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
map.putDouble("neutralZoom", characteristics.neutralZoomPercent.toDouble())
|
|
||||||
|
|
||||||
val maxImageOutputSize = cameraConfig.getOutputSizes(ImageReader::class.java).maxByOrNull { it.width * it.height }!!
|
|
||||||
|
|
||||||
// TODO: Should I really check MediaRecorder::class instead of SurfaceView::class?
|
|
||||||
// Recording should always be done in the most efficient format, which is the format native to the camera framework
|
|
||||||
cameraConfig.getOutputSizes(MediaRecorder::class.java).forEach { size ->
|
|
||||||
val isHighestPhotoQualitySupported = areUltimatelyEqual(size, maxImageOutputSize)
|
|
||||||
|
|
||||||
// Get the number of seconds that each frame will take to process
|
|
||||||
val secondsPerFrame = cameraConfig.getOutputMinFrameDuration(MediaRecorder::class.java, size) / 1_000_000_000.0
|
|
||||||
|
|
||||||
val frameRateRanges = Arguments.createArray()
|
|
||||||
if (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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Revisit getAvailableCameraDevices (colorSpaces, more than YUV?)
|
|
||||||
val colorSpaces = Arguments.createArray()
|
|
||||||
colorSpaces.pushString("yuv")
|
|
||||||
|
|
||||||
// TODO Revisit getAvailableCameraDevices (more accurate video stabilization modes)
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
map.putArray("formats", formats)
|
|
||||||
cameraDevices.pushMap(map)
|
|
||||||
}
|
|
||||||
|
|
||||||
return@withPromise cameraDevices
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@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) {
|
|
||||||
val activity = reactApplicationContext.currentActivity
|
|
||||||
if (activity is PermissionAwareActivity) {
|
|
||||||
val currentRequestCode = RequestCode
|
|
||||||
RequestCode++
|
|
||||||
val listener = PermissionListener { requestCode: Int, _: Array<String>, grantResults: IntArray ->
|
|
||||||
if (requestCode == currentRequestCode) {
|
|
||||||
val permissionStatus = grantResults[0]
|
|
||||||
promise.resolve(parsePermissionStatus(permissionStatus))
|
|
||||||
return@PermissionListener true
|
|
||||||
}
|
|
||||||
return@PermissionListener false
|
|
||||||
}
|
|
||||||
activity.requestPermissions(arrayOf(Manifest.permission.CAMERA), currentRequestCode, listener)
|
|
||||||
} else {
|
} else {
|
||||||
promise.reject("NO_ACTIVITY", "No PermissionAwareActivity was found! Make sure the app has launched before calling this function.")
|
map.putDouble("minZoom", 1.0)
|
||||||
|
map.putDouble("maxZoom", maxScalerZoom.toDouble())
|
||||||
}
|
}
|
||||||
}
|
map.putDouble("neutralZoom", characteristics.neutralZoomPercent.toDouble())
|
||||||
|
|
||||||
@ReactMethod
|
val maxImageOutputSize = cameraConfig.getOutputSizes(ImageReader::class.java).maxByOrNull { it.width * it.height }!!
|
||||||
fun requestMicrophonePermission(promise: Promise) {
|
|
||||||
val activity = reactApplicationContext.currentActivity
|
// TODO: Should I really check MediaRecorder::class instead of SurfaceView::class?
|
||||||
if (activity is PermissionAwareActivity) {
|
// Recording should always be done in the most efficient format, which is the format native to the camera framework
|
||||||
val currentRequestCode = RequestCode
|
cameraConfig.getOutputSizes(MediaRecorder::class.java).forEach { size ->
|
||||||
RequestCode++
|
val isHighestPhotoQualitySupported = areUltimatelyEqual(size, maxImageOutputSize)
|
||||||
val listener = PermissionListener { requestCode: Int, _: Array<String>, grantResults: IntArray ->
|
|
||||||
if (requestCode == currentRequestCode) {
|
// Get the number of seconds that each frame will take to process
|
||||||
val permissionStatus = grantResults[0]
|
val secondsPerFrame = cameraConfig.getOutputMinFrameDuration(MediaRecorder::class.java, size) / 1_000_000_000.0
|
||||||
promise.resolve(parsePermissionStatus(permissionStatus))
|
|
||||||
return@PermissionListener true
|
val frameRateRanges = Arguments.createArray()
|
||||||
}
|
if (secondsPerFrame > 0) {
|
||||||
return@PermissionListener false
|
val fps = (1.0 / secondsPerFrame).toInt()
|
||||||
}
|
val frameRateRange = Arguments.createMap()
|
||||||
activity.requestPermissions(arrayOf(Manifest.permission.RECORD_AUDIO), currentRequestCode, listener)
|
frameRateRange.putInt("minFrameRate", 1)
|
||||||
} else {
|
frameRateRange.putInt("maxFrameRate", fps)
|
||||||
promise.reject("NO_ACTIVITY", "No PermissionAwareActivity was found! Make sure the app has launched before calling this function.")
|
frameRateRanges.pushMap(frameRateRange)
|
||||||
|
}
|
||||||
|
fpsRanges.forEach { range ->
|
||||||
|
val frameRateRange = Arguments.createMap()
|
||||||
|
frameRateRange.putInt("minFrameRate", range.lower)
|
||||||
|
frameRateRange.putInt("maxFrameRate", range.upper)
|
||||||
|
frameRateRanges.pushMap(frameRateRange)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Revisit getAvailableCameraDevices (colorSpaces, more than YUV?)
|
||||||
|
val colorSpaces = Arguments.createArray()
|
||||||
|
colorSpaces.pushString("yuv")
|
||||||
|
|
||||||
|
// TODO Revisit getAvailableCameraDevices (more accurate video stabilization modes)
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
map.putArray("formats", formats)
|
||||||
|
cameraDevices.pushMap(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
return@withPromise cameraDevices
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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) {
|
||||||
|
val activity = reactApplicationContext.currentActivity
|
||||||
|
if (activity is PermissionAwareActivity) {
|
||||||
|
val currentRequestCode = RequestCode
|
||||||
|
RequestCode++
|
||||||
|
val listener = PermissionListener { requestCode: Int, _: Array<String>, grantResults: IntArray ->
|
||||||
|
if (requestCode == currentRequestCode) {
|
||||||
|
val permissionStatus = grantResults[0]
|
||||||
|
promise.resolve(parsePermissionStatus(permissionStatus))
|
||||||
|
return@PermissionListener true
|
||||||
|
}
|
||||||
|
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.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
fun requestMicrophonePermission(promise: Promise) {
|
||||||
|
val activity = reactApplicationContext.currentActivity
|
||||||
|
if (activity is PermissionAwareActivity) {
|
||||||
|
val currentRequestCode = RequestCode
|
||||||
|
RequestCode++
|
||||||
|
val listener = PermissionListener { requestCode: Int, _: Array<String>, grantResults: IntArray ->
|
||||||
|
if (requestCode == currentRequestCode) {
|
||||||
|
val permissionStatus = grantResults[0]
|
||||||
|
promise.resolve(parsePermissionStatus(permissionStatus))
|
||||||
|
return@PermissionListener true
|
||||||
|
}
|
||||||
|
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.")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,59 +3,64 @@ package com.mrousavy.camera
|
|||||||
import android.graphics.ImageFormat
|
import android.graphics.ImageFormat
|
||||||
|
|
||||||
abstract class CameraError(
|
abstract class CameraError(
|
||||||
/**
|
/**
|
||||||
* The domain of the error. Error domains are used to group errors.
|
* The domain of the error. Error domains are used to group errors.
|
||||||
*
|
*
|
||||||
* Example: "permission"
|
* Example: "permission"
|
||||||
*/
|
*/
|
||||||
val domain: String,
|
val domain: String,
|
||||||
/**
|
/**
|
||||||
* The id of the error. Errors are uniquely identified under a given domain.
|
* The id of the error. Errors are uniquely identified under a given domain.
|
||||||
*
|
*
|
||||||
* Example: "microphone-permission-denied"
|
* Example: "microphone-permission-denied"
|
||||||
*/
|
*/
|
||||||
val id: String,
|
val id: String,
|
||||||
/**
|
/**
|
||||||
* A detailed error description of "what went wrong".
|
* A detailed error description of "what went wrong".
|
||||||
*
|
*
|
||||||
* Example: "The microphone permission was denied!"
|
* Example: "The microphone permission was denied!"
|
||||||
*/
|
*/
|
||||||
message: String,
|
message: String,
|
||||||
/**
|
/**
|
||||||
* A throwable that caused this error.
|
* A throwable that caused this error.
|
||||||
*/
|
*/
|
||||||
cause: Throwable? = null
|
cause: Throwable? = null
|
||||||
): Throwable("[$domain/$id] $message", cause)
|
) : Throwable("[$domain/$id] $message", cause)
|
||||||
|
|
||||||
val CameraError.code: String
|
val CameraError.code: String
|
||||||
get() = "$domain/$id"
|
get() = "$domain/$id"
|
||||||
|
|
||||||
|
class MicrophonePermissionError : CameraError("permission", "microphone-permission-denied", "The Microphone permission was denied!")
|
||||||
|
class CameraPermissionError : CameraError("permission", "camera-permission-denied", "The Camera permission was denied!")
|
||||||
|
|
||||||
class MicrophonePermissionError: CameraError("permission", "microphone-permission-denied", "The Microphone permission was denied!")
|
class InvalidTypeScriptUnionError(unionName: String, unionValue: String) : CameraError("parameter", "invalid-parameter", "The given value for $unionName could not be parsed! (Received: $unionValue)")
|
||||||
class CameraPermissionError: CameraError("permission", "camera-permission-denied", "The Camera permission was denied!")
|
class UnsupportedOSError(unionName: String, unionValue: String, supportedOnOS: String) : CameraError("parameter", "unsupported-os", "The given value \"$unionValue\" could not be used for $unionName, as it is only available on Android $supportedOnOS and above!")
|
||||||
|
|
||||||
class InvalidTypeScriptUnionError(unionName: String, unionValue: String): CameraError("parameter", "invalid-parameter", "The given value for $unionName could not be parsed! (Received: $unionValue)")
|
class NoCameraDeviceError : CameraError("device", "no-device", "No device was set! Use `getAvailableCameraDevices()` to select a suitable Camera device.")
|
||||||
class UnsupportedOSError(unionName: String, unionValue: String, supportedOnOS: String): CameraError("parameter", "unsupported-os", "The given value \"$unionValue\" could not be used for $unionName, as it is only available on Android $supportedOnOS and above!")
|
class InvalidCameraDeviceError(cause: Throwable) : CameraError("device", "invalid-device", "The given Camera device could not be found for use-case binding!", cause)
|
||||||
|
|
||||||
class NoCameraDeviceError: CameraError("device", "no-device", "No device was set! Use `getAvailableCameraDevices()` to select a suitable Camera device.")
|
class FpsNotContainedInFormatError(fps: Int) : CameraError("format", "invalid-fps", "The given FPS were not valid for the currently selected format. Make sure you select a format which `frameRateRanges` includes $fps FPS!")
|
||||||
class InvalidCameraDeviceError(cause: Throwable): CameraError("device", "invalid-device", "The given Camera device could not be found for use-case binding!", cause)
|
class HdrNotContainedInFormatError() : CameraError(
|
||||||
|
"format", "invalid-hdr",
|
||||||
|
"The currently selected format does not support HDR capture! " +
|
||||||
|
"Make sure you select a format which `frameRateRanges` includes `supportsPhotoHDR`!"
|
||||||
|
)
|
||||||
|
class LowLightBoostNotContainedInFormatError() : CameraError(
|
||||||
|
"format", "invalid-low-light-boost",
|
||||||
|
"The currently selected format does not support low-light boost (night mode)! " +
|
||||||
|
"Make sure you select a format which includes `supportsLowLightBoost`."
|
||||||
|
)
|
||||||
|
|
||||||
class FpsNotContainedInFormatError(fps: Int): CameraError("format", "invalid-fps", "The given FPS were not valid for the currently selected format. Make sure you select a format which `frameRateRanges` includes $fps FPS!")
|
class CameraNotReadyError : CameraError("session", "camera-not-ready", "The Camera is not ready yet! Wait for the onInitialized() callback!")
|
||||||
class HdrNotContainedInFormatError(): CameraError("format", "invalid-hdr", "The currently selected format does not support HDR capture! " +
|
|
||||||
"Make sure you select a format which `frameRateRanges` includes `supportsPhotoHDR`!")
|
|
||||||
class LowLightBoostNotContainedInFormatError(): CameraError("format", "invalid-low-light-boost", "The currently selected format does not support low-light boost (night mode)! " +
|
|
||||||
"Make sure you select a format which includes `supportsLowLightBoost`.")
|
|
||||||
|
|
||||||
class CameraNotReadyError: CameraError("session", "camera-not-ready", "The Camera is not ready yet! Wait for the onInitialized() callback!")
|
class InvalidFormatError(format: Int) : CameraError("capture", "invalid-photo-format", "The Photo has an invalid format! Expected ${ImageFormat.YUV_420_888}, actual: $format")
|
||||||
|
class VideoEncoderError(message: String, cause: Throwable? = null) : CameraError("capture", "encoder-error", message, cause)
|
||||||
|
class VideoMuxerError(message: String, cause: Throwable? = null) : CameraError("capture", "muxer-error", message, cause)
|
||||||
|
class RecordingInProgressError(message: String, cause: Throwable? = null) : CameraError("capture", "recording-in-progress", message, cause)
|
||||||
|
class FileIOError(message: String, cause: Throwable? = null) : CameraError("capture", "file-io-error", message, cause)
|
||||||
|
class InvalidCameraError(message: String, cause: Throwable? = null) : CameraError("capture", "not-bound-error", message, cause)
|
||||||
|
|
||||||
class InvalidFormatError(format: Int): CameraError("capture", "invalid-photo-format", "The Photo has an invalid format! Expected ${ImageFormat.YUV_420_888}, actual: $format")
|
class CameraManagerUnavailableError : CameraError("system", "no-camera-manager", "The Camera manager instance was unavailable for the current Application!")
|
||||||
class VideoEncoderError(message: String, cause: Throwable? = null): CameraError("capture", "encoder-error", message, cause)
|
class ViewNotFoundError(viewId: Int) : CameraError("system", "view-not-found", "The given view (ID $viewId) was not found in the view manager.")
|
||||||
class VideoMuxerError(message: String, cause: Throwable? = null): CameraError("capture", "muxer-error", message, cause)
|
|
||||||
class RecordingInProgressError(message: String, cause: Throwable? = null): CameraError("capture", "recording-in-progress", message, cause)
|
|
||||||
class FileIOError(message: String, cause: Throwable? = null): CameraError("capture", "file-io-error", message, cause)
|
|
||||||
class InvalidCameraError(message: String, cause: Throwable? = null): CameraError("capture", "not-bound-error", message, cause)
|
|
||||||
|
|
||||||
class CameraManagerUnavailableError: CameraError("system", "no-camera-manager", "The Camera manager instance was unavailable for the current Application!")
|
class UnknownCameraError(cause: Throwable) : CameraError("unknown", "unknown", cause.message ?: "An unknown camera error occured.", cause)
|
||||||
class ViewNotFoundError(viewId: Int): CameraError("system", "view-not-found", "The given view (ID $viewId) was not found in the view manager.")
|
|
||||||
|
|
||||||
class UnknownCameraError(cause: Throwable): CameraError("unknown", "unknown", cause.message ?: "An unknown camera error occured.", cause)
|
|
||||||
|
@ -6,10 +6,10 @@ import android.hardware.camera2.CameraCharacteristics
|
|||||||
* Parses Lens Facing int to a string representation useable for the TypeScript types.
|
* Parses Lens Facing int to a string representation useable for the TypeScript types.
|
||||||
*/
|
*/
|
||||||
fun parseLensFacing(lensFacing: Int?): String? {
|
fun parseLensFacing(lensFacing: Int?): String? {
|
||||||
return when (lensFacing) {
|
return when (lensFacing) {
|
||||||
CameraCharacteristics.LENS_FACING_BACK -> "back"
|
CameraCharacteristics.LENS_FACING_BACK -> "back"
|
||||||
CameraCharacteristics.LENS_FACING_FRONT -> "front"
|
CameraCharacteristics.LENS_FACING_FRONT -> "front"
|
||||||
CameraCharacteristics.LENS_FACING_EXTERNAL -> "external"
|
CameraCharacteristics.LENS_FACING_EXTERNAL -> "external"
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,15 +6,15 @@ import kotlin.math.max
|
|||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
val Size.bigger: Int
|
val Size.bigger: Int
|
||||||
get() = max(this.width, this.height)
|
get() = max(this.width, this.height)
|
||||||
val Size.smaller: Int
|
val Size.smaller: Int
|
||||||
get() = min(this.width, this.height)
|
get() = min(this.width, this.height)
|
||||||
|
|
||||||
val SizeF.bigger: Float
|
val SizeF.bigger: Float
|
||||||
get() = max(this.width, this.height)
|
get() = max(this.width, this.height)
|
||||||
val SizeF.smaller: Float
|
val SizeF.smaller: Float
|
||||||
get() = min(this.width, this.height)
|
get() = min(this.width, this.height)
|
||||||
|
|
||||||
fun areUltimatelyEqual(size1: Size, size2: Size): Boolean {
|
fun areUltimatelyEqual(size1: Size, size2: Size): Boolean {
|
||||||
return size1.width * size1.height == size2.width * size2.height
|
return size1.width * size1.height == size2.width * size2.height
|
||||||
}
|
}
|
||||||
|
@ -20,9 +20,9 @@ private const val RATIO_16_9_VALUE = 16.0 / 9.0
|
|||||||
* @return suitable aspect ratio
|
* @return suitable aspect ratio
|
||||||
*/
|
*/
|
||||||
fun aspectRatio(width: Int, height: Int): Int {
|
fun aspectRatio(width: Int, height: Int): Int {
|
||||||
val previewRatio = max(width, height).toDouble() / min(width, height)
|
val previewRatio = max(width, height).toDouble() / min(width, height)
|
||||||
if (abs(previewRatio - RATIO_4_3_VALUE) <= abs(previewRatio - RATIO_16_9_VALUE)) {
|
if (abs(previewRatio - RATIO_4_3_VALUE) <= abs(previewRatio - RATIO_16_9_VALUE)) {
|
||||||
return AspectRatio.RATIO_4_3
|
return AspectRatio.RATIO_4_3
|
||||||
}
|
}
|
||||||
return AspectRatio.RATIO_16_9
|
return AspectRatio.RATIO_16_9
|
||||||
}
|
}
|
||||||
|
@ -3,20 +3,20 @@ package com.mrousavy.camera.utils
|
|||||||
import com.facebook.react.bridge.*
|
import com.facebook.react.bridge.*
|
||||||
|
|
||||||
private fun makeErrorCauseMap(throwable: Throwable): ReadableMap {
|
private fun makeErrorCauseMap(throwable: Throwable): ReadableMap {
|
||||||
val map = Arguments.createMap()
|
val map = Arguments.createMap()
|
||||||
map.putString("message", throwable.message)
|
map.putString("message", throwable.message)
|
||||||
map.putString("stacktrace", throwable.stackTraceToString())
|
map.putString("stacktrace", throwable.stackTraceToString())
|
||||||
if (throwable.cause != null) {
|
if (throwable.cause != null) {
|
||||||
map.putMap("cause", makeErrorCauseMap(throwable.cause!!))
|
map.putMap("cause", makeErrorCauseMap(throwable.cause!!))
|
||||||
}
|
}
|
||||||
return map
|
return map
|
||||||
}
|
}
|
||||||
|
|
||||||
fun makeErrorMap(code: String? = null, message: String? = null, throwable: Throwable? = null, userInfo: WritableMap? = null): ReadableMap {
|
fun makeErrorMap(code: String? = null, message: String? = null, throwable: Throwable? = null, userInfo: WritableMap? = null): ReadableMap {
|
||||||
val map = Arguments.createMap()
|
val map = Arguments.createMap()
|
||||||
map.putString("code", code)
|
map.putString("code", code)
|
||||||
map.putString("message", message)
|
map.putString("message", message)
|
||||||
map.putMap("cause", if (throwable != null) makeErrorCauseMap(throwable) else null)
|
map.putMap("cause", if (throwable != null) makeErrorCauseMap(throwable) else null)
|
||||||
map.putMap("userInfo", userInfo)
|
map.putMap("userInfo", userInfo)
|
||||||
return map
|
return map
|
||||||
}
|
}
|
||||||
|
@ -2,14 +2,12 @@ package com.mrousavy.camera.utils
|
|||||||
|
|
||||||
import android.hardware.camera2.CameraCharacteristics
|
import android.hardware.camera2.CameraCharacteristics
|
||||||
import android.util.Size
|
import android.util.Size
|
||||||
import com.mrousavy.camera.parsers.bigger
|
|
||||||
import com.mrousavy.camera.parsers.parseLensFacing
|
|
||||||
import com.facebook.react.bridge.Arguments
|
import com.facebook.react.bridge.Arguments
|
||||||
import com.facebook.react.bridge.ReadableArray
|
import com.facebook.react.bridge.ReadableArray
|
||||||
|
import com.mrousavy.camera.parsers.bigger
|
||||||
import kotlin.math.PI
|
import kotlin.math.PI
|
||||||
import kotlin.math.atan
|
import kotlin.math.atan
|
||||||
|
|
||||||
|
|
||||||
// 35mm is 135 film format, a standard in which focal lengths are usually measured
|
// 35mm is 135 film format, a standard in which focal lengths are usually measured
|
||||||
val Size35mm = Size(36, 24)
|
val Size35mm = Size(36, 24)
|
||||||
|
|
||||||
@ -28,40 +26,40 @@ val Size35mm = Size(36, 24)
|
|||||||
* * [Ultra-Wide-Angle Lens (wikipedia)](https://en.wikipedia.org/wiki/Ultra_wide_angle_lens)
|
* * [Ultra-Wide-Angle Lens (wikipedia)](https://en.wikipedia.org/wiki/Ultra_wide_angle_lens)
|
||||||
*/
|
*/
|
||||||
fun CameraCharacteristics.getDeviceTypes(): ReadableArray {
|
fun CameraCharacteristics.getDeviceTypes(): ReadableArray {
|
||||||
// TODO: Check if getDeviceType() works correctly, even for logical multi-cameras
|
// TODO: Check if getDeviceType() works correctly, even for logical multi-cameras
|
||||||
val focalLengths = this.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS)!!
|
val focalLengths = this.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS)!!
|
||||||
val sensorSize = this.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)!!
|
val sensorSize = this.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)!!
|
||||||
|
|
||||||
// To get valid focal length standards we have to upscale to the 35mm measurement (film standard)
|
// To get valid focal length standards we have to upscale to the 35mm measurement (film standard)
|
||||||
val cropFactor = Size35mm.bigger / sensorSize.bigger
|
val cropFactor = Size35mm.bigger / sensorSize.bigger
|
||||||
|
|
||||||
val deviceTypes = Arguments.createArray()
|
val deviceTypes = Arguments.createArray()
|
||||||
|
|
||||||
val containsTelephoto = focalLengths.any { l -> (l * cropFactor) > 35 } // TODO: Telephoto lenses are > 85mm, but we don't have anything between that range..
|
val containsTelephoto = focalLengths.any { l -> (l * cropFactor) > 35 } // TODO: Telephoto lenses are > 85mm, but we don't have anything between that range..
|
||||||
//val containsNormalLens = focalLengths.any { l -> (l * cropFactor) > 35 && (l * cropFactor) <= 55 }
|
// val containsNormalLens = focalLengths.any { l -> (l * cropFactor) > 35 && (l * cropFactor) <= 55 }
|
||||||
val containsWideAngle = focalLengths.any { l -> (l * cropFactor) >= 24 && (l * cropFactor) <= 35 }
|
val containsWideAngle = focalLengths.any { l -> (l * cropFactor) >= 24 && (l * cropFactor) <= 35 }
|
||||||
val containsUltraWideAngle = focalLengths.any { l -> (l * cropFactor) < 24 }
|
val containsUltraWideAngle = focalLengths.any { l -> (l * cropFactor) < 24 }
|
||||||
|
|
||||||
if (containsTelephoto)
|
if (containsTelephoto)
|
||||||
deviceTypes.pushString("telephoto-camera")
|
deviceTypes.pushString("telephoto-camera")
|
||||||
if (containsWideAngle)
|
if (containsWideAngle)
|
||||||
deviceTypes.pushString("wide-angle-camera")
|
deviceTypes.pushString("wide-angle-camera")
|
||||||
if (containsUltraWideAngle)
|
if (containsUltraWideAngle)
|
||||||
deviceTypes.pushString("ultra-wide-angle-camera")
|
deviceTypes.pushString("ultra-wide-angle-camera")
|
||||||
|
|
||||||
return deviceTypes
|
return deviceTypes
|
||||||
}
|
}
|
||||||
|
|
||||||
fun CameraCharacteristics.getFieldOfView(): Double {
|
fun CameraCharacteristics.getFieldOfView(): Double {
|
||||||
val focalLengths = this.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS)!!
|
val focalLengths = this.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS)!!
|
||||||
val sensorSize = this.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)!!
|
val sensorSize = this.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)!!
|
||||||
|
|
||||||
return 2 * atan(sensorSize.bigger / (focalLengths[0] * 2)) * (180 / PI)
|
return 2 * atan(sensorSize.bigger / (focalLengths[0] * 2)) * (180 / PI)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun CameraCharacteristics.supportsFps(fps: Int): Boolean {
|
fun CameraCharacteristics.supportsFps(fps: Int): Boolean {
|
||||||
return this.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES)!!
|
return this.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES)!!
|
||||||
.any { it.upper >= fps && it.lower <= fps }
|
.any { it.upper >= fps && it.lower <= fps }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -71,12 +69,12 @@ fun CameraCharacteristics.supportsFps(fps: Int): Boolean {
|
|||||||
* * On devices with multiple cameras, e.g. triple-camera, this value will be a value between 0.0 and 1.0, where the field-of-view and zoom looks "neutral"
|
* * On devices with multiple cameras, e.g. triple-camera, this value will be a value between 0.0 and 1.0, where the field-of-view and zoom looks "neutral"
|
||||||
*/
|
*/
|
||||||
val CameraCharacteristics.neutralZoomPercent: Float
|
val CameraCharacteristics.neutralZoomPercent: Float
|
||||||
get() {
|
get() {
|
||||||
val zoomRange = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R)
|
val zoomRange = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R)
|
||||||
this.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE)
|
this.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE)
|
||||||
else null
|
else null
|
||||||
return if (zoomRange != null)
|
return if (zoomRange != null)
|
||||||
((1.0f - zoomRange.lower) / (zoomRange.upper - zoomRange.lower))
|
((1.0f - zoomRange.lower) / (zoomRange.upper - zoomRange.lower))
|
||||||
else
|
else
|
||||||
0.0f
|
0.0f
|
||||||
}
|
}
|
||||||
|
@ -5,22 +5,21 @@ import androidx.camera.camera2.interop.Camera2CameraInfo
|
|||||||
import androidx.camera.core.CameraSelector
|
import androidx.camera.core.CameraSelector
|
||||||
import java.lang.IllegalArgumentException
|
import java.lang.IllegalArgumentException
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new [CameraSelector] which selects the camera with the given [cameraId]
|
* Create a new [CameraSelector] which selects the camera with the given [cameraId]
|
||||||
*/
|
*/
|
||||||
@SuppressLint("UnsafeExperimentalUsageError")
|
@SuppressLint("UnsafeExperimentalUsageError")
|
||||||
fun CameraSelector.Builder.byID(cameraId: String): CameraSelector.Builder {
|
fun CameraSelector.Builder.byID(cameraId: String): CameraSelector.Builder {
|
||||||
return this.addCameraFilter { cameras ->
|
return this.addCameraFilter { cameras ->
|
||||||
cameras.filter { cameraInfoX ->
|
cameras.filter { cameraInfoX ->
|
||||||
try {
|
try {
|
||||||
val cameraInfo = Camera2CameraInfo.from(cameraInfoX)
|
val cameraInfo = Camera2CameraInfo.from(cameraInfoX)
|
||||||
return@filter cameraInfo.cameraId == cameraId
|
return@filter cameraInfo.cameraId == cameraId
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
// Occurs when the [cameraInfoX] is not castable to a Camera2 Info object.
|
// Occurs when the [cameraInfoX] is not castable to a Camera2 Info object.
|
||||||
// We can ignore this error because the [getAvailableCameraDevices()] func only returns Camera2 devices.
|
// We can ignore this error because the [getAvailableCameraDevices()] func only returns Camera2 devices.
|
||||||
return@filter false
|
return@filter false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,28 +5,28 @@ import android.util.Size
|
|||||||
import com.facebook.react.bridge.ReadableMap
|
import com.facebook.react.bridge.ReadableMap
|
||||||
|
|
||||||
class DeviceFormat(map: ReadableMap) {
|
class DeviceFormat(map: ReadableMap) {
|
||||||
val frameRateRanges: List<Range<Int>>
|
val frameRateRanges: List<Range<Int>>
|
||||||
val photoSize: Size
|
val photoSize: Size
|
||||||
val videoSize: Size
|
val videoSize: Size
|
||||||
val maxZoom: Double
|
val maxZoom: Double
|
||||||
|
|
||||||
init {
|
init {
|
||||||
frameRateRanges = map.getArray("frameRateRanges")!!.toArrayList().map { range ->
|
frameRateRanges = map.getArray("frameRateRanges")!!.toArrayList().map { range ->
|
||||||
if (range is HashMap<*, *>)
|
if (range is HashMap<*, *>)
|
||||||
rangeFactory(range["minFrameRate"], range["maxFrameRate"])
|
rangeFactory(range["minFrameRate"], range["maxFrameRate"])
|
||||||
else
|
else
|
||||||
throw IllegalArgumentException()
|
throw IllegalArgumentException()
|
||||||
}
|
|
||||||
photoSize = Size(map.getInt("photoWidth"), map.getInt("photoHeight"))
|
|
||||||
videoSize = Size(map.getInt("videoWidth"), map.getInt("videoHeight"))
|
|
||||||
maxZoom = map.getDouble("maxZoom")
|
|
||||||
}
|
}
|
||||||
|
photoSize = Size(map.getInt("photoWidth"), map.getInt("photoHeight"))
|
||||||
|
videoSize = Size(map.getInt("videoWidth"), map.getInt("videoHeight"))
|
||||||
|
maxZoom = map.getDouble("maxZoom")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun rangeFactory(minFrameRate: Any?, maxFrameRate: Any?): Range<Int> {
|
fun rangeFactory(minFrameRate: Any?, maxFrameRate: Any?): Range<Int> {
|
||||||
return when(minFrameRate) {
|
return when (minFrameRate) {
|
||||||
is Int -> Range(minFrameRate, maxFrameRate as Int)
|
is Int -> Range(minFrameRate, maxFrameRate as Int)
|
||||||
is Double -> Range(minFrameRate.toInt(), (maxFrameRate as Double).toInt())
|
is Double -> Range(minFrameRate.toInt(), (maxFrameRate as Double).toInt())
|
||||||
else -> throw IllegalArgumentException()
|
else -> throw IllegalArgumentException()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,60 +4,59 @@ import androidx.exifinterface.media.ExifInterface
|
|||||||
import com.facebook.react.bridge.Arguments
|
import com.facebook.react.bridge.Arguments
|
||||||
import com.facebook.react.bridge.WritableMap
|
import com.facebook.react.bridge.WritableMap
|
||||||
|
|
||||||
|
|
||||||
fun ExifInterface.buildMetadataMap(): WritableMap {
|
fun ExifInterface.buildMetadataMap(): WritableMap {
|
||||||
val metadataMap = Arguments.createMap()
|
val metadataMap = Arguments.createMap()
|
||||||
metadataMap.putInt("Orientation", this.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL))
|
metadataMap.putInt("Orientation", this.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL))
|
||||||
|
|
||||||
val tiffMap = Arguments.createMap()
|
val tiffMap = Arguments.createMap()
|
||||||
tiffMap.putInt("ResolutionUnit", this.getAttributeInt(ExifInterface.TAG_RESOLUTION_UNIT, 0))
|
tiffMap.putInt("ResolutionUnit", this.getAttributeInt(ExifInterface.TAG_RESOLUTION_UNIT, 0))
|
||||||
tiffMap.putString("Software", this.getAttribute(ExifInterface.TAG_SOFTWARE))
|
tiffMap.putString("Software", this.getAttribute(ExifInterface.TAG_SOFTWARE))
|
||||||
tiffMap.putString("Make", this.getAttribute(ExifInterface.TAG_MAKE))
|
tiffMap.putString("Make", this.getAttribute(ExifInterface.TAG_MAKE))
|
||||||
tiffMap.putString("DateTime", this.getAttribute(ExifInterface.TAG_DATETIME))
|
tiffMap.putString("DateTime", this.getAttribute(ExifInterface.TAG_DATETIME))
|
||||||
tiffMap.putDouble("XResolution", this.getAttributeDouble(ExifInterface.TAG_X_RESOLUTION, 0.0))
|
tiffMap.putDouble("XResolution", this.getAttributeDouble(ExifInterface.TAG_X_RESOLUTION, 0.0))
|
||||||
tiffMap.putString("Model", this.getAttribute(ExifInterface.TAG_MODEL))
|
tiffMap.putString("Model", this.getAttribute(ExifInterface.TAG_MODEL))
|
||||||
tiffMap.putDouble("YResolution", this.getAttributeDouble(ExifInterface.TAG_Y_RESOLUTION, 0.0))
|
tiffMap.putDouble("YResolution", this.getAttributeDouble(ExifInterface.TAG_Y_RESOLUTION, 0.0))
|
||||||
metadataMap.putMap("{TIFF}", tiffMap)
|
metadataMap.putMap("{TIFF}", tiffMap)
|
||||||
|
|
||||||
val exifMap = Arguments.createMap()
|
val exifMap = Arguments.createMap()
|
||||||
exifMap.putString("DateTimeOriginal", this.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL))
|
exifMap.putString("DateTimeOriginal", this.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL))
|
||||||
exifMap.putDouble("ExposureTime", this.getAttributeDouble(ExifInterface.TAG_EXPOSURE_TIME, 0.0))
|
exifMap.putDouble("ExposureTime", this.getAttributeDouble(ExifInterface.TAG_EXPOSURE_TIME, 0.0))
|
||||||
exifMap.putDouble("FNumber", this.getAttributeDouble(ExifInterface.TAG_F_NUMBER, 0.0))
|
exifMap.putDouble("FNumber", this.getAttributeDouble(ExifInterface.TAG_F_NUMBER, 0.0))
|
||||||
val lensSpecificationArray = Arguments.createArray()
|
val lensSpecificationArray = Arguments.createArray()
|
||||||
this.getAttributeRange(ExifInterface.TAG_LENS_SPECIFICATION)?.forEach { lensSpecificationArray.pushInt(it.toInt()) }
|
this.getAttributeRange(ExifInterface.TAG_LENS_SPECIFICATION)?.forEach { lensSpecificationArray.pushInt(it.toInt()) }
|
||||||
exifMap.putArray("LensSpecification", lensSpecificationArray)
|
exifMap.putArray("LensSpecification", lensSpecificationArray)
|
||||||
exifMap.putDouble("ExposureBiasValue", this.getAttributeDouble(ExifInterface.TAG_EXPOSURE_BIAS_VALUE, 0.0))
|
exifMap.putDouble("ExposureBiasValue", this.getAttributeDouble(ExifInterface.TAG_EXPOSURE_BIAS_VALUE, 0.0))
|
||||||
exifMap.putInt("ColorSpace", this.getAttributeInt(ExifInterface.TAG_COLOR_SPACE, ExifInterface.COLOR_SPACE_S_RGB))
|
exifMap.putInt("ColorSpace", this.getAttributeInt(ExifInterface.TAG_COLOR_SPACE, ExifInterface.COLOR_SPACE_S_RGB))
|
||||||
exifMap.putInt("FocalLenIn35mmFilm", this.getAttributeInt(ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM, 0))
|
exifMap.putInt("FocalLenIn35mmFilm", this.getAttributeInt(ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM, 0))
|
||||||
exifMap.putDouble("BrightnessValue", this.getAttributeDouble(ExifInterface.TAG_BRIGHTNESS_VALUE, 0.0))
|
exifMap.putDouble("BrightnessValue", this.getAttributeDouble(ExifInterface.TAG_BRIGHTNESS_VALUE, 0.0))
|
||||||
exifMap.putInt("ExposureMode", this.getAttributeInt(ExifInterface.TAG_EXPOSURE_MODE, ExifInterface.EXPOSURE_MODE_AUTO.toInt()))
|
exifMap.putInt("ExposureMode", this.getAttributeInt(ExifInterface.TAG_EXPOSURE_MODE, ExifInterface.EXPOSURE_MODE_AUTO.toInt()))
|
||||||
exifMap.putString("LensModel", this.getAttribute(ExifInterface.TAG_LENS_MODEL))
|
exifMap.putString("LensModel", this.getAttribute(ExifInterface.TAG_LENS_MODEL))
|
||||||
exifMap.putInt("SceneType", this.getAttributeInt(ExifInterface.TAG_SCENE_TYPE, ExifInterface.SCENE_TYPE_DIRECTLY_PHOTOGRAPHED.toInt()))
|
exifMap.putInt("SceneType", this.getAttributeInt(ExifInterface.TAG_SCENE_TYPE, ExifInterface.SCENE_TYPE_DIRECTLY_PHOTOGRAPHED.toInt()))
|
||||||
exifMap.putInt("PixelXDimension", this.getAttributeInt(ExifInterface.TAG_PIXEL_X_DIMENSION, 0))
|
exifMap.putInt("PixelXDimension", this.getAttributeInt(ExifInterface.TAG_PIXEL_X_DIMENSION, 0))
|
||||||
exifMap.putDouble("ShutterSpeedValue", this.getAttributeDouble(ExifInterface.TAG_SHUTTER_SPEED_VALUE, 0.0))
|
exifMap.putDouble("ShutterSpeedValue", this.getAttributeDouble(ExifInterface.TAG_SHUTTER_SPEED_VALUE, 0.0))
|
||||||
exifMap.putInt("SensingMethod", this.getAttributeInt(ExifInterface.TAG_SENSING_METHOD, ExifInterface.SENSOR_TYPE_NOT_DEFINED.toInt()))
|
exifMap.putInt("SensingMethod", this.getAttributeInt(ExifInterface.TAG_SENSING_METHOD, ExifInterface.SENSOR_TYPE_NOT_DEFINED.toInt()))
|
||||||
val subjectAreaArray = Arguments.createArray()
|
val subjectAreaArray = Arguments.createArray()
|
||||||
this.getAttributeRange(ExifInterface.TAG_SUBJECT_AREA)?.forEach { subjectAreaArray.pushInt(it.toInt()) }
|
this.getAttributeRange(ExifInterface.TAG_SUBJECT_AREA)?.forEach { subjectAreaArray.pushInt(it.toInt()) }
|
||||||
exifMap.putArray("SubjectArea", subjectAreaArray)
|
exifMap.putArray("SubjectArea", subjectAreaArray)
|
||||||
exifMap.putDouble("ApertureValue", this.getAttributeDouble(ExifInterface.TAG_APERTURE_VALUE, 0.0))
|
exifMap.putDouble("ApertureValue", this.getAttributeDouble(ExifInterface.TAG_APERTURE_VALUE, 0.0))
|
||||||
exifMap.putString("SubsecTimeDigitized", this.getAttribute(ExifInterface.TAG_SUBSEC_TIME_DIGITIZED))
|
exifMap.putString("SubsecTimeDigitized", this.getAttribute(ExifInterface.TAG_SUBSEC_TIME_DIGITIZED))
|
||||||
exifMap.putDouble("FocalLength", this.getAttributeDouble(ExifInterface.TAG_FOCAL_LENGTH, 0.0))
|
exifMap.putDouble("FocalLength", this.getAttributeDouble(ExifInterface.TAG_FOCAL_LENGTH, 0.0))
|
||||||
exifMap.putString("LensMake", this.getAttribute(ExifInterface.TAG_LENS_MAKE))
|
exifMap.putString("LensMake", this.getAttribute(ExifInterface.TAG_LENS_MAKE))
|
||||||
exifMap.putString("SubsecTimeOriginal", this.getAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL))
|
exifMap.putString("SubsecTimeOriginal", this.getAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL))
|
||||||
exifMap.putString("OffsetTimeDigitized", this.getAttribute(ExifInterface.TAG_OFFSET_TIME_DIGITIZED))
|
exifMap.putString("OffsetTimeDigitized", this.getAttribute(ExifInterface.TAG_OFFSET_TIME_DIGITIZED))
|
||||||
exifMap.putInt("PixelYDimension", this.getAttributeInt(ExifInterface.TAG_PIXEL_Y_DIMENSION, 0))
|
exifMap.putInt("PixelYDimension", this.getAttributeInt(ExifInterface.TAG_PIXEL_Y_DIMENSION, 0))
|
||||||
val isoSpeedRatingsArray = Arguments.createArray()
|
val isoSpeedRatingsArray = Arguments.createArray()
|
||||||
this.getAttributeRange(ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY)?.forEach { isoSpeedRatingsArray.pushInt(it.toInt()) }
|
this.getAttributeRange(ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY)?.forEach { isoSpeedRatingsArray.pushInt(it.toInt()) }
|
||||||
exifMap.putArray("ISOSpeedRatings", isoSpeedRatingsArray)
|
exifMap.putArray("ISOSpeedRatings", isoSpeedRatingsArray)
|
||||||
exifMap.putInt("WhiteBalance", this.getAttributeInt(ExifInterface.TAG_WHITE_BALANCE, 0))
|
exifMap.putInt("WhiteBalance", this.getAttributeInt(ExifInterface.TAG_WHITE_BALANCE, 0))
|
||||||
exifMap.putString("DateTimeDigitized", this.getAttribute(ExifInterface.TAG_DATETIME_DIGITIZED))
|
exifMap.putString("DateTimeDigitized", this.getAttribute(ExifInterface.TAG_DATETIME_DIGITIZED))
|
||||||
exifMap.putString("OffsetTimeOriginal", this.getAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL))
|
exifMap.putString("OffsetTimeOriginal", this.getAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL))
|
||||||
exifMap.putString("ExifVersion", this.getAttribute(ExifInterface.TAG_EXIF_VERSION))
|
exifMap.putString("ExifVersion", this.getAttribute(ExifInterface.TAG_EXIF_VERSION))
|
||||||
exifMap.putString("OffsetTime", this.getAttribute(ExifInterface.TAG_OFFSET_TIME))
|
exifMap.putString("OffsetTime", this.getAttribute(ExifInterface.TAG_OFFSET_TIME))
|
||||||
exifMap.putInt("Flash", this.getAttributeInt(ExifInterface.TAG_FLASH, ExifInterface.FLAG_FLASH_FIRED.toInt()))
|
exifMap.putInt("Flash", this.getAttributeInt(ExifInterface.TAG_FLASH, ExifInterface.FLAG_FLASH_FIRED.toInt()))
|
||||||
exifMap.putInt("ExposureProgram", this.getAttributeInt(ExifInterface.TAG_EXPOSURE_PROGRAM, ExifInterface.EXPOSURE_PROGRAM_NOT_DEFINED.toInt()))
|
exifMap.putInt("ExposureProgram", this.getAttributeInt(ExifInterface.TAG_EXPOSURE_PROGRAM, ExifInterface.EXPOSURE_PROGRAM_NOT_DEFINED.toInt()))
|
||||||
exifMap.putInt("MeteringMode", this.getAttributeInt(ExifInterface.TAG_METERING_MODE, ExifInterface.METERING_MODE_UNKNOWN.toInt()))
|
exifMap.putInt("MeteringMode", this.getAttributeInt(ExifInterface.TAG_METERING_MODE, ExifInterface.METERING_MODE_UNKNOWN.toInt()))
|
||||||
metadataMap.putMap("{Exif}", exifMap)
|
metadataMap.putMap("{Exif}", exifMap)
|
||||||
|
|
||||||
return metadataMap
|
return metadataMap
|
||||||
}
|
}
|
||||||
|
@ -4,34 +4,38 @@ import androidx.camera.core.ImageCapture
|
|||||||
import androidx.camera.core.ImageCaptureException
|
import androidx.camera.core.ImageCaptureException
|
||||||
import androidx.camera.core.ImageProxy
|
import androidx.camera.core.ImageProxy
|
||||||
import java.util.concurrent.Executor
|
import java.util.concurrent.Executor
|
||||||
import java.util.concurrent.Executors
|
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.resumeWithException
|
import kotlin.coroutines.resumeWithException
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
|
|
||||||
suspend inline fun ImageCapture.takePicture(options: ImageCapture.OutputFileOptions, executor: Executor) = suspendCoroutine<ImageCapture.OutputFileResults> { cont ->
|
suspend inline fun ImageCapture.takePicture(options: ImageCapture.OutputFileOptions, executor: Executor) = suspendCoroutine<ImageCapture.OutputFileResults> { cont ->
|
||||||
this.takePicture(options, executor, object: ImageCapture.OnImageSavedCallback {
|
this.takePicture(
|
||||||
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
|
options, executor,
|
||||||
cont.resume(outputFileResults)
|
object : ImageCapture.OnImageSavedCallback {
|
||||||
}
|
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
|
||||||
|
cont.resume(outputFileResults)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onError(exception: ImageCaptureException) {
|
override fun onError(exception: ImageCaptureException) {
|
||||||
cont.resumeWithException(exception)
|
cont.resumeWithException(exception)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend inline fun ImageCapture.takePicture(executor: Executor) = suspendCoroutine<ImageProxy> { cont ->
|
suspend inline fun ImageCapture.takePicture(executor: Executor) = suspendCoroutine<ImageProxy> { cont ->
|
||||||
this.takePicture(executor, object: ImageCapture.OnImageCapturedCallback() {
|
this.takePicture(
|
||||||
override fun onCaptureSuccess(image: ImageProxy) {
|
executor,
|
||||||
super.onCaptureSuccess(image)
|
object : ImageCapture.OnImageCapturedCallback() {
|
||||||
cont.resume(image)
|
override fun onCaptureSuccess(image: ImageProxy) {
|
||||||
}
|
super.onCaptureSuccess(image)
|
||||||
|
cont.resume(image)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onError(exception: ImageCaptureException) {
|
override fun onError(exception: ImageCaptureException) {
|
||||||
super.onError(exception)
|
super.onError(exception)
|
||||||
cont.resumeWithException(exception)
|
cont.resumeWithException(exception)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -4,9 +4,9 @@ import android.graphics.ImageFormat
|
|||||||
import androidx.camera.core.ImageProxy
|
import androidx.camera.core.ImageProxy
|
||||||
|
|
||||||
val ImageProxy.isRaw: Boolean
|
val ImageProxy.isRaw: Boolean
|
||||||
get() {
|
get() {
|
||||||
return when (format) {
|
return when (format) {
|
||||||
ImageFormat.RAW_SENSOR, ImageFormat.RAW10, ImageFormat.RAW12, ImageFormat.RAW_PRIVATE -> true
|
ImageFormat.RAW_SENSOR, ImageFormat.RAW10, ImageFormat.RAW12, ImageFormat.RAW_PRIVATE -> true
|
||||||
else -> false
|
else -> false
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@ -1,79 +1,76 @@
|
|||||||
package com.mrousavy.camera.utils
|
package com.mrousavy.camera.utils
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import android.graphics.ImageFormat
|
import android.graphics.ImageFormat
|
||||||
import androidx.camera.core.ImageProxy
|
import androidx.camera.core.ImageProxy
|
||||||
import com.mrousavy.camera.InvalidFormatError
|
import com.mrousavy.camera.InvalidFormatError
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.util.stream.Stream.concat
|
|
||||||
|
|
||||||
// TODO: Fix this flip() function (this outputs a black image)
|
// TODO: Fix this flip() function (this outputs a black image)
|
||||||
fun flip(imageBytes: ByteArray, imageWidth: Int): ByteArray {
|
fun flip(imageBytes: ByteArray, imageWidth: Int): ByteArray {
|
||||||
//separate out the sub arrays
|
// separate out the sub arrays
|
||||||
var holder = ByteArray(imageBytes.size)
|
var holder = ByteArray(imageBytes.size)
|
||||||
var subArray = ByteArray(imageWidth)
|
var subArray = ByteArray(imageWidth)
|
||||||
var subCount = 0
|
var subCount = 0
|
||||||
for (i in imageBytes.indices) {
|
for (i in imageBytes.indices) {
|
||||||
subArray[subCount] = imageBytes[i]
|
subArray[subCount] = imageBytes[i]
|
||||||
subCount++
|
subCount++
|
||||||
if (i % imageWidth == 0) {
|
if (i % imageWidth == 0) {
|
||||||
subArray.reverse()
|
subArray.reverse()
|
||||||
if (i == imageWidth) {
|
if (i == imageWidth) {
|
||||||
holder = subArray
|
holder = subArray
|
||||||
} else {
|
} else {
|
||||||
holder += subArray
|
holder += subArray
|
||||||
}
|
}
|
||||||
subCount = 0
|
subCount = 0
|
||||||
subArray = ByteArray(imageWidth)
|
subArray = ByteArray(imageWidth)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
subArray = ByteArray(imageWidth)
|
}
|
||||||
System.arraycopy(imageBytes, imageBytes.size - imageWidth, subArray, 0, subArray.size)
|
subArray = ByteArray(imageWidth)
|
||||||
return holder + subArray
|
System.arraycopy(imageBytes, imageBytes.size - imageWidth, subArray, 0, subArray.size)
|
||||||
|
return holder + subArray
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("UnsafeExperimentalUsageError")
|
@SuppressLint("UnsafeExperimentalUsageError")
|
||||||
fun ImageProxy.save(file: File, flipHorizontally: Boolean) {
|
fun ImageProxy.save(file: File, flipHorizontally: Boolean) {
|
||||||
when (format) {
|
when (format) {
|
||||||
// TODO: ImageFormat.RAW_SENSOR
|
// TODO: ImageFormat.RAW_SENSOR
|
||||||
// TODO: ImageFormat.DEPTH_JPEG
|
// TODO: ImageFormat.DEPTH_JPEG
|
||||||
ImageFormat.JPEG -> {
|
ImageFormat.JPEG -> {
|
||||||
val buffer = planes[0].buffer
|
val buffer = planes[0].buffer
|
||||||
val bytes = ByteArray(buffer.remaining())
|
val bytes = ByteArray(buffer.remaining())
|
||||||
|
|
||||||
// copy image from buffer to byte array
|
// copy image from buffer to byte array
|
||||||
buffer.get(bytes)
|
buffer.get(bytes)
|
||||||
|
|
||||||
val output = FileOutputStream(file)
|
val output = FileOutputStream(file)
|
||||||
output.write(bytes)
|
output.write(bytes)
|
||||||
output.close()
|
output.close()
|
||||||
}
|
|
||||||
ImageFormat.YUV_420_888 -> {
|
|
||||||
// "prebuffer" simply contains the meta information about the following planes.
|
|
||||||
val prebuffer = ByteBuffer.allocate(16)
|
|
||||||
prebuffer.putInt(width)
|
|
||||||
.putInt(height)
|
|
||||||
.putInt(planes[1].pixelStride)
|
|
||||||
.putInt(planes[1].rowStride)
|
|
||||||
|
|
||||||
val output = FileOutputStream(file)
|
|
||||||
output.write(prebuffer.array()) // write meta information to file
|
|
||||||
// Now write the actual planes.
|
|
||||||
var buffer: ByteBuffer
|
|
||||||
var bytes: ByteArray
|
|
||||||
|
|
||||||
for (i in 0..2) {
|
|
||||||
buffer = planes[i].buffer
|
|
||||||
bytes = ByteArray(buffer.remaining()) // makes byte array large enough to hold image
|
|
||||||
buffer.get(bytes) // copies image from buffer to byte array
|
|
||||||
output.write(bytes) // write the byte array to file
|
|
||||||
}
|
|
||||||
output.close()
|
|
||||||
}
|
|
||||||
else -> throw InvalidFormatError(format)
|
|
||||||
}
|
}
|
||||||
|
ImageFormat.YUV_420_888 -> {
|
||||||
|
// "prebuffer" simply contains the meta information about the following planes.
|
||||||
|
val prebuffer = ByteBuffer.allocate(16)
|
||||||
|
prebuffer.putInt(width)
|
||||||
|
.putInt(height)
|
||||||
|
.putInt(planes[1].pixelStride)
|
||||||
|
.putInt(planes[1].rowStride)
|
||||||
|
|
||||||
|
val output = FileOutputStream(file)
|
||||||
|
output.write(prebuffer.array()) // write meta information to file
|
||||||
|
// Now write the actual planes.
|
||||||
|
var buffer: ByteBuffer
|
||||||
|
var bytes: ByteArray
|
||||||
|
|
||||||
|
for (i in 0..2) {
|
||||||
|
buffer = planes[i].buffer
|
||||||
|
bytes = ByteArray(buffer.remaining()) // makes byte array large enough to hold image
|
||||||
|
buffer.get(bytes) // copies image from buffer to byte array
|
||||||
|
output.write(bytes) // write the byte array to file
|
||||||
|
}
|
||||||
|
output.close()
|
||||||
|
}
|
||||||
|
else -> throw InvalidFormatError(format)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
package com.mrousavy.camera.utils
|
package com.mrousavy.camera.utils
|
||||||
|
|
||||||
fun <T> List<T>.containsAny(elements: List<T>): Boolean {
|
fun <T> List<T>.containsAny(elements: List<T>): Boolean {
|
||||||
return elements.any { element -> this.contains(element) }
|
return elements.any { element -> this.contains(element) }
|
||||||
}
|
}
|
||||||
|
@ -7,9 +7,12 @@ import kotlin.coroutines.resume
|
|||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
suspend fun getCameraProvider(context: Context) = suspendCoroutine<ProcessCameraProvider> { cont ->
|
suspend fun getCameraProvider(context: Context) = suspendCoroutine<ProcessCameraProvider> { cont ->
|
||||||
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
|
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
|
||||||
|
|
||||||
cameraProviderFuture.addListener({
|
cameraProviderFuture.addListener(
|
||||||
cont.resume(cameraProviderFuture.get())
|
{
|
||||||
}, ContextCompat.getMainExecutor(context))
|
cont.resume(cameraProviderFuture.get())
|
||||||
|
},
|
||||||
|
ContextCompat.getMainExecutor(context)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -3,16 +3,15 @@ package com.mrousavy.camera.utils
|
|||||||
import android.util.Size
|
import android.util.Size
|
||||||
import android.view.Surface
|
import android.view.Surface
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rotate by a given Surface Rotation
|
* Rotate by a given Surface Rotation
|
||||||
*/
|
*/
|
||||||
fun Size.rotated(surfaceRotation: Int): Size {
|
fun Size.rotated(surfaceRotation: Int): Size {
|
||||||
return when (surfaceRotation) {
|
return when (surfaceRotation) {
|
||||||
Surface.ROTATION_0 -> Size(width, height)
|
Surface.ROTATION_0 -> Size(width, height)
|
||||||
Surface.ROTATION_90 -> Size(height, width)
|
Surface.ROTATION_90 -> Size(height, width)
|
||||||
Surface.ROTATION_180 -> Size(width, height)
|
Surface.ROTATION_180 -> Size(width, height)
|
||||||
Surface.ROTATION_270 -> Size(height, width)
|
Surface.ROTATION_270 -> Size(height, width)
|
||||||
else -> Size(width, height)
|
else -> Size(width, height)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,14 +7,14 @@ import android.view.ViewGroup
|
|||||||
// This fixes that.
|
// This fixes that.
|
||||||
// https://github.com/facebook/react-native/issues/17968#issuecomment-633308615
|
// https://github.com/facebook/react-native/issues/17968#issuecomment-633308615
|
||||||
fun ViewGroup.installHierarchyFitter() {
|
fun ViewGroup.installHierarchyFitter() {
|
||||||
setOnHierarchyChangeListener(object : ViewGroup.OnHierarchyChangeListener {
|
setOnHierarchyChangeListener(object : ViewGroup.OnHierarchyChangeListener {
|
||||||
override fun onChildViewRemoved(parent: View?, child: View?) = Unit
|
override fun onChildViewRemoved(parent: View?, child: View?) = Unit
|
||||||
override fun onChildViewAdded(parent: View?, child: View?) {
|
override fun onChildViewAdded(parent: View?, child: View?) {
|
||||||
parent?.measure(
|
parent?.measure(
|
||||||
View.MeasureSpec.makeMeasureSpec(measuredWidth, View.MeasureSpec.EXACTLY),
|
View.MeasureSpec.makeMeasureSpec(measuredWidth, View.MeasureSpec.EXACTLY),
|
||||||
View.MeasureSpec.makeMeasureSpec(measuredHeight, View.MeasureSpec.EXACTLY)
|
View.MeasureSpec.makeMeasureSpec(measuredHeight, View.MeasureSpec.EXACTLY)
|
||||||
)
|
)
|
||||||
parent?.layout(0, 0, parent.measuredWidth, parent.measuredHeight)
|
parent?.layout(0, 0, parent.measuredWidth, parent.measuredHeight)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -3,22 +3,22 @@ package com.mrousavy.camera.utils
|
|||||||
import com.facebook.react.bridge.WritableArray
|
import com.facebook.react.bridge.WritableArray
|
||||||
|
|
||||||
fun WritableArray.pushInt(value: Int?) {
|
fun WritableArray.pushInt(value: Int?) {
|
||||||
if (value == null)
|
if (value == null)
|
||||||
this.pushNull()
|
this.pushNull()
|
||||||
else
|
else
|
||||||
this.pushInt(value)
|
this.pushInt(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun WritableArray.pushDouble(value: Double?) {
|
fun WritableArray.pushDouble(value: Double?) {
|
||||||
if (value == null)
|
if (value == null)
|
||||||
this.pushNull()
|
this.pushNull()
|
||||||
else
|
else
|
||||||
this.pushDouble(value)
|
this.pushDouble(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun WritableArray.pushBoolean(value: Boolean?) {
|
fun WritableArray.pushBoolean(value: Boolean?) {
|
||||||
if (value == null)
|
if (value == null)
|
||||||
this.pushNull()
|
this.pushNull()
|
||||||
else
|
else
|
||||||
this.pushBoolean(value)
|
this.pushBoolean(value)
|
||||||
}
|
}
|
||||||
|
@ -3,22 +3,22 @@ package com.mrousavy.camera.utils
|
|||||||
import com.facebook.react.bridge.WritableMap
|
import com.facebook.react.bridge.WritableMap
|
||||||
|
|
||||||
fun WritableMap.putInt(key: String, value: Int?) {
|
fun WritableMap.putInt(key: String, value: Int?) {
|
||||||
if (value == null)
|
if (value == null)
|
||||||
this.putNull(key)
|
this.putNull(key)
|
||||||
else
|
else
|
||||||
this.putInt(key, value)
|
this.putInt(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun WritableMap.putDouble(key: String, value: Double?) {
|
fun WritableMap.putDouble(key: String, value: Double?) {
|
||||||
if (value == null)
|
if (value == null)
|
||||||
this.putNull(key)
|
this.putNull(key)
|
||||||
else
|
else
|
||||||
this.putDouble(key, value)
|
this.putDouble(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun WritableMap.putBoolean(key: String, value: Boolean?) {
|
fun WritableMap.putBoolean(key: String, value: Boolean?) {
|
||||||
if (value == null)
|
if (value == null)
|
||||||
this.putNull(key)
|
this.putNull(key)
|
||||||
else
|
else
|
||||||
this.putBoolean(key, value)
|
this.putBoolean(key, value)
|
||||||
}
|
}
|
||||||
|
@ -1,27 +1,27 @@
|
|||||||
package com.mrousavy.camera.utils
|
package com.mrousavy.camera.utils
|
||||||
|
|
||||||
|
import com.facebook.react.bridge.Promise
|
||||||
import com.mrousavy.camera.CameraError
|
import com.mrousavy.camera.CameraError
|
||||||
import com.mrousavy.camera.UnknownCameraError
|
import com.mrousavy.camera.UnknownCameraError
|
||||||
import com.facebook.react.bridge.Promise
|
|
||||||
|
|
||||||
inline fun withPromise(promise: Promise, closure: () -> Any?) {
|
inline fun withPromise(promise: Promise, closure: () -> Any?) {
|
||||||
try {
|
try {
|
||||||
val result = closure()
|
val result = closure()
|
||||||
promise.resolve(result)
|
promise.resolve(result)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
val error = if (e is CameraError) e else UnknownCameraError(e)
|
val error = if (e is CameraError) e else UnknownCameraError(e)
|
||||||
promise.reject("${error.domain}/${error.id}", error.message, error.cause)
|
promise.reject("${error.domain}/${error.id}", error.message, error.cause)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun withSuspendablePromise(promise: Promise, closure: () -> Any?) {
|
inline fun withSuspendablePromise(promise: Promise, closure: () -> Any?) {
|
||||||
try {
|
try {
|
||||||
val result = closure()
|
val result = closure()
|
||||||
promise.resolve(result)
|
promise.resolve(result)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
val error = if (e is CameraError) e else UnknownCameraError(e)
|
val error = if (e is CameraError) e else UnknownCameraError(e)
|
||||||
promise.reject("${error.domain}/${error.id}", error.message, error.cause)
|
promise.reject("${error.domain}/${error.id}", error.message, error.cause)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user