feat: Implement atomically single-lock core/ library on Android (#2049)

* feat: Create base for `CameraConfiguration` diff

* Fix

* Write three configure methods

* Build?

* MOre

* Update CameraView+RecordVideo.kt

* Fix errors

* Update CameraDeviceDetails.kt

* Update CameraSession.kt

* Auto-resize Preview View

* More

* Make it work? idk

* Format

* Call `configure` under mutex, and change isActive

* fix: Make Outputs comparable

* fix: Make CodeScanner comparable

* Format

* fix: Update outputs after reconfiguring

* Update CameraPage.tsx

* fix: Close CaptureSession before
This commit is contained in:
Marc Rousavy 2023-10-24 11:19:03 +02:00 committed by GitHub
parent 23d173f6fc
commit de0d6cda5d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 821 additions and 889 deletions

View File

@ -23,10 +23,12 @@ suspend fun CameraView.startRecording(options: ReadableMap, onRecordCallback: Ca
} }
} }
if (options.hasKey("flash")) {
val enableFlash = options.getString("flash") == "on" val enableFlash = options.getString("flash") == "on"
if (enableFlash) {
// overrides current torch mode value to enable flash while recording // overrides current torch mode value to enable flash while recording
cameraSession.setTorchMode(enableFlash) cameraSession.configure { config ->
config.torch = Torch.ON
}
} }
var codec = VideoCodec.H264 var codec = VideoCodec.H264
if (options.hasKey("videoCodec")) { if (options.hasKey("videoCodec")) {
@ -67,5 +69,8 @@ suspend fun CameraView.resumeRecording() {
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
suspend fun CameraView.stopRecording() { suspend fun CameraView.stopRecording() {
cameraSession.stopRecording() cameraSession.stopRecording()
cameraSession.setTorchMode(torch == Torch.ON) // Set torch back to it's original value in case we just used it as a flash for the recording.
cameraSession.configure { config ->
config.torch = torch
}
} }

View File

@ -41,7 +41,7 @@ suspend fun CameraView.takePhoto(optionsMap: ReadableMap): WritableMap {
enableShutterSound, enableShutterSound,
enableAutoRedEyeReduction, enableAutoRedEyeReduction,
enableAutoStabilization, enableAutoStabilization,
outputOrientation orientation
) )
photo.use { photo.use {

View File

@ -1,31 +1,20 @@
package com.mrousavy.camera package com.mrousavy.camera
import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.pm.PackageManager
import android.hardware.camera2.CameraManager import android.hardware.camera2.CameraManager
import android.util.Log import android.util.Log
import android.util.Size
import android.view.Gravity
import android.view.ScaleGestureDetector import android.view.ScaleGestureDetector
import android.view.Surface
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.core.content.ContextCompat
import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.UiThreadUtil import com.google.mlkit.vision.barcode.common.Barcode
import com.mrousavy.camera.core.CameraPermissionError import com.mrousavy.camera.core.CameraConfiguration
import com.mrousavy.camera.core.CameraQueues import com.mrousavy.camera.core.CameraQueues
import com.mrousavy.camera.core.CameraSession import com.mrousavy.camera.core.CameraSession
import com.mrousavy.camera.core.NoCameraDeviceError
import com.mrousavy.camera.core.PreviewView import com.mrousavy.camera.core.PreviewView
import com.mrousavy.camera.core.outputs.CameraOutputs
import com.mrousavy.camera.extensions.bigger
import com.mrousavy.camera.extensions.containsAny
import com.mrousavy.camera.extensions.getPreviewTargetSize
import com.mrousavy.camera.extensions.installHierarchyFitter import com.mrousavy.camera.extensions.installHierarchyFitter
import com.mrousavy.camera.extensions.smaller
import com.mrousavy.camera.frameprocessor.FrameProcessor import com.mrousavy.camera.frameprocessor.FrameProcessor
import com.mrousavy.camera.types.CameraDeviceFormat
import com.mrousavy.camera.types.CodeScannerOptions import com.mrousavy.camera.types.CodeScannerOptions
import com.mrousavy.camera.types.Orientation import com.mrousavy.camera.types.Orientation
import com.mrousavy.camera.types.PixelFormat import com.mrousavy.camera.types.PixelFormat
@ -48,19 +37,23 @@ import kotlinx.coroutines.launch
@SuppressLint("ClickableViewAccessibility", "ViewConstructor", "MissingPermission") @SuppressLint("ClickableViewAccessibility", "ViewConstructor", "MissingPermission")
class CameraView(context: Context) : class CameraView(context: Context) :
FrameLayout(context), FrameLayout(context),
CoroutineScope { CoroutineScope,
CameraSession.CameraSessionCallback {
companion object { companion object {
const val TAG = "CameraView" const val TAG = "CameraView"
private val propsThatRequirePreviewReconfiguration = arrayListOf("cameraId", "format", "resizeMode")
private val propsThatRequireSessionReconfiguration =
arrayListOf("cameraId", "format", "photo", "video", "enableFrameProcessor", "codeScannerOptions", "pixelFormat")
private val propsThatRequireFormatReconfiguration = arrayListOf("fps", "hdr", "videoStabilizationMode", "lowLightBoost")
} }
// react properties // react properties
// props that require reconfiguring // props that require reconfiguring
var cameraId: String? = null var cameraId: String? = null
set(value) {
if (value != null) {
// TODO: Move this into CameraSession
val f = if (format != null) CameraDeviceFormat.fromJSValue(format!!) else null
previewView.resizeToInputCamera(value, cameraManager, f)
}
field = value
}
var enableDepthData = false var enableDepthData = false
var enableHighQualityPhotos: Boolean? = null var enableHighQualityPhotos: Boolean? = null
var enablePortraitEffectsMatteDelivery = false var enablePortraitEffectsMatteDelivery = false
@ -74,7 +67,6 @@ class CameraView(context: Context) :
// props that require format reconfiguring // props that require format reconfiguring
var format: ReadableMap? = null var format: ReadableMap? = null
var resizeMode: ResizeMode = ResizeMode.COVER
var fps: Int? = null var fps: Int? = null
var videoStabilizationMode: VideoStabilizationMode? = null var videoStabilizationMode: VideoStabilizationMode? = null
var hdr: Boolean? = null // nullable bool var hdr: Boolean? = null // nullable bool
@ -84,8 +76,17 @@ class CameraView(context: Context) :
var isActive = false var isActive = false
var torch: Torch = Torch.OFF var torch: Torch = Torch.OFF
var zoom: Float = 1f // in "factor" var zoom: Float = 1f // in "factor"
var orientation: Orientation? = null var orientation: Orientation = Orientation.PORTRAIT
var enableZoomGesture: Boolean = false var enableZoomGesture: Boolean = false
set(value) {
field = value
updateZoomGesture()
}
var resizeMode: ResizeMode = ResizeMode.COVER
set(value) {
previewView.resizeMode = value
field = value
}
// code scanner // code scanner
var codeScannerOptions: CodeScannerOptions? = null var codeScannerOptions: CodeScannerOptions? = null
@ -96,8 +97,7 @@ class CameraView(context: Context) :
// session // session
internal val cameraSession: CameraSession internal val cameraSession: CameraSession
private var previewView: PreviewView? = null private val previewView: PreviewView
private var previewSurface: Surface? = null
internal var frameProcessor: FrameProcessor? = null internal var frameProcessor: FrameProcessor? = null
set(value) { set(value) {
@ -105,165 +105,98 @@ class CameraView(context: Context) :
cameraSession.frameProcessor = frameProcessor cameraSession.frameProcessor = frameProcessor
} }
private val inputOrientation: Orientation
get() = cameraSession.orientation
internal val outputOrientation: Orientation
get() = orientation ?: inputOrientation
override val coroutineContext: CoroutineContext = CameraQueues.cameraQueue.coroutineDispatcher override val coroutineContext: CoroutineContext = CameraQueues.cameraQueue.coroutineDispatcher
init { init {
this.installHierarchyFitter() this.installHierarchyFitter()
clipToOutline = true clipToOutline = true
setupPreviewView() cameraSession = CameraSession(context, cameraManager, this)
cameraSession = CameraSession(context, cameraManager, { invokeOnInitialized() }, { error -> invokeOnError(error) }) previewView = cameraSession.createPreviewView(context)
addView(previewView)
} }
override fun onAttachedToWindow() { override fun onAttachedToWindow() {
super.onAttachedToWindow()
if (!isMounted) { if (!isMounted) {
isMounted = true isMounted = true
invokeOnViewReady() invokeOnViewReady()
} }
launch { updateLifecycle() } update()
super.onAttachedToWindow()
} }
override fun onDetachedFromWindow() { override fun onDetachedFromWindow() {
update()
super.onDetachedFromWindow() super.onDetachedFromWindow()
launch { updateLifecycle() }
} }
private fun getPreviewTargetSize(): Size { fun update() {
val cameraId = cameraId ?: throw NoCameraDeviceError() Log.i(TAG, "Updating CameraSession...")
val format = format
val targetPreviewSize = if (format != null) Size(format.getInt("videoWidth"), format.getInt("videoHeight")) else null
val formatAspectRatio = if (targetPreviewSize != null) targetPreviewSize.bigger.toDouble() / targetPreviewSize.smaller else null
return this.cameraManager.getCameraCharacteristics(cameraId).getPreviewTargetSize(formatAspectRatio)
}
private fun setupPreviewView() {
removeView(previewView)
this.previewSurface = null
if (cameraId == null) return
val previewView = PreviewView(context, this.getPreviewTargetSize(), resizeMode) { surface ->
previewSurface = surface
launch { configureSession() }
}
previewView.layoutParams = LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT,
Gravity.CENTER
)
this.previewView = previewView
UiThreadUtil.runOnUiThread {
addView(previewView)
}
}
fun update(changedProps: ArrayList<String>) {
Log.i(TAG, "Props changed: $changedProps")
val shouldReconfigurePreview = changedProps.containsAny(propsThatRequirePreviewReconfiguration)
val shouldReconfigureSession = shouldReconfigurePreview || changedProps.containsAny(propsThatRequireSessionReconfiguration)
val shouldReconfigureFormat = shouldReconfigureSession || changedProps.containsAny(propsThatRequireFormatReconfiguration)
val shouldReconfigureZoom = shouldReconfigureSession || changedProps.contains("zoom")
val shouldReconfigureTorch = shouldReconfigureSession || changedProps.contains("torch")
val shouldCheckActive = shouldReconfigureFormat || changedProps.contains("isActive")
val shouldReconfigureZoomGesture = changedProps.contains("enableZoomGesture")
launch { launch {
try { cameraSession.configure { config ->
// Expensive Calls // Input Camera Device
if (shouldReconfigurePreview) { config.cameraId = cameraId
setupPreviewView()
}
if (shouldReconfigureSession) {
configureSession()
}
if (shouldReconfigureFormat) {
configureFormat()
}
if (shouldCheckActive) {
updateLifecycle()
}
// Fast Calls
if (shouldReconfigureZoom) {
updateZoom()
}
if (shouldReconfigureTorch) {
updateTorch()
}
if (shouldReconfigureZoomGesture) {
updateZoomGesture()
}
} catch (e: Throwable) {
Log.e(TAG, "update() threw: ${e.message}")
invokeOnError(e)
}
}
}
private suspend fun configureSession() { // Photo
try { if (photo == true) {
Log.i(TAG, "Configuring Camera Device...") config.photo = CameraConfiguration.Output.Enabled.create(CameraConfiguration.Photo(Unit))
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
throw CameraPermissionError()
}
val cameraId = cameraId ?: throw NoCameraDeviceError()
val format = format
val targetVideoSize = if (format != null) Size(format.getInt("videoWidth"), format.getInt("videoHeight")) else null
val targetPhotoSize = if (format != null) Size(format.getInt("photoWidth"), format.getInt("photoHeight")) else null
// TODO: Allow previewSurface to be null/none
val previewSurface = previewSurface ?: return
val codeScannerOptions = codeScannerOptions
val previewOutput = CameraOutputs.PreviewOutput(previewSurface, previewView?.targetSize)
val photoOutput = if (photo == true) {
CameraOutputs.PhotoOutput(targetPhotoSize)
} else { } else {
null config.photo = CameraConfiguration.Output.Disabled.create()
} }
val videoOutput = if (video == true || enableFrameProcessor) {
CameraOutputs.VideoOutput(targetVideoSize, video == true, enableFrameProcessor, pixelFormat) // Video/Frame Processor
} else { if (video == true || enableFrameProcessor) {
null config.video = CameraConfiguration.Output.Enabled.create(
} CameraConfiguration.Video(
val codeScanner = if (codeScannerOptions != null) { pixelFormat,
CameraOutputs.CodeScannerOutput( enableFrameProcessor
codeScannerOptions, )
{ codes -> invokeOnCodeScanned(codes) },
{ error -> invokeOnError(error) }
) )
} else { } else {
null config.video = CameraConfiguration.Output.Disabled.create()
} }
cameraSession.configureSession(cameraId, previewOutput, photoOutput, videoOutput, codeScanner) // Audio
} catch (e: Throwable) { if (audio == true) {
Log.e(TAG, "Failed to configure session: ${e.message}", e) config.audio = CameraConfiguration.Output.Enabled.create(CameraConfiguration.Audio(Unit))
invokeOnError(e) } else {
} config.audio = CameraConfiguration.Output.Disabled.create()
} }
private suspend fun configureFormat() { // Code Scanner
cameraSession.configureFormat(fps, videoStabilizationMode, hdr, lowLightBoost) val codeScanner = codeScannerOptions
if (codeScanner != null) {
config.codeScanner = CameraConfiguration.Output.Enabled.create(
CameraConfiguration.CodeScanner(codeScanner.codeTypes)
)
} else {
config.codeScanner = CameraConfiguration.Output.Disabled.create()
} }
private suspend fun updateLifecycle() { // Orientation
cameraSession.setIsActive(isActive && isAttachedToWindow) config.orientation = orientation
// Format
val format = format
if (format != null) {
config.format = CameraDeviceFormat.fromJSValue(format)
} else {
config.format = null
} }
private suspend fun updateZoom() { // Side-Props
cameraSession.setZoom(zoom) config.fps = fps
} config.enableLowLightBoost = lowLightBoost ?: false
config.enableHdr = hdr ?: false
config.torch = torch
private suspend fun updateTorch() { // Zoom
cameraSession.setTorchMode(torch == Torch.ON) config.zoom = zoom
// isActive
config.isActive = isActive && isAttachedToWindow
}
}
} }
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
@ -274,7 +207,7 @@ class CameraView(context: Context) :
object : ScaleGestureDetector.SimpleOnScaleGestureListener() { object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean { override fun onScale(detector: ScaleGestureDetector): Boolean {
zoom *= detector.scaleFactor zoom *= detector.scaleFactor
launch { updateZoom() } update()
return true return true
} }
} }
@ -286,4 +219,16 @@ class CameraView(context: Context) :
setOnTouchListener(null) setOnTouchListener(null)
} }
} }
override fun onError(error: Throwable) {
invokeOnError(error)
}
override fun onInitialized() {
invokeOnInitialized()
}
override fun onCodeScanned(codes: List<Barcode>) {
invokeOnCodeScanned(codes)
}
} }

