Step counter test1

This commit is contained in:
2026-08-14 18:16:55 +08:00
parent 5d07c8c8bd
commit 905400a59a
17 changed files with 1397 additions and 136 deletions
@@ -1,3 +1,7 @@
/**
* @file CameraXController.kt
* @brief Owns CameraX use-case binding, recording control, and pose-overlay compositing.
*/
package com.example.jnicpp.bowling
import android.Manifest
@@ -36,25 +40,37 @@ import java.text.SimpleDateFormat
import java.util.Locale
/**
* Owns all CameraX use-case binding and recording control. Deliberately not
* an Activity/Fragment/View: it only needs a [Context], a [LifecycleOwner]
* and a couple of Android views handed to it, which keeps the CameraX wiring
* isolated from Android component lifecycle boilerplate and easy to reason
* about (and to fake out behind [Callback] in tests that don't need a real
* camera).
* @brief Owns all CameraX use-case binding and recording control.
*
* Deliberately not an Activity/Fragment/View: it only needs a [Context], a
* [LifecycleOwner] and a couple of Android views handed to it, which keeps
* the CameraX wiring isolated from Android component lifecycle boilerplate
* and easy to reason about (and to fake out behind [Callback] in tests that
* don't need a real camera).
*
* @param appContext Application context used for camera/provider/content-resolver access.
* @param cameraExecutor Background executor the pose analyzer runs inference on.
*/
class CameraXController(
private val appContext: Context,
private val cameraExecutor: java.util.concurrent.Executor
) {
/** @brief Callbacks through which [CameraXController] reports camera, recording, and pose-detection events. */
interface Callback {
/** @brief Called once the camera use cases are bound and the preview is live. */
fun onCameraReady() {}
/** @brief Called if binding the camera use cases fails. @param message Human-readable failure reason. */
fun onCameraError(message: String)
/** @brief Called once an in-progress recording has actually started writing. */
fun onRecordingStarted() {}
/** @brief Called once a recording finishes successfully. @param outputUri Location of the saved video. */
fun onRecordingFinalized(outputUri: Uri) {}
/** @brief Called if starting or finishing a recording fails. @param message Human-readable failure reason. */
fun onRecordingError(message: String)
/** @brief Called if the pose detector fails on a given frame. @param message Human-readable failure reason. */
fun onPoseDetectorError(message: String) {}
/** @brief Called with each frame's pose detection result while pose detection is enabled. @param result The frame's landmarks, angles, and image metadata. */
fun onPoseResult(result: PoseAnalyzer.PoseFrameResult)
}
@@ -100,16 +116,25 @@ class CameraXController(
}
}
/** @brief Whether a video recording is currently in progress. */
val isRecording: Boolean
get() = activeRecording != null
/**
* Binds Preview + VideoCapture (Recorder) + ImageAnalysis to a single
* camera lifecycle, all at once, so the same camera stream feeds the
* on-screen preview, the video file being recorded, and the pose
* detector simultaneously. Also attaches an [OverlayEffect] to the
* VideoCapture output so [setPoseDetectionEnabled] can bake the
* skeleton into the recorded file, not just the live preview.
* @brief Binds Preview + VideoCapture (Recorder) + ImageAnalysis to a
* single camera lifecycle, all at once, so the same camera
* stream feeds the on-screen preview, the video file being
* recorded, and the pose detector simultaneously.
*
* Also attaches an `OverlayEffect` to the VideoCapture output so
* [setPoseDetectionEnabled] can bake the skeleton into the recorded
* file, not just the live preview.
*
* @param lifecycleOwner Lifecycle the camera use cases are bound to;
* they're torn down automatically when it's destroyed.
* @param previewView View the live camera preview is drawn into.
* @param callback Receives camera/recording/pose-detection events from this point on.
* @param lensFacing Which physical camera to bind, e.g. `CameraSelector.LENS_FACING_BACK`.
*/
fun bindToLifecycle(
lifecycleOwner: LifecycleOwner,
@@ -193,6 +218,12 @@ class CameraXController(
}, ContextCompat.getMainExecutor(appContext))
}
/**
* @brief Returns the lazily-created [OverlayEffect] that composites the
* pose skeleton into recorded video frames, creating it (and its
* backing [HandlerThread]) on first use.
* @return The overlay effect to attach to the VideoCapture use case.
*/
private fun getOrCreateOverlayEffect(): OverlayEffect {
overlayEffect?.let { return it }
@@ -244,14 +275,17 @@ class CameraXController(
}
/**
* Attaches/detaches the pose analyzer from the analysis stream, and
* turns skeleton compositing into the recorded video on/off, without
* needing a full unbind/rebind of the camera use cases. Cheap and
* synchronous, so it's safe to call live from a preview-time toggle;
* whatever this is set to when [startRecording] is called becomes that
* recording's pose mode (plain video vs. with pose baked in) and stays
* fixed for its duration -- callers are expected to stop offering the
* toggle while a recording is in progress.
* @brief Attaches/detaches the pose analyzer from the analysis stream,
* and turns skeleton compositing into the recorded video on/off,
* without needing a full unbind/rebind of the camera use cases.
*
* Cheap and synchronous, so it's safe to call live from a preview-time
* toggle; whatever this is set to when [startRecording] is called
* becomes that recording's pose mode (plain video vs. with pose baked
* in) and stays fixed for its duration -- callers are expected to stop
* offering the toggle while a recording is in progress.
*
* @param enabled true to run pose detection and draw/bake the skeleton, false to stop.
*/
fun setPoseDetectionEnabled(enabled: Boolean) {
poseDetectionEnabled = enabled
@@ -261,6 +295,7 @@ class CameraXController(
applyPoseDetectionEnabled()
}
/** @brief Attaches or clears the pose analyzer on [imageAnalysis] to match [poseDetectionEnabled]. */
private fun applyPoseDetectionEnabled() {
val analysis = imageAnalysis ?: return
val analyzer = poseAnalyzer ?: return
@@ -271,6 +306,14 @@ class CameraXController(
}
}
/**
* @brief Starts recording video (with audio, if permission is granted)
* to a new file in the shared Movies/bowling collection.
*
* No-op if the camera isn't bound yet or a recording is already in
* progress. Reports [Callback.onRecordingStarted]/[Callback.onRecordingFinalized]/
* [Callback.onRecordingError] asynchronously as the recording progresses.
*/
fun startRecording() {
val cb = callback ?: return
val capture = videoCapture
@@ -326,12 +369,13 @@ class CameraXController(
}
}
/** @brief Stops the in-progress recording, if any; [Callback.onRecordingFinalized] follows asynchronously. */
fun stopRecording() {
activeRecording?.stop()
activeRecording = null
}
/** Unbinds all use cases and releases the pose detector. Call from onDestroy. */
/** @brief Unbinds all use cases and releases the pose detector. Call from onDestroy. */
fun release() {
activeRecording?.stop()
activeRecording = null