Rep counting parity across surfaces
PoseFlow’s rep count must match across every surface that runs the
SDK. Studio’s live preview, your mobile consumer app, your web
consumer app, any external integration. Same .pose file, same
rep count, every time.
This page is the contract that enforces it.
The canonical loader
Every consumer loads movements through one call:
final tracker = MovementTracker.withPreset(MovementTrackerPreset.standard);tracker.load(movement, configOverride: cfg);MovementTracker.load(...) is the only sanctioned entry point.
A movement is a .pose file decoded to Movement.
The optional configOverride: MovementConfig lets a host swap in a
published overlay (e.g. a server-side hotfix) over the inline
trackingPoints / phases / formRules, without one, the movement’s
inline lists drive the tracker.
If you embed TrackedMovementView,
the widget calls this for you. If you wire MovementTracker directly
(custom camera surface, non-camera pose source), call load exactly
once per session with the same (movement, configOverride) pair you
intend to play.
Surface map
The three surfaces a typical PoseFlow integration runs:
| Surface | Loader | Form rendered? |
|---|---|---|
| PoseFlow Studio live preview | ShowcaseTracker.loadMovement → MovementTracker.load | Yes (toast deck + HUD) |
| Consumer app (native) | TrackedMovementView → MovementTracker.load | Host choice |
| Consumer app (web) | WASM worker → MovementTracker.load directly | Host choice |
The “form rendered?” column reflects host UX policy, not tracker divergence. The tracker computes form for every loaded movement; the host decides whether to render it. Common patterns:
- Form-aware fitness apps: render everything (rep count + form score + cue toasts + per-rep quality breakdown).
- Reps-only competitive UX: render only the rep counter, suppress form at your state-management boundary. The tracker still computes form, you just ignore it.
- Headless / scoring backends: read
TrackingResultdirectly off the tracker, no UI at all.
Pose-space contract
Every surface that runs MovementTracker.processFrame(...) feeds
landmarks in display space, orientation-rotated, selfie-mirrored
on the front camera, normalised to [0, 1] against the post-rotation
buffer dimensions. There is one canonical pose space across every
surface, and PoseCameraView.onPoseUpdate
delivers it.
| Surface | Tracker pose source |
|---|---|
| TrackedMovementView (native) | PoseCameraView.onPoseUpdate |
| PoseFlow Studio (native) | PoseCameraView.onPoseUpdate via CameraTrackingView |
| PoseFlow Studio (web) | WASM worker, transformed to display space before dispatch |
| Consumer app (web) | WASM worker, transformed to display space before dispatch |
Authoring (Studio) and playback (every consumer) agree on coordinate scale by construction, distance / position / ratio thresholds authored in Studio resolve against the same numerical values in your app.
Adding a new consumer surface
If you write a new surface that loads movements:
- Build the tracker via
MovementTracker.withPreset(...). - Load via
tracker.load(movement, configOverride: cfg). - Feed
processFramewith a display-spacePose. TheonPoseUpdatecallback onPoseCameraViewalready delivers this; non-PoseCameraViewsources must apply the orientation rotation + selfie mirror before dispatch. - Subscribe to
onRepCompleted/onFeedback.
If you embed TrackedMovementView, you inherit parity for free ,
pass configOverride: yourCfg alongside movement: yourMovement.
Why this matters
Rep counting parity is the single most important guarantee PoseFlow
makes: same .pose file, same count. Trainers author against the
Studio; users count against your consumer app. If the counts drift,
the trainer’s authoring time is wasted and the user’s experience is
broken.