diff --git a/package/android/build.gradle b/package/android/build.gradle index 88ea6a5b7b..d937274a7a 100644 --- a/package/android/build.gradle +++ b/package/android/build.gradle @@ -167,7 +167,18 @@ android { dependencies { //noinspection GradleDynamicVersion implementation "com.facebook.react:react-android:+" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3" + + // CameraX dependency + def camerax_version = "1.3.1" + implementation "androidx.camera:camera-core:${camerax_version}" + implementation "androidx.camera:camera-camera2:${camerax_version}" + implementation "androidx.camera:camera-lifecycle:${camerax_version}" + implementation "androidx.camera:camera-video:${camerax_version}" + implementation "androidx.camera:camera-view:${camerax_version}" + implementation "androidx.camera:camera-extensions:${camerax_version}" + + // Some Coroutines extension functions + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0" if (enableCodeScanner) { // User enabled code-scanner, so we bundle the 2.4 MB model in the app. diff --git a/package/android/src/main/java/com/mrousavy/camera/CameraView.kt b/package/android/src/main/java/com/mrousavy/camera/CameraView.kt index bbff0795b1..bb53f4b35b 100644 --- a/package/android/src/main/java/com/mrousavy/camera/CameraView.kt +++ b/package/android/src/main/java/com/mrousavy/camera/CameraView.kt @@ -7,12 +7,12 @@ import android.util.Log import android.view.Gravity import android.view.ScaleGestureDetector import android.widget.FrameLayout +import androidx.camera.view.PreviewView 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 @@ -78,7 +78,7 @@ class CameraView(context: Context) : } var resizeMode: ResizeMode = ResizeMode.COVER set(value) { - previewView.resizeMode = value + previewView.scaleType = value.toScaleType() field = value } var enableFpsGraph = false @@ -152,6 +152,9 @@ class CameraView(context: Context) : // Input Camera Device config.cameraId = cameraId + // Preview + config.preview = CameraConfiguration.Output.Enabled.create(CameraConfiguration.Preview(previewView.surfaceProvider)) + // Photo if (photo) { config.photo = CameraConfiguration.Output.Enabled.create(CameraConfiguration.Photo(photoHdr)) diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraConfiguration.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraConfiguration.kt index 593d40a4a5..b70b6175c8 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraConfiguration.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraConfiguration.kt @@ -1,6 +1,6 @@ package com.mrousavy.camera.core -import android.view.Surface +import androidx.camera.core.Preview.SurfaceProvider import com.mrousavy.camera.types.CameraDeviceFormat import com.mrousavy.camera.types.CodeType import com.mrousavy.camera.types.Orientation @@ -46,7 +46,7 @@ data class CameraConfiguration( data class Photo(val enableHdr: Boolean) data class Video(val enableHdr: Boolean, val pixelFormat: PixelFormat, val enableFrameProcessor: Boolean, val enableGpuBuffers: Boolean) data class Audio(val nothing: Unit) - data class Preview(val surface: Surface) + data class Preview(val surfaceProvider: SurfaceProvider) @Suppress("EqualsOrHashCode") sealed class Output { diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt index 893199ef1f..9703745486 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt @@ -12,16 +12,23 @@ import android.hardware.camera2.TotalCaptureResult import android.media.Image import android.media.ImageReader import android.util.Log +import android.util.Range import android.util.Size -import android.view.Surface -import android.view.SurfaceHolder +import androidx.camera.core.Camera +import androidx.camera.core.CameraSelector +import androidx.camera.core.Preview +import androidx.camera.lifecycle.ProcessCameraProvider import androidx.core.content.ContextCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry import com.google.mlkit.vision.barcode.common.Barcode -import com.mrousavy.camera.core.capture.RepeatingCaptureRequest 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.await +import com.mrousavy.camera.extensions.byId import com.mrousavy.camera.extensions.closestToOrMax import com.mrousavy.camera.frameprocessor.Frame import com.mrousavy.camera.types.Flash @@ -33,41 +40,27 @@ import com.mrousavy.camera.utils.ImageFormatUtils import java.io.Closeable import kotlin.coroutines.cancellation.CancellationException import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock class CameraSession(private val context: Context, private val cameraManager: CameraManager, private val callback: Callback) : - Closeable, - PersistentCameraCaptureSession.Callback { + Closeable, LifecycleOwner { companion object { private const val TAG = "CameraSession" } // Camera Configuration private var configuration: CameraConfiguration? = null + private val cameraProvider = ProcessCameraProvider.getInstance(context) + private var camera: Camera? = null // Camera State - private val captureSession = PersistentCameraCaptureSession(cameraManager, this) private var photoOutput: PhotoOutput? = null private var videoOutput: VideoPipelineOutput? = null private var codeScannerOutput: BarcodeScannerOutput? = null - private var previewView: PreviewView? = null - private val photoOutputSynchronizer = PhotoOutputSynchronizer() private val mutex = Mutex() private var isDestroyed = false - private var isRunning = false - set(value) { - if (field != value) { - if (value) { - callback.onStarted() - } else { - callback.onStopped() - } - } - field = value - } + private val lifecycleRegistry = LifecycleRegistry(this) private val coroutineScope = CoroutineScope(CameraQueues.cameraQueue.coroutineDispatcher) @@ -86,18 +79,28 @@ class CameraSession(private val context: Context, private val cameraManager: Cam return Orientation.fromRotationDegrees(sensorRotation) } + init { + lifecycleRegistry.currentState = Lifecycle.State.CREATED + } + override fun close() { Log.i(TAG, "Closing CameraSession...") isDestroyed = true - runBlocking { - mutex.withLock { - destroy() - photoOutputSynchronizer.clear() - } - } + lifecycleRegistry.currentState = Lifecycle.State.DESTROYED + + photoOutput?.close() + photoOutput = null + videoOutput?.close() + videoOutput = null + codeScannerOutput?.close() + codeScannerOutput = null Log.i(TAG, "CameraSession closed!") } + override fun getLifecycle(): Lifecycle { + return lifecycleRegistry + } + suspend fun configure(lambda: (configuration: CameraConfiguration) -> Unit) { Log.i(TAG, "configure { ... }: Waiting for lock...") @@ -121,247 +124,65 @@ class CameraSession(private val context: Context, private val cameraManager: Cam Log.i(TAG, "configure { ... }: Updating CameraSession Configuration... $diff") try { - captureSession.withConfiguration { - // Build up session or update any props - if (diff.deviceChanged) { - // 1. cameraId changed, open device - configureInput(config) - } - if (diff.outputsChanged) { - // 2. outputs changed, build new session - configureOutputs(config) - } - if (diff.sidePropsChanged) { - // 3. zoom etc changed, update repeating request - configureCaptureRequest(config) - } - if (diff.isActiveChanged) { - // 4. Either start or stop the session - val isActive = config.isActive && config.preview.isEnabled - captureSession.setIsActive(isActive) - } + // Build up session or update any props + if (diff.deviceChanged || diff.outputsChanged) { + // 1. cameraId changed, open device + configureCamera(config) + } + if (diff.isActiveChanged) { + // 4. Either start or stop the session + configureIsActive(config) } Log.i( TAG, - "configure { ... }: Completed CameraSession Configuration! (isActive: ${config.isActive}, isRunning: ${captureSession.isRunning})" + "configure { ... }: Completed CameraSession Configuration! (State: ${lifecycle.currentState})" ) - isRunning = captureSession.isRunning // Notify about Camera initialization if (diff.deviceChanged) { callback.onInitialized() } } catch (error: Throwable) { - Log.e(TAG, "Failed to configure CameraSession! Error: ${error.message}, isRunning: $isRunning, Config-Diff: $diff", error) + 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.close() - - 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() - } - } - ) - this.previewView = previewView - return previewView - } - - private fun createPreviewOutput(surface: Surface) { - Log.i(TAG, "Setting Preview Output...") - coroutineScope.launch { - configure { config -> - config.preview = CameraConfiguration.Output.Enabled.create(CameraConfiguration.Preview(surface)) - } - } - } - - private fun destroyPreviewOutputSync() { - Log.i(TAG, "Destroying Preview Output...") - // This needs to run synchronously because after this method returns, the Preview Surface is no longer valid, - // and trying to use it will crash. This might result in a short UI Thread freeze though. - runBlocking { - configure { config -> - config.preview = CameraConfiguration.Output.Disabled.create() - } - } - Log.i(TAG, "Preview Output destroyed!") - } - - private fun configureInput(configuration: CameraConfiguration) { - Log.i(TAG, "Configuring inputs for CameraSession...") - val cameraId = configuration.cameraId ?: throw NoCameraDeviceError() + private fun checkPermission() { val status = ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) if (status != PackageManager.PERMISSION_GRANTED) throw CameraPermissionError() - isRunning = false - captureSession.setInput(cameraId) } - /** - * Set up the `CaptureSession` with all outputs (preview, photo, video, codeScanner) and their HDR/Format settings. - */ - private suspend fun configureOutputs(configuration: CameraConfiguration) { - val cameraId = configuration.cameraId ?: throw NoCameraDeviceError() - - // Destroy previous outputs - Log.i(TAG, "Destroying previous outputs...") - photoOutput?.close() - photoOutput = null - videoOutput?.close() - videoOutput = null - codeScannerOutput?.close() - codeScannerOutput = null - isRunning = false - - val deviceDetails = CameraDeviceDetails(cameraManager, cameraId) - val format = configuration.format - - Log.i(TAG, "Creating outputs for Camera #$cameraId...") - - val isSelfie = deviceDetails.lensFacing == LensFacing.FRONT - - val outputs = mutableListOf() - - // Photo Output - val photo = configuration.photo as? CameraConfiguration.Output.Enabled - if (photo != null) { - val imageFormat = deviceDetails.photoFormat - val sizes = deviceDetails.getPhotoSizes() - val size = sizes.closestToOrMax(format?.photoSize) - val maxImages = 10 - - 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) - imageReader.setOnImageAvailableListener({ reader -> - Log.i(TAG, "Photo Captured!") - val image = reader.acquireLatestImage() - onPhotoCaptured(image) - }, CameraQueues.cameraQueue.handler) - val output = PhotoOutput(imageReader, photo.config.enableHdr) - outputs.add(output) - photoOutput = output - } - - // Video Output - val video = configuration.video as? CameraConfiguration.Output.Enabled - if (video != null) { - val imageFormat = video.config.pixelFormat.toImageFormat() - val sizes = deviceDetails.getVideoSizes(imageFormat) - val size = sizes.closestToOrMax(format?.videoSize) - - Log.i(TAG, "Adding ${size.width}x${size.height} Video Output in ${ImageFormatUtils.imageFormatToString(imageFormat)}...") - val videoPipeline = VideoPipeline( - size.width, - size.height, - video.config.pixelFormat, - isSelfie, - video.config.enableFrameProcessor, - video.config.enableGpuBuffers, - callback - ) - val output = VideoPipelineOutput(videoPipeline, video.config.enableHdr) - outputs.add(output) - videoOutput = output - } + private suspend fun configureCamera(configuration: CameraConfiguration) { + Log.i(TAG, "Configuring Camera...") + checkPermission() - // Preview Output - val preview = configuration.preview as? CameraConfiguration.Output.Enabled - if (preview != null) { - // Compute Preview Size based on chosen video size - val videoSize = videoOutput?.size ?: format?.videoSize - val sizes = deviceDetails.getPreviewSizes() - val size = sizes.closestToOrMax(videoSize) - - val enableHdr = video?.config?.enableHdr ?: false - - Log.i(TAG, "Adding ${size.width}x${size.height} Preview Output...") - val output = SurfaceOutput( - preview.config.surface, - size, - SurfaceOutput.OutputType.PREVIEW, - enableHdr - ) - outputs.add(output) - // Size is usually landscape, so we flip it here - previewView?.setSurfaceSize(size.width, size.height, deviceDetails.sensorOrientation) - } + // Get the ProcessCameraProvider (should only await on the first call, then be an instant return) + val provider = cameraProvider.await() - // CodeScanner Output - val codeScanner = configuration.codeScanner as? CameraConfiguration.Output.Enabled - if (codeScanner != null) { - if (video != null) { - // CodeScanner and VideoPipeline are two repeating streams - they cannot be both added. - // In this case, the user should use a Frame Processor Plugin for code scanning instead. - throw CodeScannerTooManyOutputsError() - } - - val imageFormat = ImageFormat.YUV_420_888 - val sizes = deviceDetails.getVideoSizes(imageFormat) - val size = sizes.closestToOrMax(Size(1280, 720)) + // Input + val cameraId = configuration.cameraId ?: throw NoCameraDeviceError() + val cameraSelector = CameraSelector.Builder().byId(cameraId).build() - Log.i(TAG, "Adding ${size.width}x${size.height} CodeScanner Output in ${ImageFormatUtils.imageFormatToString(imageFormat)}...") - val pipeline = CodeScannerPipeline(size, imageFormat, codeScanner.config, callback) - val output = BarcodeScannerOutput(pipeline) - outputs.add(output) - codeScannerOutput = output + // Outputs + val preview = Preview.Builder() + configuration.fps?.let { fps -> + preview.setTargetFrameRate(Range(fps, fps)) } - // Create session - captureSession.setOutputs(outputs) - - Log.i(TAG, "Successfully configured Session with ${outputs.size} outputs for Camera #$cameraId!") - - // Update Frame Processor and RecordingSession for newly changed output - updateVideoOutputs() + // Bind it all together + camera = provider.bindToLifecycle(this, cameraSelector, preview.build()) } - private fun configureCaptureRequest(config: CameraConfiguration) { - val video = config.video as? CameraConfiguration.Output.Enabled - val enableVideo = video != null - val enableVideoHdr = video?.config?.enableHdr == true - - captureSession.setRepeatingRequest( - RepeatingCaptureRequest( - enableVideo, - config.torch, - config.fps, - config.videoStabilizationMode, - enableVideoHdr, - config.enableLowLightBoost, - config.exposure, - config.zoom, - config.format - ) - ) + private fun configureIsActive(config: CameraConfiguration) { + if (config.isActive) { + lifecycleRegistry.currentState = Lifecycle.State.RESUMED + } else { + // TODO: STARTED or CREATED? Which one keeps the camera warm? + lifecycleRegistry.currentState = Lifecycle.State.STARTED + } } suspend fun takePhoto( @@ -371,35 +192,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam enableAutoStabilization: Boolean, outputOrientation: Orientation ): CapturedPhoto { - val photoOutput = photoOutput ?: throw PhotoNotEnabledError() - - Log.i(TAG, "Photo capture 1/3 - capturing ${photoOutput.size.width}x${photoOutput.size.height} image...") - val result = captureSession.capture( - qualityPrioritization, - flash, - enableAutoStabilization, - photoOutput.enableHdr, - outputOrientation, - enableShutterSound - ) - - try { - val timestamp = result[CaptureResult.SENSOR_TIMESTAMP]!! - Log.i(TAG, "Photo capture 2/3 - waiting for image with timestamp $timestamp now...") - val image = photoOutputSynchronizer.await(timestamp) - - Log.i(TAG, "Photo capture 3/3 - received ${image.width} x ${image.height} image, preparing result...") - val deviceDetails = captureSession.getActiveDeviceDetails() - val isMirrored = deviceDetails?.lensFacing == LensFacing.FRONT - return CapturedPhoto(image, result, orientation, isMirrored, image.format) - } catch (e: CancellationException) { - throw CaptureAbortedError(false) - } - } - - private fun onPhotoCaptured(image: Image) { - Log.i(TAG, "Photo captured! ${image.width} x ${image.height}") - photoOutputSynchronizer.set(image.timestamp, image) + throw NotImplementedError() } private fun updateVideoOutputs() { @@ -414,28 +207,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam callback: (video: RecordingSession.Video) -> Unit, onError: (error: CameraError) -> Unit ) { - mutex.withLock { - if (recording != null) throw RecordingInProgressError() - val videoOutput = videoOutput ?: throw VideoNotEnabledError() - val cameraId = configuration?.cameraId ?: throw NoCameraDeviceError() - - val fps = configuration?.fps ?: 30 - - val recording = RecordingSession( - context, - cameraId, - videoOutput.size, - enableAudio, - fps, - videoOutput.enableHdr, - orientation, - options, - callback, - onError - ) - recording.start() - this.recording = recording - } + throw NotImplementedError() } suspend fun stopRecording() { @@ -461,16 +233,8 @@ class CameraSession(private val context: Context, private val cameraManager: Cam } } - override fun onError(error: Throwable) { - callback.onError(error) - } - suspend fun focus(x: Int, y: Int) { - val previewView = previewView ?: throw CameraNotReadyError() - val deviceDetails = captureSession.getActiveDeviceDetails() ?: throw CameraNotReadyError() - - val cameraPoint = previewView.convertLayerPointToCameraCoordinates(Point(x, y), deviceDetails) - captureSession.focus(cameraPoint) + throw NotImplementedError() } data class CapturedPhoto( diff --git a/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt b/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt deleted file mode 100644 index 6162dc2a6e..0000000000 --- a/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt +++ /dev/null @@ -1,376 +0,0 @@ -package com.mrousavy.camera.core - -import android.graphics.Point -import android.hardware.camera2.CameraAccessException -import android.hardware.camera2.CameraCaptureSession -import android.hardware.camera2.CameraDevice -import android.hardware.camera2.CameraManager -import android.hardware.camera2.CaptureRequest -import android.hardware.camera2.TotalCaptureResult -import android.util.Log -import com.mrousavy.camera.core.capture.PhotoCaptureRequest -import com.mrousavy.camera.core.capture.RepeatingCaptureRequest -import com.mrousavy.camera.core.outputs.SurfaceOutput -import com.mrousavy.camera.extensions.PrecaptureOptions -import com.mrousavy.camera.extensions.PrecaptureTrigger -import com.mrousavy.camera.extensions.capture -import com.mrousavy.camera.extensions.createCaptureSession -import com.mrousavy.camera.extensions.isValid -import com.mrousavy.camera.extensions.openCamera -import com.mrousavy.camera.extensions.precapture -import com.mrousavy.camera.extensions.tryAbortCaptures -import com.mrousavy.camera.extensions.tryStopRepeating -import com.mrousavy.camera.types.Flash -import com.mrousavy.camera.types.Orientation -import com.mrousavy.camera.types.QualityPrioritization -import java.io.Closeable -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock - -/** - * A [CameraCaptureSession] wrapper that safely handles interruptions and remains open whenever available. - * - * This class aims to be similar to Apple's `AVCaptureSession`. - */ -class PersistentCameraCaptureSession(private val cameraManager: CameraManager, private val callback: Callback) : Closeable { - companion object { - private const val TAG = "PersistentCameraCaptureSession" - private const val FOCUS_RESET_TIMEOUT = 3000L - private const val PRECAPTURE_LOCK_TIMEOUT = 5000L - } - - // Inputs/Dependencies - private var cameraId: String? = null - private var outputs: List = emptyList() - private var repeatingRequest: RepeatingCaptureRequest? = null - private var isActive = false - - // State/Dependants - private var device: CameraDevice? = null // depends on [cameraId] - private var session: CameraCaptureSession? = null // depends on [device, surfaceOutputs] - private var cameraDeviceDetails: CameraDeviceDetails? = null // depends on [device] - - private val mutex = Mutex() - private var didDestroyFromOutside = false - private var focusJob: Job? = null - private val coroutineScope = CoroutineScope(CameraQueues.cameraQueue.coroutineDispatcher) - - val isRunning: Boolean - get() = isActive && session != null && device != null && !didDestroyFromOutside - - override fun close() { - focusJob?.cancel() - session?.tryAbortCaptures() - device?.close() - } - - private fun assertLocked(method: String) { - if (!mutex.isLocked) { - throw SessionIsNotLockedError("Failed to call $method, session is not locked! Call beginConfiguration() first.") - } - } - - suspend fun withConfiguration(block: suspend () -> Unit) { - // Cancel any ongoing focus jobs - focusJob?.cancel() - focusJob = null - - mutex.withLock { - block() - configure() - } - } - - fun setInput(cameraId: String) { - Log.d(TAG, "--> setInput($cameraId)") - assertLocked("setInput") - if (this.cameraId != cameraId || device?.id != cameraId) { - this.cameraId = cameraId - - // Abort any captures in the session so we get the onCaptureFailed handler for any outstanding photos - session?.tryAbortCaptures() - session = null - // Closing the device will also close the session above - even faster than manually closing it. - device?.close() - device = null - } - } - - fun setOutputs(outputs: List) { - Log.d(TAG, "--> setOutputs($outputs)") - assertLocked("setOutputs") - if (this.outputs != outputs) { - this.outputs = outputs - - if (outputs.isNotEmpty()) { - // Outputs have changed to something else, we don't wanna destroy the session directly - // so the outputs can be kept warm. The session that gets created next will take over the outputs. - session?.tryAbortCaptures() - } else { - // Just stop it, we don't have any outputs - session?.close() - } - session = null - } - } - - fun setRepeatingRequest(request: RepeatingCaptureRequest) { - assertLocked("setRepeatingRequest") - Log.d(TAG, "--> setRepeatingRequest(...)") - if (this.repeatingRequest != request) { - this.repeatingRequest = request - } - } - - fun setIsActive(isActive: Boolean) { - assertLocked("setIsActive") - Log.d(TAG, "--> setIsActive($isActive)") - if (this.isActive != isActive) { - this.isActive = isActive - } - if (isActive && didDestroyFromOutside) { - didDestroyFromOutside = false - } - } - - suspend fun capture( - qualityPrioritization: QualityPrioritization, - flash: Flash, - enableAutoStabilization: Boolean, - enablePhotoHdr: Boolean, - orientation: Orientation, - enableShutterSound: Boolean - ): TotalCaptureResult { - // Cancel any ongoing focus jobs - focusJob?.cancel() - focusJob = null - - mutex.withLock { - Log.i(TAG, "Capturing photo...") - val session = session ?: throw CameraNotReadyError() - val repeatingRequest = repeatingRequest ?: throw CameraNotReadyError() - val photoRequest = PhotoCaptureRequest( - repeatingRequest, - qualityPrioritization, - enableAutoStabilization, - enablePhotoHdr, - orientation - ) - val device = session.device - val deviceDetails = getOrCreateCameraDeviceDetails(device) - - // Submit a single high-res capture to photo output as well as all preview outputs - val outputs = outputs - val repeatingOutputs = outputs.filter { it.isRepeating } - - if (qualityPrioritization == QualityPrioritization.SPEED && flash == Flash.OFF) { - // 0. We want to take a picture as fast as possible, so skip any precapture sequence and just capture one Frame. - Log.i(TAG, "Using fast capture path without pre-capture sequence...") - val singleRequest = photoRequest.createCaptureRequest(device, deviceDetails, outputs) - return session.capture(singleRequest.build(), enableShutterSound) - } - - Log.i(TAG, "Locking AF/AE/AWB...") - - // 1. Run precapture sequence - var needsFlash: Boolean - try { - val precaptureRequest = repeatingRequest.createCaptureRequest(device, deviceDetails, repeatingOutputs) - val skipIfPassivelyFocused = flash == Flash.OFF - val options = PrecaptureOptions( - listOf(PrecaptureTrigger.AF, PrecaptureTrigger.AE, PrecaptureTrigger.AWB), - flash, - emptyList(), - skipIfPassivelyFocused, - PRECAPTURE_LOCK_TIMEOUT - ) - val result = session.precapture(precaptureRequest, deviceDetails, options) - needsFlash = result.needsFlash - } catch (e: CaptureTimedOutError) { - // the precapture just timed out after 5 seconds, take picture anyways without focus. - needsFlash = false - } catch (e: FocusCanceledError) { - throw CaptureAbortedError(false) - } - - try { - // 2. Once precapture AF/AE/AWB successfully locked, capture the actual photo - val singleRequest = photoRequest.createCaptureRequest(device, deviceDetails, outputs) - if (needsFlash) { - singleRequest.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON) - singleRequest.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_SINGLE) - } - return session.capture(singleRequest.build(), enableShutterSound) - } finally { - // 3. After taking a photo we set the repeating request back to idle to remove the AE/AF/AWB locks again - val idleRequest = repeatingRequest.createCaptureRequest(device, deviceDetails, repeatingOutputs) - session.setRepeatingRequest(idleRequest.build(), null, null) - } - } - } - - suspend fun focus(point: Point) { - // Cancel any previous focus jobs - focusJob?.cancel() - focusJob = null - - mutex.withLock { - Log.i(TAG, "Focusing to $point...") - val session = session ?: throw CameraNotReadyError() - val repeatingRequest = repeatingRequest ?: throw CameraNotReadyError() - val device = session.device - val deviceDetails = getOrCreateCameraDeviceDetails(device) - if (!deviceDetails.supportsFocusRegions) { - throw FocusNotSupportedError() - } - val outputs = outputs.filter { it.isRepeating } - - // 1. Run a precapture sequence for AF, AE and AWB. - focusJob = coroutineScope.launch { - val request = repeatingRequest.createCaptureRequest(device, deviceDetails, outputs) - val options = - PrecaptureOptions(listOf(PrecaptureTrigger.AF, PrecaptureTrigger.AE), Flash.OFF, listOf(point), false, FOCUS_RESET_TIMEOUT) - session.precapture(request, deviceDetails, options) - } - focusJob?.join() - - // 2. Reset AF/AE/AWB again after 3 seconds timeout - focusJob = coroutineScope.launch { - delay(FOCUS_RESET_TIMEOUT) - if (!this.isActive) { - // this job got canceled from the outside - return@launch - } - if (!isRunning || this@PersistentCameraCaptureSession.session != session) { - // the view/session has already been destroyed in the meantime - return@launch - } - Log.i(TAG, "Resetting focus to auto-focus...") - repeatingRequest.createCaptureRequest(device, deviceDetails, outputs).also { request -> - session.setRepeatingRequest(request.build(), null, null) - } - } - } - } - - fun getActiveDeviceDetails(): CameraDeviceDetails? { - val device = device ?: return null - return getOrCreateCameraDeviceDetails(device) - } - - private suspend fun configure() { - if (didDestroyFromOutside && !isActive) { - Log.d(TAG, "CameraCaptureSession has been destroyed by Android, skipping configuration until isActive is set to `true` again.") - return - } - Log.d(TAG, "Configure() with isActive: $isActive, ID: $cameraId, device: $device, session: $session") - val cameraId = cameraId ?: throw NoCameraDeviceError() - val repeatingRequest = repeatingRequest ?: throw CameraNotReadyError() - val outputs = outputs - - try { - didDestroyFromOutside = false - - val device = getOrCreateDevice(cameraId) - if (didDestroyFromOutside) return - - if (outputs.isEmpty()) return - val session = getOrCreateSession(device, outputs) - if (didDestroyFromOutside) return - - if (isActive) { - Log.d(TAG, "Updating repeating request...") - val details = getOrCreateCameraDeviceDetails(device) - val repeatingOutputs = outputs.filter { it.isRepeating } - val builder = repeatingRequest.createCaptureRequest(device, details, repeatingOutputs) - session.setRepeatingRequest(builder.build(), null, null) - } else { - Log.d(TAG, "Stopping repeating request...") - session.tryStopRepeating() - } - Log.d(TAG, "Configure() done! isActive: $isActive, ID: $cameraId, device: $device, session: $session") - } catch (e: CameraAccessException) { - if (didDestroyFromOutside) { - // Camera device has been destroyed in the meantime, that's fine. - Log.d(TAG, "Configure() canceled, session has been destroyed in the meantime!") - } else { - // Camera should still be active, so not sure what went wrong. Rethrow - throw e - } - } - } - - private suspend fun getOrCreateDevice(cameraId: String): CameraDevice { - val currentDevice = device - if (currentDevice?.id == cameraId && currentDevice.isValid) { - return currentDevice - } - - this.session?.tryAbortCaptures() - this.device?.close() - this.device = null - this.session = null - - Log.i(TAG, "Creating new device...") - val newDevice = cameraManager.openCamera(cameraId, { device, error -> - Log.i(TAG, "Camera $device closed!") - if (this.device == device) { - this.didDestroyFromOutside = true - this.session?.tryAbortCaptures() - this.session = null - this.device = null - this.isActive = false - } - if (error != null) { - callback.onError(error) - } - }, CameraQueues.videoQueue) - this.device = newDevice - return newDevice - } - - private suspend fun getOrCreateSession(device: CameraDevice, outputs: List): CameraCaptureSession { - val currentSession = session - if (currentSession?.device == device) { - return currentSession - } - - if (outputs.isEmpty()) throw NoOutputsError() - - Log.i(TAG, "Creating new session...") - val newSession = device.createCaptureSession(cameraManager, outputs, { session -> - Log.i(TAG, "Session $session closed!") - if (this.session == session) { - this.didDestroyFromOutside = true - this.session?.tryAbortCaptures() - this.session = null - this.isActive = false - } - }, CameraQueues.videoQueue) - session = newSession - return newSession - } - - private fun getOrCreateCameraDeviceDetails(device: CameraDevice): CameraDeviceDetails { - val currentDetails = cameraDeviceDetails - if (currentDetails?.cameraId == device.id) { - return currentDetails - } - - val newDetails = CameraDeviceDetails(cameraManager, device.id) - cameraDeviceDetails = newDetails - return newDetails - } - - interface Callback { - fun onError(error: Throwable) - } - - class SessionIsNotLockedError(message: String) : Error(message) -} diff --git a/package/android/src/main/java/com/mrousavy/camera/core/PhotoOutputSynchronizer.kt b/package/android/src/main/java/com/mrousavy/camera/core/PhotoOutputSynchronizer.kt deleted file mode 100644 index 491ffa9bff..0000000000 --- a/package/android/src/main/java/com/mrousavy/camera/core/PhotoOutputSynchronizer.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.mrousavy.camera.core - -import android.media.Image -import kotlinx.coroutines.CompletableDeferred - -class PhotoOutputSynchronizer { - private val photoOutputQueue = HashMap>() - - private operator fun get(key: Long): CompletableDeferred { - if (!photoOutputQueue.containsKey(key)) { - photoOutputQueue[key] = CompletableDeferred() - } - return photoOutputQueue[key]!! - } - - suspend fun await(timestamp: Long): Image { - val image = this[timestamp].await() - photoOutputQueue.remove(timestamp) - return image - } - - fun set(timestamp: Long, image: Image) { - this[timestamp].complete(image) - } - - fun clear() { - photoOutputQueue.forEach { - it.value.cancel() - } - photoOutputQueue.clear() - } -} diff --git a/package/android/src/main/java/com/mrousavy/camera/core/PreviewView.kt b/package/android/src/main/java/com/mrousavy/camera/core/PreviewView.kt deleted file mode 100644 index 0727a1f82f..0000000000 --- a/package/android/src/main/java/com/mrousavy/camera/core/PreviewView.kt +++ /dev/null @@ -1,142 +0,0 @@ -package com.mrousavy.camera.core - -import android.annotation.SuppressLint -import android.content.Context -import android.graphics.Point -import android.util.Log -import android.util.Size -import android.view.SurfaceHolder -import android.view.SurfaceView -import com.facebook.react.bridge.UiThreadUtil -import com.mrousavy.camera.extensions.resize -import com.mrousavy.camera.extensions.rotatedBy -import com.mrousavy.camera.types.Orientation -import com.mrousavy.camera.types.ResizeMode -import kotlin.math.roundToInt -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -@SuppressLint("ViewConstructor") -class PreviewView(context: Context, callback: SurfaceHolder.Callback) : - SurfaceView(context), - SurfaceHolder.Callback { - var size: Size = CameraDeviceDetails.getMaximumPreviewSize() - set(value) { - if (field != value) { - Log.i(TAG, "Surface Size changed: $field -> $value") - field = value - updateLayout() - } - } - var resizeMode: ResizeMode = ResizeMode.COVER - set(value) { - if (field != value) { - Log.i(TAG, "Resize Mode changed: $field -> $value") - field = value - updateLayout() - } - } - private var inputOrientation: Orientation = Orientation.LANDSCAPE_LEFT - set(value) { - if (field != value) { - Log.i(TAG, "Input Orientation changed: $field -> $value") - field = value - updateLayout() - } - } - private val viewSize: Size - get() { - val displayMetrics = context.resources.displayMetrics - val dpX = width / displayMetrics.density - val dpY = height / displayMetrics.density - return Size(dpX.toInt(), dpY.toInt()) - } - - init { - Log.i(TAG, "Creating PreviewView...") - holder.setKeepScreenOn(true) - holder.addCallback(this) - holder.addCallback(callback) - holder.setFixedSize(size.width, size.height) - } - - override fun surfaceCreated(holder: SurfaceHolder) = Unit - override fun surfaceDestroyed(holder: SurfaceHolder) = Unit - override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { - size = Size(width, height) - } - - suspend fun setSurfaceSize(width: Int, height: Int, cameraSensorOrientation: Orientation) { - withContext(Dispatchers.Main) { - inputOrientation = cameraSensorOrientation - holder.resize(width, height) - } - } - - fun convertLayerPointToCameraCoordinates(point: Point, cameraDeviceDetails: CameraDeviceDetails): Point { - val sensorOrientation = cameraDeviceDetails.sensorOrientation - val cameraSize = Size(cameraDeviceDetails.activeSize.width(), cameraDeviceDetails.activeSize.height()) - val viewOrientation = Orientation.PORTRAIT - - val rotated = point.rotatedBy(viewSize, cameraSize, viewOrientation, sensorOrientation) - Log.i(TAG, "Converted layer point $point to camera point $rotated! ($sensorOrientation, $cameraSize -> $viewSize)") - return rotated - } - - private fun updateLayout() { - UiThreadUtil.runOnUiThread { - requestLayout() - invalidate() - } - } - - override fun requestLayout() { - super.requestLayout() - // Manually trigger measure & layout, as RN on Android skips those. - // See this issue: https://github.com/facebook/react-native/issues/17968#issuecomment-721958427 - post { - measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)) - layout(left, top, right, bottom) - } - } - - private fun getSize(contentSize: Size, containerSize: Size, resizeMode: ResizeMode): Size { - val contentAspectRatio = contentSize.width.toDouble() / contentSize.height - val containerAspectRatio = containerSize.width.toDouble() / containerSize.height - if (!(contentAspectRatio > 0 && containerAspectRatio > 0)) { - // One of the aspect ratios is 0 or NaN, maybe the view hasn't been laid out yet. - return contentSize - } - - val widthOverHeight = when (resizeMode) { - ResizeMode.COVER -> contentAspectRatio > containerAspectRatio - ResizeMode.CONTAIN -> contentAspectRatio < containerAspectRatio - } - - return if (widthOverHeight) { - // Scale by width to cover height - val scaledWidth = containerSize.height * contentAspectRatio - Size(scaledWidth.roundToInt(), containerSize.height) - } else { - // Scale by height to cover width - val scaledHeight = containerSize.width / contentAspectRatio - Size(containerSize.width, scaledHeight.roundToInt()) - } - } - - @SuppressLint("DrawAllocation") - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec) - - val viewSize = Size(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec)) - val surfaceSize = size.rotatedBy(inputOrientation) - val fittedSize = getSize(surfaceSize, viewSize, resizeMode) - - Log.i(TAG, "PreviewView is $viewSize, rendering $surfaceSize content ($inputOrientation). Resizing to: $fittedSize ($resizeMode)") - setMeasuredDimension(fittedSize.width, fittedSize.height) - } - - companion object { - private const val TAG = "PreviewView" - } -} diff --git a/package/android/src/main/java/com/mrousavy/camera/core/capture/CameraCaptureRequest.kt b/package/android/src/main/java/com/mrousavy/camera/core/capture/CameraCaptureRequest.kt deleted file mode 100644 index eeb52762d5..0000000000 --- a/package/android/src/main/java/com/mrousavy/camera/core/capture/CameraCaptureRequest.kt +++ /dev/null @@ -1,88 +0,0 @@ -package com.mrousavy.camera.core.capture - -import android.hardware.camera2.CameraDevice -import android.hardware.camera2.CaptureRequest -import com.mrousavy.camera.core.CameraDeviceDetails -import com.mrousavy.camera.core.FlashUnavailableError -import com.mrousavy.camera.core.InvalidVideoHdrError -import com.mrousavy.camera.core.LowLightBoostNotSupportedError -import com.mrousavy.camera.core.PropRequiresFormatToBeNonNullError -import com.mrousavy.camera.core.outputs.SurfaceOutput -import com.mrousavy.camera.extensions.setZoom -import com.mrousavy.camera.types.CameraDeviceFormat -import com.mrousavy.camera.types.Torch - -abstract class CameraCaptureRequest( - private val torch: Torch = Torch.OFF, - private val enableVideoHdr: Boolean = false, - val enableLowLightBoost: Boolean = false, - val exposureBias: Double? = null, - val zoom: Float = 1.0f, - val format: CameraDeviceFormat? = null -) { - enum class Template { - RECORD, - PHOTO, - PHOTO_ZSL, - PHOTO_SNAPSHOT, - PREVIEW; - - fun toRequestTemplate(): Int = - when (this) { - RECORD -> CameraDevice.TEMPLATE_RECORD - PHOTO -> CameraDevice.TEMPLATE_STILL_CAPTURE - PHOTO_ZSL -> CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG - PHOTO_SNAPSHOT -> CameraDevice.TEMPLATE_VIDEO_SNAPSHOT - PREVIEW -> CameraDevice.TEMPLATE_PREVIEW - } - } - - abstract fun createCaptureRequest( - device: CameraDevice, - deviceDetails: CameraDeviceDetails, - outputs: List - ): CaptureRequest.Builder - - protected open fun createCaptureRequest( - template: Template, - device: CameraDevice, - deviceDetails: CameraDeviceDetails, - outputs: List - ): CaptureRequest.Builder { - val builder = device.createCaptureRequest(template.toRequestTemplate()) - - // Add all repeating output surfaces - outputs.forEach { output -> - builder.addTarget(output.surface) - } - - // Set HDR - if (enableVideoHdr) { - if (format == null) throw PropRequiresFormatToBeNonNullError("videoHdr") - if (!format.supportsVideoHdr) throw InvalidVideoHdrError() - builder.set(CaptureRequest.CONTROL_SCENE_MODE, CaptureRequest.CONTROL_SCENE_MODE_HDR) - builder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_USE_SCENE_MODE) - } else if (enableLowLightBoost) { - if (!deviceDetails.supportsLowLightBoost) throw LowLightBoostNotSupportedError() - builder.set(CaptureRequest.CONTROL_SCENE_MODE, CaptureRequest.CONTROL_SCENE_MODE_NIGHT) - builder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_USE_SCENE_MODE) - } - - // Set Exposure Bias - if (exposureBias != null) { - val clamped = deviceDetails.exposureRange.clamp(exposureBias.toInt()) - builder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, clamped) - } - - // Set Zoom - builder.setZoom(zoom, deviceDetails) - - // Set Torch - if (torch == Torch.ON) { - if (!deviceDetails.hasFlash) throw FlashUnavailableError() - builder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH) - } - - return builder - } -} diff --git a/package/android/src/main/java/com/mrousavy/camera/core/capture/PhotoCaptureRequest.kt b/package/android/src/main/java/com/mrousavy/camera/core/capture/PhotoCaptureRequest.kt deleted file mode 100644 index 03ca3de61a..0000000000 --- a/package/android/src/main/java/com/mrousavy/camera/core/capture/PhotoCaptureRequest.kt +++ /dev/null @@ -1,149 +0,0 @@ -package com.mrousavy.camera.core.capture - -import android.hardware.camera2.CameraCharacteristics -import android.hardware.camera2.CameraDevice -import android.hardware.camera2.CaptureRequest -import android.os.Build -import android.util.Log -import com.mrousavy.camera.core.CameraDeviceDetails -import com.mrousavy.camera.core.outputs.SurfaceOutput -import com.mrousavy.camera.types.HardwareLevel -import com.mrousavy.camera.types.Orientation -import com.mrousavy.camera.types.QualityPrioritization -import com.mrousavy.camera.types.Torch - -class PhotoCaptureRequest( - repeatingRequest: RepeatingCaptureRequest, - private val qualityPrioritization: QualityPrioritization, - private val enableAutoStabilization: Boolean, - enablePhotoHdr: Boolean, - private val outputOrientation: Orientation -) : CameraCaptureRequest( - Torch.OFF, - enablePhotoHdr, - repeatingRequest.enableLowLightBoost, - repeatingRequest.exposureBias, - repeatingRequest.zoom, - repeatingRequest.format -) { - companion object { - private const val TAG = "PhotoCaptureRequest" - } - - override fun createCaptureRequest( - device: CameraDevice, - deviceDetails: CameraDeviceDetails, - outputs: List - ): CaptureRequest.Builder { - val template = when (qualityPrioritization) { - QualityPrioritization.QUALITY -> Template.PHOTO - QualityPrioritization.BALANCED -> { - if (deviceDetails.supportsZsl) { - Template.PHOTO_ZSL - } else { - Template.PHOTO - } - } - QualityPrioritization.SPEED -> { - if (deviceDetails.supportsSnapshotCapture) { - Template.PHOTO_SNAPSHOT - } else if (deviceDetails.supportsZsl) { - Template.PHOTO_ZSL - } else { - Template.PHOTO - } - } - } - Log.i(TAG, "Using CaptureRequest Template $template...") - return this.createCaptureRequest(template, device, deviceDetails, outputs) - } - - override fun createCaptureRequest( - template: Template, - device: CameraDevice, - deviceDetails: CameraDeviceDetails, - outputs: List - ): CaptureRequest.Builder { - val builder = super.createCaptureRequest(template, device, deviceDetails, outputs) - - // Set various speed vs quality optimization flags - when (qualityPrioritization) { - QualityPrioritization.SPEED -> { - if (deviceDetails.hardwareLevel.isAtLeast(HardwareLevel.FULL)) { - builder.set(CaptureRequest.COLOR_CORRECTION_MODE, CaptureRequest.COLOR_CORRECTION_MODE_FAST) - if (deviceDetails.availableEdgeModes.contains(CaptureRequest.EDGE_MODE_FAST)) { - builder.set(CaptureRequest.EDGE_MODE, CaptureRequest.EDGE_MODE_FAST) - } - } - if (deviceDetails.availableAberrationModes.contains(CaptureRequest.COLOR_CORRECTION_ABERRATION_MODE_FAST)) { - builder.set(CaptureRequest.COLOR_CORRECTION_ABERRATION_MODE, CaptureRequest.COLOR_CORRECTION_ABERRATION_MODE_FAST) - } - if (deviceDetails.availableHotPixelModes.contains(CaptureRequest.HOT_PIXEL_MODE_FAST)) { - builder.set(CaptureRequest.HOT_PIXEL_MODE, CaptureRequest.HOT_PIXEL_MODE_FAST) - } - if (deviceDetails.availableDistortionCorrectionModes.contains(CaptureRequest.DISTORTION_CORRECTION_MODE_FAST) && - Build.VERSION.SDK_INT >= Build.VERSION_CODES.P - ) { - builder.set(CaptureRequest.DISTORTION_CORRECTION_MODE, CaptureRequest.DISTORTION_CORRECTION_MODE_FAST) - } - if (deviceDetails.availableNoiseReductionModes.contains(CaptureRequest.NOISE_REDUCTION_MODE_FAST)) { - builder.set(CaptureRequest.NOISE_REDUCTION_MODE, CaptureRequest.NOISE_REDUCTION_MODE_FAST) - } - if (deviceDetails.availableShadingModes.contains(CaptureRequest.SHADING_MODE_FAST)) { - builder.set(CaptureRequest.SHADING_MODE, CaptureRequest.SHADING_MODE_FAST) - } - if (deviceDetails.availableToneMapModes.contains(CaptureRequest.TONEMAP_MODE_FAST)) { - builder.set(CaptureRequest.TONEMAP_MODE, CaptureRequest.TONEMAP_MODE_FAST) - } - builder.set(CaptureRequest.JPEG_QUALITY, 85) - } - QualityPrioritization.BALANCED -> { - builder.set(CaptureRequest.JPEG_QUALITY, 92) - } - QualityPrioritization.QUALITY -> { - if (deviceDetails.hardwareLevel.isAtLeast(HardwareLevel.FULL)) { - builder.set(CaptureRequest.COLOR_CORRECTION_MODE, CaptureRequest.COLOR_CORRECTION_MODE_HIGH_QUALITY) - if (deviceDetails.availableEdgeModes.contains(CaptureRequest.EDGE_MODE_HIGH_QUALITY)) { - builder.set(CaptureRequest.EDGE_MODE, CaptureRequest.EDGE_MODE_HIGH_QUALITY) - } - } - if (deviceDetails.availableAberrationModes.contains(CaptureRequest.COLOR_CORRECTION_ABERRATION_MODE_HIGH_QUALITY)) { - builder.set(CaptureRequest.COLOR_CORRECTION_ABERRATION_MODE, CaptureRequest.COLOR_CORRECTION_ABERRATION_MODE_HIGH_QUALITY) - } - if (deviceDetails.availableHotPixelModes.contains(CaptureRequest.HOT_PIXEL_MODE_HIGH_QUALITY)) { - builder.set(CaptureRequest.HOT_PIXEL_MODE, CaptureRequest.HOT_PIXEL_MODE_HIGH_QUALITY) - } - if (deviceDetails.availableDistortionCorrectionModes.contains(CaptureRequest.DISTORTION_CORRECTION_MODE_HIGH_QUALITY) && - Build.VERSION.SDK_INT >= Build.VERSION_CODES.P - ) { - builder.set(CaptureRequest.DISTORTION_CORRECTION_MODE, CaptureRequest.DISTORTION_CORRECTION_MODE_HIGH_QUALITY) - } - if (deviceDetails.availableNoiseReductionModes.contains(CaptureRequest.NOISE_REDUCTION_MODE_HIGH_QUALITY)) { - builder.set(CaptureRequest.NOISE_REDUCTION_MODE, CaptureRequest.NOISE_REDUCTION_MODE_HIGH_QUALITY) - } - if (deviceDetails.availableShadingModes.contains(CaptureRequest.SHADING_MODE_HIGH_QUALITY)) { - builder.set(CaptureRequest.SHADING_MODE, CaptureRequest.SHADING_MODE_HIGH_QUALITY) - } - if (deviceDetails.availableToneMapModes.contains(CaptureRequest.TONEMAP_MODE_HIGH_QUALITY)) { - builder.set(CaptureRequest.TONEMAP_MODE, CaptureRequest.TONEMAP_MODE_HIGH_QUALITY) - } - builder.set(CaptureRequest.JPEG_QUALITY, 100) - } - } - - // Set JPEG Orientation - val targetOrientation = outputOrientation.toSensorRelativeOrientation(deviceDetails) - builder.set(CaptureRequest.JPEG_ORIENTATION, targetOrientation.toDegrees()) - - // Set stabilization for this Frame - if (enableAutoStabilization) { - if (deviceDetails.opticalStabilizationModes.contains(CameraCharacteristics.LENS_OPTICAL_STABILIZATION_MODE_ON)) { - builder.set(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE, CameraCharacteristics.LENS_OPTICAL_STABILIZATION_MODE_ON) - } else if (deviceDetails.digitalStabilizationModes.contains(CameraCharacteristics.CONTROL_VIDEO_STABILIZATION_MODE_ON)) { - builder.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_ON) - } - } - - return builder - } -} diff --git a/package/android/src/main/java/com/mrousavy/camera/core/capture/RepeatingCaptureRequest.kt b/package/android/src/main/java/com/mrousavy/camera/core/capture/RepeatingCaptureRequest.kt deleted file mode 100644 index a95ca29a0a..0000000000 --- a/package/android/src/main/java/com/mrousavy/camera/core/capture/RepeatingCaptureRequest.kt +++ /dev/null @@ -1,113 +0,0 @@ -package com.mrousavy.camera.core.capture - -import android.hardware.camera2.CameraCharacteristics -import android.hardware.camera2.CameraDevice -import android.hardware.camera2.CaptureRequest -import android.os.Build -import android.util.Range -import com.mrousavy.camera.core.CameraDeviceDetails -import com.mrousavy.camera.core.InvalidFpsError -import com.mrousavy.camera.core.InvalidVideoStabilizationMode -import com.mrousavy.camera.core.PropRequiresFormatToBeNonNullError -import com.mrousavy.camera.core.outputs.SurfaceOutput -import com.mrousavy.camera.types.CameraDeviceFormat -import com.mrousavy.camera.types.HardwareLevel -import com.mrousavy.camera.types.Torch -import com.mrousavy.camera.types.VideoStabilizationMode - -class RepeatingCaptureRequest( - private val enableVideoPipeline: Boolean, - torch: Torch = Torch.OFF, - private val fps: Int? = null, - private val videoStabilizationMode: VideoStabilizationMode = VideoStabilizationMode.OFF, - enableVideoHdr: Boolean = false, - enableLowLightBoost: Boolean = false, - exposureBias: Double? = null, - zoom: Float = 1.0f, - format: CameraDeviceFormat? = null -) : CameraCaptureRequest(torch, enableVideoHdr, enableLowLightBoost, exposureBias, zoom, format) { - override fun createCaptureRequest( - device: CameraDevice, - deviceDetails: CameraDeviceDetails, - outputs: List - ): CaptureRequest.Builder { - val template = if (enableVideoPipeline) Template.RECORD else Template.PREVIEW - return this.createCaptureRequest(template, device, deviceDetails, outputs) - } - - private fun getBestDigitalStabilizationMode(deviceDetails: CameraDeviceDetails): Int { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - if (deviceDetails.digitalStabilizationModes.contains(CameraCharacteristics.CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION)) { - return CameraCharacteristics.CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION - } - } - return CameraCharacteristics.CONTROL_VIDEO_STABILIZATION_MODE_ON - } - - override fun createCaptureRequest( - template: Template, - device: CameraDevice, - deviceDetails: CameraDeviceDetails, - outputs: List - ): CaptureRequest.Builder { - val builder = super.createCaptureRequest(template, device, deviceDetails, outputs) - - if (deviceDetails.modes.contains(CameraCharacteristics.CONTROL_MODE_AUTO)) { - builder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO) - } - - // Set AF - if (enableVideoPipeline && deviceDetails.afModes.contains(CameraCharacteristics.CONTROL_AF_MODE_CONTINUOUS_VIDEO)) { - builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO) - } else if (deviceDetails.afModes.contains(CameraCharacteristics.CONTROL_AF_MODE_CONTINUOUS_PICTURE)) { - builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE) - } else if (deviceDetails.afModes.contains(CameraCharacteristics.CONTROL_AF_MODE_AUTO)) { - builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO) - } else if (deviceDetails.afModes.contains(CameraCharacteristics.CONTROL_AF_MODE_OFF)) { - builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF) - builder.set(CaptureRequest.LENS_FOCUS_DISTANCE, 0f) - } - - // Set AE - if (deviceDetails.aeModes.contains(CameraCharacteristics.CONTROL_AE_MODE_ON)) { - builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON) - } else if (deviceDetails.aeModes.contains(CameraCharacteristics.CONTROL_AE_MODE_OFF)) { - builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF) - } - - // Set AWB - if (deviceDetails.awbModes.contains(CameraCharacteristics.CONTROL_AWB_MODE_AUTO)) { - builder.set(CaptureRequest.CONTROL_AWB_MODE, CaptureRequest.CONTROL_AWB_MODE_AUTO) - } - - // Set FPS - if (fps != null) { - if (format == null) throw PropRequiresFormatToBeNonNullError("fps") - if (format.maxFps < fps) throw InvalidFpsError(fps) - builder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, Range(fps, fps)) - } - - // Set Video Stabilization - if (videoStabilizationMode != VideoStabilizationMode.OFF) { - if (format == null) throw PropRequiresFormatToBeNonNullError("videoStabilizationMode") - if (!format.videoStabilizationModes.contains(videoStabilizationMode)) { - throw InvalidVideoStabilizationMode(videoStabilizationMode) - } - when (videoStabilizationMode) { - VideoStabilizationMode.STANDARD -> { - builder.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, getBestDigitalStabilizationMode(deviceDetails)) - } - VideoStabilizationMode.CINEMATIC, VideoStabilizationMode.CINEMATIC_EXTENDED -> { - if (deviceDetails.hardwareLevel.isAtLeast(HardwareLevel.LIMITED)) { - builder.set(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE, CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE_ON) - } else { - builder.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, getBestDigitalStabilizationMode(deviceDetails)) - } - } - else -> throw InvalidVideoStabilizationMode(videoStabilizationMode) - } - } - - return builder - } -} diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+capture.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+capture.kt deleted file mode 100644 index 4ff3e3f6d3..0000000000 --- a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+capture.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.mrousavy.camera.extensions - -import android.hardware.camera2.CameraCaptureSession -import android.hardware.camera2.CaptureFailure -import android.hardware.camera2.CaptureRequest -import android.hardware.camera2.TotalCaptureResult -import android.media.MediaActionSound -import android.util.Log -import com.mrousavy.camera.core.CaptureAbortedError -import com.mrousavy.camera.core.CaptureTimedOutError -import com.mrousavy.camera.core.UnknownCaptureError -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.suspendCancellableCoroutine - -private const val TAG = "CameraCaptureSession" - -suspend fun CameraCaptureSession.capture(captureRequest: CaptureRequest, enableShutterSound: Boolean): TotalCaptureResult = - suspendCancellableCoroutine { continuation -> - val shutterSound = if (enableShutterSound) MediaActionSound() else null - shutterSound?.load(MediaActionSound.SHUTTER_CLICK) - - CoroutineScope(Dispatchers.Default).launch { - delay(5000) // after 5s, cancel capture - if (continuation.isActive) { - Log.e(TAG, "Capture timed out after 5 seconds!") - continuation.resumeWithException(CaptureTimedOutError()) - tryAbortCaptures() - } - } - - this.capture( - captureRequest, - object : CameraCaptureSession.CaptureCallback() { - override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) { - super.onCaptureCompleted(session, request, result) - - if (request == captureRequest) { - continuation.resume(result) - shutterSound?.release() - } - } - - override fun onCaptureStarted(session: CameraCaptureSession, request: CaptureRequest, timestamp: Long, frameNumber: Long) { - super.onCaptureStarted(session, request, timestamp, frameNumber) - - if (request == captureRequest) { - if (enableShutterSound) { - shutterSound?.play(MediaActionSound.SHUTTER_CLICK) - } - } - } - - override fun onCaptureFailed(session: CameraCaptureSession, request: CaptureRequest, failure: CaptureFailure) { - super.onCaptureFailed(session, request, failure) - - if (request == captureRequest) { - val wasImageCaptured = failure.wasImageCaptured() - val error = when (failure.reason) { - CaptureFailure.REASON_ERROR -> UnknownCaptureError(wasImageCaptured) - CaptureFailure.REASON_FLUSHED -> CaptureAbortedError(wasImageCaptured) - else -> UnknownCaptureError(wasImageCaptured) - } - continuation.resumeWithException(error) - } - } - }, - null - ) - } diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+precapture.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+precapture.kt deleted file mode 100644 index e52ad2c198..0000000000 --- a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+precapture.kt +++ /dev/null @@ -1,151 +0,0 @@ -package com.mrousavy.camera.extensions - -import android.graphics.Point -import android.hardware.camera2.CameraCaptureSession -import android.hardware.camera2.CaptureRequest -import android.hardware.camera2.CaptureResult -import android.hardware.camera2.params.MeteringRectangle -import android.util.Log -import android.util.Size -import com.mrousavy.camera.core.CameraDeviceDetails -import com.mrousavy.camera.core.FocusCanceledError -import com.mrousavy.camera.types.Flash -import com.mrousavy.camera.types.HardwareLevel -import kotlin.coroutines.coroutineContext -import kotlinx.coroutines.isActive - -data class PrecaptureOptions( - val modes: List, - val flash: Flash = Flash.OFF, - val pointsOfInterest: List, - val skipIfPassivelyFocused: Boolean, - val timeoutMs: Long -) - -data class PrecaptureResult(val needsFlash: Boolean) - -private const val TAG = "Precapture" -private val DEFAULT_METERING_SIZE = Size(100, 100) - -/** - * Run a precapture sequence to trigger an AF, AE or AWB scan and lock to the optimal values. - * After this function completes, you can capture high quality photos as AF/AE/AWB are in focused state. - * - * To reset to auto-focus again, create a new `RepeatingRequest` with a fresh set of CONTROL_MODEs set. - */ -suspend fun CameraCaptureSession.precapture( - request: CaptureRequest.Builder, - deviceDetails: CameraDeviceDetails, - options: PrecaptureOptions -): PrecaptureResult { - Log.i(TAG, "Running precapture sequence... ($options)") - request.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO) - - var enableFlash = options.flash == Flash.ON - var afState = FocusState.Inactive - var aeState = ExposureState.Inactive - var awbState = WhiteBalanceState.Inactive - val precaptureModes = options.modes.toMutableList() - - // 1. Cancel any ongoing precapture sequences - request.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_CANCEL) - request.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL) - if (options.flash == Flash.AUTO || options.skipIfPassivelyFocused) { - // We want to read the current AE/AF/AWB values to determine if we need flash or can skip AF/AE/AWB precapture - val result = this.capture(request.build(), false) - - afState = FocusState.fromAFState(result.get(CaptureResult.CONTROL_AF_STATE) ?: CaptureResult.CONTROL_AF_STATE_INACTIVE) - aeState = ExposureState.fromAEState(result.get(CaptureResult.CONTROL_AE_STATE) ?: CaptureResult.CONTROL_AE_STATE_INACTIVE) - awbState = WhiteBalanceState.fromAWBState(result.get(CaptureResult.CONTROL_AWB_STATE) ?: CaptureResult.CONTROL_AWB_STATE_INACTIVE) - - Log.i(TAG, "Precapture current states: AF: $afState, AE: $aeState, AWB: $awbState") - enableFlash = aeState == ExposureState.FlashRequired && options.flash == Flash.AUTO - } else { - // we either want Flash ON or OFF, so we don't care about lighting conditions - do a fast capture. - this.capture(request.build(), null, null) - } - - if (!coroutineContext.isActive) throw FocusCanceledError() - - val meteringWeight = MeteringRectangle.METERING_WEIGHT_MAX - 1 - val meteringRectangles = options.pointsOfInterest.map { point -> - MeteringRectangle(point, DEFAULT_METERING_SIZE, meteringWeight) - }.toTypedArray() - - if (options.skipIfPassivelyFocused) { - // If user allows us to skip precapture for values that are already focused, remove them from the precapture modes. - if (afState.isPassivelyFocused) { - Log.i(TAG, "AF is already focused, skipping...") - precaptureModes.remove(PrecaptureTrigger.AF) - } - if (aeState.isPassivelyFocused) { - Log.i(TAG, "AE is already focused, skipping...") - precaptureModes.remove(PrecaptureTrigger.AE) - } - if (awbState.isPassivelyFocused) { - Log.i(TAG, "AWB is already focused, skipping...") - precaptureModes.remove(PrecaptureTrigger.AWB) - } - } - - // 2. Submit a precapture start sequence - if (enableFlash && deviceDetails.hasFlash) { - request.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH) - } - if (precaptureModes.contains(PrecaptureTrigger.AF)) { - // AF Precapture - if (deviceDetails.afModes.contains(CaptureRequest.CONTROL_AF_MODE_AUTO)) { - request.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO) - request.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START) - if (meteringRectangles.isNotEmpty() && deviceDetails.supportsFocusRegions) { - request.set(CaptureRequest.CONTROL_AF_REGIONS, meteringRectangles) - } - } else { - // AF is not supported on this device. - precaptureModes.remove(PrecaptureTrigger.AF) - } - } - if (precaptureModes.contains(PrecaptureTrigger.AE)) { - // AE Precapture - if (deviceDetails.aeModes.contains(CaptureRequest.CONTROL_AE_MODE_ON) && deviceDetails.hardwareLevel.isAtLeast(HardwareLevel.LIMITED)) { - request.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON) - request.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START) - if (meteringRectangles.isNotEmpty() && - deviceDetails.supportsExposureRegions && - deviceDetails.hardwareLevel.isAtLeast(HardwareLevel.LIMITED) - ) { - request.set(CaptureRequest.CONTROL_AE_REGIONS, meteringRectangles) - } - } else { - // AE is not supported on this device. - precaptureModes.remove(PrecaptureTrigger.AE) - } - } - if (precaptureModes.contains(PrecaptureTrigger.AWB)) { - // AWB Precapture - if (deviceDetails.awbModes.contains(CaptureRequest.CONTROL_AWB_MODE_AUTO)) { - request.set(CaptureRequest.CONTROL_AWB_MODE, CaptureRequest.CONTROL_AWB_MODE_AUTO) - if (meteringRectangles.isNotEmpty() && deviceDetails.supportsWhiteBalanceRegions) { - request.set(CaptureRequest.CONTROL_AWB_REGIONS, meteringRectangles) - } - } else { - // AWB is not supported on this device. - precaptureModes.remove(PrecaptureTrigger.AWB) - } - } - this.capture(request.build(), null, null) - - if (!coroutineContext.isActive) throw FocusCanceledError() - - // 3. Start a repeating request without the trigger and wait until AF/AE/AWB locks - request.set(CaptureRequest.CONTROL_AF_TRIGGER, null) - request.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, null) - val result = this.setRepeatingRequestAndWaitForPrecapture(request.build(), options.timeoutMs, *precaptureModes.toTypedArray()) - - if (!coroutineContext.isActive) throw FocusCanceledError() - - Log.i(TAG, "AF/AE/AWB successfully locked!") - - val needsFlash = result.exposureState == ExposureState.FlashRequired - return PrecaptureResult(needsFlash) -} diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+setRepeatingRequestAndWaitForPrecapture.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+setRepeatingRequestAndWaitForPrecapture.kt deleted file mode 100644 index 48ca860260..0000000000 --- a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+setRepeatingRequestAndWaitForPrecapture.kt +++ /dev/null @@ -1,193 +0,0 @@ -package com.mrousavy.camera.extensions - -import android.hardware.camera2.CameraCaptureSession -import android.hardware.camera2.CaptureFailure -import android.hardware.camera2.CaptureRequest -import android.hardware.camera2.CaptureResult -import android.hardware.camera2.TotalCaptureResult -import android.util.Log -import com.mrousavy.camera.core.CaptureAbortedError -import com.mrousavy.camera.core.CaptureTimedOutError -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.suspendCancellableCoroutine - -private const val TAG = "CameraCaptureSession" - -enum class PrecaptureTrigger { - AE, - AF, - AWB -} - -interface AutoState { - val isCompleted: Boolean - val isPassivelyFocused: Boolean -} - -enum class FocusState : AutoState { - Unknown, - Inactive, - Scanning, - Focused, - Unfocused, - PassiveScanning, - PassiveFocused, - PassiveUnfocused; - - override val isCompleted: Boolean - get() = this == Focused || this == Unfocused - override val isPassivelyFocused: Boolean - get() = this == PassiveFocused - - companion object { - fun fromAFState(afState: Int): FocusState = - when (afState) { - CaptureResult.CONTROL_AF_STATE_INACTIVE -> Inactive - CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN -> Scanning - CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED -> Focused - CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED -> Unfocused - CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN -> PassiveScanning - CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED -> PassiveFocused - CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED -> PassiveUnfocused - else -> Unknown - } - } -} -enum class ExposureState : AutoState { - Unknown, - Locked, - Inactive, - Precapture, - Searching, - Converged, - FlashRequired; - - override val isCompleted: Boolean - get() = this == Converged || this == FlashRequired - override val isPassivelyFocused: Boolean - get() = this == Converged - - companion object { - fun fromAEState(aeState: Int): ExposureState = - when (aeState) { - CaptureResult.CONTROL_AE_STATE_INACTIVE -> Inactive - CaptureResult.CONTROL_AE_STATE_SEARCHING -> Searching - CaptureResult.CONTROL_AE_STATE_PRECAPTURE -> Precapture - CaptureResult.CONTROL_AE_STATE_CONVERGED -> Converged - CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED -> FlashRequired - CaptureResult.CONTROL_AE_STATE_LOCKED -> Locked - else -> Unknown - } - } -} - -enum class WhiteBalanceState : AutoState { - Unknown, - Inactive, - Locked, - Searching, - Converged; - - override val isCompleted: Boolean - get() = this == Converged - override val isPassivelyFocused: Boolean - get() = this == Converged - - companion object { - fun fromAWBState(awbState: Int): WhiteBalanceState = - when (awbState) { - CaptureResult.CONTROL_AWB_STATE_INACTIVE -> Inactive - CaptureResult.CONTROL_AWB_STATE_SEARCHING -> Searching - CaptureResult.CONTROL_AWB_STATE_CONVERGED -> Converged - CaptureResult.CONTROL_AWB_STATE_LOCKED -> Locked - else -> Unknown - } - } -} - -data class ResultState(val focusState: FocusState, val exposureState: ExposureState, val whiteBalanceState: WhiteBalanceState) - -/** - * Set a new repeating request for the [CameraCaptureSession] that contains a precapture trigger, and wait until the given precaptures have locked. - */ -suspend fun CameraCaptureSession.setRepeatingRequestAndWaitForPrecapture( - request: CaptureRequest, - timeoutMs: Long, - vararg precaptureTriggers: PrecaptureTrigger -): ResultState = - suspendCancellableCoroutine { continuation -> - // Map of all completed precaptures - val completed = precaptureTriggers.associateWith { false }.toMutableMap() - - CoroutineScope(Dispatchers.Default).launch { - delay(timeoutMs) // after timeout, cancel capture - if (continuation.isActive) { - Log.e(TAG, "Precapture timed out after ${timeoutMs / 1000} seconds!") - continuation.resumeWithException(CaptureTimedOutError()) - try { - setRepeatingRequest(request, null, null) - } catch (e: Throwable) { - // session might have already been closed - Log.e(TAG, "Error resetting session repeating request..", e) - } - } - } - - this.setRepeatingRequest( - request, - object : CameraCaptureSession.CaptureCallback() { - override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) { - super.onCaptureCompleted(session, request, result) - - if (continuation.isActive) { - val afState = FocusState.fromAFState(result.get(CaptureResult.CONTROL_AF_STATE) ?: CaptureResult.CONTROL_AF_STATE_INACTIVE) - val aeState = ExposureState.fromAEState( - result.get(CaptureResult.CONTROL_AE_STATE) ?: CaptureResult.CONTROL_AE_STATE_INACTIVE - ) - val awbState = WhiteBalanceState.fromAWBState( - result.get(CaptureResult.CONTROL_AWB_STATE) ?: CaptureResult.CONTROL_AWB_STATE_INACTIVE - ) - Log.i(TAG, "Precapture state: AF: $afState, AE: $aeState, AWB: $awbState") - - // AF Precapture - if (precaptureTriggers.contains(PrecaptureTrigger.AF)) { - completed[PrecaptureTrigger.AF] = afState.isCompleted - } - // AE Precapture - if (precaptureTriggers.contains(PrecaptureTrigger.AE)) { - completed[PrecaptureTrigger.AE] = aeState.isCompleted - } - // AWB Precapture - if (precaptureTriggers.contains(PrecaptureTrigger.AWB)) { - completed[PrecaptureTrigger.AWB] = awbState.isCompleted - } - - if (completed.values.all { it == true }) { - // All precaptures did complete! - continuation.resume(ResultState(afState, aeState, awbState)) - session.setRepeatingRequest(request, null, null) - } - } - } - override fun onCaptureFailed(session: CameraCaptureSession, request: CaptureRequest, failure: CaptureFailure) { - super.onCaptureFailed(session, request, failure) - - if (continuation.isActive) { - // Capture failed or session closed. - continuation.resumeWithException(CaptureAbortedError(failure.wasImageCaptured())) - try { - session.setRepeatingRequest(request, null, null) - } catch (e: Throwable) { - Log.e(TAG, "Failed to continue repeating request!", e) - } - } - } - }, - null - ) - } diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+tryAbortCaptures.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+tryAbortCaptures.kt deleted file mode 100644 index 24c64ce9b4..0000000000 --- a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+tryAbortCaptures.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.mrousavy.camera.extensions - -import android.hardware.camera2.CameraCaptureSession - -fun CameraCaptureSession.tryAbortCaptures() { - try { - abortCaptures() - } catch (_: Throwable) {} -} diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+tryStopRepeating.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+tryStopRepeating.kt deleted file mode 100644 index 0810978f15..0000000000 --- a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+tryStopRepeating.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.mrousavy.camera.extensions - -import android.hardware.camera2.CameraCaptureSession - -fun CameraCaptureSession.tryStopRepeating() { - try { - stopRepeating() - } catch (_: Throwable) {} -} diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraDevice+createCaptureSession.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraDevice+createCaptureSession.kt deleted file mode 100644 index 8726d5a1b7..0000000000 --- a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraDevice+createCaptureSession.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.mrousavy.camera.extensions - -import android.hardware.camera2.CameraCaptureSession -import android.hardware.camera2.CameraCharacteristics -import android.hardware.camera2.CameraDevice -import android.hardware.camera2.CameraManager -import android.hardware.camera2.params.SessionConfiguration -import android.os.Build -import android.util.Log -import com.mrousavy.camera.core.CameraQueues -import com.mrousavy.camera.core.CameraSessionCannotBeConfiguredError -import com.mrousavy.camera.core.outputs.SurfaceOutput -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlinx.coroutines.suspendCancellableCoroutine - -private const val TAG = "CreateCaptureSession" -private var sessionId = 1 - -suspend fun CameraDevice.createCaptureSession( - cameraManager: CameraManager, - outputs: List, - onClosed: (session: CameraCaptureSession) -> Unit, - queue: CameraQueues.CameraQueue -): CameraCaptureSession = - suspendCancellableCoroutine { continuation -> - val characteristics = cameraManager.getCameraCharacteristics(id) - val hardwareLevel = characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)!! - val sessionId = sessionId++ - Log.i( - TAG, - "Camera #$id: Creating Capture Session #$sessionId... " + - "(Hardware Level: $hardwareLevel | Outputs: [${outputs.joinToString()}])" - ) - - val callback = object : CameraCaptureSession.StateCallback() { - override fun onConfigured(session: CameraCaptureSession) { - Log.i(TAG, "Camera #$id: Successfully created CameraCaptureSession #$sessionId!") - continuation.resume(session) - } - - override fun onConfigureFailed(session: CameraCaptureSession) { - Log.e(TAG, "Camera #$id: Failed to create CameraCaptureSession #$sessionId!") - continuation.resumeWithException(CameraSessionCannotBeConfiguredError(id)) - } - - override fun onClosed(session: CameraCaptureSession) { - Log.i(TAG, "Camera #$id: CameraCaptureSession #$sessionId has been closed.") - super.onClosed(session) - onClosed(session) - } - } - - val configurations = outputs.map { it.toOutputConfiguration(characteristics) } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - Log.i(TAG, "Using new API (>=28)") - val config = SessionConfiguration(SessionConfiguration.SESSION_REGULAR, configurations, queue.executor, callback) - this.createCaptureSession(config) - } else { - Log.i(TAG, "Using legacy API (<28)") - this.createCaptureSessionByOutputConfigurations(configurations, callback, queue.handler) - } - } diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraDevice+isValid.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraDevice+isValid.kt deleted file mode 100644 index 4a991f45b0..0000000000 --- a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraDevice+isValid.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.mrousavy.camera.extensions - -import android.hardware.camera2.CameraDevice - -val CameraDevice.isValid: Boolean - get() { - try { - this.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW) - return true - } catch (e: Throwable) { - return false - } - } diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraInfo+id.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraInfo+id.kt new file mode 100644 index 0000000000..cf515114d4 --- /dev/null +++ b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraInfo+id.kt @@ -0,0 +1,15 @@ +package com.mrousavy.camera.extensions + +import android.annotation.SuppressLint +import androidx.camera.core.CameraInfo +import androidx.camera.core.impl.CameraInfoInternal + +val CameraInfo.id: String? + @SuppressLint("RestrictedApi") + get() { + val infoInternal = this as? CameraInfoInternal + if (infoInternal != null) { + return infoInternal.cameraId + } + return null + } diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraManager+openCamera.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraManager+openCamera.kt deleted file mode 100644 index 51cba0ee0c..0000000000 --- a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraManager+openCamera.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.mrousavy.camera.extensions - -import android.annotation.SuppressLint -import android.hardware.camera2.CameraDevice -import android.hardware.camera2.CameraManager -import android.os.Build -import android.util.Log -import com.mrousavy.camera.core.CameraCannotBeOpenedError -import com.mrousavy.camera.core.CameraDisconnectedError -import com.mrousavy.camera.core.CameraQueues -import com.mrousavy.camera.types.CameraDeviceError -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlinx.coroutines.suspendCancellableCoroutine - -private const val TAG = "CameraManager" - -@SuppressLint("MissingPermission") -suspend fun CameraManager.openCamera( - cameraId: String, - onDisconnected: (camera: CameraDevice, error: Throwable?) -> Unit, - queue: CameraQueues.CameraQueue -): CameraDevice = - suspendCancellableCoroutine { continuation -> - Log.i(TAG, "Camera #$cameraId: Opening...") - - val callback = object : CameraDevice.StateCallback() { - override fun onOpened(camera: CameraDevice) { - Log.i(TAG, "Camera #$cameraId: Opened!") - continuation.resume(camera) - } - - override fun onDisconnected(camera: CameraDevice) { - Log.i(TAG, "Camera #$cameraId: Disconnected!") - if (continuation.isActive) { - continuation.resumeWithException(CameraCannotBeOpenedError(cameraId, CameraDeviceError.DISCONNECTED)) - } else { - onDisconnected(camera, null) - } - camera.close() - } - - override fun onError(camera: CameraDevice, errorCode: Int) { - Log.e(TAG, "Camera #$cameraId: Error! $errorCode") - val error = CameraDeviceError.fromCameraDeviceError(errorCode) - if (continuation.isActive) { - continuation.resumeWithException(CameraCannotBeOpenedError(cameraId, error)) - } else { - onDisconnected(camera, CameraDisconnectedError(cameraId, error)) - } - camera.close() - } - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - this.openCamera(cameraId, queue.executor, callback) - } else { - this.openCamera(cameraId, callback, queue.handler) - } - } diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraSelector+byId.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraSelector+byId.kt new file mode 100644 index 0000000000..eb96b2101b --- /dev/null +++ b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraSelector+byId.kt @@ -0,0 +1,7 @@ +package com.mrousavy.camera.extensions + +import androidx.camera.core.CameraSelector + +fun CameraSelector.Builder.byId(id: String): CameraSelector.Builder { + return addCameraFilter { cameraInfos -> cameraInfos.filter { it.id == id }.toMutableList() } +} diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/CaptureRequest+setZoom.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/CaptureRequest+setZoom.kt deleted file mode 100644 index d097cbc2d8..0000000000 --- a/package/android/src/main/java/com/mrousavy/camera/extensions/CaptureRequest+setZoom.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.mrousavy.camera.extensions - -import android.hardware.camera2.CaptureRequest -import android.os.Build -import com.mrousavy.camera.core.CameraDeviceDetails -import com.mrousavy.camera.types.HardwareLevel - -fun CaptureRequest.Builder.setZoom(zoom: Float, deviceDetails: CameraDeviceDetails) { - val zoomRange = deviceDetails.zoomRange - val zoomClamped = zoomRange.clamp(zoom) - - if (deviceDetails.hardwareLevel.isAtLeast(HardwareLevel.LIMITED) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - this.set(CaptureRequest.CONTROL_ZOOM_RATIO, zoomClamped) - } else { - val size = deviceDetails.activeSize - this.set(CaptureRequest.SCALER_CROP_REGION, size.zoomed(zoomClamped)) - } -} diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/ListenableFuture+await.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/ListenableFuture+await.kt new file mode 100644 index 0000000000..e6e0d7953d --- /dev/null +++ b/package/android/src/main/java/com/mrousavy/camera/extensions/ListenableFuture+await.kt @@ -0,0 +1,20 @@ +package com.mrousavy.camera.extensions + +import com.google.common.util.concurrent.ListenableFuture +import com.mrousavy.camera.core.CameraQueues +import kotlinx.coroutines.isActive +import kotlin.coroutines.cancellation.CancellationException +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +suspend fun ListenableFuture.await(): V { + if (this.isCancelled) throw CancellationException("ListenableFuture has been canceled!") + if (this.isDone) return this.get() + + return suspendCoroutine { continuation -> + this.addListener({ + if (this.isCancelled || !continuation.context.isActive) throw CancellationException("ListenableFuture has been canceled!") + continuation.resume(this.get()) + }, CameraQueues.cameraQueue.executor) + } +} diff --git a/package/android/src/main/java/com/mrousavy/camera/types/ResizeMode.kt b/package/android/src/main/java/com/mrousavy/camera/types/ResizeMode.kt index 8d03d17c68..7479cf92a6 100644 --- a/package/android/src/main/java/com/mrousavy/camera/types/ResizeMode.kt +++ b/package/android/src/main/java/com/mrousavy/camera/types/ResizeMode.kt @@ -1,11 +1,20 @@ package com.mrousavy.camera.types +import androidx.camera.core.ViewPort.ScaleType +import androidx.camera.view.PreviewView import com.mrousavy.camera.core.InvalidTypeScriptUnionError enum class ResizeMode(override val unionValue: String) : JSUnionValue { COVER("cover"), CONTAIN("contain"); + fun toScaleType(): PreviewView.ScaleType { + return when (this) { + COVER -> PreviewView.ScaleType.FILL_CENTER + CONTAIN -> PreviewView.ScaleType.FIT_CENTER + } + } + companion object : JSUnionValue.Companion { override fun fromUnionValue(unionValue: String?): ResizeMode = when (unionValue) {