react-native-nitro-pose-exercises 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/LICENSE +20 -0
  2. package/NitroPoseExercises.podspec +32 -0
  3. package/README.md +538 -0
  4. package/android/CMakeLists.txt +31 -0
  5. package/android/build.gradle +121 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/cpp/cpp-adapter.cpp +11 -0
  8. package/android/src/main/java/com/margelo/nitro/nitroposeexercises/NitroPoseExercises.kt +535 -0
  9. package/android/src/main/java/com/margelo/nitro/nitroposeexercises/NitroPoseExercisesPackage.kt +22 -0
  10. package/ios/NitroPoseExercises.swift +527 -0
  11. package/lib/module/NitroPoseExercises.nitro.js +17 -0
  12. package/lib/module/NitroPoseExercises.nitro.js.map +1 -0
  13. package/lib/module/config/pushup.js +71 -0
  14. package/lib/module/config/pushup.js.map +1 -0
  15. package/lib/module/index.js +7 -0
  16. package/lib/module/index.js.map +1 -0
  17. package/lib/module/package.json +1 -0
  18. package/lib/typescript/package.json +1 -0
  19. package/lib/typescript/src/NitroPoseExercises.nitro.d.ts +97 -0
  20. package/lib/typescript/src/NitroPoseExercises.nitro.d.ts.map +1 -0
  21. package/lib/typescript/src/config/pushup.d.ts +3 -0
  22. package/lib/typescript/src/config/pushup.d.ts.map +1 -0
  23. package/lib/typescript/src/index.d.ts +6 -0
  24. package/lib/typescript/src/index.d.ts.map +1 -0
  25. package/nitro.json +23 -0
  26. package/nitrogen/generated/android/c++/JAngleDefinition.hpp +69 -0
  27. package/nitrogen/generated/android/c++/JAngleSnapshot.hpp +61 -0
  28. package/nitrogen/generated/android/c++/JExerciseConfig.hpp +166 -0
  29. package/nitrogen/generated/android/c++/JExercisePhase.hpp +67 -0
  30. package/nitrogen/generated/android/c++/JExerciseType.hpp +61 -0
  31. package/nitrogen/generated/android/c++/JFormFeedback.hpp +67 -0
  32. package/nitrogen/generated/android/c++/JFormRule.hpp +79 -0
  33. package/nitrogen/generated/android/c++/JFormSeverity.hpp +61 -0
  34. package/nitrogen/generated/android/c++/JFunc_void.hpp +75 -0
  35. package/nitrogen/generated/android/c++/JFunc_void_ExercisePhase.hpp +77 -0
  36. package/nitrogen/generated/android/c++/JFunc_void_FormFeedback.hpp +80 -0
  37. package/nitrogen/generated/android/c++/JFunc_void_HoldProgress.hpp +77 -0
  38. package/nitrogen/generated/android/c++/JFunc_void_RepData.hpp +81 -0
  39. package/nitrogen/generated/android/c++/JFunc_void_SessionResult.hpp +85 -0
  40. package/nitrogen/generated/android/c++/JHoldProgress.hpp +65 -0
  41. package/nitrogen/generated/android/c++/JHybridNitroPoseExercisesSpec.cpp +311 -0
  42. package/nitrogen/generated/android/c++/JHybridNitroPoseExercisesSpec.hpp +87 -0
  43. package/nitrogen/generated/android/c++/JLandmark.hpp +69 -0
  44. package/nitrogen/generated/android/c++/JPhaseThreshold.hpp +71 -0
  45. package/nitrogen/generated/android/c++/JRepData.hpp +90 -0
  46. package/nitrogen/generated/android/c++/JSessionResult.hpp +120 -0
  47. package/nitrogen/generated/android/c++/JSessionStatus.hpp +67 -0
  48. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/AngleDefinition.kt +66 -0
  49. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/AngleSnapshot.kt +56 -0
  50. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/ExerciseConfig.kt +81 -0
  51. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/ExercisePhase.kt +26 -0
  52. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/ExerciseType.kt +24 -0
  53. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/FormFeedback.kt +61 -0
  54. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/FormRule.kt +76 -0
  55. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/FormSeverity.kt +24 -0
  56. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/Func_void.kt +80 -0
  57. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/Func_void_ExercisePhase.kt +80 -0
  58. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/Func_void_FormFeedback.kt +80 -0
  59. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/Func_void_HoldProgress.kt +80 -0
  60. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/Func_void_RepData.kt +80 -0
  61. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/Func_void_SessionResult.kt +80 -0
  62. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/HoldProgress.kt +61 -0
  63. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/HybridNitroPoseExercisesSpec.kt +196 -0
  64. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/Landmark.kt +66 -0
  65. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/PhaseThreshold.kt +66 -0
  66. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/RepData.kt +66 -0
  67. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/SessionResult.kt +76 -0
  68. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/SessionStatus.kt +26 -0
  69. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/nitroposeexercisesOnLoad.kt +35 -0
  70. package/nitrogen/generated/android/nitroposeexercises+autolinking.cmake +81 -0
  71. package/nitrogen/generated/android/nitroposeexercises+autolinking.gradle +27 -0
  72. package/nitrogen/generated/android/nitroposeexercisesOnLoad.cpp +66 -0
  73. package/nitrogen/generated/android/nitroposeexercisesOnLoad.hpp +34 -0
  74. package/nitrogen/generated/ios/NitroPoseExercises+autolinking.rb +62 -0
  75. package/nitrogen/generated/ios/NitroPoseExercises-Swift-Cxx-Bridge.cpp +100 -0
  76. package/nitrogen/generated/ios/NitroPoseExercises-Swift-Cxx-Bridge.hpp +449 -0
  77. package/nitrogen/generated/ios/NitroPoseExercises-Swift-Cxx-Umbrella.hpp +95 -0
  78. package/nitrogen/generated/ios/NitroPoseExercisesAutolinking.mm +33 -0
  79. package/nitrogen/generated/ios/NitroPoseExercisesAutolinking.swift +26 -0
  80. package/nitrogen/generated/ios/c++/HybridNitroPoseExercisesSpecSwift.cpp +11 -0
  81. package/nitrogen/generated/ios/c++/HybridNitroPoseExercisesSpecSwift.hpp +236 -0
  82. package/nitrogen/generated/ios/swift/AngleDefinition.swift +44 -0
  83. package/nitrogen/generated/ios/swift/AngleSnapshot.swift +34 -0
  84. package/nitrogen/generated/ios/swift/ExerciseConfig.swift +83 -0
  85. package/nitrogen/generated/ios/swift/ExercisePhase.swift +52 -0
  86. package/nitrogen/generated/ios/swift/ExerciseType.swift +44 -0
  87. package/nitrogen/generated/ios/swift/FormFeedback.swift +39 -0
  88. package/nitrogen/generated/ios/swift/FormRule.swift +54 -0
  89. package/nitrogen/generated/ios/swift/FormSeverity.swift +44 -0
  90. package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
  91. package/nitrogen/generated/ios/swift/Func_void_ExercisePhase.swift +46 -0
  92. package/nitrogen/generated/ios/swift/Func_void_FormFeedback.swift +46 -0
  93. package/nitrogen/generated/ios/swift/Func_void_HoldProgress.swift +46 -0
  94. package/nitrogen/generated/ios/swift/Func_void_RepData.swift +46 -0
  95. package/nitrogen/generated/ios/swift/Func_void_SessionResult.swift +46 -0
  96. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  97. package/nitrogen/generated/ios/swift/HoldProgress.swift +39 -0
  98. package/nitrogen/generated/ios/swift/HybridNitroPoseExercisesSpec.swift +73 -0
  99. package/nitrogen/generated/ios/swift/HybridNitroPoseExercisesSpec_cxx.swift +483 -0
  100. package/nitrogen/generated/ios/swift/Landmark.swift +44 -0
  101. package/nitrogen/generated/ios/swift/PhaseThreshold.swift +44 -0
  102. package/nitrogen/generated/ios/swift/RepData.swift +50 -0
  103. package/nitrogen/generated/ios/swift/SessionResult.swift +66 -0
  104. package/nitrogen/generated/ios/swift/SessionStatus.swift +52 -0
  105. package/nitrogen/generated/shared/c++/AngleDefinition.hpp +95 -0
  106. package/nitrogen/generated/shared/c++/AngleSnapshot.hpp +87 -0
  107. package/nitrogen/generated/shared/c++/ExerciseConfig.hpp +122 -0
  108. package/nitrogen/generated/shared/c++/ExercisePhase.hpp +88 -0
  109. package/nitrogen/generated/shared/c++/ExerciseType.hpp +80 -0
  110. package/nitrogen/generated/shared/c++/FormFeedback.hpp +93 -0
  111. package/nitrogen/generated/shared/c++/FormRule.hpp +105 -0
  112. package/nitrogen/generated/shared/c++/FormSeverity.hpp +80 -0
  113. package/nitrogen/generated/shared/c++/HoldProgress.hpp +91 -0
  114. package/nitrogen/generated/shared/c++/HybridNitroPoseExercisesSpec.cpp +46 -0
  115. package/nitrogen/generated/shared/c++/HybridNitroPoseExercisesSpec.hpp +117 -0
  116. package/nitrogen/generated/shared/c++/Landmark.hpp +95 -0
  117. package/nitrogen/generated/shared/c++/PhaseThreshold.hpp +97 -0
  118. package/nitrogen/generated/shared/c++/RepData.hpp +97 -0
  119. package/nitrogen/generated/shared/c++/SessionResult.hpp +108 -0
  120. package/nitrogen/generated/shared/c++/SessionStatus.hpp +88 -0
  121. package/package.json +187 -0
  122. package/src/NitroPoseExercises.nitro.ts +155 -0
  123. package/src/config/pushup.ts +62 -0
  124. package/src/index.tsx +28 -0
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gautham495
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
@@ -0,0 +1,32 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "NitroPoseExercises"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = package["homepage"]
10
+ s.license = package["license"]
11
+ s.authors = package["author"]
12
+
13
+ s.platforms = { :ios => 16.0 }
14
+ s.source = { :git => "https://github.com/Gautham495/react-native-nitro-pose-exercises.git", :tag => "#{s.version}" }
15
+
16
+ s.source_files = [
17
+ "ios/**/*.{swift}",
18
+ "ios/**/*.{m,mm}",
19
+ "cpp/**/*.{hpp,cpp}",
20
+ ]
21
+
22
+ s.dependency 'React-jsi'
23
+ s.dependency 'React-callinvoker'
24
+
25
+ load 'nitrogen/generated/ios/NitroPoseExercises+autolinking.rb'
26
+ add_nitrogen_files(s)
27
+
28
+ s.dependency 'MediaPipeTasksVision', '~> 0.10.0'
29
+ s.dependency "VisionCamera"
30
+
31
+ install_modules_dependencies(s)
32
+ end
package/README.md ADDED
@@ -0,0 +1,538 @@
1
+ <a href="https://gauthamvijay.com">
2
+ <picture>
3
+ <img alt="react-native-nitro-pose-exercises-banner" src="./docs/img/banner.png" />
4
+ </picture>
5
+ </a>
6
+
7
+ # react-native-nitro-pose-exercises
8
+
9
+ A **React Native Nitro Module** for real-time, on-device exercise tracking using pose estimation. Built on **MediaPipe Pose Landmarker** and **VisionCamera v5**.
10
+
11
+ - 🏋️ **Rep Counting** — Automatic rep detection with configurable state machines
12
+ - 🧘 **Hold Tracking** — Duration and stability tracking for planks, yoga poses, and isometric holds
13
+ - 📐 **Form Validation** — Real-time form feedback with configurable angle-based rules
14
+ - 💀 **Skeleton Overlay** — Optional Skia-powered skeleton rendering over the camera feed
15
+ - ⚡ **Fully Native** — MediaPipe runs on-device via Nitro Modules, zero JS bridge overhead
16
+
17
+ ---
18
+
19
+ > [!IMPORTANT]
20
+ >
21
+ > - Requires React Native **0.76+** with Nitro Modules and VisionCamera **v5**.
22
+ > - Must be tested on a **physical device** — camera + ML inference don't work on simulators.
23
+ > - MediaPipe Pose Landmarker model file (`pose_landmarker_lite.task`) must be bundled with the app.
24
+
25
+ ---
26
+
27
+ ## 📦 Installation
28
+
29
+ ```bash
30
+ npm install react-native-nitro-pose-exercises react-native-nitro-modules
31
+ npm install react-native-vision-camera react-native-nitro-image
32
+ npm install react-native-vision-camera-worklets react-native-worklets
33
+ npm install react-native-reanimated
34
+ ```
35
+
36
+ **For Skia skeleton overlay (optional):**
37
+
38
+ ```bash
39
+ npm install @shopify/react-native-skia react-native-vision-camera-skia
40
+ ```
41
+
42
+ ```bash
43
+ cd ios && pod install
44
+ ```
45
+
46
+ > [!NOTE]
47
+ > This package uses **MediaPipe Pose Landmarker** natively on both platforms.
48
+ > iOS uses `MediaPipeTasksVision` via CocoaPods.
49
+ > Android uses `com.google.mediapipe:tasks-vision` via Gradle.
50
+
51
+ ---
52
+
53
+ ## Demo
54
+
55
+ <table>
56
+ <tr>
57
+ <th align="center">🍏 iOS Normal Mode</th>
58
+ <th align="center">🍏 iOS Skia Mode</th>
59
+ <th align="center">🤖 Android Demo</th>
60
+ </tr>
61
+ <tr>
62
+ <td align="center">
63
+ <img alt="android" src="./docs/img/normal-iOS.png" height="650" width="300"/>
64
+ </td>
65
+ <td align="center">
66
+ <img alt="android" src="./docs/img/skia-iOS.png" height="650" width="300"/>
67
+ </td>
68
+ <td align="center">
69
+ <div>Screenshot Coming Soon!</div>
70
+ </td>
71
+ </tr>
72
+ </table>
73
+
74
+ ---
75
+
76
+ ## 🧠 Overview
77
+
78
+ | Feature | Description |
79
+ | ------------------------ | ------------------------------------------------------------------------- |
80
+ | **Rep-Based Exercises** | Cyclic state machine (UP → DOWN → UP = 1 rep). Push-ups, squats, curls. |
81
+ | **Hold-Based Exercises** | Single target pose with duration tracking. Planks, wall sits, yoga poses. |
82
+ | **Flow-Based Exercises** | Ordered sequence of poses. Sun salutation, yoga flows. _(coming soon)_ |
83
+ | **Form Feedback** | Angle-based rules with throttled real-time callbacks. |
84
+ | **Skeleton Overlay** | 33-point body skeleton drawn over camera via Skia. |
85
+ | **Bilateral Tracking** | Left and right side angles tracked independently. |
86
+
87
+ ---
88
+
89
+ ## 🔧 Setup
90
+
91
+ ### Model File
92
+
93
+ Download the MediaPipe Pose Landmarker model:
94
+
95
+ ```
96
+ https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_lite/float16/latest/pose_landmarker_lite.task
97
+ ```
98
+
99
+ **iOS:** Drag `pose_landmarker_lite.task` into your Xcode project (Copy items if needed, add to app target).
100
+
101
+ **Android:** Place at `android/app/src/main/assets/pose_landmarker_lite.task`
102
+
103
+ ### Permissions
104
+
105
+ **iOS — `Info.plist`:**
106
+
107
+ ```xml
108
+ <key>NSCameraUsageDescription</key>
109
+ <string>Camera is needed for pose detection during exercises</string>
110
+ <key>NSMicrophoneUsageDescription</key>
111
+ <string>Microphone access for audio during exercise sessions</string>
112
+ ```
113
+
114
+ **Android — `AndroidManifest.xml`:**
115
+
116
+ ```xml
117
+ <uses-permission android:name="android.permission.CAMERA" />
118
+ ```
119
+
120
+ ### Babel Config
121
+
122
+ ```javascript
123
+ module.exports = {
124
+ presets: ['module:@react-native/babel-preset'],
125
+ plugins: [
126
+ 'react-native-worklets/plugin',
127
+ 'react-native-reanimated/plugin', // must be last
128
+ ],
129
+ };
130
+ ```
131
+
132
+ ---
133
+
134
+ # GIF Demo
135
+
136
+ <table>
137
+ <tr>
138
+ <th align="center">📸 Normal Mode</th>
139
+ <th align="center">💀 Skeleton Mode</th>
140
+ </tr>
141
+ <tr>
142
+ <td align="center">
143
+ <img alt="normal-mode" src="https://github.gauthamvijay.com/normal-iOS.gif" height="650" width="300"/>
144
+ </td>
145
+ <td align="center">
146
+ <img alt="skeleton-mode" src="https://github.gauthamvijay.com/skia-iOS.gif" height="650" width="300"/>
147
+ </td>
148
+ </tr>
149
+ </table>
150
+
151
+ ---
152
+
153
+ ## ⚙️ Usage
154
+
155
+ ### Basic — Normal Camera (No Skeleton)
156
+
157
+ ```tsx
158
+ import { useEffect, useCallback, useState } from 'react';
159
+ import { StyleSheet, View, Text, TouchableOpacity } from 'react-native';
160
+ import {
161
+ Camera,
162
+ useCameraDevice,
163
+ useCameraPermission,
164
+ useFrameOutput,
165
+ useAsyncRunner,
166
+ } from 'react-native-vision-camera';
167
+ import {
168
+ nitroPoseExercises,
169
+ PUSHUP_CONFIG,
170
+ type RepData,
171
+ type FormFeedback,
172
+ type SessionResult,
173
+ } from 'react-native-nitro-pose-exercises';
174
+
175
+ export default function App() {
176
+ const { hasPermission, requestPermission } = useCameraPermission();
177
+ const device = useCameraDevice('back');
178
+ const asyncRunner = useAsyncRunner();
179
+ const [repCount, setRepCount] = useState(0);
180
+
181
+ useEffect(() => {
182
+ if (!hasPermission) requestPermission();
183
+ }, [hasPermission]);
184
+
185
+ // Initialize pose engine
186
+ useEffect(() => {
187
+ async function init() {
188
+ await nitroPoseExercises.initialize('pose_landmarker_lite.task');
189
+ nitroPoseExercises.loadExercise(PUSHUP_CONFIG);
190
+
191
+ nitroPoseExercises.onRepComplete = (data: RepData) => {
192
+ setRepCount(data.repNumber);
193
+ console.log(`Rep ${data.repNumber} — form: ${data.formScore}`);
194
+ };
195
+
196
+ nitroPoseExercises.onFormFeedback = (feedback: FormFeedback) => {
197
+ console.log(`Form: ${feedback.message}`);
198
+ };
199
+
200
+ nitroPoseExercises.onSessionComplete = (result: SessionResult) => {
201
+ console.log(
202
+ `Done! ${result.totalReps} reps, avg form: ${result.averageFormScore}`
203
+ );
204
+ };
205
+
206
+ // Start: 10 target reps, 3 second countdown
207
+ nitroPoseExercises.startSession(10, 3);
208
+ }
209
+
210
+ init();
211
+ return () => {
212
+ nitroPoseExercises.release();
213
+ };
214
+ }, []);
215
+
216
+ // Frame processor
217
+ const frameOutput = useFrameOutput({
218
+ pixelFormat: 'rgb',
219
+ onFrame(frame) {
220
+ 'worklet';
221
+ const accepted = asyncRunner.runAsync(() => {
222
+ 'worklet';
223
+ try {
224
+ nitroPoseExercises.processFrame(frame);
225
+ } finally {
226
+ frame.dispose();
227
+ }
228
+ });
229
+ if (!accepted) frame.dispose();
230
+ },
231
+ });
232
+
233
+ if (!hasPermission || !device) return null;
234
+
235
+ return (
236
+ <View style={StyleSheet.absoluteFill}>
237
+ <Camera
238
+ style={StyleSheet.absoluteFill}
239
+ device={device}
240
+ isActive={true}
241
+ outputs={[frameOutput]}
242
+ />
243
+ <Text style={styles.repText}>{repCount} REPS</Text>
244
+ </View>
245
+ );
246
+ }
247
+
248
+ const styles = StyleSheet.create({
249
+ repText: {
250
+ position: 'absolute',
251
+ top: 100,
252
+ alignSelf: 'center',
253
+ fontSize: 48,
254
+ fontFamily: 'System',
255
+ color: '#4CAF50',
256
+ },
257
+ });
258
+ ```
259
+
260
+ ### Skeleton Overlay — SkiaCamera
261
+
262
+ ```tsx
263
+ import { SkiaCamera } from 'react-native-vision-camera-skia'
264
+ import { Skia } from '@shopify/react-native-skia'
265
+ import { nitroPoseExercises } from 'react-native-nitro-pose-exercises'
266
+
267
+ const SKELETON_CONNECTIONS: [number, number][] = [
268
+ [11, 12], [11, 23], [12, 24], [23, 24], // Torso
269
+ [11, 13], [13, 15], // Left arm
270
+ [12, 14], [14, 16], // Right arm
271
+ [23, 25], [25, 27], // Left leg
272
+ [24, 26], [26, 28], // Right leg
273
+ ]
274
+
275
+ <SkiaCamera
276
+ style={StyleSheet.absoluteFill}
277
+ isActive={true}
278
+ device="back"
279
+ pixelFormat="rgb"
280
+ onFrame={(frame, render) => {
281
+ 'worklet'
282
+ try {
283
+ nitroPoseExercises.processFrame(frame)
284
+ const landmarks = nitroPoseExercises.landmarks
285
+
286
+ render(({ frameTexture, canvas }) => {
287
+ canvas.drawImage(frameTexture, 0, 0)
288
+
289
+ if (landmarks && landmarks.length > 0) {
290
+ const w = frame.width
291
+ const h = frame.height
292
+
293
+ // Draw bones
294
+ const linePaint = Skia.Paint()
295
+ linePaint.setColor(Skia.Color('#00FF00'))
296
+ linePaint.setStrokeWidth(4)
297
+ linePaint.setStyle(1)
298
+
299
+ for (const [i, j] of SKELETON_CONNECTIONS) {
300
+ if (i < landmarks.length && j < landmarks.length) {
301
+ const a = landmarks[i]
302
+ const b = landmarks[j]
303
+ if (a.visibility > 0.5 && b.visibility > 0.5) {
304
+ canvas.drawLine(a.x * w, a.y * h, b.x * w, b.y * h, linePaint)
305
+ }
306
+ }
307
+ }
308
+
309
+ // Draw joints
310
+ const jointPaint = Skia.Paint()
311
+ jointPaint.setColor(Skia.Color('#00FFFF'))
312
+ jointPaint.setStyle(0)
313
+
314
+ for (let idx = 0; idx < landmarks.length; idx++) {
315
+ const lm = landmarks[idx]
316
+ if (lm.visibility > 0.5) {
317
+ canvas.drawCircle(lm.x * w, lm.y * h, 6, jointPaint)
318
+ }
319
+ }
320
+ }
321
+ })
322
+ } finally {
323
+ frame.dispose()
324
+ }
325
+ }}
326
+ />
327
+ ```
328
+
329
+ ---
330
+
331
+ ## 🧩 API Reference
332
+
333
+ ### Lifecycle
334
+
335
+ ```ts
336
+ // Initialize MediaPipe with model file path
337
+ initialize(modelPath: string): Promise<void>
338
+
339
+ // Clean up resources
340
+ release(): void
341
+ ```
342
+
343
+ ### Exercise Setup
344
+
345
+ ```ts
346
+ // Load an exercise config (built-in or custom)
347
+ loadExercise(config: ExerciseConfig): void
348
+ ```
349
+
350
+ ### Session Control
351
+
352
+ ```ts
353
+ startSession(targetReps: number, countdownSeconds: number): void
354
+ pauseSession(): void
355
+ resumeSession(): void
356
+ stopSession(): void
357
+ ```
358
+
359
+ ### Frame Processing
360
+
361
+ ```ts
362
+ // Pass VisionCamera frame for pose detection — call from frame processor
363
+ processFrame(frame: Frame): void
364
+ ```
365
+
366
+ ### State (Readable)
367
+
368
+ ```ts
369
+ readonly status: SessionStatus // 'idle' | 'countdown' | 'active' | 'paused' | 'completed'
370
+ readonly currentPhase: ExercisePhase // 'up' | 'down' | 'hold' | 'transition' | 'unknown'
371
+ readonly repCount: number
372
+ readonly landmarks: Landmark[] // 33 body landmarks from MediaPipe
373
+ ```
374
+
375
+ ### Callbacks
376
+
377
+ ```ts
378
+ onRepComplete: ((data: RepData) => void) | undefined
379
+ onPhaseChange: ((phase: ExercisePhase) => void) | undefined
380
+ onFormFeedback: ((feedback: FormFeedback) => void) | undefined
381
+ onHoldProgress: ((progress: HoldProgress) => void) | undefined
382
+ onPoseLost: (() => void) | undefined
383
+ onPoseRegained: (() => void) | undefined
384
+ onSessionComplete: ((result: SessionResult) => void) | undefined
385
+ ```
386
+
387
+ ---
388
+
389
+ ### Callback Payloads
390
+
391
+ #### RepData
392
+
393
+ ```ts
394
+ {
395
+ repNumber: number // Current rep count
396
+ durationMs: number // Time taken for this rep
397
+ formScore: number // 0-100 form quality score
398
+ angles: AngleSnapshot[] // Joint angles at rep completion
399
+ }
400
+ ```
401
+
402
+ #### FormFeedback
403
+
404
+ ```ts
405
+ {
406
+ ruleName: string; // e.g. 'hipSag'
407
+ message: string; // e.g. 'Keep your hips up'
408
+ severity: FormSeverity; // 'info' | 'warning' | 'error'
409
+ }
410
+ ```
411
+
412
+ #### SessionResult
413
+
414
+ ```ts
415
+ {
416
+ totalReps: number
417
+ totalDurationMs: number
418
+ averageRepDurationMs: number
419
+ averageFormScore: number
420
+ formViolations: FormFeedback[]
421
+ angleHistory: AngleSnapshot[]
422
+ }
423
+ ```
424
+
425
+ ---
426
+
427
+ ## 🏋️ Built-In Exercise Configs
428
+
429
+ ### Push-Up (`PUSHUP_CONFIG`)
430
+
431
+ | Parameter | Value |
432
+ | ------------- | ------------------------------------- |
433
+ | Type | `rep` |
434
+ | Primary Angle | Left elbow (shoulder → elbow → wrist) |
435
+ | UP Phase | Elbow angle 140°–180° |
436
+ | DOWN Phase | Elbow angle 30°–110° |
437
+ | Rep Sequence | UP → DOWN → UP |
438
+ | Form Rules | Hip sag detection, hip pike detection |
439
+
440
+ ### Custom Exercise Config
441
+
442
+ ```ts
443
+ import type { ExerciseConfig } from 'react-native-nitro-pose-exercises';
444
+
445
+ const SQUAT_CONFIG: ExerciseConfig = {
446
+ name: 'Squat',
447
+ type: 'rep',
448
+ angles: [
449
+ { name: 'leftKnee', landmarkA: 23, landmarkB: 25, landmarkC: 27 },
450
+ { name: 'rightKnee', landmarkA: 24, landmarkB: 26, landmarkC: 28 },
451
+ ],
452
+ phases: [
453
+ { phase: 'up', angleName: 'leftKnee', minAngle: 160, maxAngle: 180 },
454
+ { phase: 'down', angleName: 'leftKnee', minAngle: 50, maxAngle: 110 },
455
+ ],
456
+ repSequence: ['up', 'down', 'up'],
457
+ formRules: [
458
+ {
459
+ name: 'kneesCaving',
460
+ message: 'Push your knees out over your toes',
461
+ severity: 'warning',
462
+ angleName: 'leftKnee',
463
+ minAngle: 50,
464
+ maxAngle: 180,
465
+ },
466
+ ],
467
+ holdDurationMs: 0,
468
+ };
469
+ ```
470
+
471
+ ---
472
+
473
+ ## 📐 MediaPipe Landmark Index Reference
474
+
475
+ | Index | Landmark | Index | Landmark |
476
+ | ----- | -------------- | ----- | ----------- |
477
+ | 0 | Nose | 16 | Right wrist |
478
+ | 11 | Left shoulder | 23 | Left hip |
479
+ | 12 | Right shoulder | 24 | Right hip |
480
+ | 13 | Left elbow | 25 | Left knee |
481
+ | 14 | Right elbow | 26 | Right knee |
482
+ | 15 | Left wrist | 27 | Left ankle |
483
+
484
+ Full 33-point reference: [MediaPipe Pose Landmarks](https://ai.google.dev/edge/mediapipe/solutions/vision/pose_landmarker#pose_landmarker_model)
485
+
486
+ ---
487
+
488
+ ## 📏 Camera Angle Guide
489
+
490
+ For best results, the camera should see the exerciser from a **side profile**:
491
+
492
+ | ✅ Good | ❌ Bad |
493
+ | ---------------------------------- | ------------------------ |
494
+ | Side view, full body visible | Front-facing view |
495
+ | Phone at waist height, 6-8 ft away | Ground-level angle |
496
+ | Well-lit environment | Heavy glare or backlight |
497
+
498
+ ---
499
+
500
+ ## 🧩 Supported Platforms
501
+
502
+ | Platform | Status | Notes |
503
+ | -------------------- | ---------------- | --------------------------------- |
504
+ | **iOS** | ✅ Supported | Requires physical device, iOS 14+ |
505
+ | **Android** | ✅ Supported | Min SDK 24 (Android 7.0) |
506
+ | **iOS Simulator** | ❌ Not supported | No camera access |
507
+ | **Android Emulator** | ❌ Not supported | No real camera feed |
508
+
509
+ ---
510
+
511
+ ## 📊 App Size Impact
512
+
513
+ | Component | Size |
514
+ | ---------------------------- | ------------- |
515
+ | Pose model (Lite) | ~3 MB |
516
+ | MediaPipe SDK (per platform) | ~8–12 MB |
517
+ | Nitro module code | ~200 KB |
518
+ | **Total new addition** | **~11–15 MB** |
519
+
520
+ ---
521
+
522
+ ## 🤝 Contributing
523
+
524
+ PRs welcome!
525
+
526
+ - [Development Workflow](CONTRIBUTING.md#development-workflow)
527
+ - [Sending a PR](CONTRIBUTING.md#sending-a-pull-request)
528
+ - [Code of Conduct](CODE_OF_CONDUCT.md)
529
+
530
+ ---
531
+
532
+ ## 🪪 License
533
+
534
+ MIT © [**Gautham Vijayan**](https://gauthamvijay.com)
535
+
536
+ ---
537
+
538
+ Made with ❤️ and [**Nitro Modules**](https://nitro.margelo.com) + [**VisionCamera**](https://visioncamera.margelo.com) + [**MediaPipe**](https://ai.google.dev/edge/mediapipe)
@@ -0,0 +1,31 @@
1
+ project(nitroposeexercises)
2
+ cmake_minimum_required(VERSION 3.9.0)
3
+
4
+ set(PACKAGE_NAME nitroposeexercises)
5
+ set(CMAKE_VERBOSE_MAKEFILE ON)
6
+ set(CMAKE_CXX_STANDARD 20)
7
+
8
+ # Define C++ library and add all sources
9
+ add_library(${PACKAGE_NAME} SHARED src/main/cpp/cpp-adapter.cpp)
10
+
11
+ # Add Nitrogen specs :)
12
+ include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/nitroposeexercises+autolinking.cmake)
13
+
14
+ # Set up local includes
15
+ include_directories("src/main/cpp" "../cpp")
16
+
17
+ find_library(LOG_LIB log)
18
+
19
+ # Link all libraries together
20
+ target_link_libraries(
21
+ ${PACKAGE_NAME}
22
+ ${LOG_LIB}
23
+ android # <-- Android core
24
+ )
25
+
26
+ find_package(react-native-vision-camera REQUIRED)
27
+
28
+ target_link_libraries(
29
+ ${PACKAGE_NAME}
30
+ react-native-vision-camera::VisionCamera
31
+ )