npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-native-nitro-pose-exercises

v1.0.8

Published

Real-time on-device exercise tracking for React Native. Rep counting, form validation, and skeleton overlay powered by MediaPipe Pose Landmarker and VisionCamera v5 via Nitro Modules.

Readme

react-native-nitro-pose-exercises

A React Native Nitro Module for real-time, on-device exercise tracking using pose estimation. Built on MediaPipe Pose Landmarker and VisionCamera v5.

  • 🏋️ Rep Counting — Automatic rep detection with configurable state machines
  • 🧘 Hold Tracking — Duration and stability tracking for planks, yoga poses, and isometric holds
  • 📐 Form Validation — Real-time form feedback with configurable angle-based rules
  • 💀 Skeleton Overlay — Optional Skia-powered skeleton rendering over the camera feed
  • Fully Native — MediaPipe runs on-device via Nitro Modules, zero JS bridge overhead

[!IMPORTANT]

  • Requires React Native 0.76+ with Nitro Modules and VisionCamera v5.
  • Must be tested on a physical device — camera + ML inference don't work on simulators.
  • MediaPipe Pose Landmarker model file (pose_landmarker_lite.task) must be bundled with the app.

📦 Installation

npm install react-native-nitro-pose-exercises react-native-nitro-modules
npm install react-native-vision-camera react-native-nitro-image
npm install react-native-vision-camera-worklets react-native-worklets
npm install react-native-reanimated

For Skia skeleton overlay (optional):

npm install @shopify/react-native-skia react-native-vision-camera-skia
cd ios && pod install

[!NOTE] This package uses MediaPipe Pose Landmarker natively on both platforms. iOS uses MediaPipeTasksVision via CocoaPods. Android uses com.google.mediapipe:tasks-vision via Gradle.


Demo


🧠 Overview

| Feature | Description | | ------------------------ | ------------------------------------------------------------------------- | | Rep-Based Exercises | Cyclic state machine (UP → DOWN → UP = 1 rep). Push-ups, squats, curls. | | Hold-Based Exercises | Single target pose with duration tracking. Planks, wall sits, yoga poses. | | Flow-Based Exercises | Ordered sequence of poses. Sun salutation, yoga flows. (coming soon) | | Form Feedback | Angle-based rules with throttled real-time callbacks. | | Skeleton Overlay | 33-point body skeleton drawn over camera via Skia. | | Bilateral Tracking | Left and right side angles tracked independently. |


🔧 Setup

Model File

Download the MediaPipe Pose Landmarker model:

https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_lite/float16/latest/pose_landmarker_lite.task

iOS: Drag pose_landmarker_lite.task into your Xcode project (Copy items if needed, add to app target).

Android: Place at android/app/src/main/assets/pose_landmarker_lite.task

Permissions

iOS — Info.plist:

<key>NSCameraUsageDescription</key>
<string>Camera is needed for pose detection during exercises</string>
<key>NSMicrophoneUsageDescription</key>
<string>Microphone access for audio during exercise sessions</string>

Android — AndroidManifest.xml:

<uses-permission android:name="android.permission.CAMERA" />

Babel Config

module.exports = {
  presets: ['module:@react-native/babel-preset'],
  plugins: [
    'react-native-worklets/plugin',
    'react-native-reanimated/plugin', // must be last
  ],
};

⚙️ Usage

Basic — Normal Camera (No Skeleton)

import { useEffect, useCallback, useState } from 'react';
import { StyleSheet, View, Text, TouchableOpacity } from 'react-native';
import {
  Camera,
  useCameraDevice,
  useCameraPermission,
  useFrameOutput,
  useAsyncRunner,
} from 'react-native-vision-camera';
import {
  nitroPoseExercises,
  PUSHUP_CONFIG,
  type RepData,
  type FormFeedback,
  type SessionResult,
} from 'react-native-nitro-pose-exercises';

