2026-01-28 15:26:41 +01:00
|
|
|
package no.naiv.tiltshift.camera
|
|
|
|
|
|
|
|
|
|
import android.content.Context
|
|
|
|
|
import android.graphics.SurfaceTexture
|
|
|
|
|
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) {
|
|
|
|
|
|
|
|
|
|
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 _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()
|
|
|
|
|
|
2026-01-29 11:13:31 +01:00
|
|
|
private val _isFrontCamera = MutableStateFlow(false)
|
|
|
|
|
val isFrontCamera: StateFlow<Boolean> = _isFrontCamera.asStateFlow()
|
|
|
|
|
|
2026-01-28 15:26:41 +01:00
|
|
|
private var surfaceTextureProvider: (() -> SurfaceTexture?)? = null
|
|
|
|
|
private var surfaceSize: Size = Size(1920, 1080)
|
2026-01-29 11:13:31 +01:00
|
|
|
private var lifecycleOwnerRef: LifecycleOwner? = null
|
2026-01-28 15:26:41 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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
|
2026-01-29 11:13:31 +01:00
|
|
|
this.lifecycleOwnerRef = lifecycleOwner
|
2026-01-28 15:26:41 +01:00
|
|
|
|
|
|
|
|
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 for high-res photos
|
|
|
|
|
imageCapture = ImageCapture.Builder()
|
|
|
|
|
.setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
|
|
|
|
|
.build()
|
|
|
|
|
|
2026-01-29 11:13:31 +01:00
|
|
|
// 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
|
|
|
|
|
}
|
2026-01-28 15:26:41 +01:00
|
|
|
|
|
|
|
|
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) {
|
|
|
|
|
// Camera binding failed
|
|
|
|
|
e.printStackTrace()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-29 11:13:31 +01:00
|
|
|
* 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).
|
2026-01-28 15:26:41 +01:00
|
|
|
*/
|
|
|
|
|
fun switchLens(lensId: String, lifecycleOwner: LifecycleOwner) {
|
2026-01-29 11:13:31 +01:00
|
|
|
if (!_isFrontCamera.value && lensController.selectLens(lensId)) {
|
2026-01-28 15:26:41 +01:00
|
|
|
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
|
2026-01-29 11:13:31 +01:00
|
|
|
lifecycleOwnerRef = null
|
2026-01-28 15:26:41 +01:00
|
|
|
}
|
|
|
|
|
}
|