View File

@ -18,9 +18,7 @@ class CameraViewManager : ViewGroupManager<CameraView>() {
override fun onAfterUpdateTransaction(view: CameraView) { override fun onAfterUpdateTransaction(view: CameraView) {
super.onAfterUpdateTransaction(view) super.onAfterUpdateTransaction(view)
val changedProps = cameraViewTransactions[view] ?: ArrayList() view.update()
view.update(changedProps)
cameraViewTransactions.remove(view)
} }
override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any>? = override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any>? =
@ -35,108 +33,69 @@ class CameraViewManager : ViewGroupManager<CameraView>() {
@ReactProp(name = "cameraId") @ReactProp(name = "cameraId")
fun setCameraId(view: CameraView, cameraId: String) { fun setCameraId(view: CameraView, cameraId: String) {
if (view.cameraId != cameraId) {
addChangedPropToTransaction(view, "cameraId")
}
view.cameraId = cameraId view.cameraId = cameraId
} }
@ReactProp(name = "photo") @ReactProp(name = "photo")
fun setPhoto(view: CameraView, photo: Boolean?) { fun setPhoto(view: CameraView, photo: Boolean?) {
if (view.photo != photo) {
addChangedPropToTransaction(view, "photo")
}
view.photo = photo view.photo = photo
} }
@ReactProp(name = "video") @ReactProp(name = "video")
fun setVideo(view: CameraView, video: Boolean?) { fun setVideo(view: CameraView, video: Boolean?) {
if (view.video != video) {
addChangedPropToTransaction(view, "video")
}
view.video = video view.video = video
} }
@ReactProp(name = "audio") @ReactProp(name = "audio")
fun setAudio(view: CameraView, audio: Boolean?) { fun setAudio(view: CameraView, audio: Boolean?) {
if (view.audio != audio) {
addChangedPropToTransaction(view, "audio")
}
view.audio = audio view.audio = audio
} }
@ReactProp(name = "enableFrameProcessor") @ReactProp(name = "enableFrameProcessor")
fun setEnableFrameProcessor(view: CameraView, enableFrameProcessor: Boolean) { fun setEnableFrameProcessor(view: CameraView, enableFrameProcessor: Boolean) {
if (view.enableFrameProcessor != enableFrameProcessor) {
addChangedPropToTransaction(view, "enableFrameProcessor")
}
view.enableFrameProcessor = enableFrameProcessor view.enableFrameProcessor = enableFrameProcessor
} }
@ReactProp(name = "pixelFormat") @ReactProp(name = "pixelFormat")
fun setPixelFormat(view: CameraView, pixelFormat: String?) { fun setPixelFormat(view: CameraView, pixelFormat: String?) {
val newPixelFormat = PixelFormat.fromUnionValue(pixelFormat) val newPixelFormat = PixelFormat.fromUnionValue(pixelFormat)
if (view.pixelFormat != newPixelFormat) { view.pixelFormat = newPixelFormat
addChangedPropToTransaction(view, "pixelFormat")
}
view.pixelFormat = newPixelFormat ?: PixelFormat.NATIVE
} }
@ReactProp(name = "enableDepthData") @ReactProp(name = "enableDepthData")
fun setEnableDepthData(view: CameraView, enableDepthData: Boolean) { fun setEnableDepthData(view: CameraView, enableDepthData: Boolean) {
if (view.enableDepthData != enableDepthData) {
addChangedPropToTransaction(view, "enableDepthData")
}
view.enableDepthData = enableDepthData view.enableDepthData = enableDepthData
} }
@ReactProp(name = "enableZoomGesture") @ReactProp(name = "enableZoomGesture")
fun setEnableZoomGesture(view: CameraView, enableZoomGesture: Boolean) { fun setEnableZoomGesture(view: CameraView, enableZoomGesture: Boolean) {
if (view.enableZoomGesture != enableZoomGesture) {
addChangedPropToTransaction(view, "enableZoomGesture")
}
view.enableZoomGesture = enableZoomGesture view.enableZoomGesture = enableZoomGesture
} }
@ReactProp(name = "videoStabilizationMode") @ReactProp(name = "videoStabilizationMode")
fun setVideoStabilizationMode(view: CameraView, videoStabilizationMode: String?) { fun setVideoStabilizationMode(view: CameraView, videoStabilizationMode: String?) {
val newMode = VideoStabilizationMode.fromUnionValue(videoStabilizationMode) val newMode = VideoStabilizationMode.fromUnionValue(videoStabilizationMode)
if (view.videoStabilizationMode != newMode) {
addChangedPropToTransaction(view, "videoStabilizationMode")
}
view.videoStabilizationMode = newMode view.videoStabilizationMode = newMode
} }
@ReactProp(name = "enableHighQualityPhotos") @ReactProp(name = "enableHighQualityPhotos")
fun setEnableHighQualityPhotos(view: CameraView, enableHighQualityPhotos: Boolean?) { fun setEnableHighQualityPhotos(view: CameraView, enableHighQualityPhotos: Boolean?) {
if (view.enableHighQualityPhotos != enableHighQualityPhotos) {
addChangedPropToTransaction(view, "enableHighQualityPhotos")
}
view.enableHighQualityPhotos = enableHighQualityPhotos view.enableHighQualityPhotos = enableHighQualityPhotos
} }
@ReactProp(name = "enablePortraitEffectsMatteDelivery") @ReactProp(name = "enablePortraitEffectsMatteDelivery")
fun setEnablePortraitEffectsMatteDelivery(view: CameraView, enablePortraitEffectsMatteDelivery: Boolean) { fun setEnablePortraitEffectsMatteDelivery(view: CameraView, enablePortraitEffectsMatteDelivery: Boolean) {
if (view.enablePortraitEffectsMatteDelivery != enablePortraitEffectsMatteDelivery) {
addChangedPropToTransaction(view, "enablePortraitEffectsMatteDelivery")
}
view.enablePortraitEffectsMatteDelivery = enablePortraitEffectsMatteDelivery view.enablePortraitEffectsMatteDelivery = enablePortraitEffectsMatteDelivery
} }
@ReactProp(name = "format") @ReactProp(name = "format")
fun setFormat(view: CameraView, format: ReadableMap?) { fun setFormat(view: CameraView, format: ReadableMap?) {
if (view.format != format) {
addChangedPropToTransaction(view, "format")
}
view.format = format view.format = format
} }
@ReactProp(name = "resizeMode") @ReactProp(name = "resizeMode")
fun setResizeMode(view: CameraView, resizeMode: String) { fun setResizeMode(view: CameraView, resizeMode: String) {
val newMode = ResizeMode.fromUnionValue(resizeMode) val newMode = ResizeMode.fromUnionValue(resizeMode)
if (view.resizeMode != newMode) {
addChangedPropToTransaction(view, "resizeMode")
}
view.resizeMode = newMode view.resizeMode = newMode
} }
@ -145,82 +104,49 @@ class CameraViewManager : ViewGroupManager<CameraView>() {
// 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) {
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) {
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) {
addChangedPropToTransaction(view, "lowLightBoost")
}
view.lowLightBoost = lowLightBoost view.lowLightBoost = lowLightBoost
} }
@ReactProp(name = "isActive") @ReactProp(name = "isActive")
fun setIsActive(view: CameraView, isActive: Boolean) { fun setIsActive(view: CameraView, isActive: Boolean) {
if (view.isActive != 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) {
val newMode = Torch.fromUnionValue(torch) val newMode = Torch.fromUnionValue(torch)
if (view.torch != newMode) {
addChangedPropToTransaction(view, "torch")
}
view.torch = newMode view.torch = newMode
} }
@ReactProp(name = "zoom") @ReactProp(name = "zoom")
fun setZoom(view: CameraView, zoom: Double) { fun setZoom(view: CameraView, zoom: Double) {
val zoomFloat = zoom.toFloat() val zoomFloat = zoom.toFloat()
if (view.zoom != zoomFloat) {
addChangedPropToTransaction(view, "zoom")
}
view.zoom = zoomFloat view.zoom = zoomFloat
} }
@ReactProp(name = "orientation") @ReactProp(name = "orientation")
fun setOrientation(view: CameraView, orientation: String?) { fun setOrientation(view: CameraView, orientation: String?) {
val newMode = Orientation.fromUnionValue(orientation) val newMode = Orientation.fromUnionValue(orientation)
if (view.orientation != newMode) {
addChangedPropToTransaction(view, "orientation")
}
view.orientation = newMode view.orientation = newMode
} }
@ReactProp(name = "codeScannerOptions") @ReactProp(name = "codeScannerOptions")
fun setCodeScanner(view: CameraView, codeScannerOptions: ReadableMap) { fun setCodeScanner(view: CameraView, codeScannerOptions: ReadableMap) {
val newCodeScannerOptions = CodeScannerOptions(codeScannerOptions) val newCodeScannerOptions = CodeScannerOptions(codeScannerOptions)
if (view.codeScannerOptions != newCodeScannerOptions) {
addChangedPropToTransaction(view, "codeScannerOptions")
}
view.codeScannerOptions = newCodeScannerOptions view.codeScannerOptions = newCodeScannerOptions
} }
companion object { companion object {
const val TAG = "CameraView" const val TAG = "CameraView"
val cameraViewTransactions: HashMap<CameraView, ArrayList<String>> = HashMap()
private fun addChangedPropToTransaction(view: CameraView, changedProp: String) {
if (cameraViewTransactions[view] == null) {
cameraViewTransactions[view] = ArrayList()
}
cameraViewTransactions[view]!!.add(changedProp)
}
} }
} }

View File

