From 7a8dcfba2513a39fbb93317fda979ad8ff238a4f Mon Sep 17 00:00:00 2001 From: zijunchiang Date: Mon, 14 Sep 2026 20:40:16 +0800 Subject: [PATCH] =?UTF-8?q?feat(android):=20T-AND-002=20CameraX=20?= =?UTF-8?q?=E9=87=87=E9=9B=86=EF=BC=88KEEP=5FONLY=5FLATEST=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studio/camera/CameraController.kt | 163 ++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 app/src/main/java/com/opendance/studio/camera/CameraController.kt diff --git a/app/src/main/java/com/opendance/studio/camera/CameraController.kt b/app/src/main/java/com/opendance/studio/camera/CameraController.kt new file mode 100644 index 0000000..347dc4c --- /dev/null +++ b/app/src/main/java/com/opendance/studio/camera/CameraController.kt @@ -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" + } +}