Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d93c1c8dd | ||
|
|
c9323f26a7 | ||
|
|
7dafd61314 |
@@ -3,8 +3,13 @@ package com.run.sportsx.recording
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.hardware.camera2.CameraCharacteristics
|
import android.hardware.camera2.CameraCharacteristics
|
||||||
import android.hardware.camera2.CameraManager
|
import android.hardware.camera2.CameraManager
|
||||||
|
import android.hardware.camera2.CaptureRequest
|
||||||
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.camera.camera2.interop.Camera2CameraControl
|
||||||
import androidx.camera.camera2.interop.Camera2CameraInfo
|
import androidx.camera.camera2.interop.Camera2CameraInfo
|
||||||
|
import androidx.camera.camera2.interop.CaptureRequestOptions
|
||||||
|
import androidx.camera.camera2.interop.ExperimentalCamera2Interop
|
||||||
import androidx.camera.core.Camera
|
import androidx.camera.core.Camera
|
||||||
import androidx.camera.core.CameraSelector
|
import androidx.camera.core.CameraSelector
|
||||||
import androidx.camera.core.Preview
|
import androidx.camera.core.Preview
|
||||||
@@ -18,8 +23,9 @@ import androidx.camera.video.VideoRecordEvent
|
|||||||
import androidx.camera.view.PreviewView
|
import androidx.camera.view.PreviewView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import kotlin.math.atan
|
|
||||||
import java.util.concurrent.Executor
|
import java.util.concurrent.Executor
|
||||||
|
import kotlin.math.atan
|
||||||
|
import kotlin.math.round
|
||||||
|
|
||||||
class RecordingCameraController(
|
class RecordingCameraController(
|
||||||
private val appContext: Context,
|
private val appContext: Context,
|
||||||
@@ -225,15 +231,18 @@ class RecordingCameraController(
|
|||||||
|
|
||||||
fun zoomCapabilitiesMap(): Map<String, Any> {
|
fun zoomCapabilitiesMap(): Map<String, Any> {
|
||||||
val zoomState = camera?.cameraInfo?.zoomState?.value
|
val zoomState = camera?.cameraInfo?.zoomState?.value
|
||||||
val logicalMin = zoomState?.minZoomRatio ?: 1f
|
val cameraXMin = zoomState?.minZoomRatio ?: 1f
|
||||||
// 兜底两路超广角来源:独立超广角镜头(0.6) 与 逻辑相机原生 <1.0 变焦范围,取更小者。
|
val cameraXMax = zoomState?.maxZoomRatio ?: 3f
|
||||||
|
val camera2Range = mainCameraZoomRatioRange()
|
||||||
|
val logicalMin = camera2Range?.lower?.let { minOf(cameraXMin, it) } ?: cameraXMin
|
||||||
|
val logicalMax = camera2Range?.upper?.let { maxOf(cameraXMax, it) } ?: cameraXMax
|
||||||
val minZoom =
|
val minZoom =
|
||||||
if (hasUltraWideCamera()) {
|
if (hasUltraWideCamera()) {
|
||||||
minOf(ultraWideZoomRatio, logicalMin)
|
minOf(ultraWideZoomRatio, logicalMin)
|
||||||
} else {
|
} else {
|
||||||
logicalMin
|
logicalMin
|
||||||
}
|
}
|
||||||
val maxZoom = zoomState?.maxZoomRatio ?: 3f
|
val maxZoom = logicalMax
|
||||||
val zoom =
|
val zoom =
|
||||||
if (currentLensMode == LensMode.ULTRA_WIDE) {
|
if (currentLensMode == LensMode.ULTRA_WIDE) {
|
||||||
ultraWideZoomRatio
|
ultraWideZoomRatio
|
||||||
@@ -243,7 +252,8 @@ class RecordingCameraController(
|
|||||||
currentZoomRatio = zoom
|
currentZoomRatio = zoom
|
||||||
Log.d(
|
Log.d(
|
||||||
TAG,
|
TAG,
|
||||||
"zoomCapabilities hasUltraWide=${hasUltraWideCamera()} logicalMin=$logicalMin " +
|
"zoomCapabilities hasUltraWide=${hasUltraWideCamera()} cameraXMin=$cameraXMin " +
|
||||||
|
"cameraXMax=$cameraXMax camera2Range=${camera2Range?.description()} " +
|
||||||
"ultraWideZoomRatio=$ultraWideZoomRatio minZoom=$minZoom maxZoom=$maxZoom zoom=$zoom",
|
"ultraWideZoomRatio=$ultraWideZoomRatio minZoom=$minZoom maxZoom=$maxZoom zoom=$zoom",
|
||||||
)
|
)
|
||||||
return mapOf(
|
return mapOf(
|
||||||
@@ -271,9 +281,12 @@ class RecordingCameraController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (ratio < 1.0 && hasUltraWideCamera()) {
|
if (ratio < 1.0 && hasUltraWideCamera()) {
|
||||||
|
val logicalRange = mainCameraZoomRatioRange()
|
||||||
|
if (logicalRange == null || !logicalRange.contains(ratio.toFloat())) {
|
||||||
switchToUltraWide(onComplete)
|
switchToUltraWide(onComplete)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (currentLensMode == LensMode.ULTRA_WIDE) {
|
if (currentLensMode == LensMode.ULTRA_WIDE) {
|
||||||
switchToMainAndZoom(ratio, onComplete)
|
switchToMainAndZoom(ratio, onComplete)
|
||||||
@@ -281,12 +294,21 @@ class RecordingCameraController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val zoomState = boundCamera.cameraInfo.zoomState.value
|
val zoomState = boundCamera.cameraInfo.zoomState.value
|
||||||
val minZoom = zoomState?.minZoomRatio ?: 1f
|
val camera2Range = mainCameraZoomRatioRange()
|
||||||
val maxZoom = zoomState?.maxZoomRatio ?: clampedMaxZoom()
|
val minZoom = camera2Range?.lower ?: zoomState?.minZoomRatio ?: 1f
|
||||||
|
val maxZoom = camera2Range?.upper ?: zoomState?.maxZoomRatio ?: clampedMaxZoom()
|
||||||
val nextZoom = ratio.toFloat().coerceIn(minZoom, maxZoom)
|
val nextZoom = ratio.toFloat().coerceIn(minZoom, maxZoom)
|
||||||
currentZoomRatio = nextZoom
|
currentZoomRatio = nextZoom
|
||||||
|
|
||||||
val future = boundCamera.cameraControl.setZoomRatio(nextZoom)
|
val future =
|
||||||
|
if (
|
||||||
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
|
||||||
|
camera2Range?.contains(nextZoom) == true
|
||||||
|
) {
|
||||||
|
applyCamera2ZoomRatio(boundCamera, nextZoom)
|
||||||
|
} else {
|
||||||
|
boundCamera.cameraControl.setZoomRatio(nextZoom)
|
||||||
|
}
|
||||||
future.addListener(
|
future.addListener(
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
@@ -334,20 +356,30 @@ class RecordingCameraController(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
val zoomState = boundCamera.cameraInfo.zoomState.value
|
val zoomState = boundCamera.cameraInfo.zoomState.value
|
||||||
val minZoom = zoomState?.minZoomRatio ?: 1f
|
val camera2Range = mainCameraZoomRatioRange()
|
||||||
val maxZoom = zoomState?.maxZoomRatio ?: clampedMaxZoom()
|
val minZoom = camera2Range?.lower ?: zoomState?.minZoomRatio ?: 1f
|
||||||
|
val maxZoom = camera2Range?.upper ?: zoomState?.maxZoomRatio ?: clampedMaxZoom()
|
||||||
currentZoomRatio = currentZoomRatio.coerceIn(minZoom, maxZoom)
|
currentZoomRatio = currentZoomRatio.coerceIn(minZoom, maxZoom)
|
||||||
|
if (
|
||||||
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
|
||||||
|
camera2Range?.contains(currentZoomRatio) == true
|
||||||
|
) {
|
||||||
|
applyCamera2ZoomRatio(boundCamera, currentZoomRatio)
|
||||||
|
} else {
|
||||||
boundCamera.cameraControl.setZoomRatio(currentZoomRatio)
|
boundCamera.cameraControl.setZoomRatio(currentZoomRatio)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun clampedMaxZoom(): Float {
|
private fun clampedMaxZoom(): Float {
|
||||||
return camera?.cameraInfo?.zoomState?.value?.maxZoomRatio ?: 3f
|
return camera?.cameraInfo?.zoomState?.value?.maxZoomRatio ?: 3f
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun discoverBackCameras(provider: ProcessCameraProvider) {
|
private fun discoverBackCameras(provider: ProcessCameraProvider) {
|
||||||
|
val manager = appContext.getSystemService(Context.CAMERA_SERVICE) as CameraManager
|
||||||
if (mainCameraId == null) {
|
if (mainCameraId == null) {
|
||||||
mainCameraId = cameraIdForSelector(provider, CameraSelector.DEFAULT_BACK_CAMERA)
|
mainCameraId = cameraIdForSelector(provider, CameraSelector.DEFAULT_BACK_CAMERA)
|
||||||
}
|
}
|
||||||
|
logPublicCameraDiagnostics(provider, manager, mainCameraId)
|
||||||
val ultraWideCamera = findUltraWideCamera(provider, mainCameraId)
|
val ultraWideCamera = findUltraWideCamera(provider, mainCameraId)
|
||||||
ultraWideCameraId = ultraWideCamera?.cameraId
|
ultraWideCameraId = ultraWideCamera?.cameraId
|
||||||
ultraWideZoomRatio = ultraWideCamera?.zoomRatio ?: DEFAULT_ULTRA_WIDE_ZOOM_RATIO
|
ultraWideZoomRatio = ultraWideCamera?.zoomRatio ?: DEFAULT_ULTRA_WIDE_ZOOM_RATIO
|
||||||
@@ -383,6 +415,13 @@ class RecordingCameraController(
|
|||||||
val candidates =
|
val candidates =
|
||||||
manager.cameraIdList
|
manager.cameraIdList
|
||||||
.mapNotNull { cameraId -> backCameraProfile(manager, cameraId) }
|
.mapNotNull { cameraId -> backCameraProfile(manager, cameraId) }
|
||||||
|
.onEach { profile ->
|
||||||
|
Log.d(
|
||||||
|
TAG,
|
||||||
|
"backCamera ${profile.description()} " +
|
||||||
|
"bindable=${provider.hasCameraSafely(selectorForCameraId(profile.cameraId))}",
|
||||||
|
)
|
||||||
|
}
|
||||||
.filter { it.cameraId != excludedCameraId }
|
.filter { it.cameraId != excludedCameraId }
|
||||||
.filter { provider.hasCameraSafely(selectorForCameraId(it.cameraId)) }
|
.filter { provider.hasCameraSafely(selectorForCameraId(it.cameraId)) }
|
||||||
.sortedWith(
|
.sortedWith(
|
||||||
@@ -391,11 +430,16 @@ class RecordingCameraController(
|
|||||||
)
|
)
|
||||||
|
|
||||||
val mainProfile = excludedCameraId?.let { backCameraProfile(manager, it) }
|
val mainProfile = excludedCameraId?.let { backCameraProfile(manager, it) }
|
||||||
val widest = candidates.firstOrNull() ?: return null
|
val widest =
|
||||||
|
candidates.firstOrNull()
|
||||||
|
?: run {
|
||||||
|
logPhysicalOnlyUltraWideDiagnostics(manager, mainProfile, excludedCameraId)
|
||||||
|
return null
|
||||||
|
}
|
||||||
val candidatesDesc =
|
val candidatesDesc =
|
||||||
candidates.joinToString { "id=${it.cameraId} fov=${it.horizontalFov} focal=${it.minFocalLength}" }
|
candidates.joinToString { it.description() }
|
||||||
val mainDesc =
|
val mainDesc =
|
||||||
mainProfile?.let { "id=${it.cameraId} fov=${it.horizontalFov} focal=${it.minFocalLength}" }
|
mainProfile?.description()
|
||||||
Log.d(TAG, "ultraWide candidates=[$candidatesDesc] main=$mainDesc")
|
Log.d(TAG, "ultraWide candidates=[$candidatesDesc] main=$mainDesc")
|
||||||
if (mainProfile == null) {
|
if (mainProfile == null) {
|
||||||
return UltraWideCamera(widest.cameraId, DEFAULT_ULTRA_WIDE_ZOOM_RATIO)
|
return UltraWideCamera(widest.cameraId, DEFAULT_ULTRA_WIDE_ZOOM_RATIO)
|
||||||
@@ -410,10 +454,98 @@ class RecordingCameraController(
|
|||||||
"(fovFactor=$ULTRA_WIDE_FOV_FACTOR focalFactor=$ULTRA_WIDE_FOCAL_FACTOR)",
|
"(fovFactor=$ULTRA_WIDE_FOV_FACTOR focalFactor=$ULTRA_WIDE_FOCAL_FACTOR)",
|
||||||
)
|
)
|
||||||
if (!meaningfullyWider) {
|
if (!meaningfullyWider) {
|
||||||
|
logPhysicalOnlyUltraWideDiagnostics(manager, mainProfile, excludedCameraId)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return UltraWideCamera(widest.cameraId, DEFAULT_ULTRA_WIDE_ZOOM_RATIO)
|
return UltraWideCamera(widest.cameraId, estimateUltraWideZoomRatio(widest, mainProfile))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun logPublicCameraDiagnostics(
|
||||||
|
provider: ProcessCameraProvider,
|
||||||
|
manager: CameraManager,
|
||||||
|
mainCameraId: String?,
|
||||||
|
) {
|
||||||
|
Log.d(TAG, "publicCameraIds=[${manager.cameraIdList.joinToString()}] mainCameraId=$mainCameraId")
|
||||||
|
manager.cameraIdList.forEach { cameraId ->
|
||||||
|
try {
|
||||||
|
val characteristics = manager.getCameraCharacteristics(cameraId)
|
||||||
|
val bindable = provider.hasCameraSafely(selectorForCameraId(cameraId))
|
||||||
|
val physicalIds = physicalCameraIds(characteristics)
|
||||||
|
Log.d(
|
||||||
|
TAG,
|
||||||
|
"publicCamera id=$cameraId facing=${lensFacingDescription(characteristics)} " +
|
||||||
|
"physicalIds=[${physicalIds.joinToString()}] " +
|
||||||
|
"capabilities=[${capabilitiesDescription(characteristics)}] " +
|
||||||
|
"zoomRange=${zoomRatioRangeFromCharacteristics(characteristics)?.description()} " +
|
||||||
|
"focals=${focalLengthsDescription(characteristics)} " +
|
||||||
|
"sensor=${sensorSizeDescription(characteristics)} bindable=$bindable",
|
||||||
|
)
|
||||||
|
if (cameraId == mainCameraId) {
|
||||||
|
logMainPhysicalCameraDiagnostics(manager, cameraId, physicalIds)
|
||||||
|
}
|
||||||
|
} catch (error: Exception) {
|
||||||
|
Log.w(TAG, "publicCamera diagnostics failed for cameraId=$cameraId", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun logMainPhysicalCameraDiagnostics(
|
||||||
|
manager: CameraManager,
|
||||||
|
mainCameraId: String,
|
||||||
|
physicalIds: Set<String>,
|
||||||
|
) {
|
||||||
|
Log.d(TAG, "mainCamera id=$mainCameraId physicalIds=[${physicalIds.joinToString()}]")
|
||||||
|
physicalIds.forEach { physicalId ->
|
||||||
|
try {
|
||||||
|
val characteristics = manager.getCameraCharacteristics(physicalId)
|
||||||
|
Log.d(
|
||||||
|
TAG,
|
||||||
|
"mainPhysicalCamera id=$physicalId facing=${lensFacingDescription(characteristics)} " +
|
||||||
|
"zoomRange=${zoomRatioRangeFromCharacteristics(characteristics)?.description()} " +
|
||||||
|
"focals=${focalLengthsDescription(characteristics)} " +
|
||||||
|
"sensor=${sensorSizeDescription(characteristics)}",
|
||||||
|
)
|
||||||
|
} catch (error: Exception) {
|
||||||
|
Log.w(TAG, "mainPhysicalCamera diagnostics failed for physicalId=$physicalId", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun logPhysicalOnlyUltraWideDiagnostics(
|
||||||
|
manager: CameraManager,
|
||||||
|
mainProfile: CameraProfile?,
|
||||||
|
mainCameraId: String?,
|
||||||
|
) {
|
||||||
|
if (mainProfile == null || mainCameraId == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val characteristics =
|
||||||
|
try {
|
||||||
|
manager.getCameraCharacteristics(mainCameraId)
|
||||||
|
} catch (error: Exception) {
|
||||||
|
Log.w(TAG, "physicalOnlyUltraWide diagnostics failed for mainCameraId=$mainCameraId", error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val physicalProfiles =
|
||||||
|
physicalCameraIds(characteristics)
|
||||||
|
.mapNotNull { physicalId -> backCameraProfile(manager, physicalId) }
|
||||||
|
.filter { it.cameraId != mainCameraId }
|
||||||
|
.sortedBy { it.minFocalLength }
|
||||||
|
val widestPhysical = physicalProfiles.firstOrNull()
|
||||||
|
if (widestPhysical == null) {
|
||||||
|
Log.d(TAG, "physicalOnlyUltraWide none main=${mainProfile.description()}")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val meaningfullyWider =
|
||||||
|
widestPhysical.horizontalFov > mainProfile.horizontalFov * ULTRA_WIDE_FOV_FACTOR ||
|
||||||
|
widestPhysical.minFocalLength < mainProfile.minFocalLength * ULTRA_WIDE_FOCAL_FACTOR
|
||||||
|
Log.d(
|
||||||
|
TAG,
|
||||||
|
"physicalOnlyUltraWide widest=${widestPhysical.description()} " +
|
||||||
|
"main=${mainProfile.description()} meaningfullyWider=$meaningfullyWider " +
|
||||||
|
"exposedAsBindableCamera=false action=diagnostic_only",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun backCameraProfile(
|
private fun backCameraProfile(
|
||||||
@@ -435,13 +567,145 @@ class RecordingCameraController(
|
|||||||
val minFocalLength = focalLengths.minOrNull() ?: return null
|
val minFocalLength = focalLengths.minOrNull() ?: return null
|
||||||
val horizontalFov =
|
val horizontalFov =
|
||||||
2.0 * atan((physicalSize.width / (2.0f * minFocalLength)).toDouble())
|
2.0 * atan((physicalSize.width / (2.0f * minFocalLength)).toDouble())
|
||||||
CameraProfile(cameraId, minFocalLength, horizontalFov)
|
CameraProfile(
|
||||||
|
cameraId,
|
||||||
|
minFocalLength,
|
||||||
|
horizontalFov,
|
||||||
|
zoomRatioRangeFromCharacteristics(characteristics),
|
||||||
|
)
|
||||||
} catch (error: Exception) {
|
} catch (error: Exception) {
|
||||||
Log.w(TAG, "backCameraProfile failed for cameraId=$cameraId", error)
|
Log.w(TAG, "backCameraProfile failed for cameraId=$cameraId", error)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun mainCameraZoomRatioRange(): ZoomRatioRange? {
|
||||||
|
val cameraId = mainCameraId ?: activeCameraId()
|
||||||
|
return zoomRatioRangeForCamera(cameraId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun activeCameraId(): String? {
|
||||||
|
val boundCamera = camera ?: return null
|
||||||
|
return try {
|
||||||
|
Camera2CameraInfo.from(boundCamera.cameraInfo).cameraId
|
||||||
|
} catch (error: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun zoomRatioRangeForCamera(cameraId: String?): ZoomRatioRange? {
|
||||||
|
if (cameraId == null) return null
|
||||||
|
return try {
|
||||||
|
val manager = appContext.getSystemService(Context.CAMERA_SERVICE) as CameraManager
|
||||||
|
zoomRatioRangeFromCharacteristics(manager.getCameraCharacteristics(cameraId))
|
||||||
|
} catch (error: Exception) {
|
||||||
|
Log.w(TAG, "zoomRatioRangeForCamera failed for cameraId=$cameraId", error)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun zoomRatioRangeFromCharacteristics(
|
||||||
|
characteristics: CameraCharacteristics,
|
||||||
|
): ZoomRatioRange? {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val range = characteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE)
|
||||||
|
?: return null
|
||||||
|
return ZoomRatioRange(range.lower, range.upper)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun physicalCameraIds(characteristics: CameraCharacteristics): Set<String> {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||||
|
return emptySet()
|
||||||
|
}
|
||||||
|
return characteristics.physicalCameraIds
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun lensFacingDescription(characteristics: CameraCharacteristics): String {
|
||||||
|
return when (val facing = characteristics.get(CameraCharacteristics.LENS_FACING)) {
|
||||||
|
CameraCharacteristics.LENS_FACING_BACK -> "BACK"
|
||||||
|
CameraCharacteristics.LENS_FACING_FRONT -> "FRONT"
|
||||||
|
CameraCharacteristics.LENS_FACING_EXTERNAL -> "EXTERNAL"
|
||||||
|
null -> "null"
|
||||||
|
else -> "UNKNOWN($facing)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun capabilitiesDescription(characteristics: CameraCharacteristics): String {
|
||||||
|
val capabilities =
|
||||||
|
characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)
|
||||||
|
?: return "null"
|
||||||
|
return capabilities.joinToString { capabilityDescription(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun capabilityDescription(capability: Int): String {
|
||||||
|
return when (capability) {
|
||||||
|
CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE ->
|
||||||
|
"BACKWARD_COMPATIBLE"
|
||||||
|
CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_MANUAL_SENSOR -> "MANUAL_SENSOR"
|
||||||
|
CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_MANUAL_POST_PROCESSING ->
|
||||||
|
"MANUAL_POST_PROCESSING"
|
||||||
|
CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW -> "RAW"
|
||||||
|
CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_PRIVATE_REPROCESSING ->
|
||||||
|
"PRIVATE_REPROCESSING"
|
||||||
|
CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_READ_SENSOR_SETTINGS ->
|
||||||
|
"READ_SENSOR_SETTINGS"
|
||||||
|
CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE -> "BURST_CAPTURE"
|
||||||
|
CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_YUV_REPROCESSING ->
|
||||||
|
"YUV_REPROCESSING"
|
||||||
|
CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT -> "DEPTH_OUTPUT"
|
||||||
|
CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO ->
|
||||||
|
"CONSTRAINED_HIGH_SPEED_VIDEO"
|
||||||
|
CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_MOTION_TRACKING -> "MOTION_TRACKING"
|
||||||
|
CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA ->
|
||||||
|
"LOGICAL_MULTI_CAMERA"
|
||||||
|
CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_MONOCHROME -> "MONOCHROME"
|
||||||
|
CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_SECURE_IMAGE_DATA -> "SECURE_IMAGE_DATA"
|
||||||
|
else -> "UNKNOWN($capability)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun focalLengthsDescription(characteristics: CameraCharacteristics): String {
|
||||||
|
val focalLengths =
|
||||||
|
characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS)
|
||||||
|
?: return "null"
|
||||||
|
return focalLengths.joinToString(prefix = "[", postfix = "]")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sensorSizeDescription(characteristics: CameraCharacteristics): String {
|
||||||
|
val size = characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)
|
||||||
|
?: return "null"
|
||||||
|
return "${size.width}x${size.height}"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun estimateUltraWideZoomRatio(
|
||||||
|
ultraWide: CameraProfile,
|
||||||
|
main: CameraProfile,
|
||||||
|
): Float {
|
||||||
|
if (main.minFocalLength <= 0f) {
|
||||||
|
return DEFAULT_ULTRA_WIDE_ZOOM_RATIO
|
||||||
|
}
|
||||||
|
val rawRatio = ultraWide.minFocalLength / main.minFocalLength
|
||||||
|
val roundedRatio = round(rawRatio * 10f) / 10f
|
||||||
|
return roundedRatio.coerceIn(MIN_ULTRA_WIDE_ZOOM_RATIO, MAX_ULTRA_WIDE_ZOOM_RATIO)
|
||||||
|
}
|
||||||
|
|
||||||
|
@androidx.annotation.OptIn(ExperimentalCamera2Interop::class)
|
||||||
|
private fun applyCamera2ZoomRatio(
|
||||||
|
boundCamera: Camera,
|
||||||
|
zoomRatio: Float,
|
||||||
|
) =
|
||||||
|
Camera2CameraControl.from(boundCamera.cameraControl)
|
||||||
|
.setCaptureRequestOptions(
|
||||||
|
CaptureRequestOptions.Builder()
|
||||||
|
.setCaptureRequestOption(
|
||||||
|
CaptureRequest.CONTROL_ZOOM_RATIO,
|
||||||
|
zoomRatio,
|
||||||
|
)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
|
||||||
private fun selectorForCurrentLensMode(): CameraSelector {
|
private fun selectorForCurrentLensMode(): CameraSelector {
|
||||||
val cameraId =
|
val cameraId =
|
||||||
if (currentLensMode == LensMode.ULTRA_WIDE) {
|
if (currentLensMode == LensMode.ULTRA_WIDE) {
|
||||||
@@ -571,7 +835,26 @@ class RecordingCameraController(
|
|||||||
val cameraId: String,
|
val cameraId: String,
|
||||||
val minFocalLength: Float,
|
val minFocalLength: Float,
|
||||||
val horizontalFov: Double,
|
val horizontalFov: Double,
|
||||||
)
|
val zoomRatioRange: ZoomRatioRange?,
|
||||||
|
) {
|
||||||
|
fun description(): String {
|
||||||
|
return "id=$cameraId fov=$horizontalFov focal=$minFocalLength " +
|
||||||
|
"zoomRange=${zoomRatioRange?.description()}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class ZoomRatioRange(
|
||||||
|
val lower: Float,
|
||||||
|
val upper: Float,
|
||||||
|
) {
|
||||||
|
fun contains(ratio: Float): Boolean {
|
||||||
|
return ratio >= lower && ratio <= upper
|
||||||
|
}
|
||||||
|
|
||||||
|
fun description(): String {
|
||||||
|
return "$lower..$upper"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private data class UltraWideCamera(
|
private data class UltraWideCamera(
|
||||||
val cameraId: String,
|
val cameraId: String,
|
||||||
@@ -581,6 +864,8 @@ class RecordingCameraController(
|
|||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "RecordingCamera"
|
private const val TAG = "RecordingCamera"
|
||||||
private const val DEFAULT_ULTRA_WIDE_ZOOM_RATIO = 0.6f
|
private const val DEFAULT_ULTRA_WIDE_ZOOM_RATIO = 0.6f
|
||||||
|
private const val MIN_ULTRA_WIDE_ZOOM_RATIO = 0.3f
|
||||||
|
private const val MAX_ULTRA_WIDE_ZOOM_RATIO = 0.99f
|
||||||
// 适度放宽判定宽容度,覆盖更多机型(更小的 FOV/焦距差异也视为超广角)。
|
// 适度放宽判定宽容度,覆盖更多机型(更小的 FOV/焦距差异也视为超广角)。
|
||||||
private const val ULTRA_WIDE_FOV_FACTOR = 1.04
|
private const val ULTRA_WIDE_FOV_FACTOR = 1.04
|
||||||
private const val ULTRA_WIDE_FOCAL_FACTOR = 0.96
|
private const val ULTRA_WIDE_FOCAL_FACTOR = 0.96
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 262 KiB After Width: | Height: | Size: 256 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 248 KiB |
|
Before Width: | Height: | Size: 184 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 976 B After Width: | Height: | Size: 761 B |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 262 KiB After Width: | Height: | Size: 256 KiB |
@@ -56,7 +56,6 @@ class RecordingHudWidget extends StatelessWidget {
|
|||||||
static double get _recordButtonBottom => 63.r;
|
static double get _recordButtonBottom => 63.r;
|
||||||
static double get _overlayInfoLeft => 13.r;
|
static double get _overlayInfoLeft => 13.r;
|
||||||
static double get _overlayInfoBottom => 10.r;
|
static double get _overlayInfoBottom => 10.r;
|
||||||
static const List<double> _zoomPresets = [0.6, 1.0];
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -150,7 +149,7 @@ class RecordingHudWidget extends StatelessWidget {
|
|||||||
zoomRatio: zoomRatio,
|
zoomRatio: zoomRatio,
|
||||||
minZoomRatio: minZoomRatio,
|
minZoomRatio: minZoomRatio,
|
||||||
maxZoomRatio: maxZoomRatio,
|
maxZoomRatio: maxZoomRatio,
|
||||||
presets: _zoomPresets,
|
presets: _zoomPresetsForRange(minZoomRatio),
|
||||||
onSelected: onZoomSelected,
|
onSelected: onZoomSelected,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -191,6 +190,13 @@ class RecordingHudWidget extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<double> _zoomPresetsForRange(double minZoomRatio) {
|
||||||
|
return [
|
||||||
|
if (minZoomRatio < 1.0) minZoomRatio,
|
||||||
|
1.0,
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ZoomPresetControl extends StatelessWidget {
|
class _ZoomPresetControl extends StatelessWidget {
|
||||||
@@ -215,7 +221,6 @@ class _ZoomPresetControl extends StatelessWidget {
|
|||||||
final availablePresets = presets
|
final availablePresets = presets
|
||||||
.where(_isPresetAvailable)
|
.where(_isPresetAvailable)
|
||||||
.toList(growable: false);
|
.toList(growable: false);
|
||||||
|
|
||||||
if (availablePresets.isEmpty) {
|
if (availablePresets.isEmpty) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,9 +76,7 @@ void main() {
|
|||||||
expect(session.errorMessage, isNull);
|
expect(session.errorMessage, isNull);
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
test('passes 0.5x to native when camera capabilities allow it', () async {
|
||||||
'clamps legacy 0.5x request to 0.6x ultra-wide ratio',
|
|
||||||
() async {
|
|
||||||
final calls = <MethodCall>[];
|
final calls = <MethodCall>[];
|
||||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
.setMockMethodCallHandler(
|
.setMockMethodCallHandler(
|
||||||
@@ -86,8 +84,8 @@ void main() {
|
|||||||
(call) async {
|
(call) async {
|
||||||
calls.add(call);
|
calls.add(call);
|
||||||
return <String, dynamic>{
|
return <String, dynamic>{
|
||||||
'zoomRatio': 0.6,
|
'zoomRatio': 0.5,
|
||||||
'minZoomRatio': 0.6,
|
'minZoomRatio': 0.5,
|
||||||
'maxZoomRatio': 3.0,
|
'maxZoomRatio': 3.0,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -101,20 +99,19 @@ void main() {
|
|||||||
.copyWith(
|
.copyWith(
|
||||||
session: const RecordingSessionState(
|
session: const RecordingSessionState(
|
||||||
zoomRatio: 1.0,
|
zoomRatio: 1.0,
|
||||||
minZoomRatio: 0.6,
|
minZoomRatio: 0.5,
|
||||||
maxZoomRatio: 3.0,
|
maxZoomRatio: 3.0,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
await notifier.setZoomRatio(0.5);
|
await notifier.setZoomRatio(0.5);
|
||||||
|
|
||||||
expect(calls.single.arguments, <String, dynamic>{'zoomRatio': 0.6});
|
expect(calls.single.arguments, <String, dynamic>{'zoomRatio': 0.5});
|
||||||
final session = container.read(recordingViewModelProvider).session;
|
final session = container.read(recordingViewModelProvider).session;
|
||||||
expect(session.zoomRatio, 0.6);
|
expect(session.zoomRatio, 0.5);
|
||||||
expect(session.minZoomRatio, 0.6);
|
expect(session.minZoomRatio, 0.5);
|
||||||
expect(session.maxZoomRatio, 3.0);
|
expect(session.maxZoomRatio, 3.0);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
test('passes 0.6x to native when camera capabilities allow it', () async {
|
test('passes 0.6x to native when camera capabilities allow it', () async {
|
||||||
final calls = <MethodCall>[];
|
final calls = <MethodCall>[];
|
||||||
|
|||||||
@@ -54,13 +54,13 @@ void main() {
|
|||||||
expect(find.text('3x'), findsNothing);
|
expect(find.text('3x'), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('shows 0.6x when ultra-wide camera capability is below 0.6', (
|
testWidgets('shows 0.5x when ultra-wide camera capability is 0.5', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
await pumpHud(tester, minZoomRatio: 0.5);
|
await pumpHud(tester, minZoomRatio: 0.5);
|
||||||
|
|
||||||
expect(find.text('0.5x'), findsNothing);
|
expect(find.text('0.5x'), findsOneWidget);
|
||||||
expect(find.text('0.6x'), findsOneWidget);
|
expect(find.text('0.6x'), findsNothing);
|
||||||
expect(find.text('1x'), findsOneWidget);
|
expect(find.text('1x'), findsOneWidget);
|
||||||
expect(find.text('2x'), findsNothing);
|
expect(find.text('2x'), findsNothing);
|
||||||
expect(find.text('3x'), findsNothing);
|
expect(find.text('3x'), findsNothing);
|
||||||
@@ -75,13 +75,13 @@ void main() {
|
|||||||
expect(find.text('1x'), findsOneWidget);
|
expect(find.text('1x'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('marks current ultra-wide zoom ratio as selected on 0.6x UI', (
|
testWidgets('marks current 0.5x zoom ratio as selected', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
await pumpHud(tester, zoomRatio: 0.5, minZoomRatio: 0.5);
|
await pumpHud(tester, zoomRatio: 0.5, minZoomRatio: 0.5);
|
||||||
|
|
||||||
final selectedButton = tester.widget<TextButton>(
|
final selectedButton = tester.widget<TextButton>(
|
||||||
find.ancestor(of: find.text('0.6x'), matching: find.byType(TextButton)),
|
find.ancestor(of: find.text('0.5x'), matching: find.byType(TextButton)),
|
||||||
);
|
);
|
||||||
expect(selectedButton.enabled, isFalse);
|
expect(selectedButton.enabled, isFalse);
|
||||||
});
|
});
|
||||||
@@ -98,11 +98,12 @@ void main() {
|
|||||||
testWidgets('does not expose presets beyond max zoom ratio', (tester) async {
|
testWidgets('does not expose presets beyond max zoom ratio', (tester) async {
|
||||||
await pumpHud(tester, minZoomRatio: 0.5, maxZoomRatio: 0.55);
|
await pumpHud(tester, minZoomRatio: 0.5, maxZoomRatio: 0.55);
|
||||||
|
|
||||||
|
expect(find.text('0.5x'), findsOneWidget);
|
||||||
expect(find.text('0.6x'), findsNothing);
|
expect(find.text('0.6x'), findsNothing);
|
||||||
expect(find.text('1x'), findsNothing);
|
expect(find.text('1x'), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('tapping 0.6x reports 0.6 when camera capability is below 0.6', (
|
testWidgets('tapping 0.5x reports 0.5 when camera capability is 0.5', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
double? selected;
|
double? selected;
|
||||||
@@ -112,10 +113,10 @@ void main() {
|
|||||||
onZoomSelected: (ratio) => selected = ratio,
|
onZoomSelected: (ratio) => selected = ratio,
|
||||||
);
|
);
|
||||||
|
|
||||||
await tester.tap(find.text('0.6x'));
|
await tester.tap(find.text('0.5x'));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(selected, 0.6);
|
expect(selected, 0.5);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('tapping 0.6x reports 0.6 when camera only supports 0.6x', (
|
testWidgets('tapping 0.6x reports 0.6 when camera only supports 0.6x', (
|
||||||
@@ -135,7 +136,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('disables 0.6x while recording on main camera', (tester) async {
|
testWidgets('disables 0.6x while recording on main camera', (tester) async {
|
||||||
await pumpHud(tester, minZoomRatio: 0.5, isRecording: true);
|
await pumpHud(tester, minZoomRatio: 0.6, isRecording: true);
|
||||||
|
|
||||||
final ultraWideButton = tester.widget<TextButton>(
|
final ultraWideButton = tester.widget<TextButton>(
|
||||||
find.ancestor(of: find.text('0.6x'), matching: find.byType(TextButton)),
|
find.ancestor(of: find.text('0.6x'), matching: find.byType(TextButton)),
|
||||||
@@ -154,7 +155,7 @@ void main() {
|
|||||||
await pumpHud(tester, zoomRatio: 0.5, minZoomRatio: 0.5, isRecording: true);
|
await pumpHud(tester, zoomRatio: 0.5, minZoomRatio: 0.5, isRecording: true);
|
||||||
|
|
||||||
final ultraWideButton = tester.widget<TextButton>(
|
final ultraWideButton = tester.widget<TextButton>(
|
||||||
find.ancestor(of: find.text('0.6x'), matching: find.byType(TextButton)),
|
find.ancestor(of: find.text('0.5x'), matching: find.byType(TextButton)),
|
||||||
);
|
);
|
||||||
final mainButton = tester.widget<TextButton>(
|
final mainButton = tester.widget<TextButton>(
|
||||||
find.ancestor(of: find.text('1x'), matching: find.byType(TextButton)),
|
find.ancestor(of: find.text('1x'), matching: find.byType(TextButton)),
|
||||||
|
|||||||