282 lines
8.0 KiB
Kotlin
282 lines
8.0 KiB
Kotlin
package com.mrousavy.camera
|
|
|
|
import android.annotation.SuppressLint
|
|
import android.content.Context
|
|
import android.hardware.camera2.CameraManager
|
|
import android.util.Log
|
|
import android.view.Gravity
|
|
import android.view.ScaleGestureDetector
|
|
import android.widget.FrameLayout
|
|
import com.google.mlkit.vision.barcode.common.Barcode
|
|
import com.mrousavy.camera.core.CameraConfiguration
|
|
import com.mrousavy.camera.core.CameraQueues
|
|
import com.mrousavy.camera.core.CameraSession
|
|
import com.mrousavy.camera.core.CodeScannerFrame
|
|
import com.mrousavy.camera.core.PreviewView
|
|
import com.mrousavy.camera.extensions.installHierarchyFitter
|
|
import com.mrousavy.camera.frameprocessor.Frame
|
|
import com.mrousavy.camera.frameprocessor.FrameProcessor
|
|
import com.mrousavy.camera.types.CameraDeviceFormat
|
|
import com.mrousavy.camera.types.CodeScannerOptions
|
|
import com.mrousavy.camera.types.Orientation
|
|
import com.mrousavy.camera.types.PixelFormat
|
|
import com.mrousavy.camera.types.ResizeMode
|
|
import com.mrousavy.camera.types.Torch
|
|
import com.mrousavy.camera.types.VideoStabilizationMode
|
|
import kotlinx.coroutines.CoroutineScope
|
|
import kotlinx.coroutines.launch
|
|
import java.io.File
|
|
|
|
//
|
|
// TODOs for the CameraView which are currently too hard to implement either because of CameraX' limitations, or my brain capacity.
|
|
//
|
|
// TODO: High-speed video recordings (export in CameraViewModule::getAvailableVideoDevices(), and set in CameraView::configurePreview()) (120FPS+)
|
|
// TODO: Better startRecording()/stopRecording() (promise + callback, wait for TurboModules/JSI)
|
|
// TODO: takePhoto() depth data
|
|
// TODO: takePhoto() raw capture
|
|
// TODO: takePhoto() return with jsi::Value Image reference for faster capture
|
|
|
|
@SuppressLint("ClickableViewAccessibility", "ViewConstructor", "MissingPermission")
|
|
class CameraView(context: Context) :
|
|
FrameLayout(context),
|
|
CameraSession.Callback {
|
|
companion object {
|
|
const val TAG = "CameraView"
|
|
}
|
|
|
|
// react properties
|
|
// props that require reconfiguring
|
|
var cameraId: String? = null
|
|
var enableDepthData = false
|
|
var enablePortraitEffectsMatteDelivery = false
|
|
|
|
// use-cases
|
|
var photo = false
|
|
var video = false
|
|
var audio = false
|
|
var enableFrameProcessor = false
|
|
var pixelFormat: PixelFormat = PixelFormat.NATIVE
|
|
|
|
// props that require format reconfiguring
|
|
var format: CameraDeviceFormat? = null
|
|
var fps: Int? = null
|
|
var videoStabilizationMode: VideoStabilizationMode? = null
|
|
var videoHdr = false
|
|
var photoHdr = false
|
|
var lowLightBoost = false
|
|
var enableGpuBuffers = false
|
|
|
|
// other props
|
|
var isActive = false
|
|
var torch: Torch = Torch.OFF
|
|
var zoom: Float = 1f // in "factor"
|
|
var exposure: Double = 1.0
|
|
var orientation: Orientation = Orientation.PORTRAIT
|
|
set(value) {
|
|
field = value
|
|
previewView.orientation = value
|
|
}
|
|
var enableZoomGesture = false
|
|
set(value) {
|
|
field = value
|
|
updateZoomGesture()
|
|
}
|
|
var resizeMode: ResizeMode = ResizeMode.COVER
|
|
set(value) {
|
|
previewView.resizeMode = value
|
|
field = value
|
|
}
|
|
var enableFpsGraph = false
|
|
set(value) {
|
|
field = value
|
|
updateFpsGraph()
|
|
}
|
|
|
|
// code scanner
|
|
var codeScannerOptions: CodeScannerOptions? = null
|
|
|
|
// private properties
|
|
private var isMounted = false
|
|
private val coroutineScope = CoroutineScope(CameraQueues.cameraQueue.coroutineDispatcher)
|
|
internal val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
|
|
|
|
// session
|
|
internal val cameraSession: CameraSession
|
|
val previewView: PreviewView
|
|
private var currentConfigureCall: Long = System.currentTimeMillis()
|
|
internal var frameProcessor: FrameProcessor? = null
|
|
|
|
// other
|
|
private var fpsGraph: FpsGraphView? = null
|
|
|
|
init {
|
|
this.installHierarchyFitter()
|
|
clipToOutline = true
|
|
cameraSession = CameraSession(context, cameraManager, this)
|
|
previewView = cameraSession.createPreviewView(context)
|
|
previewView.layoutParams = LayoutParams(
|
|
LayoutParams.MATCH_PARENT,
|
|
LayoutParams.MATCH_PARENT,
|
|
Gravity.CENTER
|
|
)
|
|
addView(previewView)
|
|
}
|
|
|
|
override fun onAttachedToWindow() {
|
|
super.onAttachedToWindow()
|
|
if (!isMounted) {
|
|
isMounted = true
|
|
invokeOnViewReady()
|
|
}
|
|
update()
|
|
}
|
|
|
|
override fun onDetachedFromWindow() {
|
|
super.onDetachedFromWindow()
|
|
update()
|
|
}
|
|
|
|
fun destroy() {
|
|
cameraSession.close()
|
|
}
|
|
|
|
fun update() {
|
|
Log.i(TAG, "Updating CameraSession...")
|
|
val now = System.currentTimeMillis()
|
|
currentConfigureCall = now
|
|
|
|
coroutineScope.launch {
|
|
cameraSession.configure { config ->
|
|
if (currentConfigureCall != now) {
|
|
// configure waits for a lock, and if a new call to update() happens in the meantime we can drop this one.
|
|
// this works similar to how React implemented concurrent rendering, the newer call to update() has higher priority.
|
|
Log.i(TAG, "A new configure { ... } call arrived, aborting this one...")
|
|
return@configure
|
|
}
|
|
|
|
// Input Camera Device
|
|
config.cameraId = cameraId
|
|
|
|
// Photo
|
|
if (photo) {
|
|
config.photo = CameraConfiguration.Output.Enabled.create(CameraConfiguration.Photo(photoHdr))
|
|
} else {
|
|
config.photo = CameraConfiguration.Output.Disabled.create()
|
|
}
|
|
|
|
// Video/Frame Processor
|
|
if (video || enableFrameProcessor) {
|
|
config.video = CameraConfiguration.Output.Enabled.create(
|
|
CameraConfiguration.Video(
|
|
videoHdr,
|
|
pixelFormat,
|
|
enableFrameProcessor,
|
|
enableGpuBuffers
|
|
)
|
|
)
|
|
} else {
|
|
config.video = CameraConfiguration.Output.Disabled.create()
|
|
}
|
|
|
|
// Audio
|
|
if (audio) {
|
|
config.audio = CameraConfiguration.Output.Enabled.create(CameraConfiguration.Audio(Unit))
|
|
} else {
|
|
config.audio = CameraConfiguration.Output.Disabled.create()
|
|
}
|
|
|
|
// Code Scanner
|
|
val codeScanner = codeScannerOptions
|
|
if (codeScanner != null) {
|
|
config.codeScanner = CameraConfiguration.Output.Enabled.create(
|
|
CameraConfiguration.CodeScanner(codeScanner.codeTypes)
|
|
)
|
|
} else {
|
|
config.codeScanner = CameraConfiguration.Output.Disabled.create()
|
|
}
|
|
|
|
// Orientation
|
|
config.orientation = orientation
|
|
|
|
// Format
|
|
config.format = format
|
|
|
|
// Side-Props
|
|
config.fps = fps
|
|
config.enableLowLightBoost = lowLightBoost ?: false
|
|
config.torch = torch
|
|
config.exposure = exposure
|
|
|
|
// Zoom
|
|
config.zoom = zoom
|
|
|
|
// isActive
|
|
config.isActive = isActive && isAttachedToWindow
|
|
}
|
|
}
|
|
}
|
|
|
|
@SuppressLint("ClickableViewAccessibility")
|
|
private fun updateZoomGesture() {
|
|
if (enableZoomGesture) {
|
|
val scaleGestureDetector = ScaleGestureDetector(
|
|
context,
|
|
object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
|
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
|
zoom *= detector.scaleFactor
|
|
update()
|
|
return true
|
|
}
|
|
}
|
|
)
|
|
setOnTouchListener { _, event ->
|
|
scaleGestureDetector.onTouchEvent(event)
|
|
}
|
|
} else {
|
|
setOnTouchListener(null)
|
|
}
|
|
}
|
|
|
|
private fun updateFpsGraph() {
|
|
if (enableFpsGraph) {
|
|
if (fpsGraph != null) return
|
|
fpsGraph = FpsGraphView(context)
|
|
addView(fpsGraph)
|
|
} else {
|
|
if (fpsGraph == null) return
|
|
removeView(fpsGraph)
|
|
fpsGraph = null
|
|
}
|
|
}
|
|
|
|
override fun onFrame(frame: Frame) {
|
|
frameProcessor?.call(frame)
|
|
|
|
fpsGraph?.onTick()
|
|
}
|
|
|
|
override fun onError(error: Throwable) {
|
|
invokeOnError(error)
|
|
}
|
|
|
|
override fun onInitialized() {
|
|
invokeOnInitialized()
|
|
}
|
|
|
|
override fun onStarted() {
|
|
invokeOnStarted()
|
|
}
|
|
|
|
override fun onStopped() {
|
|
invokeOnStopped()
|
|
}
|
|
|
|
override fun onVideoChunkReady(filepath: File, index: Int) {
|
|
invokeOnChunkReady(filepath, index)
|
|
}
|
|
|
|
override fun onCodeScanned(codes: List<Barcode>, scannerFrame: CodeScannerFrame) {
|
|
invokeOnCodeScanned(codes, scannerFrame)
|
|
}
|
|
}
|