Skip to content

`MovementTracker`

The PoseFlow runtime. One instance per session. Wires PhaseStateMachine (rep counting + phase transitions), FormServiceV2 (rule-based form analysis), and per-frame tracking-value computation (angles, distances, ratios, positions, velocity, stability) for a single loaded Movement.

import 'package:pose_flow/pose_flow.dart';
final tracker = MovementTracker.withPreset(MovementTrackerPreset.standard);
tracker.load(movement);
tracker.onRepCompleted.listen((event) {
debugPrint('Rep ${event.repNumber}: ${event.quality.grade.displayName}');
});
// Per-frame:
final result = tracker.processFrame(pose);

Lifecycle

MovementTracker()
v
tracker.load(movement, configOverride: cfg)
v
tracker.processFrame(pose) → TrackingResult (every frame)
v
onRepCompleted stream (on rep)
onFeedback stream (on form-rule violation)
v
reset() (start a new set)
dispose() (tear down)

Constructors

MovementTracker({MovementTrackerConfig? config});
MovementTracker.withPreset(MovementTrackerPreset preset);

MovementTrackerPreset values: casual, standard, strict, competition. Each tweaks the underlying MovementTrackerConfig , rep-scoring gate floor, real-time feedback emission.

Loading a movement

tracker.load(movement, configOverride: cfg);
ParamNotes
movementThe Movement decoded from a .pose file.
configOverrideOptional published MovementConfig that swaps in different trackingPoints / phases / formRules from external storage (e.g. a Firestore hotfix on a deployed movement). When null, the movement’s inline lists drive the tracker.

Behavioural notes

  • The previous baseline snapshots (for relativeToBaseline tracking points) are cleared.
  • Rolling sample history (velocity + stability) is cleared.
  • The bucket smoother is cleared so the previous session’s committed bucket doesn’t bleed in.
  • load() calls reset() internally, rep state resets too.

Per-frame processing

final result = tracker.processFrame(pose);

Returns TrackingResult synchronously. The result also carries result.pose so downstream consumers can read landmarks without re-running pose detection.

Streams

Stream<RepCompletedEvent> get onRepCompleted;
Stream<RepQuality> get onRepQuality;
Stream<FormFeedbackEvent> get onFeedback;

All broadcast streams, subscribe from any number of listeners.

Reading state

int get repCount; // current count
double get formScore; // current form score (0-100)
String? get currentPhaseId; // phase machine state
MovementTrackerConfig get config;
Movement? get movement; // currently loaded
bool get isMovementLoaded;
int get frameCount;
RepScoringConfig get repScoring; // active scoring gate
ScoringProfile get scoringProfile; // inferred profile for this movement

Mutating state

void updateRepScoring(RepScoringConfig cfg); // live-tune the quality gate
void reset(); // clear state, keep loaded movement
void dispose(); // release resources, close streams

updateRepScoring is what the Studio’s live-tuning panel uses to react to slider input without restarting the session.

Configuration

MovementTrackerConfig has four knobs:

FieldDefaultNotes
useFormRulestrueDisable form analysis entirely (reps still count).
realtimeFeedbacktrueEmit feedback events per frame. Set false for headless rep counting.
useRepScoringGatetrueWhether the minimum-quality floor gates rep counting. When false, every rep counts regardless of quality.
minRepQualityScore50.0Default minimum quality score when the movement doesn’t publish its own gate.

The presets pre-tune these:

PresetReal-time feedback?Gate?Floor
casualoffoff0
standardonon50
strictonon70
competitiononon80

Movements that publish their own RepScoringConfig always win, the preset’s floor is only the default for movements that don’t.

Performance

  • Per-frame work in the tracker: ~3 ms on a 2023 phone. Dominated by the pose engine upstream, not the tracker itself.
  • Loading a Movement: ~5 ms.
  • Memory: tracker state is small (<100 KB); the dominant cost is the loaded movement itself.