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