export default function App() {
  const { hasPermission, requestPermission } = useCameraPermission();
  const device = useCameraDevice('back');
  const asyncRunner = useAsyncRunner();
  const [repCount, setRepCount] = useState(0);

  useEffect(() => {
    if (!hasPermission) requestPermission();
  }, [hasPermission]);

  // Initialize pose engine
  useEffect(() => {
    async function init() {
      await nitroPoseExercises.initialize('pose_landmarker_lite.task');
      nitroPoseExercises.loadExercise(PUSHUP_CONFIG);

      nitroPoseExercises.onRepComplete = (data: RepData) => {
        setRepCount(data.repNumber);
        console.log(`Rep ${data.repNumber} — form: ${data.formScore}`);
      };

      nitroPoseExercises.onFormFeedback = (feedback: FormFeedback) => {
        console.log(`Form: ${feedback.message}`);
      };

      nitroPoseExercises.onSessionComplete = (result: SessionResult) => {
        console.log(
          `Done! ${result.totalReps} reps, avg form: ${result.averageFormScore}`
        );
      };

      // Start: 10 target reps, 3 second countdown
      nitroPoseExercises.startSession(10, 3);
    }

    init();
    return () => {
      nitroPoseExercises.release();
    };
  }, []);

  // Frame processor
  const frameOutput = useFrameOutput({
    pixelFormat: 'rgb',
    onFrame(frame) {
      'worklet';
      const accepted = asyncRunner.runAsync(() => {
        'worklet';
        try {
          nitroPoseExercises.processFrame(frame);
        } finally {
          frame.dispose();
        }
      });
      if (!accepted) frame.dispose();
    },
  });

  if (!hasPermission || !device) return null;

  return (
    <View style={StyleSheet.absoluteFill}>
      <Camera
        style={StyleSheet.absoluteFill}
        device={device}
        isActive={true}
        outputs={[frameOutput]}
      />
      <Text style={styles.repText}>{repCount} REPS</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  repText: {
    position: 'absolute',
    top: 100,
    alignSelf: 'center',
    fontSize: 48,
    fontFamily: 'System',
    color: '#4CAF50',
  },
});

Skeleton Overlay — SkiaCamera

import { SkiaCamera } from 'react-native-vision-camera-skia'
import { Skia } from '@shopify/react-native-skia'
import { nitroPoseExercises } from 'react-native-nitro-pose-exercises'

const SKELETON_CONNECTIONS: [number, number][] = [
  [11, 12], [11, 23], [12, 24], [23, 24],  // Torso
  [11, 13], [13, 15],                        // Left arm
  [12, 14], [14, 16],                        // Right arm
  [23, 25], [25, 27],                        // Left leg
  [24, 26], [26, 28],                        // Right leg
]

<SkiaCamera
  style={StyleSheet.absoluteFill}
  isActive={true}
  device="back"
  pixelFormat="rgb"
  onFrame={(frame, render) => {
    'worklet'
    try {
      nitroPoseExercises.processFrame(frame)
      const landmarks = nitroPoseExercises.landmarks

      render(({ frameTexture, canvas }) => {
        canvas.drawImage(frameTexture, 0, 0)

        if (landmarks && landmarks.length > 0) {
          const w = frame.width
          const h = frame.height

          // Draw bones
          const linePaint = Skia.Paint()
          linePaint.setColor(Skia.Color('#00FF00'))
          linePaint.setStrokeWidth(4)
          linePaint.setStyle(1)

          for (const [i, j] of SKELETON_CONNECTIONS) {
            if (i < landmarks.length && j < landmarks.length) {
              const a = landmarks[i]
              const b = landmarks[j]
              if (a.visibility > 0.5 && b.visibility > 0.5) {
                canvas.drawLine(a.x * w, a.y * h, b.x * w, b.y * h, linePaint)
              }
            }
          }

          // Draw joints
          const jointPaint = Skia.Paint()
          jointPaint.setColor(Skia.Color('#00FFFF'))
          jointPaint.setStyle(0)

          for (let idx = 0; idx < landmarks.length; idx++) {
            const lm = landmarks[idx]
            if (lm.visibility > 0.5) {
              canvas.drawCircle(lm.x * w, lm.y * h, 6, jointPaint)
            }
          }
        }
      })
    } finally {
      frame.dispose()
    }
  }}
/>

🧩 API Reference

Lifecycle

// Initialize MediaPipe with model file path
initialize(modelPath: string): Promise<void>

// Clean up resources
release(): void

Exercise Setup

// Load an exercise config (built-in or custom)
loadExercise(config: ExerciseConfig): void

Session Control

startSession(targetReps: number, countdownSeconds: number): void
pauseSession(): void
resumeSession(): void
stopSession(): void

Frame Processing

// Pass VisionCamera frame for pose detection — call from frame processor
processFrame(frame: Frame): void

State (Readable)