@ -0,0 +1,103 @@
package com.mrousavy.camera.core
import android.view.Surface
import com.mrousavy.camera.types.CameraDeviceFormat
import com.mrousavy.camera.types.CodeType
import com.mrousavy.camera.types.Orientation
import com.mrousavy.camera.types.PixelFormat
import com.mrousavy.camera.types.Torch
import com.mrousavy.camera.types.VideoStabilizationMode
data class CameraConfiguration(
// Input
var cameraId: String? = null,
// Outputs
var preview: Output<Preview> = Output.Disabled.create(),
var photo: Output<Photo> = Output.Disabled.create(),
var video: Output<Video> = Output.Disabled.create(),
var codeScanner: Output<CodeScanner> = Output.Disabled.create(),
var enableHdr: Boolean = false,
// Orientation
var orientation: Orientation = Orientation.PORTRAIT,
// Format
var format: CameraDeviceFormat? = null,
// Side-Props
var fps: Int? = null,
var enableLowLightBoost: Boolean = false,
var torch: Torch = Torch.OFF,
var videoStabilizationMode: VideoStabilizationMode = VideoStabilizationMode.OFF,
// Zoom
var zoom: Float = 1f,
// isActive (Start/Stop)
var isActive: Boolean = false,
// Audio Session
var audio: Output<Audio> = Output.Disabled.create()
) {
// Output<T> types, those need to be comparable
data class CodeScanner(val codeTypes: List<CodeType>)
data class Photo(val nothing: Unit)
data class Video(val pixelFormat: PixelFormat, val enableFrameProcessor: Boolean)
data class Audio(val nothing: Unit)
data class Preview(val surface: Surface)
@Suppress("EqualsOrHashCode")
sealed class Output<T> {
val isEnabled: Boolean
get() = this is Enabled<*>
class Disabled<T> private constructor() : Output<T>() {
override fun equals(other: Any?): Boolean = other is Disabled<*>
companion object {
fun <T> create(): Disabled<T> = Disabled()
}
}
class Enabled<T> private constructor(val config: T) : Output<T>() {
override fun equals(other: Any?): Boolean = other is Enabled<*> && config == other.config
companion object {
fun <T> create(config: T): Enabled<T> = Enabled(config)
}
}
}
data class Difference(
// Input Camera (cameraId and isActive)
val deviceChanged: Boolean,
// Outputs & Session (Photo, Video, CodeScanner, HDR, Format)
val outputsChanged: Boolean,
// Side-Props for CaptureRequest (fps, low-light-boost, torch, zoom, videoStabilization)
val sidePropsChanged: Boolean
) {
val hasAnyDifference: Boolean
get() = sidePropsChanged || outputsChanged || deviceChanged
}
companion object {
fun copyOf(other: CameraConfiguration?): CameraConfiguration = other?.copy() ?: CameraConfiguration()
fun difference(left: CameraConfiguration?, right: CameraConfiguration): Difference {
val deviceChanged = left?.cameraId != right.cameraId
val outputsChanged = deviceChanged || // input device
left?.photo != right.photo || left.video != right.video || left.codeScanner != right.codeScanner ||
left.preview != right.preview || // outputs
left.enableHdr != right.enableHdr || left.format != right.format // props that affect the outputs (hdr, format, ..)
val sidePropsChanged = outputsChanged || // depend on outputs
left?.torch != right.torch || left.enableLowLightBoost != right.enableLowLightBoost || left.fps != right.fps ||
left.zoom != right.zoom || left.videoStabilizationMode != right.videoStabilizationMode || left.isActive != right.isActive
return Difference(
deviceChanged,
outputsChanged,
sidePropsChanged
)
}
}
}

View File

@ -14,6 +14,7 @@ import com.facebook.react.bridge.ReadableMap
import com.mrousavy.camera.extensions.bigger import com.mrousavy.camera.extensions.bigger
import com.mrousavy.camera.extensions.getPhotoSizes import com.mrousavy.camera.extensions.getPhotoSizes
import com.mrousavy.camera.extensions.getVideoSizes import com.mrousavy.camera.extensions.getVideoSizes
import com.mrousavy.camera.types.AutoFocusSystem
import com.mrousavy.camera.types.HardwareLevel import com.mrousavy.camera.types.HardwareLevel
import com.mrousavy.camera.types.LensFacing import com.mrousavy.camera.types.LensFacing
import com.mrousavy.camera.types.Orientation import com.mrousavy.camera.types.Orientation
@ -175,11 +176,12 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, private val
map.putInt("maxISO", isoRange.upper) map.putInt("maxISO", isoRange.upper)
map.putInt("minFps", fpsRange.lower) map.putInt("minFps", fpsRange.lower)
map.putInt("maxFps", fpsRange.upper) map.putInt("maxFps", fpsRange.upper)
map.putDouble("maxZoom", maxZoom)
map.putDouble("fieldOfView", getFieldOfView()) map.putDouble("fieldOfView", getFieldOfView())
map.putBoolean("supportsVideoHDR", supportsVideoHdr) map.putBoolean("supportsVideoHDR", supportsVideoHdr)
map.putBoolean("supportsPhotoHDR", supportsPhotoHdr) map.putBoolean("supportsPhotoHDR", supportsPhotoHdr)
map.putBoolean("supportsDepthCapture", supportsDepthCapture) map.putBoolean("supportsDepthCapture", supportsDepthCapture)
map.putString("autoFocusSystem", "contrast-detection") // TODO: Is this wrong? map.putString("autoFocusSystem", AutoFocusSystem.CONTRAST_DETECTION.unionValue)
map.putArray("videoStabilizationModes", createStabilizationModes()) map.putArray("videoStabilizationModes", createStabilizationModes())
map.putArray("pixelFormats", createPixelFormats()) map.putArray("pixelFormats", createPixelFormats())
return map return map

View File

@ -1,6 +1,5 @@
package com.mrousavy.camera.core package com.mrousavy.camera.core
import com.mrousavy.camera.core.outputs.CameraOutputs
import com.mrousavy.camera.types.CameraDeviceError import com.mrousavy.camera.types.CameraDeviceError
abstract class CameraError( abstract class CameraError(
@ -39,7 +38,7 @@ class MicrophonePermissionError :
) )
class CameraPermissionError : CameraError("permission", "camera-permission-denied", "The Camera permission was denied!") class CameraPermissionError : CameraError("permission", "camera-permission-denied", "The Camera permission was denied!")
class InvalidTypeScriptUnionError(unionName: String, unionValue: String) : class InvalidTypeScriptUnionError(unionName: String, unionValue: String?) :
CameraError("parameter", "invalid-parameter", "The given value for $unionName could not be parsed! (Received: $unionValue)") CameraError("parameter", "invalid-parameter", "The given value for $unionName could not be parsed! (Received: $unionValue)")
class NoCameraDeviceError : class NoCameraDeviceError :
@ -55,8 +54,8 @@ class CameraNotReadyError :
CameraError("session", "camera-not-ready", "The Camera is not ready yet! Wait for the onInitialized() callback!") CameraError("session", "camera-not-ready", "The Camera is not ready yet! Wait for the onInitialized() callback!")
class CameraCannotBeOpenedError(cameraId: String, error: CameraDeviceError) : class CameraCannotBeOpenedError(cameraId: String, error: CameraDeviceError) :
CameraError("session", "camera-cannot-be-opened", "The given Camera device (id: $cameraId) could not be opened! Error: $error") CameraError("session", "camera-cannot-be-opened", "The given Camera device (id: $cameraId) could not be opened! Error: $error")
class CameraSessionCannotBeConfiguredError(cameraId: String, outputs: CameraOutputs) : class CameraSessionCannotBeConfiguredError(cameraId: String) :
CameraError("session", "cannot-create-session", "Failed to create a Camera Session for Camera $cameraId! Outputs: $outputs") CameraError("session", "cannot-create-session", "Failed to create a Camera Session for Camera $cameraId!")
class CameraDisconnectedError(cameraId: String, error: CameraDeviceError) : class CameraDisconnectedError(cameraId: String, error: CameraDeviceError) :
CameraError("session", "camera-has-been-disconnected", "The given Camera device (id: $cameraId) has been disconnected! Error: $error") CameraError("session", "camera-has-been-disconnected", "The given Camera device (id: $cameraId) has been disconnected! Error: $error")

View File

