feat(android): T-AND-002 CameraX 采集(KEEP_ONLY_LATEST)

This commit is contained in:
2026-09-14 20:40:16 +08:00
parent 94ec0a68d9
commit 7a8dcfba25
@@ -0,0 +1,163 @@
package com.opendance.studio.camera
import android.content.Context
import android.util.Log
import android.util.Size
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import androidx.camera.core.Preview
import androidx.camera.core.resolutionselector.ResolutionSelector
import androidx.camera.core.resolutionselector.ResolutionStrategy
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import com.opendance.studio.data.Resolutions
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
/**
* T-AND-002:CameraX 采集
*
* 只用一个 Preview + 一个 ImageAnalysis,分析策略 KEEP_ONLY_LATEST:
* 推理慢时自动丢帧,绝不让分析队列积压导致预览卡顿。
*
* 相机帧不落盘、不外传 —— 只有推理出的 33 个关键点会离开设备。
*/
class CameraController(private val context: Context) {
/** 推理回调:ImageProxy 由调用方负责 close() */
fun interface FrameAnalyzer {
fun analyze(imageProxy: ImageProxy)
}
private var cameraProvider: ProcessCameraProvider? = null
private var analysisExecutor: ExecutorService? = null
private var imageAnalysis: ImageAnalysis? = null
private var preview: Preview? = null
private var analyzer: FrameAnalyzer? = null
var resolution: String = Resolutions.MEDIUM
private set
var useFrontCamera: Boolean = true
private set
val isRunning: Boolean get() = cameraProvider != null && imageAnalysis != null
/**
* 绑定相机到生命周期。
* @param onError 绑定失败时的回调(如无相机权限、相机被占用)
*/
fun start(
lifecycleOwner: LifecycleOwner,
previewView: PreviewView,
resolution: String,
frontCamera: Boolean = true,
analyzer: FrameAnalyzer,
onError: (String) -> Unit = {},
) {
this.resolution = resolution
this.useFrontCamera = frontCamera
this.analyzer = analyzer
val future = ProcessCameraProvider.getInstance(context)
future.addListener({
try {
val provider = future.get()
cameraProvider = provider
bind(provider, lifecycleOwner, previewView, resolution, frontCamera)
} catch (e: Exception) {
Log.e(TAG, "相机初始化失败", e)
onError(e.message ?: "相机初始化失败")
}
}, ContextCompat.getMainExecutor(context))
}
private fun bind(
provider: ProcessCameraProvider,
lifecycleOwner: LifecycleOwner,
previewView: PreviewView,
resolution: String,
frontCamera: Boolean,
) {
provider.unbindAll()
val (width, height) = Resolutions.parse(resolution)
val resolutionSelector = ResolutionSelector.Builder()
.setResolutionStrategy(
ResolutionStrategy(
Size(width, height),
ResolutionStrategy.FALLBACK_RULE_CLOSEST_LOWER_THEN_HIGHER,
),
)
.build()
val previewUseCase = Preview.Builder()
.setResolutionSelector(resolutionSelector)
.build()
.also { it.setSurfaceProvider(previewView.surfaceProvider) }
val executor = analysisExecutor ?: Executors.newSingleThreadExecutor().also { analysisExecutor = it }
val analysisUseCase = ImageAnalysis.Builder()
.setResolutionSelector(resolutionSelector)
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888)
.build()
.also { analysis ->
analysis.setAnalyzer(executor) { imageProxy ->
try {
this.analyzer?.analyze(imageProxy)
} catch (e: Exception) {
Log.w(TAG, "分析回调异常", e)
imageProxy.close()
}
}
}
val selector = if (frontCamera) {
CameraSelector.DEFAULT_FRONT_CAMERA
} else {
CameraSelector.DEFAULT_BACK_CAMERA
}
try {
provider.bindToLifecycle(lifecycleOwner, selector, previewUseCase, analysisUseCase)
preview = previewUseCase
imageAnalysis = analysisUseCase
Log.i(TAG, "相机已绑定:${if (frontCamera) "前置" else "后置"} @ $resolution")
} catch (e: Exception) {
Log.e(TAG, "相机绑定失败", e)
imageAnalysis = null
}
}
/** 切换前后摄像头(保持当前分析器) */
fun switchCamera(lifecycleOwner: LifecycleOwner, previewView: PreviewView, analyzer: FrameAnalyzer) {
val provider = cameraProvider ?: return
bind(provider, lifecycleOwner, previewView, resolution, !useFrontCamera)
}
fun stop() {
try {
cameraProvider?.unbindAll()
} catch (e: Exception) {
Log.w(TAG, "解绑相机失败:${e.message}")
}
imageAnalysis = null
preview = null
cameraProvider = null
}
fun release() {
stop()
analysisExecutor?.shutdown()
analysisExecutor = null
analyzer = null
}
companion object {
private const val TAG = "CameraController"
}
}