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.
- package/LICENSE +20 -0
- package/NitroPoseExercises.podspec +32 -0
- package/README.md +538 -0
- package/android/CMakeLists.txt +31 -0
- package/android/build.gradle +121 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +11 -0
- package/android/src/main/java/com/margelo/nitro/nitroposeexercises/NitroPoseExercises.kt +535 -0
- package/android/src/main/java/com/margelo/nitro/nitroposeexercises/NitroPoseExercisesPackage.kt +22 -0
- package/ios/NitroPoseExercises.swift +527 -0
- package/lib/module/NitroPoseExercises.nitro.js +17 -0
- package/lib/module/NitroPoseExercises.nitro.js.map +1 -0
- package/lib/module/config/pushup.js +71 -0
- package/lib/module/config/pushup.js.map +1 -0
- package/lib/module/index.js +7 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NitroPoseExercises.nitro.d.ts +97 -0
- package/lib/typescript/src/NitroPoseExercises.nitro.d.ts.map +1 -0
- package/lib/typescript/src/config/pushup.d.ts +3 -0
- package/lib/typescript/src/config/pushup.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +6 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/nitro.json +23 -0
- package/nitrogen/generated/android/c++/JAngleDefinition.hpp +69 -0
- package/nitrogen/generated/android/c++/JAngleSnapshot.hpp +61 -0
- package/nitrogen/generated/android/c++/JExerciseConfig.hpp +166 -0
- package/nitrogen/generated/android/c++/JExercisePhase.hpp +67 -0
- package/nitrogen/generated/android/c++/JExerciseType.hpp +61 -0
- package/nitrogen/generated/android/c++/JFormFeedback.hpp +67 -0
- package/nitrogen/generated/android/c++/JFormRule.hpp +79 -0
- package/nitrogen/generated/android/c++/JFormSeverity.hpp +61 -0
- package/nitrogen/generated/android/c++/JFunc_void.hpp +75 -0
- package/nitrogen/generated/android/c++/JFunc_void_ExercisePhase.hpp +77 -0
- package/nitrogen/generated/android/c++/JFunc_void_FormFeedback.hpp +80 -0
- package/nitrogen/generated/android/c++/JFunc_void_HoldProgress.hpp +77 -0
- package/nitrogen/generated/android/c++/JFunc_void_RepData.hpp +81 -0
- package/nitrogen/generated/android/c++/JFunc_void_SessionResult.hpp +85 -0
- package/nitrogen/generated/android/c++/JHoldProgress.hpp +65 -0
- package/nitrogen/generated/android/c++/JHybridNitroPoseExercisesSpec.cpp +311 -0
- package/nitrogen/generated/android/c++/JHybridNitroPoseExercisesSpec.hpp +87 -0
- package/nitrogen/generated/android/c++/JLandmark.hpp +69 -0
- package/nitrogen/generated/android/c++/JPhaseThreshold.hpp +71 -0
- package/nitrogen/generated/android/c++/JRepData.hpp +90 -0
- package/nitrogen/generated/android/c++/JSessionResult.hpp +120 -0
- package/nitrogen/generated/android/c++/JSessionStatus.hpp +67 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/AngleDefinition.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/AngleSnapshot.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/ExerciseConfig.kt +81 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/ExercisePhase.kt +26 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/ExerciseType.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/FormFeedback.kt +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/FormRule.kt +76 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/FormSeverity.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/Func_void.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/Func_void_ExercisePhase.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/Func_void_FormFeedback.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/Func_void_HoldProgress.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/Func_void_RepData.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/Func_void_SessionResult.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/HoldProgress.kt +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/HybridNitroPoseExercisesSpec.kt +196 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/Landmark.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/PhaseThreshold.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/RepData.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/SessionResult.kt +76 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/SessionStatus.kt +26 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/nitroposeexercisesOnLoad.kt +35 -0
- package/nitrogen/generated/android/nitroposeexercises+autolinking.cmake +81 -0
- package/nitrogen/generated/android/nitroposeexercises+autolinking.gradle +27 -0
- package/nitrogen/generated/android/nitroposeexercisesOnLoad.cpp +66 -0
- package/nitrogen/generated/android/nitroposeexercisesOnLoad.hpp +34 -0
- package/nitrogen/generated/ios/NitroPoseExercises+autolinking.rb +62 -0
- package/nitrogen/generated/ios/NitroPoseExercises-Swift-Cxx-Bridge.cpp +100 -0
- package/nitrogen/generated/ios/NitroPoseExercises-Swift-Cxx-Bridge.hpp +449 -0
- package/nitrogen/generated/ios/NitroPoseExercises-Swift-Cxx-Umbrella.hpp +95 -0
- package/nitrogen/generated/ios/NitroPoseExercisesAutolinking.mm +33 -0
- package/nitrogen/generated/ios/NitroPoseExercisesAutolinking.swift +26 -0
- package/nitrogen/generated/ios/c++/HybridNitroPoseExercisesSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridNitroPoseExercisesSpecSwift.hpp +236 -0
- package/nitrogen/generated/ios/swift/AngleDefinition.swift +44 -0
- package/nitrogen/generated/ios/swift/AngleSnapshot.swift +34 -0
- package/nitrogen/generated/ios/swift/ExerciseConfig.swift +83 -0
- package/nitrogen/generated/ios/swift/ExercisePhase.swift +52 -0
- package/nitrogen/generated/ios/swift/ExerciseType.swift +44 -0
- package/nitrogen/generated/ios/swift/FormFeedback.swift +39 -0
- package/nitrogen/generated/ios/swift/FormRule.swift +54 -0
- package/nitrogen/generated/ios/swift/FormSeverity.swift +44 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_ExercisePhase.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_FormFeedback.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_HoldProgress.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_RepData.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_SessionResult.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/HoldProgress.swift +39 -0
- package/nitrogen/generated/ios/swift/HybridNitroPoseExercisesSpec.swift +73 -0
- package/nitrogen/generated/ios/swift/HybridNitroPoseExercisesSpec_cxx.swift +483 -0
- package/nitrogen/generated/ios/swift/Landmark.swift +44 -0
- package/nitrogen/generated/ios/swift/PhaseThreshold.swift +44 -0
- package/nitrogen/generated/ios/swift/RepData.swift +50 -0
- package/nitrogen/generated/ios/swift/SessionResult.swift +66 -0
- package/nitrogen/generated/ios/swift/SessionStatus.swift +52 -0
- package/nitrogen/generated/shared/c++/AngleDefinition.hpp +95 -0
- package/nitrogen/generated/shared/c++/AngleSnapshot.hpp +87 -0
- package/nitrogen/generated/shared/c++/ExerciseConfig.hpp +122 -0
- package/nitrogen/generated/shared/c++/ExercisePhase.hpp +88 -0
- package/nitrogen/generated/shared/c++/ExerciseType.hpp +80 -0
- package/nitrogen/generated/shared/c++/FormFeedback.hpp +93 -0
- package/nitrogen/generated/shared/c++/FormRule.hpp +105 -0
- package/nitrogen/generated/shared/c++/FormSeverity.hpp +80 -0
- package/nitrogen/generated/shared/c++/HoldProgress.hpp +91 -0
- package/nitrogen/generated/shared/c++/HybridNitroPoseExercisesSpec.cpp +46 -0
- package/nitrogen/generated/shared/c++/HybridNitroPoseExercisesSpec.hpp +117 -0
- package/nitrogen/generated/shared/c++/Landmark.hpp +95 -0
- package/nitrogen/generated/shared/c++/PhaseThreshold.hpp +97 -0
- package/nitrogen/generated/shared/c++/RepData.hpp +97 -0
- package/nitrogen/generated/shared/c++/SessionResult.hpp +108 -0
- package/nitrogen/generated/shared/c++/SessionStatus.hpp +88 -0
- package/package.json +187 -0
- package/src/NitroPoseExercises.nitro.ts +155 -0
- package/src/config/pushup.ts +62 -0
- 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
|
+
)
|