@ -1,6 +1,7 @@
package com.mrousavy.camera.core package com.mrousavy.camera.core
import android.content.Context import android.content.Context
import android.graphics.ImageFormat
import android.graphics.Point import android.graphics.Point
import android.hardware.camera2.CameraCaptureSession import android.hardware.camera2.CameraCaptureSession
import android.hardware.camera2.CameraCharacteristics import android.hardware.camera2.CameraCharacteristics
@ -11,38 +12,51 @@ import android.hardware.camera2.CaptureRequest
import android.hardware.camera2.CaptureResult import android.hardware.camera2.CaptureResult
import android.hardware.camera2.TotalCaptureResult import android.hardware.camera2.TotalCaptureResult
import android.hardware.camera2.params.MeteringRectangle import android.hardware.camera2.params.MeteringRectangle
import android.hardware.camera2.params.OutputConfiguration
import android.media.Image import android.media.Image
import android.media.ImageReader
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
import android.util.Range import android.util.Range
import android.util.Size import android.util.Size
import com.mrousavy.camera.CameraView import android.view.Surface
import com.mrousavy.camera.core.outputs.CameraOutputs import android.view.SurfaceHolder
import com.google.mlkit.vision.barcode.common.Barcode
import com.mrousavy.camera.core.outputs.BarcodeScannerOutput
import com.mrousavy.camera.core.outputs.PhotoOutput
import com.mrousavy.camera.core.outputs.SurfaceOutput
import com.mrousavy.camera.core.outputs.VideoPipelineOutput
import com.mrousavy.camera.extensions.bigger
import com.mrousavy.camera.extensions.capture import com.mrousavy.camera.extensions.capture
import com.mrousavy.camera.extensions.closestToOrMax
import com.mrousavy.camera.extensions.createCaptureSession import com.mrousavy.camera.extensions.createCaptureSession
import com.mrousavy.camera.extensions.createPhotoCaptureRequest import com.mrousavy.camera.extensions.createPhotoCaptureRequest
import com.mrousavy.camera.extensions.getPhotoSizes
import com.mrousavy.camera.extensions.getPreviewTargetSize
import com.mrousavy.camera.extensions.getVideoSizes
import com.mrousavy.camera.extensions.openCamera import com.mrousavy.camera.extensions.openCamera
import com.mrousavy.camera.extensions.setZoom import com.mrousavy.camera.extensions.setZoom
import com.mrousavy.camera.extensions.smaller
import com.mrousavy.camera.frameprocessor.FrameProcessor import com.mrousavy.camera.frameprocessor.FrameProcessor
import com.mrousavy.camera.types.Flash import com.mrousavy.camera.types.Flash
import com.mrousavy.camera.types.Orientation import com.mrousavy.camera.types.Orientation
import com.mrousavy.camera.types.QualityPrioritization import com.mrousavy.camera.types.QualityPrioritization
import com.mrousavy.camera.types.Torch
import com.mrousavy.camera.types.VideoCodec import com.mrousavy.camera.types.VideoCodec
import com.mrousavy.camera.types.VideoFileType import com.mrousavy.camera.types.VideoFileType
import com.mrousavy.camera.types.VideoStabilizationMode import com.mrousavy.camera.types.VideoStabilizationMode
import java.io.Closeable import java.io.Closeable
import java.util.concurrent.CancellationException import java.util.concurrent.CancellationException
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
class CameraSession( class CameraSession(private val context: Context, private val cameraManager: CameraManager, private val callback: CameraSessionCallback) :
private val context: Context,
private val cameraManager: CameraManager,
private val onInitialized: () -> Unit,
private val onError: (e: Throwable) -> Unit
) : CameraManager.AvailabilityCallback(),
Closeable, Closeable,
CameraOutputs.Callback { CoroutineScope {
companion object { companion object {
private const val TAG = "CameraSession" private const val TAG = "CameraSession"
@ -50,43 +64,24 @@ class CameraSession(
private val CAN_SET_FPS = !Build.MANUFACTURER.equals("samsung", true) private val CAN_SET_FPS = !Build.MANUFACTURER.equals("samsung", true)
} }
data class CapturedPhoto( // Camera Configuration
val image: Image, private var configuration: CameraConfiguration? = null
val metadata: TotalCaptureResult,
val orientation: Orientation,
val isMirrored: Boolean,
val format: Int
) : Closeable {
override fun close() {
image.close()
}
}
// setInput(..)
private var cameraId: String? = null
// setOutputs(..)
private var outputs: CameraOutputs? = null
// setIsActive(..)
private var isActive = false
// configureFormat(..)
private var fps: Int? = null
private var videoStabilizationMode: VideoStabilizationMode? = null
private var lowLightBoost: Boolean? = null
private var hdr: Boolean? = null
// zoom(..)
private var zoom: Float = 1.0f
// Camera State
private var captureSession: CameraCaptureSession? = null private var captureSession: CameraCaptureSession? = null
private var cameraDevice: CameraDevice? = null private var cameraDevice: CameraDevice? = null
private var previewRequest: CaptureRequest.Builder? = null private var previewRequest: CaptureRequest.Builder? = null
private var photoOutput: PhotoOutput? = null
private var videoOutput: VideoPipelineOutput? = null
private var previewOutput: SurfaceOutput? = null
private var codeScannerOutput: BarcodeScannerOutput? = null
private var previewView: PreviewView? = null
private val photoOutputSynchronizer = PhotoOutputSynchronizer() private val photoOutputSynchronizer = PhotoOutputSynchronizer()
private val mutex = Mutex() private val mutex = Mutex()
private var isRunning = false private var isRunning = false
private var enableTorch = false
override val coroutineContext: CoroutineContext
get() = CameraQueues.cameraQueue.coroutineDispatcher
// Video Outputs // Video Outputs
private var recording: RecordingSession? = null private var recording: RecordingSession? = null
@ -100,115 +95,341 @@ class CameraSession(
updateVideoOutputs() updateVideoOutputs()
} }
init {
cameraManager.registerAvailabilityCallback(this, CameraQueues.cameraQueue.handler)
}
override fun close() { override fun close() {
cameraManager.unregisterAvailabilityCallback(this) runBlocking {
mutex.withLock {
destroy()
photoOutputSynchronizer.clear() photoOutputSynchronizer.clear()
captureSession?.close() }
cameraDevice?.close() }
outputs?.close()
isRunning = false
} }
val orientation: Orientation val orientation: Orientation
get() { get() {
val cameraId = cameraId ?: return Orientation.PORTRAIT val cameraId = configuration?.cameraId ?: return Orientation.PORTRAIT
val characteristics = cameraManager.getCameraCharacteristics(cameraId) val characteristics = cameraManager.getCameraCharacteristics(cameraId)
val sensorRotation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0 val sensorRotation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0
return Orientation.fromRotationDegrees(sensorRotation) return Orientation.fromRotationDegrees(sensorRotation)
} }
suspend fun configureSession( suspend fun configure(lambda: (configuration: CameraConfiguration) -> Unit) {
cameraId: String, mutex.withLock {
preview: CameraOutputs.PreviewOutput? = null, Log.i(TAG, "Updating CameraSession Configuration...")
photo: CameraOutputs.PhotoOutput? = null,
video: CameraOutputs.VideoOutput? = null, val config = CameraConfiguration.copyOf(this.configuration)
codeScanner: CameraOutputs.CodeScannerOutput? = null lambda(config)
) { val diff = CameraConfiguration.difference(this.configuration, config)
Log.i(TAG, "Configuring Session for Camera $cameraId...")
val outputs = CameraOutputs( if (!diff.hasAnyDifference) {
cameraId, Log.w(TAG, "Called configure(...) but nothing changed...")
cameraManager, return
preview, }
photo,
video, try {
codeScanner, // Build up session or update any props
hdr == true, if (diff.deviceChanged) {
this // 1. cameraId changed, open device
configureCameraDevice(config)
}
if (diff.outputsChanged) {
// 2. outputs changed, build new session
configureOutputs(config)
}
if (diff.sidePropsChanged) {
// 3. zoom etc changed, update repeating request
configureCaptureRequest(config)
}
Log.i(TAG, "Successfully updated CameraSession Configuration! isActive: ${config.isActive}")
this.configuration = config
} catch (error: Throwable) {
Log.e(TAG, "Failed to configure CameraSession! Error: ${error.message}, Config-Diff: $diff", error)
callback.onError(error)
}
}
}
private fun destroy() {
Log.i(TAG, "Destroying session..")
captureSession?.stopRepeating()
captureSession?.close()
captureSession = null
cameraDevice?.close()
cameraDevice = null
previewOutput?.close()
previewOutput = null
photoOutput?.close()
photoOutput = null
videoOutput?.close()
videoOutput = null
codeScannerOutput?.close()
codeScannerOutput = null
isRunning = false
}
fun createPreviewView(context: Context): PreviewView {
val previewView = PreviewView(
context,
object : SurfaceHolder.Callback {
override fun surfaceCreated(holder: SurfaceHolder) {
Log.i(TAG, "PreviewView Surface created! ${holder.surface}")
createPreviewOutput(holder.surface)
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
Log.i(TAG, "PreviewView Surface updated! ${holder.surface} $width x $height")
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
Log.i(TAG, "PreviewView Surface destroyed! ${holder.surface}")
destroyPreviewOutputSync()
}
}
) )
if (this.cameraId == cameraId && this.outputs == outputs && isActive == isRunning) { this.previewView = previewView
Log.i(TAG, "Nothing changed in configuration, canceling..") return previewView
} }
// 1. Close previous outputs private fun createPreviewOutput(surface: Surface) {
this.outputs?.close() Log.i(TAG, "Setting Preview Output...")
// 2. Assign new outputs launch {
this.outputs = outputs configure { config ->
// 3. Update with existing render targets (surfaces) config.preview = CameraConfiguration.Output.Enabled.create(CameraConfiguration.Preview(surface))
updateVideoOutputs() }
}
this.cameraId = cameraId
startRunning()
} }
suspend fun configureFormat( private fun destroyPreviewOutputSync() {
fps: Int? = null, Log.i(TAG, "Destroying Preview Output...")
videoStabilizationMode: VideoStabilizationMode? = null, runBlocking {
hdr: Boolean? = null, configure { config ->
lowLightBoost: Boolean? = null config.preview = CameraConfiguration.Output.Disabled.create()
) {
Log.i(TAG, "Setting Format (fps: $fps | videoStabilization: $videoStabilizationMode | hdr: $hdr | lowLightBoost: $lowLightBoost)...")
this.fps = fps
this.videoStabilizationMode = videoStabilizationMode
this.hdr = hdr
this.lowLightBoost = lowLightBoost
var needsReconfiguration = false
val currentOutputs = outputs
if (currentOutputs != null && currentOutputs.enableHdr != hdr) {
// Update existing HDR for Outputs
this.outputs?.close()
this.outputs = CameraOutputs(
currentOutputs.cameraId,
cameraManager,
currentOutputs.preview,
currentOutputs.photo,
currentOutputs.video,
currentOutputs.codeScanner,
hdr,
this
)
needsReconfiguration = true
} }
if (needsReconfiguration) {
startRunning()
} else {
updateRepeatingRequest()
} }
} }
/** /**
* Starts or stops the Camera. * Set up the `CameraDevice` (`cameraId`)
*/ */
suspend fun setIsActive(isActive: Boolean) { private suspend fun configureCameraDevice(configuration: CameraConfiguration) {
Log.i(TAG, "Setting isActive: $isActive (isRunning: $isRunning)") val cameraId = configuration.cameraId ?: throw NoCameraDeviceError()
this.isActive = isActive
if (isActive == isRunning) return
if (isActive) { Log.i(TAG, "Configuring Camera #$cameraId...")
startRunning()
cameraDevice?.close()
cameraDevice = cameraManager.openCamera(cameraId, { device, error ->
if (this.cameraDevice == device) {
Log.e(TAG, "Camera Device $device has been disconnected!", error)
callback.onError(error)
} else { } else {
stopRunning() // a previous device has been disconnected, but we already have a new one.
// this is just normal behavior
}
}, CameraQueues.cameraQueue)
// Update PreviewView's Surface Size to a supported value from this Capture Device
previewView?.resizeToInputCamera(cameraId, cameraManager, configuration.format)
Log.i(TAG, "Successfully configured Camera #$cameraId!")
}
/**
* Set up the `CaptureSession` with all outputs (preview, photo, video, codeScanner) and their HDR/Format settings.
*/
private suspend fun configureOutputs(configuration: CameraConfiguration) {
val cameraDevice = cameraDevice ?: throw NoCameraDeviceError()
val characteristics = cameraManager.getCameraCharacteristics(cameraDevice.id)
val format = configuration.format
Log.i(TAG, "Configuring Session for Camera #${cameraDevice.id}...")
// TODO: Do we want to skip this is this.cameraSession already contains all outputs?
// Destroy previous CaptureSession
captureSession?.close()
captureSession = null
// Destroy previous outputs
photoOutput?.close()
photoOutput = null
videoOutput?.close()
videoOutput = null
previewOutput?.close()
previewOutput = null
codeScannerOutput?.close()
codeScannerOutput = null
val isSelfie = characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT
val outputs = mutableListOf<OutputConfiguration>()
// Photo Output
val photo = configuration.photo as? CameraConfiguration.Output.Enabled<CameraConfiguration.Photo>
if (photo != null) {
val imageFormat = ImageFormat.JPEG
val sizes = characteristics.getPhotoSizes(imageFormat)
val size = sizes.closestToOrMax(format?.photoSize)
val maxImages = 3
Log.i(TAG, "Adding ${size.width} x ${size.height} Photo Output in Format #$imageFormat...")
val imageReader = ImageReader.newInstance(size.width, size.height, imageFormat, maxImages)
imageReader.setOnImageAvailableListener({ reader ->
Log.i(TAG, "Photo Captured!")
val image = reader.acquireLatestImage()
onPhotoCaptured(image)
}, CameraQueues.cameraQueue.handler)
val output = PhotoOutput(imageReader, configuration.enableHdr)
outputs.add(output.toOutputConfiguration(characteristics))
photoOutput = output
}
// Video Output
val video = configuration.video as? CameraConfiguration.Output.Enabled<CameraConfiguration.Video>
if (video != null) {
val imageFormat = video.config.pixelFormat.toImageFormat()
val sizes = characteristics.getVideoSizes(cameraDevice.id, imageFormat)
val size = sizes.closestToOrMax(format?.videoSize)
Log.i(TAG, "Adding ${size.width} x ${size.height} Video Output in Format #$imageFormat...")
val videoPipeline = VideoPipeline(
size.width,
size.height,
video.config.pixelFormat,
isSelfie,
video.config.enableFrameProcessor
)
val output = VideoPipelineOutput(videoPipeline, configuration.enableHdr)
outputs.add(output.toOutputConfiguration(characteristics))
videoOutput = output
}
// Preview Output
val preview = configuration.preview as? CameraConfiguration.Output.Enabled<CameraConfiguration.Preview>
if (preview != null) {
// Compute Preview Size based on chosen video size
val videoSize = videoOutput?.size ?: format?.videoSize
val size = if (videoSize != null) {
val formatAspectRatio = videoSize.bigger.toDouble() / videoSize.smaller
characteristics.getPreviewTargetSize(formatAspectRatio)
} else {
characteristics.getPreviewTargetSize(null)
}
Log.i(TAG, "Adding ${size.width} x ${size.height} Preview Output...")
val output = SurfaceOutput(
preview.config.surface,
size,
SurfaceOutput.OutputType.PREVIEW,
configuration.enableHdr
)
outputs.add(output.toOutputConfiguration(characteristics))
previewOutput = output
previewView?.size = size
}
// CodeScanner Output
val codeScanner = configuration.codeScanner as? CameraConfiguration.Output.Enabled<CameraConfiguration.CodeScanner>
if (codeScanner != null) {
val imageFormat = ImageFormat.YUV_420_888
val sizes = characteristics.getVideoSizes(cameraDevice.id, imageFormat)
val size = sizes.closestToOrMax(Size(1280, 720))
Log.i(TAG, "Adding ${size.width} x ${size.height} CodeScanner Output in Format #$imageFormat...")
val pipeline = CodeScannerPipeline(size, imageFormat, codeScanner.config, callback)
val output = BarcodeScannerOutput(pipeline)
outputs.add(output.toOutputConfiguration(characteristics))
codeScannerOutput = output
}
// Create new session
captureSession = cameraDevice.createCaptureSession(cameraManager, outputs, { session ->
if (this.captureSession == session) {
Log.i(TAG, "Camera Session $session has been closed!")
isRunning = false
}
}, CameraQueues.cameraQueue)
Log.i(TAG, "Successfully configured Session with ${outputs.size} outputs for Camera #${cameraDevice.id}!")
callback.onInitialized()
// Update Frame Processor and RecordingSession for newly changed output
updateVideoOutputs()
}
private fun configureCaptureRequest(config: CameraConfiguration) {
val device = cameraDevice ?: throw NoCameraDeviceError()
val captureSession = captureSession ?: throw CameraNotReadyError()
if (!config.isActive) {
// TODO: Do we want to do stopRepeating() or entirely destroy the session?
// If the Camera is not active, we don't do anything.
captureSession.stopRepeating()
return
}
val cameraCharacteristics = cameraManager.getCameraCharacteristics(device.id)
val template = if (config.video.isEnabled) CameraDevice.TEMPLATE_RECORD else CameraDevice.TEMPLATE_PREVIEW
val captureRequest = device.createCaptureRequest(template)
previewOutput?.let { output ->
captureRequest.addTarget(output.surface)
}
videoOutput?.let { output ->
captureRequest.addTarget(output.surface)
}
// Set FPS
// TODO: Check if the FPS range is actually supported in the current configuration.
val fps = config.fps
if (fps != null && CAN_SET_FPS) {
captureRequest.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, Range(fps, fps))
}
// Set Video Stabilization
when (config.videoStabilizationMode) {
VideoStabilizationMode.OFF -> {
// do nothing
}
VideoStabilizationMode.STANDARD -> {
// TODO: Check if that stabilization mode is even supported
val mode = if (Build.VERSION.SDK_INT >=
Build.VERSION_CODES.TIRAMISU
) {
CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION
} else {
CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_ON
}
captureRequest.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, mode)
}
VideoStabilizationMode.CINEMATIC, VideoStabilizationMode.CINEMATIC_EXTENDED -> {
// TODO: Check if that stabilization mode is even supported
captureRequest.set(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE, CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE_ON)
} }
} }
private fun updateVideoOutputs() { // Set HDR
val videoPipeline = outputs?.videoOutput?.videoPipeline ?: return // TODO: Check if that value is even supported
videoPipeline.setRecordingSessionOutput(this.recording) if (config.enableHdr) {
videoPipeline.setFrameProcessorOutput(this.frameProcessor) captureRequest.set(CaptureRequest.CONTROL_SCENE_MODE, CaptureRequest.CONTROL_SCENE_MODE_HDR)
} else if (config.enableLowLightBoost) {
captureRequest.set(CaptureRequest.CONTROL_SCENE_MODE, CaptureRequest.CONTROL_SCENE_MODE_NIGHT)
}
// Set Zoom
// TODO: Check if that zoom value is even supported
captureRequest.setZoom(config.zoom, cameraCharacteristics)
// Set Torch
// TODO: Check if torch is even supported
if (config.torch == Torch.ON) {
captureRequest.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH)
}
// Start repeating request if the Camera is active
val request = captureRequest.build()
captureSession.setRepeatingRequest(request, null, null)
} }
suspend fun takePhoto( suspend fun takePhoto(
@ -220,12 +441,12 @@ class CameraSession(
outputOrientation: Orientation outputOrientation: Orientation
): CapturedPhoto { ): CapturedPhoto {
val captureSession = captureSession ?: throw CameraNotReadyError() val captureSession = captureSession ?: throw CameraNotReadyError()
val outputs = outputs ?: throw CameraNotReadyError() val photoOutput = photoOutput ?: throw PhotoNotEnabledError()
val photoOutput = outputs.photoOutput ?: throw PhotoNotEnabledError()
Log.i(TAG, "Photo capture 0/3 - preparing capture request (${photoOutput.size.width}x${photoOutput.size.height})...") Log.i(TAG, "Photo capture 0/3 - preparing capture request (${photoOutput.size.width}x${photoOutput.size.height})...")
val zoom = configuration?.zoom ?: 1f
val cameraCharacteristics = cameraManager.getCameraCharacteristics(captureSession.device.id) val cameraCharacteristics = cameraManager.getCameraCharacteristics(captureSession.device.id)
val orientation = outputOrientation.toSensorRelativeOrientation(cameraCharacteristics) val orientation = outputOrientation.toSensorRelativeOrientation(cameraCharacteristics)
val captureRequest = captureSession.device.createPhotoCaptureRequest( val captureRequest = captureSession.device.createPhotoCaptureRequest(
@ -254,11 +475,18 @@ class CameraSession(
} }
} }
override fun onPhotoCaptured(image: Image) { private fun onPhotoCaptured(image: Image) {
Log.i(CameraView.TAG, "Photo captured! ${image.width} x ${image.height}") Log.i(TAG, "Photo captured! ${image.width} x ${image.height}")
photoOutputSynchronizer.set(image.timestamp, image) photoOutputSynchronizer.set(image.timestamp, image)
} }
private fun updateVideoOutputs() {
val videoOutput = videoOutput ?: return
Log.i(TAG, "Updating Video Outputs...")
videoOutput.videoPipeline.setFrameProcessorOutput(frameProcessor)
videoOutput.videoPipeline.setRecordingSessionOutput(recording)
}
suspend fun startRecording( suspend fun startRecording(
enableAudio: Boolean, enableAudio: Boolean,
codec: VideoCodec, codec: VideoCodec,
@ -269,8 +497,9 @@ class CameraSession(
) { ) {
mutex.withLock { mutex.withLock {
if (recording != null) throw RecordingInProgressError() if (recording != null) throw RecordingInProgressError()
val outputs = outputs ?: throw CameraNotReadyError() val videoOutput = videoOutput ?: throw VideoNotEnabledError()
val videoOutput = outputs.videoOutput ?: throw VideoNotEnabledError()
val fps = configuration?.fps ?: 30
val recording = val recording =
RecordingSession(context, videoOutput.size, enableAudio, fps, codec, orientation, fileType, bitRate, callback, onError) RecordingSession(context, videoOutput.size, enableAudio, fps, codec, orientation, fileType, bitRate, callback, onError)
@ -302,23 +531,9 @@ class CameraSession(
} }
} }
suspend fun setTorchMode(enableTorch: Boolean) {
if (this.enableTorch != enableTorch) {
this.enableTorch = enableTorch
updateRepeatingRequest()
}
}
suspend fun setZoom(zoom: Float) {
if (this.zoom != zoom) {
this.zoom = zoom
updateRepeatingRequest()
}
}
suspend fun focus(x: Int, y: Int) { suspend fun focus(x: Int, y: Int) {
val captureSession = captureSession ?: throw CameraNotReadyError() val captureSession = captureSession ?: throw CameraNotReadyError()
val previewOutput = outputs?.previewOutput ?: throw CameraNotReadyError() val previewOutput = previewOutput ?: throw CameraNotReadyError()
val characteristics = cameraManager.getCameraCharacteristics(captureSession.device.id) val characteristics = cameraManager.getCameraCharacteristics(captureSession.device.id)
val sensorSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)!! val sensorSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)!!
val previewSize = previewOutput.size val previewSize = previewOutput.size
@ -330,18 +545,9 @@ class CameraSession(
focus(point) focus(point)
} }
override fun onCameraAvailable(cameraId: String) {
super.onCameraAvailable(cameraId)
Log.i(TAG, "Camera became available: $cameraId")
}
override fun onCameraUnavailable(cameraId: String) {
super.onCameraUnavailable(cameraId)
Log.i(TAG, "Camera became un-available: $cameraId")
}
private suspend fun focus(point: Point) { private suspend fun focus(point: Point) {
mutex.withLock { mutex.withLock {
// TODO: Fix this method
val captureSession = captureSession ?: throw CameraNotReadyError() val captureSession = captureSession ?: throw CameraNotReadyError()
val request = previewRequest ?: throw CameraNotReadyError() val request = previewRequest ?: throw CameraNotReadyError()
@ -373,207 +579,21 @@ class CameraSession(
} }
} }
/** data class CapturedPhoto(
* Opens a [CameraDevice]. If there already is an open Camera for the given [cameraId], use that. val image: Image,
*/ val metadata: TotalCaptureResult,
private suspend fun getCameraDevice(cameraId: String, onClosed: (error: Throwable) -> Unit): CameraDevice { val orientation: Orientation,
val currentDevice = cameraDevice val isMirrored: Boolean,
if (currentDevice?.id == cameraId) { val format: Int
// We already opened that device ) : Closeable {
return currentDevice override fun close() {
} image.close()
// Close previous device
cameraDevice?.close()
cameraDevice = null
val device = cameraManager.openCamera(cameraId, { camera, reason ->
Log.d(TAG, "Camera Closed ($cameraDevice == $camera)")
if (cameraDevice == camera) {
// The current CameraDevice has been closed, handle that!
onClosed(reason)
cameraDevice = null
} else {
// A new CameraDevice has been opened, we don't care about this one anymore.
}
}, CameraQueues.cameraQueue)
// Cache device in memory
cameraDevice = device
return device
}
// Caches the result of outputs.hashCode() of the last getCaptureSession call
private var lastOutputsHashCode: Int? = null
private suspend fun getCaptureSession(cameraDevice: CameraDevice, outputs: CameraOutputs, onClosed: () -> Unit): CameraCaptureSession {
val currentSession = captureSession
if (currentSession?.device == cameraDevice && outputs.hashCode() == lastOutputsHashCode) {
// We already opened a CameraCaptureSession on this device
return currentSession
}
captureSession?.close()
captureSession = null
val session = cameraDevice.createCaptureSession(cameraManager, outputs, { session ->
Log.d(TAG, "Capture Session Closed ($captureSession == $session)")
if (captureSession == session) {
// The current CameraCaptureSession has been closed, handle that!
onClosed()
captureSession = null
} else {
// A new CameraCaptureSession has been opened, we don't care about this one anymore.
}
}, CameraQueues.cameraQueue)
// Cache session in memory
captureSession = session
lastOutputsHashCode = outputs.hashCode()
// New session initialized
onInitialized()
return session
}
private fun getPreviewCaptureRequest(
fps: Int? = null,
videoStabilizationMode: VideoStabilizationMode? = null,
lowLightBoost: Boolean? = null,
hdr: Boolean? = null,
torch: Boolean? = null
): CaptureRequest {
val captureRequest = previewRequest ?: throw CameraNotReadyError()
// FPS
val fpsRange = if (fps != null && CAN_SET_FPS) Range(fps, fps) else Range(30, 30)
captureRequest.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange)
// Video Stabilization
captureRequest.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, videoStabilizationMode?.toDigitalStabilizationMode())
captureRequest.set(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE, videoStabilizationMode?.toOpticalStabilizationMode())
// Night/HDR Mode
val sceneMode = if (hdr ==
true
) {
CaptureRequest.CONTROL_SCENE_MODE_HDR
} else if (lowLightBoost == true) CaptureRequest.CONTROL_SCENE_MODE_NIGHT else null
captureRequest.set(CaptureRequest.CONTROL_SCENE_MODE, sceneMode)
captureRequest.set(
CaptureRequest.CONTROL_MODE,
if (sceneMode != null) CaptureRequest.CONTROL_MODE_USE_SCENE_MODE else CaptureRequest.CONTROL_MODE_AUTO
)
// Zoom
val cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId!!)
captureRequest.setZoom(zoom, cameraCharacteristics)
// Torch Mode
val torchMode = if (torch == true) CaptureRequest.FLASH_MODE_TORCH else CaptureRequest.FLASH_MODE_OFF
captureRequest.set(CaptureRequest.FLASH_MODE, torchMode)
return captureRequest.build()
}
private fun destroy() {
Log.i(TAG, "Destroying session..")
captureSession?.stopRepeating()
captureSession?.close()
captureSession = null
cameraDevice?.close()
cameraDevice = null
outputs?.close()
outputs = null
isRunning = false
}
private suspend fun startRunning() {
isRunning = false
val cameraId = cameraId ?: return
if (!isActive) return
Log.i(TAG, "Starting Camera Session...")
try {
mutex.withLock {
val outputs = outputs
if (outputs == null || outputs.size == 0) {
Log.i(TAG, "CameraSession doesn't have any Outputs, canceling..")
destroy()
return@withLock
}
// 1. Open Camera Device
val camera = getCameraDevice(cameraId) { reason ->
isRunning = false
onError(reason)
}
// 2. Create capture session with outputs
val session = getCaptureSession(camera, outputs) {
isRunning = false
}
// 3. Create request template
val template = if (outputs.videoOutput != null) CameraDevice.TEMPLATE_RECORD else CameraDevice.TEMPLATE_PREVIEW
val captureRequest = camera.createCaptureRequest(template)
outputs.previewOutput?.let { output ->
Log.i(TAG, "Adding preview output surface ${output.outputType}..")
captureRequest.addTarget(output.surface)
}
outputs.videoOutput?.let { output ->
Log.i(TAG, "Adding video output surface ${output.outputType}..")
captureRequest.addTarget(output.surface)
}
outputs.codeScannerOutput?.let { output ->
Log.i(TAG, "Adding code scanner output surface ${output.outputType}")
captureRequest.addTarget(output.surface)
}
Log.i(TAG, "Camera Session initialized! Starting repeating request..")
isRunning = true
this.previewRequest = captureRequest
this.captureSession = session
this.cameraDevice = camera
}
updateRepeatingRequest()
} catch (e: IllegalStateException) {
Log.e(TAG, "Failed to start Camera Session, this session is already closed.", e)
} }
} }
private suspend fun updateRepeatingRequest() { interface CameraSessionCallback {
mutex.withLock { fun onError(error: Throwable)
val session = captureSession fun onInitialized()
if (session == null) { fun onCodeScanned(codes: List<Barcode>)
// Not yet ready. Start session first, then it will update repeating request.
startRunning()
return
}
val fps = fps
val videoStabilizationMode = videoStabilizationMode
val lowLightBoost = lowLightBoost
val hdr = hdr
val enableTorch = enableTorch
val repeatingRequest = getPreviewCaptureRequest(fps, videoStabilizationMode, lowLightBoost, hdr, enableTorch)
Log.d(TAG, "Setting Repeating Request..")
session.setRepeatingRequest(repeatingRequest, null, null)
}
}
private suspend fun stopRunning() {
Log.i(TAG, "Stopping Camera Session...")
try {
mutex.withLock {
destroy()
Log.i(TAG, "Camera Session stopped!")
}
} catch (e: IllegalStateException) {
Log.e(TAG, "Failed to stop Camera Session, this session is already closed.", e)
}
} }
} }

