package no.naiv.tiltshift.camera import android.content.Context import android.graphics.SurfaceTexture import android.util.Log import android.util.Size import android.view.Surface import androidx.camera.core.Camera import androidx.camera.core.CameraSelector import androidx.camera.core.ImageCapture import androidx.camera.core.Preview import androidx.camera.core.SurfaceRequest import androidx.camera.core.resolutionselector.AspectRatioStrategy import androidx.camera.core.resolutionselector.ResolutionSelector import androidx.camera.lifecycle.ProcessCameraProvider import androidx.core.content.ContextCompat import androidx.lifecycle.LifecycleOwner import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import java.util.concurrent.Executor /** * Manages CameraX camera setup and controls. */ class CameraManager(private val context: Context) { companion object { private const val TAG = "CameraManager" } private var cameraProvider: ProcessCameraProvider? = null private var camera: Camera? = null private var preview: Preview? = null var imageCapture: ImageCapture? = null private set val lensController = LensController() private val _error = MutableStateFlow(null) val error: StateFlow = _error.asStateFlow() fun clearError() { _error.value = null } private val _zoomRatio = MutableStateFlow(1.0f) val zoomRatio: StateFlow = _zoomRatio.asStateFlow() private val _minZoomRatio = MutableStateFlow(1.0f) val minZoomRatio: StateFlow = _minZoomRatio.asStateFlow() private val _maxZoomRatio = MutableStateFlow(1.0f) val maxZoomRatio: StateFlow = _maxZoomRatio.asStateFlow() private val _isFrontCamera = MutableStateFlow(false) val isFrontCamera: StateFlow = _isFrontCamera.asStateFlow() private var surfaceTextureProvider: (() -> SurfaceTexture?)? = null private var surfaceSize: Size = Size(1920, 1080) private var lifecycleOwnerRef: LifecycleOwner? = null /** * Starts the camera with the given lifecycle owner. * The surfaceTextureProvider should return the SurfaceTexture from the GL renderer. */ fun startCamera( lifecycleOwner: LifecycleOwner, surfaceTextureProvider: () -> SurfaceTexture? ) { this.surfaceTextureProvider = surfaceTextureProvider this.lifecycleOwnerRef = lifecycleOwner val cameraProviderFuture = ProcessCameraProvider.getInstance(context) cameraProviderFuture.addListener({ cameraProvider = cameraProviderFuture.get() lensController.initialize(cameraProvider?.availableCameraInfos ?: emptyList()) bindCameraUseCases(lifecycleOwner) }, ContextCompat.getMainExecutor(context)) } private fun bindCameraUseCases(lifecycleOwner: LifecycleOwner) { val provider = cameraProvider ?: return // Unbind all use cases before rebinding provider.unbindAll() // Preview use case with resolution selector val resolutionSelector = ResolutionSelector.Builder() .setAspectRatioStrategy(AspectRatioStrategy.RATIO_16_9_FALLBACK_AUTO_STRATEGY) .build() preview = Preview.Builder() .setResolutionSelector(resolutionSelector) .build() // Image capture use case val captureBuilder = ImageCapture.Builder() .setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY) imageCapture = captureBuilder.build() // Select camera based on front/back preference val cameraSelector = if (_isFrontCamera.value) { CameraSelector.DEFAULT_FRONT_CAMERA } else { // Use lens controller for back camera lens selection lensController.getCurrentLens()?.selector ?: CameraSelector.DEFAULT_BACK_CAMERA } try { // Bind use cases to camera camera = provider.bindToLifecycle( lifecycleOwner, cameraSelector, preview, imageCapture ) // Update zoom info camera?.cameraInfo?.let { info -> _minZoomRatio.value = info.zoomState.value?.minZoomRatio ?: 1.0f _maxZoomRatio.value = info.zoomState.value?.maxZoomRatio ?: 1.0f _zoomRatio.value = info.zoomState.value?.zoomRatio ?: 1.0f } // Set up surface provider for preview preview?.setSurfaceProvider { request -> provideSurface(request) } } catch (e: Exception) { Log.e(TAG, "Camera binding failed", e) _error.value = "Camera failed: ${e.message}" } } private fun provideSurface(request: SurfaceRequest) { val surfaceTexture = surfaceTextureProvider?.invoke() if (surfaceTexture == null) { request.willNotProvideSurface() return } surfaceSize = request.resolution surfaceTexture.setDefaultBufferSize(surfaceSize.width, surfaceSize.height) val surface = Surface(surfaceTexture) request.provideSurface(surface, ContextCompat.getMainExecutor(context)) { result -> surface.release() } } /** * Sets the zoom ratio. */ fun setZoom(ratio: Float) { val clamped = ratio.coerceIn(_minZoomRatio.value, _maxZoomRatio.value) camera?.cameraControl?.setZoomRatio(clamped) _zoomRatio.value = clamped } /** * Sets zoom by linear percentage (0.0 to 1.0). */ fun setZoomLinear(percentage: Float) { camera?.cameraControl?.setLinearZoom(percentage.coerceIn(0f, 1f)) } /** * Switches between front and back camera. */ fun switchCamera() { _isFrontCamera.value = !_isFrontCamera.value _zoomRatio.value = 1.0f // Reset zoom when switching lifecycleOwnerRef?.let { bindCameraUseCases(it) } } /** * Switches to a different lens (back camera only). */ fun switchLens(lensId: String, lifecycleOwner: LifecycleOwner) { if (!_isFrontCamera.value && lensController.selectLens(lensId)) { bindCameraUseCases(lifecycleOwner) } } /** * Gets the executor for image capture callbacks. */ fun getExecutor(): Executor { return ContextCompat.getMainExecutor(context) } /** * Releases camera resources. */ fun release() { cameraProvider?.unbindAll() camera = null preview = null imageCapture = null lifecycleOwnerRef = null } }