feat(android): T-AND-002 CameraX 采集(KEEP_ONLY_LATEST)
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user