View File

@ -7,11 +7,15 @@ import com.google.mlkit.vision.barcode.BarcodeScanner
import com.google.mlkit.vision.barcode.BarcodeScannerOptions import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import com.google.mlkit.vision.barcode.BarcodeScanning import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.common.InputImage import com.google.mlkit.vision.common.InputImage
import com.mrousavy.camera.core.outputs.CameraOutputs
import com.mrousavy.camera.types.Orientation import com.mrousavy.camera.types.Orientation
import java.io.Closeable import java.io.Closeable
class CodeScannerPipeline(val size: Size, val format: Int, val output: CameraOutputs.CodeScannerOutput) : Closeable { class CodeScannerPipeline(
val size: Size,
val format: Int,
val configuration: CameraConfiguration.CodeScanner,
val callback: CameraSession.CameraSessionCallback
) : Closeable {
companion object { companion object {
// We want to have a buffer of 2 images, but we always only acquire one. // We want to have a buffer of 2 images, but we always only acquire one.
// That way the pipeline is free to stream one frame into the unused buffer, // That way the pipeline is free to stream one frame into the unused buffer,
@ -26,7 +30,7 @@ class CodeScannerPipeline(val size: Size, val format: Int, val output: CameraOut
get() = imageReader.surface get() = imageReader.surface
init { init {
val types = output.options.codeTypes.map { it.toBarcodeType() } val types = configuration.codeTypes.map { it.toBarcodeType() }
val barcodeScannerOptions = BarcodeScannerOptions.Builder() val barcodeScannerOptions = BarcodeScannerOptions.Builder()
.setBarcodeFormats(types[0], *types.toIntArray()) .setBarcodeFormats(types[0], *types.toIntArray())
.build() .build()
@ -52,13 +56,13 @@ class CodeScannerPipeline(val size: Size, val format: Int, val output: CameraOut
image.close() image.close()
isBusy = false isBusy = false
if (barcodes.isNotEmpty()) { if (barcodes.isNotEmpty()) {
output.onCodeScanned(barcodes) callback.onCodeScanned(barcodes)
} }
} }
.addOnFailureListener { error -> .addOnFailureListener { error ->
image.close() image.close()
isBusy = false isBusy = false
output.onError(error) callback.onError(error)
} }
}, CameraQueues.videoQueue.handler) }, CameraQueues.videoQueue.handler)
} }
@ -69,7 +73,7 @@ class CodeScannerPipeline(val size: Size, val format: Int, val output: CameraOut
} }
override fun toString(): String { override fun toString(): String {
val codeTypes = output.options.codeTypes.joinToString(", ") val codeTypes = configuration.codeTypes.joinToString(", ")
return "${size.width} x ${size.height} CodeScanner for [$codeTypes] ($format)" return "${size.width} x ${size.height} CodeScanner for [$codeTypes] ($format)"
} }
} }