readonly status: SessionStatus        // 'idle' | 'countdown' | 'active' | 'paused' | 'completed'
readonly currentPhase: ExercisePhase   // 'up' | 'down' | 'hold' | 'transition' | 'unknown'
readonly repCount: number
readonly landmarks: Landmark[]         // 33 body landmarks from MediaPipe

Callbacks

onRepComplete: ((data: RepData) => void) | undefined
onPhaseChange: ((phase: ExercisePhase) => void) | undefined
onFormFeedback: ((feedback: FormFeedback) => void) | undefined
onHoldProgress: ((progress: HoldProgress) => void) | undefined
onPoseLost: (() => void) | undefined
onPoseRegained: (() => void) | undefined
onSessionComplete: ((result: SessionResult) => void) | undefined

Callback Payloads

RepData

{
  repNumber: number       // Current rep count
  durationMs: number      // Time taken for this rep
  formScore: number       // 0-100 form quality score
  angles: AngleSnapshot[] // Joint angles at rep completion
}

FormFeedback

{
  ruleName: string; // e.g. 'hipSag'
  message: string; // e.g. 'Keep your hips up'
  severity: FormSeverity; // 'info' | 'warning' | 'error'
}

SessionResult

{
  totalReps: number
  totalDurationMs: number
  averageRepDurationMs: number
  averageFormScore: number
  formViolations: FormFeedback[]
  angleHistory: AngleSnapshot[]
}

🏋️ Built-In Exercise Configs

Push-Up (PUSHUP_CONFIG)

| Parameter | Value | | ------------- | ------------------------------------- | | Type | rep | | Primary Angle | Left elbow (shoulder → elbow → wrist) | | UP Phase | Elbow angle 140°–180° | | DOWN Phase | Elbow angle 30°–110° | | Rep Sequence | UP → DOWN → UP | | Form Rules | Hip sag detection, hip pike detection |

Custom Exercise Config

import type { ExerciseConfig } from 'react-native-nitro-pose-exercises';

const SQUAT_CONFIG: ExerciseConfig = {
  name: 'Squat',
  type: 'rep',
  angles: [
    { name: 'leftKnee', landmarkA: 23, landmarkB: 25, landmarkC: 27 },
    { name: 'rightKnee', landmarkA: 24, landmarkB: 26, landmarkC: 28 },
  ],
  phases: [
    { phase: 'up', angleName: 'leftKnee', minAngle: 160, maxAngle: 180 },
    { phase: 'down', angleName: 'leftKnee', minAngle: 50, maxAngle: 110 },
  ],
  repSequence: ['up', 'down', 'up'],
  formRules: [
    {
      name: 'kneesCaving',
      message: 'Push your knees out over your toes',
      severity: 'warning',
      angleName: 'leftKnee',
      minAngle: 50,
      maxAngle: 180,
    },
  ],
  holdDurationMs: 0,
};

📐 MediaPipe Landmark Index Reference

| Index | Landmark | Index | Landmark | | ----- | -------------- | ----- | ----------- | | 0 | Nose | 16 | Right wrist | | 11 | Left shoulder | 23 | Left hip | | 12 | Right shoulder | 24 | Right hip | | 13 | Left elbow | 25 | Left knee | | 14 | Right elbow | 26 | Right knee | | 15 | Left wrist | 27 | Left ankle |

Full 33-point reference: MediaPipe Pose Landmarks


📏 Camera Angle Guide

For best results, the camera should see the exerciser from a side profile:

| ✅ Good | ❌ Bad | | ---------------------------------- | ------------------------ | | Side view, full body visible | Front-facing view | | Phone at waist height, 6-8 ft away | Ground-level angle | | Well-lit environment | Heavy glare or backlight |


🧩 Supported Platforms

| Platform | Status | Notes | | -------------------- | ---------------- | --------------------------------- | | iOS | ✅ Supported | Requires physical device, iOS 14+ | | Android | ✅ Supported | Min SDK 24 (Android 7.0) | | iOS Simulator | ❌ Not supported | No camera access | | Android Emulator | ❌ Not supported | No real camera feed |


📊 App Size Impact

| Component | Size | | ---------------------------- | ------------- | | Pose model (Lite) | ~3 MB | | MediaPipe SDK (per platform) | ~8–12 MB | | Nitro module code | ~200 KB | | Total new addition | ~11–15 MB |


🤝 Contributing

PRs welcome!


🪪 License

MIT © Gautham Vijayan


Made with ❤️ and Nitro Modules + VisionCamera + MediaPipe