tilt-shift-camera/app/src/main/java/no/naiv/tiltshift/camera/CameraManager.kt

205 lines
6.6 KiB
Kotlin
Raw Normal View History

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<String?>(null)
val error: StateFlow<String?> = _error.asStateFlow()
fun clearError() {
_error.value = null
}
private val _zoomRatio = MutableStateFlow(1.0f)
val zoomRatio: StateFlow<Float> = _zoomRatio.asStateFlow()
private val _minZoomRatio = MutableStateFlow(1.0f)
val minZoomRatio: StateFlow<Float> = _minZoomRatio.asStateFlow()
private val _maxZoomRatio = MutableStateFlow(1.0f)
val maxZoomRatio: StateFlow<Float> = _maxZoomRatio.asStateFlow()
private val _isFrontCamera = MutableStateFlow(false)
val isFrontCamera: StateFlow<Boolean> = _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
}
}