Skip to content

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:

SurfaceLoaderForm rendered?
PoseFlow Studio live previewShowcaseTracker.loadMovementMovementTracker.loadYes (toast deck + HUD)
Consumer app (native)TrackedMovementViewMovementTracker.loadHost choice
Consumer app (web)WASM worker → MovementTracker.load directlyHost 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 TrackingResult directly 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.

SurfaceTracker 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:

  1. Build the tracker via MovementTracker.withPreset(...).
  2. Load via tracker.load(movement, configOverride: cfg).
  3. Feed processFrame with a display-space Pose. The onPoseUpdate callback on PoseCameraView already delivers this; non-PoseCameraView sources must apply the orientation rotation + selfie mirror before dispatch.
  4. 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.