View File

@ -2,87 +2,89 @@ package com.mrousavy.camera.core
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.hardware.camera2.CameraManager
import android.util.Log import android.util.Log
import android.util.Size import android.util.Size
import android.view.Surface import android.view.Gravity
import android.view.SurfaceHolder import android.view.SurfaceHolder
import android.view.SurfaceView import android.view.SurfaceView
import android.widget.FrameLayout
import com.mrousavy.camera.extensions.bigger
import com.mrousavy.camera.extensions.getMaximumPreviewSize
import com.mrousavy.camera.extensions.getPreviewTargetSize
import com.mrousavy.camera.extensions.smaller
import com.mrousavy.camera.types.CameraDeviceFormat
import com.mrousavy.camera.types.ResizeMode import com.mrousavy.camera.types.ResizeMode
import kotlin.math.roundToInt import kotlin.math.roundToInt
@SuppressLint("ViewConstructor") @SuppressLint("ViewConstructor")
class PreviewView( class PreviewView(context: Context, callback: SurfaceHolder.Callback) : SurfaceView(context) {
context: Context, var size: Size = getMaximumPreviewSize()
val targetSize: Size, set(value) {
private val resizeMode: ResizeMode, Log.i(TAG, "Resizing PreviewView to ${value.width} x ${value.height}...")
private val onSurfaceChanged: (surface: Surface?) -> Unit holder.setFixedSize(value.width, value.height)
) : SurfaceView(context) { requestLayout()
invalidate()
field = value
}
var resizeMode: ResizeMode = ResizeMode.COVER
set(value) {
if (value != field) {
requestLayout()
invalidate()
}
field = value
}
init { init {
Log.i(TAG, "Using Preview Size ${targetSize.width} x ${targetSize.height}.") Log.i(TAG, "Creating PreviewView...")
holder.setFixedSize(targetSize.width, targetSize.height) layoutParams = FrameLayout.LayoutParams(
holder.addCallback(object : SurfaceHolder.Callback { FrameLayout.LayoutParams.MATCH_PARENT,
override fun surfaceCreated(holder: SurfaceHolder) { FrameLayout.LayoutParams.MATCH_PARENT,
Log.i(TAG, "Surface created! ${holder.surface}") Gravity.CENTER
onSurfaceChanged(holder.surface) )
holder.addCallback(callback)
} }
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { fun resizeToInputCamera(cameraId: String, cameraManager: CameraManager, format: CameraDeviceFormat?) {
Log.i(TAG, "Surface resized! ${holder.surface} ($width x $height in format #$format)") val characteristics = cameraManager.getCameraCharacteristics(cameraId)
val targetPreviewSize = format?.videoSize
val formatAspectRatio = if (targetPreviewSize != null) targetPreviewSize.bigger.toDouble() / targetPreviewSize.smaller else null
size = characteristics.getPreviewTargetSize(formatAspectRatio)
} }
override fun surfaceDestroyed(holder: SurfaceHolder) { private fun getSize(contentSize: Size, containerSize: Size, resizeMode: ResizeMode): Size {
Log.i(TAG, "Surface destroyed! ${holder.surface}")
onSurfaceChanged(null)
}
})
}
private fun coverSize(contentSize: Size, containerWidth: Int, containerHeight: Int): Size {
val contentAspectRatio = contentSize.height.toDouble() / contentSize.width val contentAspectRatio = contentSize.height.toDouble() / contentSize.width
val containerAspectRatio = containerWidth.toDouble() / containerHeight val containerAspectRatio = containerSize.width.toDouble() / containerSize.height
Log.d(TAG, "coverSize :: $contentSize ($contentAspectRatio), ${containerWidth}x$containerHeight ($containerAspectRatio)") Log.d(TAG, "coverSize :: $contentSize ($contentAspectRatio), ${containerSize.width}x${containerSize.height} ($containerAspectRatio)")
return if (contentAspectRatio > containerAspectRatio) { val widthOverHeight = when (resizeMode) {
ResizeMode.COVER -> contentAspectRatio > containerAspectRatio
ResizeMode.CONTAIN -> contentAspectRatio < containerAspectRatio
}
return if (widthOverHeight) {
// Scale by width to cover height // Scale by width to cover height
val scaledWidth = containerHeight * contentAspectRatio val scaledWidth = containerSize.height * contentAspectRatio
Size(scaledWidth.roundToInt(), containerHeight) Size(scaledWidth.roundToInt(), containerSize.height)
} else { } else {
// Scale by height to cover width // Scale by height to cover width
val scaledHeight = containerWidth / contentAspectRatio val scaledHeight = containerSize.width / contentAspectRatio
Size(containerWidth, scaledHeight.roundToInt()) Size(containerSize.width, scaledHeight.roundToInt())
}
}
private fun containSize(contentSize: Size, containerWidth: Int, containerHeight: Int): Size {
val contentAspectRatio = contentSize.height.toDouble() / contentSize.width
val containerAspectRatio = containerWidth.toDouble() / containerHeight
Log.d(TAG, "containSize :: $contentSize ($contentAspectRatio), ${containerWidth}x$containerHeight ($containerAspectRatio)")
return if (contentAspectRatio > containerAspectRatio) {
// Scale by height to fit within width
val scaledHeight = containerWidth / contentAspectRatio
return Size(containerWidth, scaledHeight.roundToInt())
} else {
// Scale by width to fit within height
val scaledWidth = containerHeight * contentAspectRatio
return Size(scaledWidth.roundToInt(), containerHeight)
} }
} }
@SuppressLint("DrawAllocation")
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec) super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val viewWidth = MeasureSpec.getSize(widthMeasureSpec) val viewWidth = MeasureSpec.getSize(widthMeasureSpec)
val viewHeight = MeasureSpec.getSize(heightMeasureSpec) val viewHeight = MeasureSpec.getSize(heightMeasureSpec)
Log.d(TAG, "onMeasure($viewWidth, $viewHeight)") Log.i(TAG, "PreviewView onMeasure($viewWidth, $viewHeight)")
val fittedSize = when (resizeMode) { val fittedSize = getSize(size, Size(viewWidth, viewHeight), resizeMode)
ResizeMode.COVER -> this.coverSize(targetSize, viewWidth, viewHeight)
ResizeMode.CONTAIN -> this.containSize(targetSize, viewWidth, viewHeight)
}
Log.d(TAG, "Fitted dimensions set: $fittedSize") Log.d(TAG, "Fitted dimensions set: $fittedSize")
setMeasuredDimension(fittedSize.width, fittedSize.height) setMeasuredDimension(fittedSize.width, fittedSize.height)

View File

@ -105,9 +105,7 @@ class VideoPipeline(
Log.i(TAG, "ImageReader::onImageAvailable!") Log.i(TAG, "ImageReader::onImageAvailable!")
val image = reader.acquireNextImage() ?: return@setOnImageAvailableListener val image = reader.acquireNextImage() ?: return@setOnImageAvailableListener
Log.i(TAG, "Image Format: ${image.format}") // TODO: Get correct orientation and isMirrored
// // TODO: Get correct orientation and isMirrored
val frame = Frame(image, image.timestamp, Orientation.PORTRAIT, isMirrored) val frame = Frame(image, image.timestamp, Orientation.PORTRAIT, isMirrored)
frame.incrementRefCount() frame.incrementRefCount()
frameProcessor?.call(frame) frameProcessor?.call(frame)
@ -177,7 +175,11 @@ class VideoPipeline(
*/ */
fun setFrameProcessorOutput(frameProcessor: FrameProcessor?) { fun setFrameProcessorOutput(frameProcessor: FrameProcessor?) {
synchronized(this) { synchronized(this) {
if (frameProcessor != null) {
Log.i(TAG, "Setting $width x $height FrameProcessor Output...") Log.i(TAG, "Setting $width x $height FrameProcessor Output...")
} else {
Log.i(TAG, "Removing FrameProcessor Output...")
}
this.frameProcessor = frameProcessor this.frameProcessor = frameProcessor
} }
} }
@ -187,13 +189,14 @@ class VideoPipeline(
*/ */
fun setRecordingSessionOutput(recordingSession: RecordingSession?) { fun setRecordingSessionOutput(recordingSession: RecordingSession?) {
synchronized(this) { synchronized(this) {
Log.i(TAG, "Setting $width x $height RecordingSession Output...")
if (recordingSession != null) { if (recordingSession != null) {
// Configure OpenGL pipeline to stream Frames into the Recording Session's surface // Configure OpenGL pipeline to stream Frames into the Recording Session's surface
Log.i(TAG, "Setting $width x $height RecordingSession Output...")
setRecordingSessionOutputSurface(recordingSession.surface) setRecordingSessionOutputSurface(recordingSession.surface)
this.recordingSession = recordingSession this.recordingSession = recordingSession
} else { } else {
// Configure OpenGL pipeline to stop streaming Frames into the Recording Session's surface // Configure OpenGL pipeline to stop streaming Frames into the Recording Session's surface
Log.i(TAG, "Removing RecordingSession Output...")
removeRecordingSessionOutputSurface() removeRecordingSessionOutputSurface()
this.recordingSession = null this.recordingSession = null
} }

View File

@ -1,177 +0,0 @@
package com.mrousavy.camera.core.outputs
import android.graphics.ImageFormat
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager
import android.media.Image
import android.media.ImageReader
import android.util.Log
import android.util.Size
import android.view.Surface
import com.google.mlkit.vision.barcode.common.Barcode
import com.mrousavy.camera.core.CameraQueues
import com.mrousavy.camera.core.CodeScannerPipeline
import com.mrousavy.camera.core.VideoPipeline
import com.mrousavy.camera.extensions.bigger
import com.mrousavy.camera.extensions.closestToOrMax
import com.mrousavy.camera.extensions.getPhotoSizes
import com.mrousavy.camera.extensions.getPreviewTargetSize
import com.mrousavy.camera.extensions.getVideoSizes
import com.mrousavy.camera.extensions.smaller
import com.mrousavy.camera.types.CodeScannerOptions
import com.mrousavy.camera.types.PixelFormat
import java.io.Closeable
class CameraOutputs(
val cameraId: String,
cameraManager: CameraManager,
val preview: PreviewOutput? = null,
val photo: PhotoOutput? = null,
val video: VideoOutput? = null,
val codeScanner: CodeScannerOutput? = null,
val enableHdr: Boolean? = false,
val callback: Callback
) : Closeable {
companion object {
private const val TAG = "CameraOutputs"
const val PHOTO_OUTPUT_BUFFER_SIZE = 3
}
data class PreviewOutput(val surface: Surface, val targetSize: Size? = null)
data class PhotoOutput(val targetSize: Size? = null, val format: Int = ImageFormat.JPEG)
data class VideoOutput(
val targetSize: Size? = null,
val enableRecording: Boolean = false,
val enableFrameProcessor: Boolean? = false,
val format: PixelFormat = PixelFormat.NATIVE
)
data class CodeScannerOutput(
val options: CodeScannerOptions,
val onCodeScanned: (codes: List<Barcode>) -> Unit,
val onError: (error: Throwable) -> Unit
)
interface Callback {
fun onPhotoCaptured(image: Image)
}
var previewOutput: SurfaceOutput? = null
private set
var photoOutput: ImageReaderOutput? = null
private set
var videoOutput: VideoPipelineOutput? = null
private set
var codeScannerOutput: BarcodeScannerOutput? = null
private set
val size: Int
get() {
var size = 0
if (previewOutput != null) size++
if (photoOutput != null) size++
if (videoOutput != null) size++
if (codeScannerOutput != null) size++
return size
}
override fun equals(other: Any?): Boolean {
if (other !is CameraOutputs) return false
return this.cameraId == other.cameraId &&
this.preview?.surface == other.preview?.surface &&
this.preview?.targetSize == other.preview?.targetSize &&
this.photo?.targetSize == other.photo?.targetSize &&
this.photo?.format == other.photo?.format &&
this.video?.enableRecording == other.video?.enableRecording &&
this.video?.targetSize == other.video?.targetSize &&
this.video?.format == other.video?.format &&
this.codeScanner?.options == other.codeScanner?.options &&
this.enableHdr == other.enableHdr
}
override fun hashCode(): Int {
var result = cameraId.hashCode()
result += (preview?.hashCode() ?: 0)
result += (photo?.hashCode() ?: 0)
result += (video?.hashCode() ?: 0)
result += (codeScanner?.hashCode() ?: 0)
return result
}
override fun close() {
previewOutput?.close()
photoOutput?.close()
videoOutput?.close()
codeScannerOutput?.close()
}
override fun toString(): String {
val strings = arrayListOf<String>()
previewOutput?.let { strings.add(it.toString()) }
photoOutput?.let { strings.add(it.toString()) }
videoOutput?.let { strings.add(it.toString()) }
codeScannerOutput?.let { strings.add(it.toString()) }
return strings.joinToString(", ", "[", "]")
}
init {
val characteristics = cameraManager.getCameraCharacteristics(cameraId)
val isMirrored = characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT
Log.i(TAG, "Preparing Outputs for Camera $cameraId...")
// Preview output: Low resolution repeating images (SurfaceView)
if (preview != null) {
Log.i(TAG, "Adding native preview view output.")
val previewSizeAspectRatio = if (preview.targetSize !=
null
) {
preview.targetSize.bigger.toDouble() / preview.targetSize.smaller
} else {
null
}
previewOutput = SurfaceOutput(
preview.surface,
characteristics.getPreviewTargetSize(previewSizeAspectRatio),
SurfaceOutput.OutputType.PREVIEW
)
}
// Photo output: High quality still images (takePhoto())
if (photo != null) {
val size = characteristics.getPhotoSizes(photo.format).closestToOrMax(photo.targetSize)
val imageReader = ImageReader.newInstance(size.width, size.height, photo.format, PHOTO_OUTPUT_BUFFER_SIZE)
imageReader.setOnImageAvailableListener({ reader ->
val image = reader.acquireLatestImage() ?: return@setOnImageAvailableListener
callback.onPhotoCaptured(image)
}, CameraQueues.cameraQueue.handler)
Log.i(TAG, "Adding ${size.width}x${size.height} photo output. (Format: ${photo.format})")
photoOutput = ImageReaderOutput(imageReader, SurfaceOutput.OutputType.PHOTO)
}
// Video output: High resolution repeating images (startRecording() or useFrameProcessor())
if (video != null) {
val format = video.format.toImageFormat()
val size = characteristics.getVideoSizes(cameraId, format).closestToOrMax(video.targetSize)
val enableFrameProcessor = video.enableFrameProcessor ?: false
val videoPipeline = VideoPipeline(size.width, size.height, video.format, isMirrored, enableFrameProcessor)
Log.i(TAG, "Adding ${size.width}x${size.height} video output. (Format: ${video.format})")
videoOutput = VideoPipelineOutput(videoPipeline, SurfaceOutput.OutputType.VIDEO)
}
// Code Scanner
if (codeScanner != null) {
val format = ImageFormat.YUV_420_888
val targetSize = Size(1280, 720)
val size = characteristics.getVideoSizes(cameraId, format).closestToOrMax(targetSize)
val pipeline = CodeScannerPipeline(size, format, codeScanner)
Log.i(TAG, "Adding ${size.width}x${size.height} code scanner output. (Code Types: ${codeScanner.options.codeTypes})")
codeScannerOutput = BarcodeScannerOutput(pipeline)
}
Log.i(TAG, "Prepared $size Outputs for Camera $cameraId!")
}
}

View File

@ -5,12 +5,12 @@ import android.util.Log
import android.util.Size import android.util.Size
import java.io.Closeable import java.io.Closeable
open class ImageReaderOutput(private val imageReader: ImageReader, outputType: OutputType, dynamicRangeProfile: Long? = null) : open class PhotoOutput(private val imageReader: ImageReader, enableHdr: Boolean = false) :
SurfaceOutput( SurfaceOutput(
imageReader.surface, imageReader.surface,
Size(imageReader.width, imageReader.height), Size(imageReader.width, imageReader.height),
outputType, OutputType.PHOTO,
dynamicRangeProfile enableHdr
), ),
Closeable { Closeable {
override fun close() { override fun close() {

View File

@ -14,7 +14,7 @@ open class SurfaceOutput(
val surface: Surface, val surface: Surface,
val size: Size, val size: Size,
val outputType: OutputType, val outputType: OutputType,
private val dynamicRangeProfile: Long? = null, private val enableHdr: Boolean = false,
private val closeSurfaceOnEnd: Boolean = false private val closeSurfaceOnEnd: Boolean = false
) : Closeable { ) : Closeable {
companion object { companion object {
@ -37,10 +37,13 @@ open class SurfaceOutput(
fun toOutputConfiguration(characteristics: CameraCharacteristics): OutputConfiguration { fun toOutputConfiguration(characteristics: CameraCharacteristics): OutputConfiguration {
val result = OutputConfiguration(surface) val result = OutputConfiguration(surface)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (dynamicRangeProfile != null) { if (enableHdr) {
result.dynamicRangeProfile = dynamicRangeProfile val profile = characteristics.get(CameraCharacteristics.REQUEST_RECOMMENDED_TEN_BIT_DYNAMIC_RANGE_PROFILE)
if (profile != null) {
result.dynamicRangeProfile = profile
Log.i(TAG, "Using dynamic range profile ${result.dynamicRangeProfile} for $outputType output.") Log.i(TAG, "Using dynamic range profile ${result.dynamicRangeProfile} for $outputType output.")
} }
}
if (supportsOutputType(characteristics, outputType)) { if (supportsOutputType(characteristics, outputType)) {
result.streamUseCase = outputType.toOutputType().toLong() result.streamUseCase = outputType.toOutputType().toLong()
Log.i(TAG, "Using optimized stream use case ${result.streamUseCase} for $outputType output.") Log.i(TAG, "Using optimized stream use case ${result.streamUseCase} for $outputType output.")

View File

@ -5,12 +5,12 @@ import android.util.Size
import com.mrousavy.camera.core.VideoPipeline import com.mrousavy.camera.core.VideoPipeline
import java.io.Closeable import java.io.Closeable
class VideoPipelineOutput(val videoPipeline: VideoPipeline, outputType: OutputType, dynamicRangeProfile: Long? = null) : class VideoPipelineOutput(val videoPipeline: VideoPipeline, enableHdr: Boolean = false) :
SurfaceOutput( SurfaceOutput(
videoPipeline.surface, videoPipeline.surface,
Size(videoPipeline.width, videoPipeline.height), Size(videoPipeline.width, videoPipeline.height),
outputType, OutputType.VIDEO,
dynamicRangeProfile enableHdr
), ),
Closeable { Closeable {
override fun close() { override fun close() {

View File

@ -6,7 +6,7 @@ import android.util.Size
import android.view.SurfaceHolder import android.view.SurfaceHolder
import kotlin.math.abs import kotlin.math.abs
private fun getMaximumPreviewSize(): Size { fun getMaximumPreviewSize(): Size {
// See https://developer.android.com/reference/android/hardware/camera2/params/StreamConfigurationMap // See https://developer.android.com/reference/android/hardware/camera2/params/StreamConfigurationMap
// According to the Android Developer documentation, PREVIEW streams can have a resolution // According to the Android Developer documentation, PREVIEW streams can have a resolution
// of up to the phone's display's resolution, with a maximum of 1920x1080. // of up to the phone's display's resolution, with a maximum of 1920x1080.

View File

@ -10,7 +10,6 @@ import android.os.Build
import android.util.Log import android.util.Log
import com.mrousavy.camera.core.CameraQueues import com.mrousavy.camera.core.CameraQueues
import com.mrousavy.camera.core.CameraSessionCannotBeConfiguredError import com.mrousavy.camera.core.CameraSessionCannotBeConfiguredError
import com.mrousavy.camera.core.outputs.CameraOutputs
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
@ -20,7 +19,7 @@ private var sessionId = 1000
suspend fun CameraDevice.createCaptureSession( suspend fun CameraDevice.createCaptureSession(
cameraManager: CameraManager, cameraManager: CameraManager,
outputs: CameraOutputs, outputs: List<OutputConfiguration>,
onClosed: (session: CameraCaptureSession) -> Unit, onClosed: (session: CameraCaptureSession) -> Unit,
queue: CameraQueues.CameraQueue queue: CameraQueues.CameraQueue
): CameraCaptureSession = ): CameraCaptureSession =
@ -42,7 +41,7 @@ suspend fun CameraDevice.createCaptureSession(
override fun onConfigureFailed(session: CameraCaptureSession) { override fun onConfigureFailed(session: CameraCaptureSession) {
Log.e(TAG, "Camera $id: Failed to configure Capture Session #$sessionId!") Log.e(TAG, "Camera $id: Failed to configure Capture Session #$sessionId!")
continuation.resumeWithException(CameraSessionCannotBeConfiguredError(id, outputs)) continuation.resumeWithException(CameraSessionCannotBeConfiguredError(id))
} }
override fun onClosed(session: CameraCaptureSession) { override fun onClosed(session: CameraCaptureSession) {
@ -52,36 +51,12 @@ suspend fun CameraDevice.createCaptureSession(
} }
} }
val outputConfigurations = arrayListOf<OutputConfiguration>()
outputs.previewOutput?.let { output ->
outputConfigurations.add(output.toOutputConfiguration(characteristics))
}
outputs.photoOutput?.let { output ->
outputConfigurations.add(output.toOutputConfiguration(characteristics))
}
outputs.videoOutput?.let { output ->
outputConfigurations.add(output.toOutputConfiguration(characteristics))
}
outputs.codeScannerOutput?.let { output ->
outputConfigurations.add(output.toOutputConfiguration(characteristics))
}
if (outputs.enableHdr == true && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val supportedProfiles = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES)
val hdrProfile = supportedProfiles?.bestProfile ?: supportedProfiles?.supportedProfiles?.firstOrNull()
if (hdrProfile != null) {
Log.i(TAG, "Camera $id: Using HDR Profile $hdrProfile...")
outputConfigurations.forEach { it.dynamicRangeProfile = hdrProfile }
} else {
Log.w(TAG, "Camera $id: HDR was enabled, but the device does not support any matching HDR profile!")
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
Log.i(TAG, "Using new API (>=28)") Log.i(TAG, "Using new API (>=28)")
val config = SessionConfiguration(SessionConfiguration.SESSION_REGULAR, outputConfigurations, queue.executor, callback) val config = SessionConfiguration(SessionConfiguration.SESSION_REGULAR, outputs, queue.executor, callback)
this.createCaptureSession(config) this.createCaptureSession(config)
} else { } else {
Log.i(TAG, "Using legacy API (<28)") Log.i(TAG, "Using legacy API (<28)")
this.createCaptureSessionByOutputConfigurations(outputConfigurations, callback, queue.handler) this.createCaptureSessionByOutputConfigurations(outputs, callback, queue.handler)
} }
} }

View File

@ -0,0 +1,14 @@
package com.mrousavy.camera.types
enum class AutoFocusSystem(override val unionValue: String) : JSUnionValue {
CONTRAST_DETECTION("contrast-detection"),
NONE("none");
companion object : JSUnionValue.Companion<AutoFocusSystem> {
override fun fromUnionValue(unionValue: String?): AutoFocusSystem =
when (unionValue) {
"contrast-detection" -> CONTRAST_DETECTION
else -> NONE
}
}
}

View File

@ -0,0 +1,106 @@
package com.mrousavy.camera.types
import android.util.Size
import com.facebook.react.bridge.ReadableMap
import com.mrousavy.camera.core.InvalidTypeScriptUnionError
data class CameraDeviceFormat(
val videoWidth: Int,
val videoHeight: Int,
val photoWidth: Int,
val photoHeight: Int,
val minFps: Double,
val maxFps: Double,
val minISO: Double,
val maxISO: Double,
val fieldOfView: Double,
val maxZoom: Double,
val videoStabilizationModes: Array<VideoStabilizationMode>,
val autoFocusSystem: AutoFocusSystem,
val supportsVideoHDR: Boolean,
val supportsPhotoHDR: Boolean,
val pixelFormats: Array<PixelFormat>,
val supportsDepthCapture: Boolean
) {
val photoSize: Size
get() = Size(photoWidth, photoHeight)
val videoSize: Size
get() = Size(videoWidth, videoHeight)
companion object {
fun fromJSValue(value: ReadableMap): CameraDeviceFormat {
val modes = value.getArray("videoStabilizationModes") ?: throw InvalidTypeScriptUnionError("format", value.toString())
val videoStabilizationModes = modes.toArrayList().map { VideoStabilizationMode.fromUnionValue(it as String) }
val formats = value.getArray("pixelFormats") ?: throw InvalidTypeScriptUnionError("format", value.toString())
val pixelFormats = formats.toArrayList().map { PixelFormat.fromUnionValue(it as String) }
val autoFocusSystem = AutoFocusSystem.fromUnionValue(value.getString("autoFocusSystem"))
return CameraDeviceFormat(
value.getInt("videoWidth"),
value.getInt("videoHeight"),
value.getInt("photoWidth"),
value.getInt("photoHeight"),
value.getDouble("minFps"),
value.getDouble("maxFps"),
value.getDouble("minISO"),
value.getDouble("maxISO"),
value.getDouble("fieldOfView"),
value.getDouble("maxZoom"),
videoStabilizationModes.toTypedArray(),
autoFocusSystem,
value.getBoolean("supportsVideoHDR"),
value.getBoolean("supportsPhotoHDR"),
pixelFormats.toTypedArray(),
value.getBoolean("supportsDepthCapture")
)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as CameraDeviceFormat
if (videoWidth != other.videoWidth) return false
if (videoHeight != other.videoHeight) return false
if (photoWidth != other.photoWidth) return false
if (photoHeight != other.photoHeight) return false
if (minFps != other.minFps) return false
if (maxFps != other.maxFps) return false
if (minISO != other.minISO) return false
if (maxISO != other.maxISO) return false
if (fieldOfView != other.fieldOfView) return false
if (maxZoom != other.maxZoom) return false
if (!videoStabilizationModes.contentEquals(other.videoStabilizationModes)) return false
if (autoFocusSystem != other.autoFocusSystem) return false
if (supportsVideoHDR != other.supportsVideoHDR) return false
if (supportsPhotoHDR != other.supportsPhotoHDR) return false
if (!pixelFormats.contentEquals(other.pixelFormats)) return false
if (supportsDepthCapture != other.supportsDepthCapture) return false
return true
}
override fun hashCode(): Int {
var result = videoWidth
result = 31 * result + videoHeight
result = 31 * result + photoWidth
result = 31 * result + photoHeight
result = 31 * result + minFps.hashCode()
result = 31 * result + maxFps.hashCode()
result = 31 * result + minISO.hashCode()
result = 31 * result + maxISO.hashCode()
result = 31 * result + fieldOfView.hashCode()
result = 31 * result + maxZoom.hashCode()
result = 31 * result + videoStabilizationModes.contentHashCode()
result = 31 * result + autoFocusSystem.hashCode()
result = 31 * result + supportsVideoHDR.hashCode()
result = 31 * result + supportsPhotoHDR.hashCode()
result = 31 * result + pixelFormats.contentHashCode()
result = 31 * result + supportsDepthCapture.hashCode()
return result
}
}

View File

@ -33,13 +33,13 @@ enum class Orientation(override val unionValue: String) : JSUnionValue {
} }
companion object : JSUnionValue.Companion<Orientation> { companion object : JSUnionValue.Companion<Orientation> {
override fun fromUnionValue(unionValue: String?): Orientation? = override fun fromUnionValue(unionValue: String?): Orientation =
when (unionValue) { when (unionValue) {
"portrait" -> PORTRAIT "portrait" -> PORTRAIT
"landscape-right" -> LANDSCAPE_RIGHT "landscape-right" -> LANDSCAPE_RIGHT
"portrait-upside-down" -> PORTRAIT_UPSIDE_DOWN "portrait-upside-down" -> PORTRAIT_UPSIDE_DOWN
"landscape-left" -> LANDSCAPE_LEFT "landscape-left" -> LANDSCAPE_LEFT
else -> null else -> PORTRAIT
} }
fun fromRotationDegrees(rotationDegrees: Int): Orientation = fun fromRotationDegrees(rotationDegrees: Int): Orientation =

View File

@ -1,6 +1,7 @@
package com.mrousavy.camera.types package com.mrousavy.camera.types
import android.graphics.ImageFormat import android.graphics.ImageFormat
import com.mrousavy.camera.core.InvalidTypeScriptUnionError
import com.mrousavy.camera.core.PixelFormatNotSupportedError import com.mrousavy.camera.core.PixelFormatNotSupportedError
enum class PixelFormat(override val unionValue: String) : JSUnionValue { enum class PixelFormat(override val unionValue: String) : JSUnionValue {
@ -24,13 +25,13 @@ enum class PixelFormat(override val unionValue: String) : JSUnionValue {
else -> UNKNOWN else -> UNKNOWN
} }
override fun fromUnionValue(unionValue: String?): PixelFormat? = override fun fromUnionValue(unionValue: String?): PixelFormat =
when (unionValue) { when (unionValue) {
"yuv" -> YUV "yuv" -> YUV
"rgb" -> RGB "rgb" -> RGB
"native" -> NATIVE "native" -> NATIVE
"unknown" -> UNKNOWN "unknown" -> UNKNOWN
else -> null else -> throw InvalidTypeScriptUnionError("pixelFormat", unionValue)
} }
} }
} }

View File

@ -5,6 +5,7 @@ import android.hardware.camera2.CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_
import android.hardware.camera2.CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION import android.hardware.camera2.CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION
import android.hardware.camera2.CameraMetadata.LENS_OPTICAL_STABILIZATION_MODE_OFF import android.hardware.camera2.CameraMetadata.LENS_OPTICAL_STABILIZATION_MODE_OFF
import android.hardware.camera2.CameraMetadata.LENS_OPTICAL_STABILIZATION_MODE_ON import android.hardware.camera2.CameraMetadata.LENS_OPTICAL_STABILIZATION_MODE_ON
import com.mrousavy.camera.core.InvalidTypeScriptUnionError
enum class VideoStabilizationMode(override val unionValue: String) : JSUnionValue { enum class VideoStabilizationMode(override val unionValue: String) : JSUnionValue {
OFF("off"), OFF("off"),
@ -28,13 +29,13 @@ enum class VideoStabilizationMode(override val unionValue: String) : JSUnionValu
} }
companion object : JSUnionValue.Companion<VideoStabilizationMode> { companion object : JSUnionValue.Companion<VideoStabilizationMode> {
override fun fromUnionValue(unionValue: String?): VideoStabilizationMode? = override fun fromUnionValue(unionValue: String?): VideoStabilizationMode =
when (unionValue) { when (unionValue) {
"off" -> OFF "off" -> OFF
"standard" -> STANDARD "standard" -> STANDARD
"cinematic" -> CINEMATIC "cinematic" -> CINEMATIC
"cinematic-extended" -> CINEMATIC_EXTENDED "cinematic-extended" -> CINEMATIC_EXTENDED
else -> null else -> throw InvalidTypeScriptUnionError("videoStabilizationMode", unionValue)
} }
fun fromDigitalVideoStabilizationMode(stabiliazionMode: Int): VideoStabilizationMode = fun fromDigitalVideoStabilizationMode(stabiliazionMode: Int): VideoStabilizationMode =

View File

@ -507,7 +507,7 @@ PODS:
- libwebp (~> 1.0) - libwebp (~> 1.0)
- SDWebImage/Core (~> 5.10) - SDWebImage/Core (~> 5.10)
- SocketRocket (0.6.1) - SocketRocket (0.6.1)
- VisionCamera (3.4.0): - VisionCamera (3.5.1):
- React - React
- React-callinvoker - React-callinvoker
- React-Core - React-Core
@ -747,7 +747,7 @@ SPEC CHECKSUMS:
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
VisionCamera: eead9df29ac5935d5685b5ecaea3ae8b6da84bff VisionCamera: 2f5f9841150719f73b9efb9e0c8fa48fe8556f2d
Yoga: 8796b55dba14d7004f980b54bcc9833ee45b28ce Yoga: 8796b55dba14d7004f980b54bcc9833ee45b28ce
PODFILE CHECKSUM: 27f53791141a3303d814e09b55770336416ff4eb PODFILE CHECKSUM: 27f53791141a3303d814e09b55770336416ff4eb