fix: Fix blackscreen issues and lifecycle when closing Camera (#2339)
* fix: Fix Blackscreen by deterministically destroying session if `isActive=false` * Re-open Camera if session died * Simplify Camera * Disconnect is optional, block when resetting state * fix: Log in `configure { ... }` * fix: Make concurrent configure safe * fix: Don't resize preview * fix: Use current `CameraConfiguration` * Don't start if no outputs are available * Only mount with preview outputs * Update CameraSession.kt * Update PreviewView.kt * Better logging * Update CameraSession.kt * Extract * fix: Rebuild entire session if `isActive` changed * isActive safe * Start session at 1 * Create ActiveCameraDevice.kt * interrupts * chore: Freeze `frame` in `useFrameProcessor` * Revert "chore: Freeze `frame` in `useFrameProcessor`" This reverts commit dff93d506e29a791d8dea8842b880ab5c892211e. * chore: Better logging * fix: Move HDR to `video`/`photo` config * fix: Fix hdr usage * fix: Ignore any updates after destroying Camera * fix: Fix video HDR * chore: Format code * fix: Check Camera permission * Remove unneeded error * Update CameraSession.kt * Update CameraPage.tsx * Delete OutputConfiguration.toDebugString.kt * Update CameraSession.kt
This commit is contained in:
parent
2cd22ad236
commit
0d21bc3a57
@ -30,7 +30,7 @@ class CameraDevicesManager(private val reactContext: ReactApplicationContext) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCameraAvailable(cameraId: String) {
|
override fun onCameraAvailable(cameraId: String) {
|
||||||
Log.i(TAG, "Camera #$cameraId: Available!")
|
Log.i(TAG, "Camera #$cameraId is now available.")
|
||||||
if (!devices.contains(cameraId)) {
|
if (!devices.contains(cameraId)) {
|
||||||
devices.add(cameraId)
|
devices.add(cameraId)
|
||||||
sendAvailableDevicesChangedEvent()
|
sendAvailableDevicesChangedEvent()
|
||||||
@ -38,7 +38,7 @@ class CameraDevicesManager(private val reactContext: ReactApplicationContext) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCameraUnavailable(cameraId: String) {
|
override fun onCameraUnavailable(cameraId: String) {
|
||||||
Log.i(TAG, "Camera #$cameraId: Unavailable!")
|
Log.i(TAG, "Camera #$cameraId is now unavailable.")
|
||||||
if (devices.contains(cameraId) && !isDeviceConnected(cameraId)) {
|
if (devices.contains(cameraId) && !isDeviceConnected(cameraId)) {
|
||||||
devices.remove(cameraId)
|
devices.remove(cameraId)
|
||||||
sendAvailableDevicesChangedEvent()
|
sendAvailableDevicesChangedEvent()
|
||||||
|
@ -47,14 +47,6 @@ class CameraView(context: Context) :
|
|||||||
// 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
|
||||||
@ -101,6 +93,7 @@ class CameraView(context: Context) :
|
|||||||
// session
|
// session
|
||||||
internal val cameraSession: CameraSession
|
internal val cameraSession: CameraSession
|
||||||
private val previewView: PreviewView
|
private val previewView: PreviewView
|
||||||
|
private var currentConfigureCall: Long = System.currentTimeMillis()
|
||||||
|
|
||||||
internal var frameProcessor: FrameProcessor? = null
|
internal var frameProcessor: FrameProcessor? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
@ -138,15 +131,24 @@ class CameraView(context: Context) :
|
|||||||
|
|
||||||
fun update() {
|
fun update() {
|
||||||
Log.i(TAG, "Updating CameraSession...")
|
Log.i(TAG, "Updating CameraSession...")
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
currentConfigureCall = now
|
||||||
|
|
||||||
launch {
|
launch {
|
||||||
cameraSession.configure { config ->
|
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
|
// Input Camera Device
|
||||||
config.cameraId = cameraId
|
config.cameraId = cameraId
|
||||||
|
|
||||||
// Photo
|
// Photo
|
||||||
if (photo == true) {
|
if (photo == true) {
|
||||||
config.photo = CameraConfiguration.Output.Enabled.create(CameraConfiguration.Photo(Unit))
|
config.photo = CameraConfiguration.Output.Enabled.create(CameraConfiguration.Photo(photoHdr))
|
||||||
} else {
|
} else {
|
||||||
config.photo = CameraConfiguration.Output.Disabled.create()
|
config.photo = CameraConfiguration.Output.Disabled.create()
|
||||||
}
|
}
|
||||||
@ -155,6 +157,7 @@ class CameraView(context: Context) :
|
|||||||
if (video == true || enableFrameProcessor) {
|
if (video == true || enableFrameProcessor) {
|
||||||
config.video = CameraConfiguration.Output.Enabled.create(
|
config.video = CameraConfiguration.Output.Enabled.create(
|
||||||
CameraConfiguration.Video(
|
CameraConfiguration.Video(
|
||||||
|
videoHdr,
|
||||||
pixelFormat,
|
pixelFormat,
|
||||||
enableFrameProcessor
|
enableFrameProcessor
|
||||||
)
|
)
|
||||||
@ -183,10 +186,6 @@ class CameraView(context: Context) :
|
|||||||
// Orientation
|
// Orientation
|
||||||
config.orientation = orientation
|
config.orientation = orientation
|
||||||
|
|
||||||
// HDR
|
|
||||||
config.videoHdr = videoHdr
|
|
||||||
config.photoHdr = photoHdr
|
|
||||||
|
|
||||||
// Format
|
// Format
|
||||||
val format = format
|
val format = format
|
||||||
if (format != null) {
|
if (format != null) {
|
||||||
|
@ -18,10 +18,6 @@ data class CameraConfiguration(
|
|||||||
var video: Output<Video> = Output.Disabled.create(),
|
var video: Output<Video> = Output.Disabled.create(),
|
||||||
var codeScanner: Output<CodeScanner> = Output.Disabled.create(),
|
var codeScanner: Output<CodeScanner> = Output.Disabled.create(),
|
||||||
|
|
||||||
// HDR
|
|
||||||
var videoHdr: Boolean = false,
|
|
||||||
var photoHdr: Boolean = false,
|
|
||||||
|
|
||||||
// Orientation
|
// Orientation
|
||||||
var orientation: Orientation = Orientation.PORTRAIT,
|
var orientation: Orientation = Orientation.PORTRAIT,
|
||||||
|
|
||||||
@ -47,8 +43,8 @@ data class CameraConfiguration(
|
|||||||
|
|
||||||
// Output<T> types, those need to be comparable
|
// Output<T> types, those need to be comparable
|
||||||
data class CodeScanner(val codeTypes: List<CodeType>)
|
data class CodeScanner(val codeTypes: List<CodeType>)
|
||||||
data class Photo(val nothing: Unit)
|
data class Photo(val enableHdr: Boolean)
|
||||||
data class Video(val pixelFormat: PixelFormat, val enableFrameProcessor: Boolean)
|
data class Video(val enableHdr: Boolean, val pixelFormat: PixelFormat, val enableFrameProcessor: Boolean)
|
||||||
data class Audio(val nothing: Unit)
|
data class Audio(val nothing: Unit)
|
||||||
data class Preview(val surface: Surface)
|
data class Preview(val surface: Surface)
|
||||||
|
|
||||||
@ -71,7 +67,7 @@ data class CameraConfiguration(
|
|||||||
}
|
}
|
||||||
|
|
||||||
data class Difference(
|
data class Difference(
|
||||||
// Input Camera (cameraId and isActive)
|
// Input Camera (cameraId, isActive)
|
||||||
val deviceChanged: Boolean,
|
val deviceChanged: Boolean,
|
||||||
// Outputs & Session (Photo, Video, CodeScanner, HDR, Format)
|
// Outputs & Session (Photo, Video, CodeScanner, HDR, Format)
|
||||||
val outputsChanged: Boolean,
|
val outputsChanged: Boolean,
|
||||||
@ -79,36 +75,30 @@ data class CameraConfiguration(
|
|||||||
val sidePropsChanged: Boolean,
|
val sidePropsChanged: Boolean,
|
||||||
// (isActive) changed
|
// (isActive) changed
|
||||||
val isActiveChanged: Boolean
|
val isActiveChanged: Boolean
|
||||||
) {
|
)
|
||||||
val hasAnyDifference: Boolean
|
|
||||||
get() = sidePropsChanged || outputsChanged || deviceChanged
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun copyOf(other: CameraConfiguration?): CameraConfiguration = other?.copy() ?: CameraConfiguration()
|
fun copyOf(other: CameraConfiguration?): CameraConfiguration = other?.copy() ?: CameraConfiguration()
|
||||||
|
|
||||||
fun difference(left: CameraConfiguration?, right: CameraConfiguration): Difference {
|
fun difference(left: CameraConfiguration?, right: CameraConfiguration): Difference {
|
||||||
val deviceChanged = left?.cameraId != right.cameraId
|
// input device
|
||||||
|
val deviceChanged = left?.cameraId != right.cameraId || left?.isActive != right.isActive
|
||||||
|
|
||||||
|
// outputs
|
||||||
val outputsChanged = deviceChanged ||
|
val outputsChanged = deviceChanged ||
|
||||||
// input device
|
|
||||||
left?.photo != right.photo ||
|
left?.photo != right.photo ||
|
||||||
left.video != right.video ||
|
left.video != right.video ||
|
||||||
left.codeScanner != right.codeScanner ||
|
left.codeScanner != right.codeScanner ||
|
||||||
left.preview != right.preview ||
|
left.preview != right.preview ||
|
||||||
// outputs
|
left.format != right.format
|
||||||
left.videoHdr != right.videoHdr ||
|
|
||||||
left.photoHdr != right.photoHdr ||
|
|
||||||
left.format != right.format // props that affect the outputs
|
|
||||||
|
|
||||||
|
// repeating request
|
||||||
val sidePropsChanged = outputsChanged ||
|
val sidePropsChanged = outputsChanged ||
|
||||||
// depend on outputs
|
|
||||||
left?.torch != right.torch ||
|
left?.torch != right.torch ||
|
||||||
left.enableLowLightBoost != right.enableLowLightBoost ||
|
left.enableLowLightBoost != right.enableLowLightBoost ||
|
||||||
left.fps != right.fps ||
|
left.fps != right.fps ||
|
||||||
left.zoom != right.zoom ||
|
left.zoom != right.zoom ||
|
||||||
left.videoStabilizationMode != right.videoStabilizationMode ||
|
left.videoStabilizationMode != right.videoStabilizationMode ||
|
||||||
left.isActive != right.isActive ||
|
|
||||||
left.exposure != right.exposure
|
left.exposure != right.exposure
|
||||||
|
|
||||||
val isActiveChanged = left?.isActive != right.isActive
|
val isActiveChanged = left?.isActive != right.isActive
|
||||||
|
@ -55,7 +55,7 @@ class CameraNotReadyError :
|
|||||||
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) :
|
class CameraSessionCannotBeConfiguredError(cameraId: String) :
|
||||||
CameraError("session", "cannot-create-session", "Failed to create a Camera Session for Camera $cameraId!")
|
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")
|
||||||
|
|
||||||
@ -87,8 +87,6 @@ class CodeTypeNotSupportedError(codeType: String) :
|
|||||||
|
|
||||||
class ViewNotFoundError(viewId: Int) :
|
class ViewNotFoundError(viewId: Int) :
|
||||||
CameraError("system", "view-not-found", "The given view (ID $viewId) was not found in the view manager.")
|
CameraError("system", "view-not-found", "The given view (ID $viewId) was not found in the view manager.")
|
||||||
class FrameProcessorsUnavailableError(reason: String) :
|
|
||||||
CameraError("system", "frame-processors-unavailable", "Frame Processors are unavailable! Reason: $reason")
|
|
||||||
class HardwareBuffersNotAvailableError :
|
class HardwareBuffersNotAvailableError :
|
||||||
CameraError("system", "hardware-buffers-unavailable", "HardwareBuffers are only available on API 28 or higher!")
|
CameraError("system", "hardware-buffers-unavailable", "HardwareBuffers are only available on API 28 or higher!")
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package com.mrousavy.camera.core
|
package com.mrousavy.camera.core
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.graphics.ImageFormat
|
import android.graphics.ImageFormat
|
||||||
import android.graphics.Point
|
import android.graphics.Point
|
||||||
import android.hardware.camera2.CameraCaptureSession
|
import android.hardware.camera2.CameraCaptureSession
|
||||||
@ -12,7 +14,6 @@ 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.media.ImageReader
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@ -21,6 +22,7 @@ import android.util.Range
|
|||||||
import android.util.Size
|
import android.util.Size
|
||||||
import android.view.Surface
|
import android.view.Surface
|
||||||
import android.view.SurfaceHolder
|
import android.view.SurfaceHolder
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import com.google.mlkit.vision.barcode.common.Barcode
|
import com.google.mlkit.vision.barcode.common.Barcode
|
||||||
import com.mrousavy.camera.core.outputs.BarcodeScannerOutput
|
import com.mrousavy.camera.core.outputs.BarcodeScannerOutput
|
||||||
import com.mrousavy.camera.core.outputs.PhotoOutput
|
import com.mrousavy.camera.core.outputs.PhotoOutput
|
||||||
@ -44,6 +46,7 @@ import com.mrousavy.camera.types.QualityPrioritization
|
|||||||
import com.mrousavy.camera.types.RecordVideoOptions
|
import com.mrousavy.camera.types.RecordVideoOptions
|
||||||
import com.mrousavy.camera.types.Torch
|
import com.mrousavy.camera.types.Torch
|
||||||
import com.mrousavy.camera.types.VideoStabilizationMode
|
import com.mrousavy.camera.types.VideoStabilizationMode
|
||||||
|
import com.mrousavy.camera.utils.ImageFormatUtils
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
import java.util.concurrent.CancellationException
|
import java.util.concurrent.CancellationException
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
@ -54,18 +57,15 @@ import kotlinx.coroutines.sync.Mutex
|
|||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
|
||||||
class CameraSession(private val context: Context, private val cameraManager: CameraManager, private val callback: CameraSessionCallback) :
|
class CameraSession(private val context: Context, private val cameraManager: CameraManager, private val callback: CameraSessionCallback) :
|
||||||
|
CameraManager.AvailabilityCallback(),
|
||||||
Closeable,
|
Closeable,
|
||||||
CoroutineScope {
|
CoroutineScope {
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "CameraSession"
|
private const val TAG = "CameraSession"
|
||||||
|
|
||||||
// TODO: Samsung advertises 60 FPS but only allows 30 FPS for some reason.
|
|
||||||
private val CAN_DO_60_FPS = !Build.MANUFACTURER.equals("samsung", true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Camera Configuration
|
// Camera Configuration
|
||||||
private var configuration: CameraConfiguration? = null
|
private var configuration: CameraConfiguration? = null
|
||||||
private var currentConfigureCall: Long = System.currentTimeMillis()
|
|
||||||
|
|
||||||
// Camera State
|
// Camera State
|
||||||
private var captureSession: CameraCaptureSession? = null
|
private var captureSession: CameraCaptureSession? = null
|
||||||
@ -73,12 +73,22 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
private var previewRequest: CaptureRequest.Builder? = null
|
private var previewRequest: CaptureRequest.Builder? = null
|
||||||
private var photoOutput: PhotoOutput? = null
|
private var photoOutput: PhotoOutput? = null
|
||||||
private var videoOutput: VideoPipelineOutput? = null
|
private var videoOutput: VideoPipelineOutput? = null
|
||||||
private var previewOutput: SurfaceOutput? = null
|
|
||||||
private var codeScannerOutput: BarcodeScannerOutput? = null
|
private var codeScannerOutput: BarcodeScannerOutput? = null
|
||||||
private var previewView: PreviewView? = null
|
private var previewView: PreviewView? = null
|
||||||
private val photoOutputSynchronizer = PhotoOutputSynchronizer()
|
private val photoOutputSynchronizer = PhotoOutputSynchronizer()
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
|
private var isDestroyed = false
|
||||||
private var isRunning = false
|
private var isRunning = false
|
||||||
|
set(value) {
|
||||||
|
if (field != value) {
|
||||||
|
if (value) {
|
||||||
|
callback.onStarted()
|
||||||
|
} else {
|
||||||
|
callback.onStopped()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
override val coroutineContext: CoroutineContext
|
override val coroutineContext: CoroutineContext
|
||||||
get() = CameraQueues.cameraQueue.coroutineDispatcher
|
get() = CameraQueues.cameraQueue.coroutineDispatcher
|
||||||
@ -95,15 +105,6 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
updateVideoOutputs()
|
updateVideoOutputs()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun close() {
|
|
||||||
runBlocking {
|
|
||||||
mutex.withLock {
|
|
||||||
destroy()
|
|
||||||
photoOutputSynchronizer.clear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val orientation: Orientation
|
val orientation: Orientation
|
||||||
get() {
|
get() {
|
||||||
val cameraId = configuration?.cameraId ?: return Orientation.PORTRAIT
|
val cameraId = configuration?.cameraId ?: return Orientation.PORTRAIT
|
||||||
@ -112,41 +113,67 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
return Orientation.fromRotationDegrees(sensorRotation)
|
return Orientation.fromRotationDegrees(sensorRotation)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun configure(lambda: (configuration: CameraConfiguration) -> Unit) {
|
init {
|
||||||
// This is the latest call to configure()
|
cameraManager.registerAvailabilityCallback(this, CameraQueues.cameraQueue.handler)
|
||||||
val now = System.currentTimeMillis()
|
}
|
||||||
currentConfigureCall = now
|
|
||||||
|
|
||||||
Log.i(TAG, "Updating CameraSession Configuration...")
|
override fun close() {
|
||||||
|
Log.i(TAG, "Closing CameraSession...")
|
||||||
// Let caller configure a new configuration for the Camera.
|
isDestroyed = true
|
||||||
val config = CameraConfiguration.copyOf(this.configuration)
|
cameraManager.unregisterAvailabilityCallback(this)
|
||||||
lambda(config)
|
runBlocking {
|
||||||
val diff = CameraConfiguration.difference(this.configuration, config)
|
mutex.withLock {
|
||||||
|
destroy()
|
||||||
if (!diff.hasAnyDifference) {
|
photoOutputSynchronizer.clear()
|
||||||
Log.w(TAG, "Called configure(...) but nothing changed...")
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
Log.i(TAG, "CameraSession closed!")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCameraAvailable(cameraId: String) {
|
||||||
|
super.onCameraAvailable(cameraId)
|
||||||
|
if (this.configuration?.cameraId == cameraId && cameraDevice == null && configuration?.isActive == true) {
|
||||||
|
Log.i(TAG, "Camera #$cameraId is now available again, trying to re-open it now...")
|
||||||
|
launch {
|
||||||
|
configure {
|
||||||
|
// re-open CameraDevice if needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun configure(lambda: (configuration: CameraConfiguration) -> Unit) {
|
||||||
|
Log.i(TAG, "configure { ... }: Waiting for lock...")
|
||||||
|
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
// Cancel configuration if there has already been a new config
|
// Let caller configure a new configuration for the Camera.
|
||||||
if (currentConfigureCall != now) {
|
val config = CameraConfiguration.copyOf(this.configuration)
|
||||||
// configure() has been called again just now, skip this one so the new call takes over.
|
lambda(config)
|
||||||
return
|
val diff = CameraConfiguration.difference(this.configuration, config)
|
||||||
|
|
||||||
|
if (isDestroyed) {
|
||||||
|
Log.i(TAG, "CameraSession is already destroyed. Skipping configure { ... }")
|
||||||
|
return@withLock
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "configure { ... }: Updating CameraSession Configuration... $diff")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
val needsRebuild = config.isActive && (cameraDevice == null || captureSession == null)
|
||||||
|
if (needsRebuild) {
|
||||||
|
Log.i(TAG, "Need to rebuild CameraDevice and CameraCaptureSession...")
|
||||||
|
}
|
||||||
|
|
||||||
// Build up session or update any props
|
// Build up session or update any props
|
||||||
if (diff.deviceChanged) {
|
if (diff.deviceChanged || needsRebuild) {
|
||||||
// 1. cameraId changed, open device
|
// 1. cameraId changed, open device
|
||||||
configureCameraDevice(config)
|
configureCameraDevice(config)
|
||||||
}
|
}
|
||||||
if (diff.outputsChanged) {
|
if (diff.outputsChanged || needsRebuild) {
|
||||||
// 2. outputs changed, build new session
|
// 2. outputs changed, build new session
|
||||||
configureOutputs(config)
|
configureOutputs(config)
|
||||||
}
|
}
|
||||||
if (diff.sidePropsChanged) {
|
if (diff.sidePropsChanged || needsRebuild) {
|
||||||
// 3. zoom etc changed, update repeating request
|
// 3. zoom etc changed, update repeating request
|
||||||
configureCaptureRequest(config)
|
configureCaptureRequest(config)
|
||||||
}
|
}
|
||||||
@ -155,21 +182,11 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
this.configuration = config
|
this.configuration = config
|
||||||
|
|
||||||
// Notify about Camera initialization
|
// Notify about Camera initialization
|
||||||
if (diff.deviceChanged) {
|
if (diff.deviceChanged && config.isActive) {
|
||||||
callback.onInitialized()
|
callback.onInitialized()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify about Camera start/stop
|
|
||||||
if (diff.isActiveChanged) {
|
|
||||||
// TODO: Move that into the CaptureRequest callback to get actual first-frame arrive time?
|
|
||||||
if (config.isActive) {
|
|
||||||
callback.onStarted()
|
|
||||||
} else {
|
|
||||||
callback.onStopped()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error: Throwable) {
|
} catch (error: Throwable) {
|
||||||
Log.e(TAG, "Failed to configure CameraSession! Error: ${error.message}, Config-Diff: $diff", error)
|
Log.e(TAG, "Failed to configure CameraSession! Error: ${error.message}, isRunning: $isRunning, Config-Diff: $diff", error)
|
||||||
callback.onError(error)
|
callback.onError(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -177,15 +194,9 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
|
|
||||||
private fun destroy() {
|
private fun destroy() {
|
||||||
Log.i(TAG, "Destroying session..")
|
Log.i(TAG, "Destroying session..")
|
||||||
captureSession?.stopRepeating()
|
|
||||||
captureSession?.close()
|
|
||||||
captureSession = null
|
|
||||||
|
|
||||||
cameraDevice?.close()
|
cameraDevice?.close()
|
||||||
cameraDevice = null
|
cameraDevice = null
|
||||||
|
|
||||||
previewOutput?.close()
|
|
||||||
previewOutput = null
|
|
||||||
photoOutput?.close()
|
photoOutput?.close()
|
||||||
photoOutput = null
|
photoOutput = null
|
||||||
videoOutput?.close()
|
videoOutput?.close()
|
||||||
@ -193,7 +204,6 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
codeScannerOutput?.close()
|
codeScannerOutput?.close()
|
||||||
codeScannerOutput = null
|
codeScannerOutput = null
|
||||||
|
|
||||||
configuration = null
|
|
||||||
isRunning = false
|
isRunning = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,31 +246,53 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
config.preview = CameraConfiguration.Output.Disabled.create()
|
config.preview = CameraConfiguration.Output.Disabled.create()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Log.i(TAG, "Preview Output destroyed!")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set up the `CameraDevice` (`cameraId`)
|
* Set up the `CameraDevice` (`cameraId`)
|
||||||
*/
|
*/
|
||||||
private suspend fun configureCameraDevice(configuration: CameraConfiguration) {
|
private suspend fun configureCameraDevice(configuration: CameraConfiguration) {
|
||||||
|
if (!configuration.isActive) {
|
||||||
|
// If isActive=false, we don't care if the device is opened or closed.
|
||||||
|
// Android OS can close the CameraDevice if it needs it, otherwise we keep it warm.
|
||||||
|
Log.i(TAG, "isActive is false, skipping CameraDevice configuration.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cameraDevice != null) {
|
||||||
|
// Close existing device
|
||||||
|
Log.i(TAG, "Closing previous Camera #${cameraDevice?.id}...")
|
||||||
|
cameraDevice?.close()
|
||||||
|
cameraDevice = null
|
||||||
|
}
|
||||||
|
isRunning = false
|
||||||
|
|
||||||
|
// Check Camera Permission
|
||||||
|
val cameraPermission = ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
|
||||||
|
if (cameraPermission != PackageManager.PERMISSION_GRANTED) throw CameraPermissionError()
|
||||||
|
|
||||||
|
// Open new device
|
||||||
val cameraId = configuration.cameraId ?: throw NoCameraDeviceError()
|
val cameraId = configuration.cameraId ?: throw NoCameraDeviceError()
|
||||||
|
|
||||||
Log.i(TAG, "Configuring Camera #$cameraId...")
|
Log.i(TAG, "Configuring Camera #$cameraId...")
|
||||||
|
|
||||||
cameraDevice?.close()
|
|
||||||
cameraDevice = cameraManager.openCamera(cameraId, { device, error ->
|
cameraDevice = cameraManager.openCamera(cameraId, { device, error ->
|
||||||
if (this.cameraDevice == device) {
|
if (cameraDevice != device) {
|
||||||
Log.e(TAG, "Camera Device $device has been disconnected!", error)
|
|
||||||
isRunning = false
|
|
||||||
callback.onError(error)
|
|
||||||
} else {
|
|
||||||
// a previous device has been disconnected, but we already have a new one.
|
// a previous device has been disconnected, but we already have a new one.
|
||||||
// this is just normal behavior
|
// this is just normal behavior
|
||||||
|
return@openCamera
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cameraDevice = null
|
||||||
|
isRunning = false
|
||||||
|
|
||||||
|
if (error != null) {
|
||||||
|
Log.e(TAG, "Camera #${device.id} has been unexpectedly disconnected!", error)
|
||||||
|
callback.onError(error)
|
||||||
|
} else {
|
||||||
|
Log.i(TAG, "Camera #${device.id} has been gracefully disconnected!")
|
||||||
}
|
}
|
||||||
}, CameraQueues.cameraQueue)
|
}, 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!")
|
Log.i(TAG, "Successfully configured Camera #$cameraId!")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -268,29 +300,34 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
* Set up the `CaptureSession` with all outputs (preview, photo, video, codeScanner) and their HDR/Format settings.
|
* Set up the `CaptureSession` with all outputs (preview, photo, video, codeScanner) and their HDR/Format settings.
|
||||||
*/
|
*/
|
||||||
private suspend fun configureOutputs(configuration: CameraConfiguration) {
|
private suspend fun configureOutputs(configuration: CameraConfiguration) {
|
||||||
val cameraDevice = cameraDevice ?: throw NoCameraDeviceError()
|
if (!configuration.isActive) {
|
||||||
val characteristics = cameraManager.getCameraCharacteristics(cameraDevice.id)
|
Log.i(TAG, "isActive is false, skipping CameraCaptureSession configuration.")
|
||||||
val format = configuration.format
|
return
|
||||||
|
}
|
||||||
|
val cameraDevice = cameraDevice
|
||||||
|
if (cameraDevice == null) {
|
||||||
|
Log.i(TAG, "CameraSession hasn't configured a CameraDevice, skipping session configuration...")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
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
|
// Destroy previous outputs
|
||||||
|
Log.i(TAG, "Destroying previous outputs...")
|
||||||
photoOutput?.close()
|
photoOutput?.close()
|
||||||
photoOutput = null
|
photoOutput = null
|
||||||
videoOutput?.close()
|
videoOutput?.close()
|
||||||
videoOutput = null
|
videoOutput = null
|
||||||
previewOutput?.close()
|
|
||||||
previewOutput = null
|
|
||||||
codeScannerOutput?.close()
|
codeScannerOutput?.close()
|
||||||
codeScannerOutput = null
|
codeScannerOutput = null
|
||||||
|
isRunning = false
|
||||||
|
|
||||||
|
val characteristics = cameraManager.getCameraCharacteristics(cameraDevice.id)
|
||||||
|
val format = configuration.format
|
||||||
|
|
||||||
|
Log.i(TAG, "Creating outputs for Camera #${cameraDevice.id}...")
|
||||||
|
|
||||||
val isSelfie = characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT
|
val isSelfie = characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT
|
||||||
|
|
||||||
val outputs = mutableListOf<OutputConfiguration>()
|
val outputs = mutableListOf<SurfaceOutput>()
|
||||||
|
|
||||||
// Photo Output
|
// Photo Output
|
||||||
val photo = configuration.photo as? CameraConfiguration.Output.Enabled<CameraConfiguration.Photo>
|
val photo = configuration.photo as? CameraConfiguration.Output.Enabled<CameraConfiguration.Photo>
|
||||||
@ -300,15 +337,15 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
val size = sizes.closestToOrMax(format?.photoSize)
|
val size = sizes.closestToOrMax(format?.photoSize)
|
||||||
val maxImages = 10
|
val maxImages = 10
|
||||||
|
|
||||||
Log.i(TAG, "Adding ${size.width} x ${size.height} Photo Output in Format #$imageFormat...")
|
Log.i(TAG, "Adding ${size.width}x${size.height} Photo Output in ${ImageFormatUtils.imageFormatToString(imageFormat)}...")
|
||||||
val imageReader = ImageReader.newInstance(size.width, size.height, imageFormat, maxImages)
|
val imageReader = ImageReader.newInstance(size.width, size.height, imageFormat, maxImages)
|
||||||
imageReader.setOnImageAvailableListener({ reader ->
|
imageReader.setOnImageAvailableListener({ reader ->
|
||||||
Log.i(TAG, "Photo Captured!")
|
Log.i(TAG, "Photo Captured!")
|
||||||
val image = reader.acquireLatestImage()
|
val image = reader.acquireLatestImage()
|
||||||
onPhotoCaptured(image)
|
onPhotoCaptured(image)
|
||||||
}, CameraQueues.cameraQueue.handler)
|
}, CameraQueues.cameraQueue.handler)
|
||||||
val output = PhotoOutput(imageReader, configuration.photoHdr)
|
val output = PhotoOutput(imageReader, photo.config.enableHdr)
|
||||||
outputs.add(output.toOutputConfiguration(characteristics))
|
outputs.add(output)
|
||||||
photoOutput = output
|
photoOutput = output
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -319,7 +356,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
val sizes = characteristics.getVideoSizes(cameraDevice.id, imageFormat)
|
val sizes = characteristics.getVideoSizes(cameraDevice.id, imageFormat)
|
||||||
val size = sizes.closestToOrMax(format?.videoSize)
|
val size = sizes.closestToOrMax(format?.videoSize)
|
||||||
|
|
||||||
Log.i(TAG, "Adding ${size.width} x ${size.height} Video Output in Format #$imageFormat...")
|
Log.i(TAG, "Adding ${size.width}x${size.height} Video Output in ${ImageFormatUtils.imageFormatToString(imageFormat)}...")
|
||||||
val videoPipeline = VideoPipeline(
|
val videoPipeline = VideoPipeline(
|
||||||
size.width,
|
size.width,
|
||||||
size.height,
|
size.height,
|
||||||
@ -327,8 +364,8 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
isSelfie,
|
isSelfie,
|
||||||
video.config.enableFrameProcessor
|
video.config.enableFrameProcessor
|
||||||
)
|
)
|
||||||
val output = VideoPipelineOutput(videoPipeline, configuration.videoHdr)
|
val output = VideoPipelineOutput(videoPipeline, video.config.enableHdr)
|
||||||
outputs.add(output.toOutputConfiguration(characteristics))
|
outputs.add(output)
|
||||||
videoOutput = output
|
videoOutput = output
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -344,15 +381,16 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
characteristics.getPreviewTargetSize(null)
|
characteristics.getPreviewTargetSize(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.i(TAG, "Adding ${size.width} x ${size.height} Preview Output...")
|
val enableHdr = video?.config?.enableHdr ?: false
|
||||||
|
|
||||||
|
Log.i(TAG, "Adding ${size.width}x${size.height} Preview Output...")
|
||||||
val output = SurfaceOutput(
|
val output = SurfaceOutput(
|
||||||
preview.config.surface,
|
preview.config.surface,
|
||||||
size,
|
size,
|
||||||
SurfaceOutput.OutputType.PREVIEW,
|
SurfaceOutput.OutputType.PREVIEW,
|
||||||
configuration.videoHdr
|
enableHdr
|
||||||
)
|
)
|
||||||
outputs.add(output.toOutputConfiguration(characteristics))
|
outputs.add(output)
|
||||||
previewOutput = output
|
|
||||||
previewView?.size = size
|
previewView?.size = size
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -363,19 +401,26 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
val sizes = characteristics.getVideoSizes(cameraDevice.id, imageFormat)
|
val sizes = characteristics.getVideoSizes(cameraDevice.id, imageFormat)
|
||||||
val size = sizes.closestToOrMax(Size(1280, 720))
|
val size = sizes.closestToOrMax(Size(1280, 720))
|
||||||
|
|
||||||
Log.i(TAG, "Adding ${size.width} x ${size.height} CodeScanner Output in Format #$imageFormat...")
|
Log.i(TAG, "Adding ${size.width}x${size.height} CodeScanner Output in ${ImageFormatUtils.imageFormatToString(imageFormat)}...")
|
||||||
val pipeline = CodeScannerPipeline(size, imageFormat, codeScanner.config, callback)
|
val pipeline = CodeScannerPipeline(size, imageFormat, codeScanner.config, callback)
|
||||||
val output = BarcodeScannerOutput(pipeline)
|
val output = BarcodeScannerOutput(pipeline)
|
||||||
outputs.add(output.toOutputConfiguration(characteristics))
|
outputs.add(output)
|
||||||
codeScannerOutput = output
|
codeScannerOutput = output
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new session
|
// Create session
|
||||||
captureSession = cameraDevice.createCaptureSession(cameraManager, outputs, { session ->
|
captureSession = cameraDevice.createCaptureSession(cameraManager, outputs, { session ->
|
||||||
if (this.captureSession == session) {
|
if (this.captureSession != session) {
|
||||||
Log.i(TAG, "Camera Session $session has been closed!")
|
// a previous session has been closed, but we already have a new one.
|
||||||
isRunning = false
|
// this is just normal behavior
|
||||||
|
return@createCaptureSession
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// onClosed
|
||||||
|
this.captureSession = null
|
||||||
|
isRunning = false
|
||||||
|
|
||||||
|
Log.i(TAG, "Camera Session $session has been closed.")
|
||||||
}, CameraQueues.cameraQueue)
|
}, CameraQueues.cameraQueue)
|
||||||
|
|
||||||
Log.i(TAG, "Successfully configured Session with ${outputs.size} outputs for Camera #${cameraDevice.id}!")
|
Log.i(TAG, "Successfully configured Session with ${outputs.size} outputs for Camera #${cameraDevice.id}!")
|
||||||
@ -384,45 +429,18 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
updateVideoOutputs()
|
updateVideoOutputs()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun configureCaptureRequest(config: CameraConfiguration) {
|
private fun createRepeatingRequest(device: CameraDevice, targets: List<Surface>, config: CameraConfiguration): CaptureRequest {
|
||||||
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()
|
|
||||||
isRunning = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val device = cameraDevice ?: throw NoCameraDeviceError()
|
|
||||||
val captureSession = captureSession ?: throw CameraNotReadyError()
|
|
||||||
|
|
||||||
val previewOutput = previewOutput
|
|
||||||
if (previewOutput == null) {
|
|
||||||
Log.w(TAG, "Preview Output is null, aborting...")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val cameraCharacteristics = cameraManager.getCameraCharacteristics(device.id)
|
val cameraCharacteristics = cameraManager.getCameraCharacteristics(device.id)
|
||||||
|
|
||||||
val template = if (config.video.isEnabled) CameraDevice.TEMPLATE_RECORD else CameraDevice.TEMPLATE_PREVIEW
|
val template = if (config.video.isEnabled) CameraDevice.TEMPLATE_RECORD else CameraDevice.TEMPLATE_PREVIEW
|
||||||
val captureRequest = device.createCaptureRequest(template)
|
val captureRequest = device.createCaptureRequest(template)
|
||||||
|
|
||||||
captureRequest.addTarget(previewOutput.surface)
|
targets.forEach { t -> captureRequest.addTarget(t) }
|
||||||
videoOutput?.let { output ->
|
|
||||||
captureRequest.addTarget(output.surface)
|
|
||||||
}
|
|
||||||
codeScannerOutput?.let { output ->
|
|
||||||
captureRequest.addTarget(output.surface)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set FPS
|
// Set FPS
|
||||||
// TODO: Check if the FPS range is actually supported in the current configuration.
|
// TODO: Check if the FPS range is actually supported in the current configuration.
|
||||||
var fps = config.fps
|
val fps = config.fps
|
||||||
if (fps != null) {
|
if (fps != null) {
|
||||||
if (!CAN_DO_60_FPS) {
|
|
||||||
// If we can't do 60 FPS, we clamp it to 30 FPS - that's always supported.
|
|
||||||
fps = 30.coerceAtMost(fps)
|
|
||||||
}
|
|
||||||
captureRequest.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, Range(fps, fps))
|
captureRequest.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, Range(fps, fps))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -450,7 +468,9 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
|
|
||||||
// Set HDR
|
// Set HDR
|
||||||
// TODO: Check if that value is even supported
|
// TODO: Check if that value is even supported
|
||||||
if (config.videoHdr) {
|
val video = config.video as? CameraConfiguration.Output.Enabled<CameraConfiguration.Video>
|
||||||
|
val videoHdr = video?.config?.enableHdr
|
||||||
|
if (videoHdr == true) {
|
||||||
captureRequest.set(CaptureRequest.CONTROL_SCENE_MODE, CaptureRequest.CONTROL_SCENE_MODE_HDR)
|
captureRequest.set(CaptureRequest.CONTROL_SCENE_MODE, CaptureRequest.CONTROL_SCENE_MODE_HDR)
|
||||||
} else if (config.enableLowLightBoost) {
|
} else if (config.enableLowLightBoost) {
|
||||||
captureRequest.set(CaptureRequest.CONTROL_SCENE_MODE, CaptureRequest.CONTROL_SCENE_MODE_NIGHT)
|
captureRequest.set(CaptureRequest.CONTROL_SCENE_MODE, CaptureRequest.CONTROL_SCENE_MODE_NIGHT)
|
||||||
@ -474,7 +494,30 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start repeating request if the Camera is active
|
// Start repeating request if the Camera is active
|
||||||
val request = captureRequest.build()
|
return captureRequest.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun configureCaptureRequest(config: CameraConfiguration) {
|
||||||
|
val captureSession = captureSession
|
||||||
|
|
||||||
|
if (!config.isActive) {
|
||||||
|
isRunning = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (captureSession == null) {
|
||||||
|
Log.i(TAG, "CameraSession hasn't configured the capture session, skipping CaptureRequest...")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val preview = config.preview as? CameraConfiguration.Output.Enabled<CameraConfiguration.Preview>
|
||||||
|
val previewSurface = preview?.config?.surface
|
||||||
|
val targets = listOfNotNull(previewSurface, videoOutput?.surface, codeScannerOutput?.surface)
|
||||||
|
if (targets.isEmpty()) {
|
||||||
|
Log.i(TAG, "CameraSession has no repeating outputs (Preview, Video, CodeScanner), skipping CaptureRequest...")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val request = createRepeatingRequest(captureSession.device, targets, config)
|
||||||
captureSession.setRepeatingRequest(request, null, null)
|
captureSession.setRepeatingRequest(request, null, null)
|
||||||
isRunning = true
|
isRunning = true
|
||||||
}
|
}
|
||||||
@ -496,7 +539,6 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
|
|
||||||
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 enableHdr = configuration?.photoHdr ?: false
|
|
||||||
val captureRequest = captureSession.device.createPhotoCaptureRequest(
|
val captureRequest = captureSession.device.createPhotoCaptureRequest(
|
||||||
cameraManager,
|
cameraManager,
|
||||||
photoOutput.surface,
|
photoOutput.surface,
|
||||||
@ -505,7 +547,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
flashMode,
|
flashMode,
|
||||||
enableRedEyeReduction,
|
enableRedEyeReduction,
|
||||||
enableAutoStabilization,
|
enableAutoStabilization,
|
||||||
enableHdr,
|
photoOutput.enableHdr,
|
||||||
orientation
|
orientation
|
||||||
)
|
)
|
||||||
Log.i(TAG, "Photo capture 1/3 - starting capture...")
|
Log.i(TAG, "Photo capture 1/3 - starting capture...")
|
||||||
@ -547,12 +589,20 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
val videoOutput = videoOutput ?: throw VideoNotEnabledError()
|
val videoOutput = videoOutput ?: throw VideoNotEnabledError()
|
||||||
val cameraDevice = cameraDevice ?: throw CameraNotReadyError()
|
val cameraDevice = cameraDevice ?: throw CameraNotReadyError()
|
||||||
|
|
||||||
// TODO: Implement HDR
|
|
||||||
val hdr = configuration?.videoHdr ?: false
|
|
||||||
val fps = configuration?.fps ?: 30
|
val fps = configuration?.fps ?: 30
|
||||||
|
|
||||||
val recording =
|
val recording = RecordingSession(
|
||||||
RecordingSession(context, cameraDevice.id, videoOutput.size, enableAudio, fps, hdr, orientation, options, callback, onError)
|
context,
|
||||||
|
cameraDevice.id,
|
||||||
|
videoOutput.size,
|
||||||
|
enableAudio,
|
||||||
|
fps,
|
||||||
|
videoOutput.enableHdr,
|
||||||
|
orientation,
|
||||||
|
options,
|
||||||
|
callback,
|
||||||
|
onError
|
||||||
|
)
|
||||||
recording.start()
|
recording.start()
|
||||||
this.recording = recording
|
this.recording = recording
|
||||||
}
|
}
|
||||||
@ -581,19 +631,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun focus(x: Int, y: Int) {
|
suspend fun focus(x: Int, y: Int): Unit = throw NotImplementedError("focus() is not yet implemented!")
|
||||||
val captureSession = captureSession ?: throw CameraNotReadyError()
|
|
||||||
val previewOutput = previewOutput ?: throw CameraNotReadyError()
|
|
||||||
val characteristics = cameraManager.getCameraCharacteristics(captureSession.device.id)
|
|
||||||
val sensorSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)!!
|
|
||||||
val previewSize = previewOutput.size
|
|
||||||
val pX = x.toDouble() / previewSize.width * sensorSize.height()
|
|
||||||
val pY = y.toDouble() / previewSize.height * sensorSize.width()
|
|
||||||
val point = Point(pX.toInt(), pY.toInt())
|
|
||||||
|
|
||||||
Log.i(TAG, "Focusing (${point.x}, ${point.y})...")
|
|
||||||
focus(point)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun focus(point: Point) {
|
private suspend fun focus(point: Point) {
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
|
@ -2,7 +2,6 @@ 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.Gravity
|
import android.view.Gravity
|
||||||
@ -10,11 +9,7 @@ import android.view.SurfaceHolder
|
|||||||
import android.view.SurfaceView
|
import android.view.SurfaceView
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import com.facebook.react.bridge.UiThreadUtil
|
import com.facebook.react.bridge.UiThreadUtil
|
||||||
import com.mrousavy.camera.extensions.bigger
|
|
||||||
import com.mrousavy.camera.extensions.getMaximumPreviewSize
|
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
|
||||||
|
|
||||||
@ -49,19 +44,19 @@ class PreviewView(context: Context, callback: SurfaceHolder.Callback) : SurfaceV
|
|||||||
holder.addCallback(callback)
|
holder.addCallback(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resizeToInputCamera(cameraId: String, cameraManager: CameraManager, format: CameraDeviceFormat?) {
|
/*fun resizeToInputCamera(cameraId: String, cameraManager: CameraManager, format: CameraDeviceFormat?) {
|
||||||
val characteristics = cameraManager.getCameraCharacteristics(cameraId)
|
val characteristics = cameraManager.getCameraCharacteristics(cameraId)
|
||||||
|
|
||||||
val targetPreviewSize = format?.videoSize
|
val targetPreviewSize = format?.videoSize
|
||||||
val formatAspectRatio = if (targetPreviewSize != null) targetPreviewSize.bigger.toDouble() / targetPreviewSize.smaller else null
|
val formatAspectRatio = if (targetPreviewSize != null) targetPreviewSize.bigger.toDouble() / targetPreviewSize.smaller else null
|
||||||
size = characteristics.getPreviewTargetSize(formatAspectRatio)
|
size = characteristics.getPreviewTargetSize(formatAspectRatio)
|
||||||
}
|
}*/
|
||||||
|
|
||||||
private fun getSize(contentSize: Size, containerSize: Size, resizeMode: ResizeMode): Size {
|
private fun getSize(contentSize: Size, containerSize: Size, resizeMode: ResizeMode): Size {
|
||||||
val contentAspectRatio = contentSize.height.toDouble() / contentSize.width
|
val contentAspectRatio = contentSize.height.toDouble() / contentSize.width
|
||||||
val containerAspectRatio = containerSize.width.toDouble() / containerSize.height
|
val containerAspectRatio = containerSize.width.toDouble() / containerSize.height
|
||||||
|
|
||||||
Log.d(TAG, "coverSize :: $contentSize ($contentAspectRatio), ${containerSize.width}x${containerSize.height} ($containerAspectRatio)")
|
Log.i(TAG, "Content Size: $contentSize ($contentAspectRatio) | Container Size: $containerSize ($containerAspectRatio)")
|
||||||
|
|
||||||
val widthOverHeight = when (resizeMode) {
|
val widthOverHeight = when (resizeMode) {
|
||||||
ResizeMode.COVER -> contentAspectRatio > containerAspectRatio
|
ResizeMode.COVER -> contentAspectRatio > containerAspectRatio
|
||||||
@ -82,14 +77,11 @@ class PreviewView(context: Context, callback: SurfaceHolder.Callback) : SurfaceV
|
|||||||
@SuppressLint("DrawAllocation")
|
@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 viewHeight = MeasureSpec.getSize(heightMeasureSpec)
|
|
||||||
|
|
||||||
Log.i(TAG, "PreviewView onMeasure($viewWidth, $viewHeight)")
|
val viewSize = Size(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec))
|
||||||
|
val fittedSize = getSize(size, viewSize, resizeMode)
|
||||||
|
|
||||||
val fittedSize = getSize(size, Size(viewWidth, viewHeight), resizeMode)
|
Log.i(TAG, "PreviewView is $viewSize, rendering $size content. Resizing to: $fittedSize ($resizeMode)")
|
||||||
|
|
||||||
Log.d(TAG, "Fitted dimensions set: $fittedSize")
|
|
||||||
setMeasuredDimension(fittedSize.width, fittedSize.height)
|
setMeasuredDimension(fittedSize.width, fittedSize.height)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,6 +40,7 @@ class RecordingSession(
|
|||||||
private var startTime: Long? = null
|
private var startTime: Long? = null
|
||||||
val surface: Surface = MediaCodec.createPersistentInputSurface()
|
val surface: Surface = MediaCodec.createPersistentInputSurface()
|
||||||
|
|
||||||
|
// TODO: Implement HDR
|
||||||
init {
|
init {
|
||||||
outputFile = File.createTempFile("mrousavy", options.fileType.toExtension(), context.cacheDir)
|
outputFile = File.createTempFile("mrousavy", options.fileType.toExtension(), context.cacheDir)
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ package com.mrousavy.camera.core.outputs
|
|||||||
import android.media.ImageReader
|
import android.media.ImageReader
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.Size
|
import android.util.Size
|
||||||
import java.io.Closeable
|
import com.mrousavy.camera.utils.ImageFormatUtils
|
||||||
|
|
||||||
open class PhotoOutput(private val imageReader: ImageReader, enableHdr: Boolean = false) :
|
open class PhotoOutput(private val imageReader: ImageReader, enableHdr: Boolean = false) :
|
||||||
SurfaceOutput(
|
SurfaceOutput(
|
||||||
@ -11,13 +11,15 @@ open class PhotoOutput(private val imageReader: ImageReader, enableHdr: Boolean
|
|||||||
Size(imageReader.width, imageReader.height),
|
Size(imageReader.width, imageReader.height),
|
||||||
OutputType.PHOTO,
|
OutputType.PHOTO,
|
||||||
enableHdr
|
enableHdr
|
||||||
),
|
) {
|
||||||
Closeable {
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
Log.i(TAG, "Closing ${imageReader.width}x${imageReader.height} $outputType ImageReader..")
|
Log.i(TAG, "Closing ${imageReader.width}x${imageReader.height} $outputType ImageReader..")
|
||||||
imageReader.close()
|
imageReader.close()
|
||||||
super.close()
|
super.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String = "$outputType (${imageReader.width} x ${imageReader.height} in format #${imageReader.imageFormat})"
|
override fun toString(): String =
|
||||||
|
"$outputType (${size.width}x${size.height} in ${ImageFormatUtils.imageFormatToString(
|
||||||
|
imageReader.imageFormat
|
||||||
|
)})"
|
||||||
}
|
}
|
||||||
|
@ -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 enableHdr: Boolean = false,
|
val enableHdr: Boolean = false,
|
||||||
private val closeSurfaceOnEnd: Boolean = false
|
private val closeSurfaceOnEnd: Boolean = false
|
||||||
) : Closeable {
|
) : Closeable {
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -19,5 +19,5 @@ class VideoPipelineOutput(val videoPipeline: VideoPipeline, enableHdr: Boolean =
|
|||||||
super.close()
|
super.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String = "$outputType (${videoPipeline.width} x ${videoPipeline.height} in format #${videoPipeline.format})"
|
override fun toString(): String = "$outputType (${size.width}x${size.height} in ${videoPipeline.format})"
|
||||||
}
|
}
|
||||||
|
@ -4,22 +4,22 @@ import android.hardware.camera2.CameraCaptureSession
|
|||||||
import android.hardware.camera2.CameraCharacteristics
|
import android.hardware.camera2.CameraCharacteristics
|
||||||
import android.hardware.camera2.CameraDevice
|
import android.hardware.camera2.CameraDevice
|
||||||
import android.hardware.camera2.CameraManager
|
import android.hardware.camera2.CameraManager
|
||||||
import android.hardware.camera2.params.OutputConfiguration
|
|
||||||
import android.hardware.camera2.params.SessionConfiguration
|
import android.hardware.camera2.params.SessionConfiguration
|
||||||
import android.os.Build
|
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.SurfaceOutput
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.resumeWithException
|
import kotlin.coroutines.resumeWithException
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
|
||||||
private const val TAG = "CreateCaptureSession"
|
private const val TAG = "CreateCaptureSession"
|
||||||
private var sessionId = 1000
|
private var sessionId = 1
|
||||||
|
|
||||||
suspend fun CameraDevice.createCaptureSession(
|
suspend fun CameraDevice.createCaptureSession(
|
||||||
cameraManager: CameraManager,
|
cameraManager: CameraManager,
|
||||||
outputs: List<OutputConfiguration>,
|
outputs: List<SurfaceOutput>,
|
||||||
onClosed: (session: CameraCaptureSession) -> Unit,
|
onClosed: (session: CameraCaptureSession) -> Unit,
|
||||||
queue: CameraQueues.CameraQueue
|
queue: CameraQueues.CameraQueue
|
||||||
): CameraCaptureSession =
|
): CameraCaptureSession =
|
||||||
@ -29,34 +29,35 @@ suspend fun CameraDevice.createCaptureSession(
|
|||||||
val sessionId = sessionId++
|
val sessionId = sessionId++
|
||||||
Log.i(
|
Log.i(
|
||||||
TAG,
|
TAG,
|
||||||
"Camera $id: Creating Capture Session #$sessionId... " +
|
"Camera #$id: Creating Capture Session #$sessionId... " +
|
||||||
"Hardware Level: $hardwareLevel} | Outputs: $outputs"
|
"(Hardware Level: $hardwareLevel | Outputs: [${outputs.joinToString()}])"
|
||||||
)
|
)
|
||||||
|
|
||||||
val callback = object : CameraCaptureSession.StateCallback() {
|
val callback = object : CameraCaptureSession.StateCallback() {
|
||||||
override fun onConfigured(session: CameraCaptureSession) {
|
override fun onConfigured(session: CameraCaptureSession) {
|
||||||
Log.i(TAG, "Camera $id: Capture Session #$sessionId configured!")
|
Log.i(TAG, "Camera #$id: Successfully created CameraCaptureSession #$sessionId!")
|
||||||
continuation.resume(session)
|
continuation.resume(session)
|
||||||
}
|
}
|
||||||
|
|
||||||
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 create CameraCaptureSession #$sessionId!")
|
||||||
continuation.resumeWithException(CameraSessionCannotBeConfiguredError(id))
|
continuation.resumeWithException(CameraSessionCannotBeConfiguredError(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClosed(session: CameraCaptureSession) {
|
override fun onClosed(session: CameraCaptureSession) {
|
||||||
|
Log.i(TAG, "Camera #$id: CameraCaptureSession #$sessionId has been closed.")
|
||||||
super.onClosed(session)
|
super.onClosed(session)
|
||||||
Log.i(TAG, "Camera $id: Capture Session #$sessionId closed!")
|
|
||||||
onClosed(session)
|
onClosed(session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val configurations = outputs.map { it.toOutputConfiguration(characteristics) }
|
||||||
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, outputs, queue.executor, callback)
|
val config = SessionConfiguration(SessionConfiguration.SESSION_REGULAR, configurations, 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(outputs, callback, queue.handler)
|
this.createCaptureSessionByOutputConfigurations(configurations, callback, queue.handler)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,30 +18,30 @@ private const val TAG = "CameraManager"
|
|||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
suspend fun CameraManager.openCamera(
|
suspend fun CameraManager.openCamera(
|
||||||
cameraId: String,
|
cameraId: String,
|
||||||
onDisconnected: (camera: CameraDevice, reason: Throwable) -> Unit,
|
onDisconnected: (camera: CameraDevice, error: Throwable?) -> Unit,
|
||||||
queue: CameraQueues.CameraQueue
|
queue: CameraQueues.CameraQueue
|
||||||
): CameraDevice =
|
): CameraDevice =
|
||||||
suspendCancellableCoroutine { continuation ->
|
suspendCancellableCoroutine { continuation ->
|
||||||
Log.i(TAG, "Camera $cameraId: Opening...")
|
Log.i(TAG, "Camera #$cameraId: Opening...")
|
||||||
|
|
||||||
val callback = object : CameraDevice.StateCallback() {
|
val callback = object : CameraDevice.StateCallback() {
|
||||||
override fun onOpened(camera: CameraDevice) {
|
override fun onOpened(camera: CameraDevice) {
|
||||||
Log.i(TAG, "Camera $cameraId: Opened!")
|
Log.i(TAG, "Camera #$cameraId: Opened!")
|
||||||
continuation.resume(camera)
|
continuation.resume(camera)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDisconnected(camera: CameraDevice) {
|
override fun onDisconnected(camera: CameraDevice) {
|
||||||
Log.i(TAG, "Camera $cameraId: Disconnected!")
|
Log.i(TAG, "Camera #$cameraId: Disconnected!")
|
||||||
if (continuation.isActive) {
|
if (continuation.isActive) {
|
||||||
continuation.resumeWithException(CameraCannotBeOpenedError(cameraId, CameraDeviceError.DISCONNECTED))
|
continuation.resumeWithException(CameraCannotBeOpenedError(cameraId, CameraDeviceError.DISCONNECTED))
|
||||||
} else {
|
} else {
|
||||||
onDisconnected(camera, CameraDisconnectedError(cameraId, CameraDeviceError.DISCONNECTED))
|
onDisconnected(camera, null)
|
||||||
}
|
}
|
||||||
camera.close()
|
camera.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onError(camera: CameraDevice, errorCode: Int) {
|
override fun onError(camera: CameraDevice, errorCode: Int) {
|
||||||
Log.e(TAG, "Camera $cameraId: Error! $errorCode")
|
Log.e(TAG, "Camera #$cameraId: Error! $errorCode")
|
||||||
val error = CameraDeviceError.fromCameraDeviceError(errorCode)
|
val error = CameraDeviceError.fromCameraDeviceError(errorCode)
|
||||||
if (continuation.isActive) {
|
if (continuation.isActive) {
|
||||||
continuation.resumeWithException(CameraCannotBeOpenedError(cameraId, error))
|
continuation.resumeWithException(CameraCannotBeOpenedError(cameraId, error))
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
package com.mrousavy.camera.types
|
package com.mrousavy.camera.types
|
||||||
|
|
||||||
import android.graphics.ImageFormat
|
import android.graphics.ImageFormat
|
||||||
|
import android.util.Log
|
||||||
import com.mrousavy.camera.core.InvalidTypeScriptUnionError
|
import com.mrousavy.camera.core.InvalidTypeScriptUnionError
|
||||||
import com.mrousavy.camera.core.PixelFormatNotSupportedError
|
import com.mrousavy.camera.core.PixelFormatNotSupportedError
|
||||||
|
import com.mrousavy.camera.utils.ImageFormatUtils
|
||||||
|
|
||||||
enum class PixelFormat(override val unionValue: String) : JSUnionValue {
|
enum class PixelFormat(override val unionValue: String) : JSUnionValue {
|
||||||
YUV("yuv"),
|
YUV("yuv"),
|
||||||
@ -18,11 +20,15 @@ enum class PixelFormat(override val unionValue: String) : JSUnionValue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object : JSUnionValue.Companion<PixelFormat> {
|
companion object : JSUnionValue.Companion<PixelFormat> {
|
||||||
|
private const val TAG = "PixelFormat"
|
||||||
fun fromImageFormat(imageFormat: Int): PixelFormat =
|
fun fromImageFormat(imageFormat: Int): PixelFormat =
|
||||||
when (imageFormat) {
|
when (imageFormat) {
|
||||||
ImageFormat.YUV_420_888 -> YUV
|
ImageFormat.YUV_420_888 -> YUV
|
||||||
ImageFormat.PRIVATE -> NATIVE
|
ImageFormat.PRIVATE -> NATIVE
|
||||||
else -> UNKNOWN
|
else -> {
|
||||||
|
Log.w(TAG, "Unknown PixelFormat! ${ImageFormatUtils.imageFormatToString(imageFormat)}")
|
||||||
|
UNKNOWN
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun fromUnionValue(unionValue: String?): PixelFormat =
|
override fun fromUnionValue(unionValue: String?): PixelFormat =
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
package com.mrousavy.camera.utils
|
||||||
|
|
||||||
|
import android.graphics.ImageFormat
|
||||||
|
import android.graphics.PixelFormat
|
||||||
|
|
||||||
|
class ImageFormatUtils {
|
||||||
|
companion object {
|
||||||
|
fun imageFormatToString(format: Int): String =
|
||||||
|
when (format) {
|
||||||
|
ImageFormat.YUV_420_888 -> "YUV_420_888"
|
||||||
|
ImageFormat.NV21 -> "NV21"
|
||||||
|
ImageFormat.NV16 -> "NV16"
|
||||||
|
ImageFormat.YV12 -> "YV12"
|
||||||
|
ImageFormat.YUV_422_888 -> "YUV_422_888"
|
||||||
|
ImageFormat.YCBCR_P010 -> "YCBCR_P010"
|
||||||
|
ImageFormat.YUV_444_888 -> "YUV_444_888"
|
||||||
|
ImageFormat.YUY2 -> "YUY2"
|
||||||
|
ImageFormat.Y8 -> "Y8"
|
||||||
|
ImageFormat.JPEG -> "JPEG"
|
||||||
|
ImageFormat.RGB_565 -> "RGB_565"
|
||||||
|
ImageFormat.FLEX_RGB_888 -> "FLEX_RGB_888"
|
||||||
|
ImageFormat.FLEX_RGBA_8888 -> "FLEX_RGBA_8888"
|
||||||
|
PixelFormat.RGB_888 -> "RGB_888"
|
||||||
|
ImageFormat.PRIVATE -> "PRIVATE"
|
||||||
|
else -> "UNKNOWN ($format)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -84,6 +84,7 @@ public final class CameraView: UIView, CameraSessionDelegate {
|
|||||||
// CameraView+Zoom
|
// CameraView+Zoom
|
||||||
var pinchGestureRecognizer: UIPinchGestureRecognizer?
|
var pinchGestureRecognizer: UIPinchGestureRecognizer?
|
||||||
var pinchScaleOffset: CGFloat = 1.0
|
var pinchScaleOffset: CGFloat = 1.0
|
||||||
|
private var currentConfigureCall: DispatchTime?
|
||||||
|
|
||||||
var previewView: PreviewView
|
var previewView: PreviewView
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
@ -150,8 +151,18 @@ public final class CameraView: UIView, CameraSessionDelegate {
|
|||||||
// pragma MARK: Props updating
|
// pragma MARK: Props updating
|
||||||
override public final func didSetProps(_ changedProps: [String]!) {
|
override public final func didSetProps(_ changedProps: [String]!) {
|
||||||
ReactLogger.log(level: .info, message: "Updating \(changedProps.count) props: [\(changedProps.joined(separator: ", "))]")
|
ReactLogger.log(level: .info, message: "Updating \(changedProps.count) props: [\(changedProps.joined(separator: ", "))]")
|
||||||
|
let now = DispatchTime.now()
|
||||||
|
currentConfigureCall = now
|
||||||
|
|
||||||
|
cameraSession.configure { [self] config in
|
||||||
|
// Check if we're still the latest call to configure { ... }
|
||||||
|
guard currentConfigureCall == now else {
|
||||||
|
// 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.
|
||||||
|
ReactLogger.log(level: .info, message: "A new configure { ... } call arrived, aborting this one...")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
cameraSession.configure { config in
|
|
||||||
// Input Camera Device
|
// Input Camera Device
|
||||||
config.cameraId = cameraId as? String
|
config.cameraId = cameraId as? String
|
||||||
|
|
||||||
|
@ -98,29 +98,21 @@ class CameraSession: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, AVC
|
|||||||
Any changes in here will be re-configured only if required, and under a lock.
|
Any changes in here will be re-configured only if required, and under a lock.
|
||||||
The `configuration` object is a copy of the currently active configuration that can be modified by the caller in the lambda.
|
The `configuration` object is a copy of the currently active configuration that can be modified by the caller in the lambda.
|
||||||
*/
|
*/
|
||||||
func configure(_ lambda: (_ configuration: CameraConfiguration) throws -> Void) {
|
func configure(_ lambda: @escaping (_ configuration: CameraConfiguration) throws -> Void) {
|
||||||
// This is the latest call to configure()
|
ReactLogger.log(level: .info, message: "configure { ... }: Waiting for lock...")
|
||||||
let time = DispatchTime.now()
|
|
||||||
currentConfigureCall = time
|
|
||||||
|
|
||||||
ReactLogger.log(level: .info, message: "Updating Session Configuration...")
|
|
||||||
|
|
||||||
// Let caller configure a new configuration for the Camera.
|
|
||||||
let config = CameraConfiguration(copyOf: configuration)
|
|
||||||
do {
|
|
||||||
try lambda(config)
|
|
||||||
} catch {
|
|
||||||
onConfigureError(error)
|
|
||||||
}
|
|
||||||
let difference = CameraConfiguration.Difference(between: configuration, and: config)
|
|
||||||
|
|
||||||
// Set up Camera (Video) Capture Session (on camera queue, acts like a lock)
|
// Set up Camera (Video) Capture Session (on camera queue, acts like a lock)
|
||||||
CameraQueues.cameraQueue.async {
|
CameraQueues.cameraQueue.async {
|
||||||
// Cancel configuration if there has already been a new config
|
// Let caller configure a new configuration for the Camera.
|
||||||
guard self.currentConfigureCall == time else {
|
let config = CameraConfiguration(copyOf: self.configuration)
|
||||||
// configure() has been called again just now, skip this one so the new call takes over.
|
do {
|
||||||
return
|
try lambda(config)
|
||||||
|
} catch {
|
||||||
|
self.onConfigureError(error)
|
||||||
}
|
}
|
||||||
|
let difference = CameraConfiguration.Difference(between: self.configuration, and: config)
|
||||||
|
|
||||||
|
ReactLogger.log(level: .info, message: "configure { ... }: Updating CameraSession Configuration... \(difference)")
|
||||||
|
|
||||||
do {
|
do {
|
||||||
// If needed, configure the AVCaptureSession (inputs, outputs)
|
// If needed, configure the AVCaptureSession (inputs, outputs)
|
||||||
|
Loading…
Reference in New Issue
Block a user