Merge branch 'linfeng/dev/20260603' into linfeng/dev/2026612
This commit is contained in:
@@ -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,10 @@ 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 ultraWideZoomRatio: Float = DEFAULT_ULTRA_WIDE_ZOOM_RATIO
|
||||
private var currentLensMode: LensMode = LensMode.MAIN
|
||||
private var activeRecording: Recording? = null
|
||||
private var boundLifecycleOwner: LifecycleOwner? = null
|
||||
private var currentZoomRatio: Float = 1f
|
||||
@@ -63,14 +71,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 +114,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 +225,19 @@ class RecordingCameraController(
|
||||
|
||||
fun zoomCapabilitiesMap(): Map<String, Any> {
|
||||
val zoomState = camera?.cameraInfo?.zoomState?.value
|
||||
val minZoom = zoomState?.minZoomRatio ?: 1f
|
||||
val minZoom =
|
||||
if (hasUltraWideCamera()) {
|
||||
ultraWideZoomRatio
|
||||
} else {
|
||||
zoomState?.minZoomRatio ?: 1f
|
||||
}
|
||||
val maxZoom = zoomState?.maxZoomRatio ?: 3f
|
||||
val zoom = (zoomState?.zoomRatio ?: currentZoomRatio).coerceIn(minZoom, maxZoom)
|
||||
val zoom =
|
||||
if (currentLensMode == LensMode.ULTRA_WIDE) {
|
||||
ultraWideZoomRatio
|
||||
} else {
|
||||
(zoomState?.zoomRatio ?: currentZoomRatio).coerceIn(minZoom, maxZoom)
|
||||
}
|
||||
currentZoomRatio = zoom
|
||||
return mapOf(
|
||||
"zoomRatio" to zoom.toDouble(),
|
||||
@@ -247,12 +252,27 @@ class RecordingCameraController(
|
||||
) {
|
||||
val boundCamera = camera
|
||||
if (boundCamera == null) {
|
||||
val clamped = ratio.toFloat().coerceAtLeast(1f)
|
||||
val clamped =
|
||||
if (ratio < 1.0 && hasUltraWideCamera()) {
|
||||
ultraWideZoomRatio
|
||||
} 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,7 +303,9 @@ class RecordingCameraController(
|
||||
videoCapture = null
|
||||
camera = null
|
||||
boundLifecycleOwner = null
|
||||
currentLensMode = LensMode.MAIN
|
||||
currentZoomRatio = 1f
|
||||
ultraWideZoomRatio = DEFAULT_ULTRA_WIDE_ZOOM_RATIO
|
||||
updateStatus(RecordingStatus(RecordingState.IDLE))
|
||||
}
|
||||
|
||||
@@ -299,6 +321,11 @@ class RecordingCameraController(
|
||||
|
||||
private fun applyCurrentZoom() {
|
||||
val boundCamera = camera ?: return
|
||||
if (currentLensMode == LensMode.ULTRA_WIDE) {
|
||||
currentZoomRatio = ultraWideZoomRatio
|
||||
boundCamera.cameraControl.setZoomRatio(1f)
|
||||
return
|
||||
}
|
||||
val zoomState = boundCamera.cameraInfo.zoomState.value
|
||||
val minZoom = zoomState?.minZoomRatio ?: 1f
|
||||
val maxZoom = zoomState?.maxZoomRatio ?: clampedMaxZoom()
|
||||
@@ -310,7 +337,234 @@ class RecordingCameraController(
|
||||
return camera?.cameraInfo?.zoomState?.value?.maxZoomRatio ?: 3f
|
||||
}
|
||||
|
||||
private fun discoverBackCameras(provider: ProcessCameraProvider) {
|
||||
if (mainCameraId == null) {
|
||||
mainCameraId = cameraIdForSelector(provider, CameraSelector.DEFAULT_BACK_CAMERA)
|
||||
}
|
||||
val ultraWideCamera = findUltraWideCamera(provider, mainCameraId)
|
||||
ultraWideCameraId = ultraWideCamera?.cameraId
|
||||
ultraWideZoomRatio = ultraWideCamera?.zoomRatio ?: DEFAULT_ULTRA_WIDE_ZOOM_RATIO
|
||||
if (ultraWideCamera == null && currentLensMode == LensMode.ULTRA_WIDE) {
|
||||
currentLensMode = LensMode.MAIN
|
||||
currentZoomRatio = 1f
|
||||
}
|
||||
Log.d(
|
||||
TAG,
|
||||
"mainCameraId=$mainCameraId ultraWideCameraId=$ultraWideCameraId " +
|
||||
"ultraWideZoomRatio=$ultraWideZoomRatio",
|
||||
)
|
||||
}
|
||||
|
||||
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 findUltraWideCamera(
|
||||
provider: ProcessCameraProvider,
|
||||
excludedCameraId: String?,
|
||||
): UltraWideCamera? {
|
||||
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 UltraWideCamera(widest.cameraId, DEFAULT_ULTRA_WIDE_ZOOM_RATIO)
|
||||
}
|
||||
|
||||
val meaningfullyWider =
|
||||
widest.horizontalFov > mainProfile.horizontalFov * ULTRA_WIDE_FOV_FACTOR ||
|
||||
widest.minFocalLength < mainProfile.minFocalLength * ULTRA_WIDE_FOCAL_FACTOR
|
||||
if (!meaningfullyWider) {
|
||||
return null
|
||||
}
|
||||
|
||||
return UltraWideCamera(widest.cameraId, DEFAULT_ULTRA_WIDE_ZOOM_RATIO)
|
||||
}
|
||||
|
||||
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 = ultraWideZoomRatio
|
||||
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 = ultraWideZoomRatio
|
||||
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,
|
||||
)
|
||||
|
||||
private data class UltraWideCamera(
|
||||
val cameraId: String,
|
||||
val zoomRatio: Float,
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val TAG = "RecordingCamera"
|
||||
private const val DEFAULT_ULTRA_WIDE_ZOOM_RATIO = 0.6f
|
||||
private const val ULTRA_WIDE_FOV_FACTOR = 1.08
|
||||
private const val ULTRA_WIDE_FOCAL_FACTOR = 0.92
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user