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:
Marc Rousavy 2021-02-26 10:56:20 +01:00 committed by GitHub
parent 2e60110070
commit 03b9246afe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1233 additions and 1190 deletions

View File

@ -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
View 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
View 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.

View File

@ -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]

View File

@ -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())
} }
} }

View File

@ -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()
} }

View File

@ -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")
} }

View File

@ -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
} }

View File

@ -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
} }

View File

@ -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")
}
} }

View File

@ -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()
} }
} }

View File

@ -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.")
}
}
} }

View File

@ -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)

View File

@ -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
} }
} }

View File

@ -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
} }

View File

@ -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
} }

View File

@ -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
} }

View File

@ -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
} }

View File

@ -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
} }
} }
} }
} }

View File

@ -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()
} }
} }

View File

@ -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
} }

View File

@ -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)
} }
}) }
)
} }

View File

@ -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
}
} }
}

View File

@ -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)
}
} }

View File

@ -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) }
} }

View File

@ -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)
)
} }

View File

@ -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)
} }
} }

View File

@ -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)
} }
}) })
} }

View File

@ -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)
} }

View File

@ -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)
} }

View File

@ -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)
} }
} }