增强变焦功能

This commit is contained in:
2026-06-12 18:35:18 +08:00
parent 25ac9c4c35
commit d39d85cd99
4 changed files with 513 additions and 41 deletions

View File

@@ -1,7 +1,10 @@
package com.dronex.rec.recording
import android.content.Context
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager
import android.util.Log
import androidx.camera.camera2.interop.Camera2CameraInfo
import androidx.camera.core.Camera
import androidx.camera.core.CameraSelector
import androidx.camera.core.Preview
@@ -15,6 +18,7 @@ import androidx.camera.video.VideoRecordEvent
import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import kotlin.math.atan
import java.util.concurrent.Executor
class RecordingCameraController(
@@ -26,6 +30,9 @@ class RecordingCameraController(
private var preview: Preview? = null
private var videoCapture: VideoCapture<Recorder>? = null
private var camera: Camera? = null
private var mainCameraId: String? = null
private var ultraWideCameraId: String? = null
private var currentLensMode: LensMode = LensMode.MAIN
private var activeRecording: Recording? = null
private var boundLifecycleOwner: LifecycleOwner? = null
private var currentZoomRatio: Float = 1f
@@ -63,14 +70,8 @@ class RecordingCameraController(
.build()
videoCapture = VideoCapture.withOutput(recorder)
provider.unbindAll()
camera =
provider.bindToLifecycle(
lifecycleOwner,
CameraSelector.DEFAULT_BACK_CAMERA,
preview,
videoCapture,
)
discoverBackCameras(provider)
bindUseCases(provider, lifecycleOwner, selectorForCurrentLensMode())
applyCurrentZoom()
updateStatus(RecordingStatus(RecordingState.PREVIEWING))
@@ -112,14 +113,7 @@ class RecordingCameraController(
try {
boundLifecycleOwner = lifecycleOwner
provider.unbindAll()
camera =
provider.bindToLifecycle(
lifecycleOwner,
CameraSelector.DEFAULT_BACK_CAMERA,
preview,
videoCapture,
)
bindUseCases(provider, lifecycleOwner, selectorForCurrentLensMode())
applyCurrentZoom()
onReady(true)
} catch (error: Exception) {
@@ -230,9 +224,14 @@ class RecordingCameraController(
fun zoomCapabilitiesMap(): Map<String, Any> {
val zoomState = camera?.cameraInfo?.zoomState?.value
val minZoom = zoomState?.minZoomRatio ?: 1f
val minZoom = if (hasUltraWideCamera()) 0.5f else (zoomState?.minZoomRatio ?: 1f)
val maxZoom = zoomState?.maxZoomRatio ?: 3f
val zoom = (zoomState?.zoomRatio ?: currentZoomRatio).coerceIn(minZoom, maxZoom)
val zoom =
if (currentLensMode == LensMode.ULTRA_WIDE) {
0.5f
} else {
(zoomState?.zoomRatio ?: currentZoomRatio).coerceIn(minZoom, maxZoom)
}
currentZoomRatio = zoom
return mapOf(
"zoomRatio" to zoom.toDouble(),
@@ -247,12 +246,27 @@ class RecordingCameraController(
) {
val boundCamera = camera
if (boundCamera == null) {
val clamped = ratio.toFloat().coerceAtLeast(1f)
val clamped =
if (ratio < 1.0 && hasUltraWideCamera()) {
0.5f
} else {
ratio.toFloat().coerceAtLeast(1f)
}
currentZoomRatio = clamped
onComplete(true, zoomCapabilitiesMap(), null)
return
}
if (ratio < 1.0 && hasUltraWideCamera()) {
switchToUltraWide(onComplete)
return
}
if (currentLensMode == LensMode.ULTRA_WIDE) {
switchToMainAndZoom(ratio, onComplete)
return
}
val zoomState = boundCamera.cameraInfo.zoomState.value
val minZoom = zoomState?.minZoomRatio ?: 1f
val maxZoom = zoomState?.maxZoomRatio ?: clampedMaxZoom()
@@ -283,6 +297,7 @@ class RecordingCameraController(
videoCapture = null
camera = null
boundLifecycleOwner = null
currentLensMode = LensMode.MAIN
currentZoomRatio = 1f
updateStatus(RecordingStatus(RecordingState.IDLE))
}
@@ -299,6 +314,11 @@ class RecordingCameraController(
private fun applyCurrentZoom() {
val boundCamera = camera ?: return
if (currentLensMode == LensMode.ULTRA_WIDE) {
currentZoomRatio = 0.5f
boundCamera.cameraControl.setZoomRatio(1f)
return
}
val zoomState = boundCamera.cameraInfo.zoomState.value
val minZoom = zoomState?.minZoomRatio ?: 1f
val maxZoom = zoomState?.maxZoomRatio ?: clampedMaxZoom()
@@ -310,7 +330,218 @@ class RecordingCameraController(
return camera?.cameraInfo?.zoomState?.value?.maxZoomRatio ?: 3f
}
private fun discoverBackCameras(provider: ProcessCameraProvider) {
if (mainCameraId == null) {
mainCameraId = cameraIdForSelector(provider, CameraSelector.DEFAULT_BACK_CAMERA)
}
ultraWideCameraId = findUltraWideCameraId(provider, mainCameraId)
if (ultraWideCameraId == null && currentLensMode == LensMode.ULTRA_WIDE) {
currentLensMode = LensMode.MAIN
currentZoomRatio = 1f
}
Log.d(TAG, "mainCameraId=$mainCameraId ultraWideCameraId=$ultraWideCameraId")
}
private fun cameraIdForSelector(
provider: ProcessCameraProvider,
selector: CameraSelector,
): String? {
return try {
val infos = selector.filter(provider.availableCameraInfos)
infos.firstOrNull()?.let { Camera2CameraInfo.from(it).cameraId }
} catch (error: Exception) {
Log.w(TAG, "cameraIdForSelector failed", error)
null
}
}
private fun findUltraWideCameraId(
provider: ProcessCameraProvider,
excludedCameraId: String?,
): String? {
val manager = appContext.getSystemService(Context.CAMERA_SERVICE) as CameraManager
val candidates =
manager.cameraIdList
.mapNotNull { cameraId -> backCameraProfile(manager, cameraId) }
.filter { it.cameraId != excludedCameraId }
.filter { provider.hasCameraSafely(selectorForCameraId(it.cameraId)) }
.sortedWith(
compareByDescending<CameraProfile> { it.horizontalFov }
.thenBy { it.minFocalLength },
)
val mainProfile = excludedCameraId?.let { backCameraProfile(manager, it) }
val widest = candidates.firstOrNull() ?: return null
if (mainProfile == null) {
return widest.cameraId
}
val meaningfullyWider =
widest.horizontalFov > mainProfile.horizontalFov * ULTRA_WIDE_FOV_FACTOR ||
widest.minFocalLength < mainProfile.minFocalLength * ULTRA_WIDE_FOCAL_FACTOR
return if (meaningfullyWider) widest.cameraId else null
}
private fun backCameraProfile(
manager: CameraManager,
cameraId: String,
): CameraProfile? {
return try {
val characteristics = manager.getCameraCharacteristics(cameraId)
val facing = characteristics.get(CameraCharacteristics.LENS_FACING)
if (facing != CameraCharacteristics.LENS_FACING_BACK) {
return null
}
val focalLengths =
characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS)
?: return null
val physicalSize =
characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)
?: return null
val minFocalLength = focalLengths.minOrNull() ?: return null
val horizontalFov =
2.0 * atan((physicalSize.width / (2.0f * minFocalLength)).toDouble())
CameraProfile(cameraId, minFocalLength, horizontalFov)
} catch (error: Exception) {
Log.w(TAG, "backCameraProfile failed for cameraId=$cameraId", error)
null
}
}
private fun selectorForCurrentLensMode(): CameraSelector {
val cameraId =
if (currentLensMode == LensMode.ULTRA_WIDE) {
ultraWideCameraId
} else {
mainCameraId
}
return if (cameraId != null) {
selectorForCameraId(cameraId)
} else {
CameraSelector.DEFAULT_BACK_CAMERA
}
}
private fun selectorForCameraId(cameraId: String): CameraSelector {
return CameraSelector.Builder()
.addCameraFilter { cameraInfos ->
cameraInfos.filter { Camera2CameraInfo.from(it).cameraId == cameraId }
}
.build()
}
private fun bindUseCases(
provider: ProcessCameraProvider,
lifecycleOwner: LifecycleOwner,
selector: CameraSelector,
) {
val boundPreview = preview ?: throw IllegalStateException("Preview is not ready")
val boundVideoCapture =
videoCapture ?: throw IllegalStateException("Video capture is not ready")
provider.unbindAll()
camera =
provider.bindToLifecycle(
lifecycleOwner,
selector,
boundPreview,
boundVideoCapture,
)
}
private fun switchToUltraWide(
onComplete: (Boolean, Map<String, Any>, String?) -> Unit,
) {
val ultraWideId = ultraWideCameraId
if (ultraWideId == null) {
onComplete(false, zoomCapabilitiesMap(), "Ultra-wide camera is unavailable")
return
}
if (currentLensMode == LensMode.ULTRA_WIDE) {
currentZoomRatio = 0.5f
onComplete(true, zoomCapabilitiesMap(), null)
return
}
if (activeRecording != null) {
onComplete(false, zoomCapabilitiesMap(), "Cannot switch physical camera while recording")
return
}
val provider = cameraProvider
val lifecycleOwner = boundLifecycleOwner
if (provider == null || lifecycleOwner == null) {
onComplete(false, zoomCapabilitiesMap(), "Camera is not ready")
return
}
try {
currentLensMode = LensMode.ULTRA_WIDE
currentZoomRatio = 0.5f
bindUseCases(provider, lifecycleOwner, selectorForCameraId(ultraWideId))
applyCurrentZoom()
onComplete(true, zoomCapabilitiesMap(), null)
} catch (error: Exception) {
Log.e(TAG, "switchToUltraWide failed", error)
currentLensMode = LensMode.MAIN
currentZoomRatio = 1f
try {
bindUseCases(provider, lifecycleOwner, selectorForCurrentLensMode())
applyCurrentZoom()
} catch (restoreError: Exception) {
Log.e(TAG, "restore main camera after ultra-wide failure failed", restoreError)
}
onComplete(false, zoomCapabilitiesMap(), error.message)
}
}
private fun switchToMainAndZoom(
ratio: Double,
onComplete: (Boolean, Map<String, Any>, String?) -> Unit,
) {
if (activeRecording != null) {
onComplete(false, zoomCapabilitiesMap(), "Cannot switch physical camera while recording")
return
}
val provider = cameraProvider
val lifecycleOwner = boundLifecycleOwner
if (provider == null || lifecycleOwner == null) {
onComplete(false, zoomCapabilitiesMap(), "Camera is not ready")
return
}
try {
currentLensMode = LensMode.MAIN
currentZoomRatio = ratio.toFloat().coerceAtLeast(1f)
bindUseCases(provider, lifecycleOwner, selectorForCurrentLensMode())
setZoomRatio(ratio, onComplete)
} catch (error: Exception) {
Log.e(TAG, "switchToMainAndZoom failed", error)
onComplete(false, zoomCapabilitiesMap(), error.message)
}
}
private fun hasUltraWideCamera(): Boolean {
return ultraWideCameraId != null
}
private fun ProcessCameraProvider.hasCameraSafely(selector: CameraSelector): Boolean {
return try {
hasCamera(selector)
} catch (error: Exception) {
false
}
}
private enum class LensMode {
MAIN,
ULTRA_WIDE,
}
private data class CameraProfile(
val cameraId: String,
val minFocalLength: Float,
val horizontalFov: Double,
)
companion object {
private const val TAG = "RecordingCamera"
private const val ULTRA_WIDE_FOV_FACTOR = 1.08
private const val ULTRA_WIDE_FOCAL_FACTOR = 0.92
}
}