@pose-tracker/react-native-pose-estimation-light 0.1.0
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 +45 -0
- package/PoseTrackerVision.podspec +28 -0
- package/README.md +161 -0
- package/THIRD_PARTY_NOTICES.md +19 -0
- package/ios/PoseTrackerVision/PoseTrackerBodyPosePlugin.m +18 -0
- package/ios/PoseTrackerVision/PoseTrackerBodyPosePlugin.swift +108 -0
- package/lib/PoseTrackerProvider.d.ts +102 -0
- package/lib/PoseTrackerProvider.js +204 -0
- package/lib/api/configure.d.ts +29 -0
- package/lib/api/configure.js +64 -0
- package/lib/api/skeleton.d.ts +17 -0
- package/lib/api/skeleton.js +53 -0
- package/lib/api/track.d.ts +61 -0
- package/lib/api/track.js +146 -0
- package/lib/backends/PoseBackend.d.ts +50 -0
- package/lib/backends/PoseBackend.js +11 -0
- package/lib/backends/vision/VisionPoseBackend.d.ts +72 -0
- package/lib/backends/vision/VisionPoseBackend.js +177 -0
- package/lib/backends/vision/mapVisionJoints.d.ts +35 -0
- package/lib/backends/vision/mapVisionJoints.js +162 -0
- package/lib/backends/vision/optionalVision.d.ts +32 -0
- package/lib/backends/vision/optionalVision.js +76 -0
- package/lib/backends/webview/WebViewPoseBackend.d.ts +223 -0
- package/lib/backends/webview/WebViewPoseBackend.js +382 -0
- package/lib/backends/webview/brandAssets.d.ts +5 -0
- package/lib/backends/webview/brandAssets.js +8 -0
- package/lib/backends/webview/onlineRuntime.d.ts +45 -0
- package/lib/backends/webview/onlineRuntime.js +57 -0
- package/lib/backends/webview/poseHtml.d.ts +71 -0
- package/lib/backends/webview/poseHtml.js +188 -0
- package/lib/backends/webview/poseRuntimeSource.d.ts +3 -0
- package/lib/backends/webview/poseRuntimeSource.js +4 -0
- package/lib/cache/obfuscate.d.ts +18 -0
- package/lib/cache/obfuscate.js +90 -0
- package/lib/camera/PoseCameraView.d.ts +51 -0
- package/lib/camera/PoseCameraView.js +99 -0
- package/lib/camera/WebViewPoseView.d.ts +81 -0
- package/lib/camera/WebViewPoseView.js +312 -0
- package/lib/client.d.ts +361 -0
- package/lib/client.js +1046 -0
- package/lib/diagnostics/logReport.d.ts +23 -0
- package/lib/diagnostics/logReport.js +102 -0
- package/lib/engine/EngineLoader.d.ts +95 -0
- package/lib/engine/EngineLoader.js +347 -0
- package/lib/engine/types.d.ts +83 -0
- package/lib/engine/types.js +11 -0
- package/lib/events/classicMessage.d.ts +31 -0
- package/lib/events/classicMessage.js +259 -0
- package/lib/exercises/aliases.d.ts +18 -0
- package/lib/exercises/aliases.js +48 -0
- package/lib/index.d.ts +72 -0
- package/lib/index.js +170 -0
- package/lib/models/poseModels.d.ts +39 -0
- package/lib/models/poseModels.js +63 -0
- package/lib/quality/AdaptiveQualityController.d.ts +120 -0
- package/lib/quality/AdaptiveQualityController.js +423 -0
- package/lib/quality/RuntimeGuard.d.ts +34 -0
- package/lib/quality/RuntimeGuard.js +105 -0
- package/lib/quality/captureMode.d.ts +82 -0
- package/lib/quality/captureMode.js +75 -0
- package/lib/quality/deviceCapability.d.ts +37 -0
- package/lib/quality/deviceCapability.js +177 -0
- package/lib/quality/profiles.d.ts +125 -0
- package/lib/quality/profiles.js +202 -0
- package/lib/runtime/RuntimeCache.d.ts +75 -0
- package/lib/runtime/RuntimeCache.js +230 -0
- package/lib/sdkVersion.d.ts +2 -0
- package/lib/sdkVersion.js +5 -0
- package/lib/support/optionalModules.d.ts +46 -0
- package/lib/support/optionalModules.js +82 -0
- package/lib/types/acceleration.d.ts +86 -0
- package/lib/types/acceleration.js +15 -0
- package/lib/types/events.d.ts +345 -0
- package/lib/types/events.js +10 -0
- package/lib/types/features.d.ts +90 -0
- package/lib/types/features.js +122 -0
- package/lib/types/manifest.d.ts +197 -0
- package/lib/types/manifest.js +10 -0
- package/lib/types/pose.d.ts +36 -0
- package/lib/types/pose.js +41 -0
- package/lib/types/preload.d.ts +17 -0
- package/lib/types/preload.js +10 -0
- package/lib/types/skeleton.d.ts +29 -0
- package/lib/types/skeleton.js +52 -0
- package/package.json +92 -0
- package/react-native.config.js +12 -0
- package/src/PoseTrackerProvider.tsx +332 -0
- package/src/api/configure.ts +82 -0
- package/src/api/skeleton.ts +60 -0
- package/src/api/track.ts +182 -0
- package/src/backends/PoseBackend.ts +59 -0
- package/src/backends/vision/VisionPoseBackend.ts +238 -0
- package/src/backends/vision/mapVisionJoints.ts +185 -0
- package/src/backends/vision/optionalVision.ts +94 -0
- package/src/backends/webview/WebViewPoseBackend.ts +582 -0
- package/src/backends/webview/brandAssets.ts +5 -0
- package/src/backends/webview/onlineRuntime.ts +94 -0
- package/src/backends/webview/poseHtml.ts +259 -0
- package/src/backends/webview/poseRuntimeSource.d.ts +3 -0
- package/src/cache/obfuscate.ts +94 -0
- package/src/camera/PoseCameraView.tsx +223 -0
- package/src/camera/WebViewPoseView.tsx +466 -0
- package/src/client.ts +1295 -0
- package/src/diagnostics/logReport.ts +131 -0
- package/src/engine/EngineLoader.ts +428 -0
- package/src/engine/types.ts +93 -0
- package/src/events/classicMessage.ts +271 -0
- package/src/exercises/aliases.ts +50 -0
- package/src/index.ts +243 -0
- package/src/models/poseModels.ts +93 -0
- package/src/quality/AdaptiveQualityController.ts +579 -0
- package/src/quality/RuntimeGuard.ts +119 -0
- package/src/quality/captureMode.ts +97 -0
- package/src/quality/deviceCapability.ts +187 -0
- package/src/quality/profiles.ts +253 -0
- package/src/runtime/RuntimeCache.ts +300 -0
- package/src/sdkVersion.ts +2 -0
- package/src/support/optionalModules.ts +114 -0
- package/src/types/acceleration.ts +94 -0
- package/src/types/events.ts +437 -0
- package/src/types/features.ts +176 -0
- package/src/types/manifest.ts +230 -0
- package/src/types/pose.ts +85 -0
- package/src/types/preload.ts +19 -0
- package/src/types/skeleton.ts +77 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React layer: `PoseTrackerProvider` owns a `PoseTrackerClient`, and
|
|
3
|
+
* `usePoseTracker` gives screens access to the pipeline plus typed event
|
|
4
|
+
* callbacks.
|
|
5
|
+
*
|
|
6
|
+
* Recommended usage:
|
|
7
|
+
* - Mount the provider near the app root — with or without an API token
|
|
8
|
+
* (without token, the SDK runs in keypoints-only mode).
|
|
9
|
+
* - **Mounting the provider does not load MoveNet** unless `autoPreload` is
|
|
10
|
+
* true. On the screen *before* the camera (e.g. home), call `preload()` /
|
|
11
|
+
* `warmup()` (default **basic** = model only, no camera permission). For
|
|
12
|
+
* WebView, mount a 1×1 `<WebViewPoseView coldStart="basic" />` warmer so
|
|
13
|
+
* MoveNet can load — see docs/PRELOAD.md.
|
|
14
|
+
* - Call `configure(apiToken)` at any time to upgrade keypoints-only →
|
|
15
|
+
* full-engine without restarting the camera pipeline.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import React, {
|
|
19
|
+
createContext,
|
|
20
|
+
useCallback,
|
|
21
|
+
useContext,
|
|
22
|
+
useEffect,
|
|
23
|
+
useMemo,
|
|
24
|
+
useRef,
|
|
25
|
+
useState,
|
|
26
|
+
} from 'react';
|
|
27
|
+
|
|
28
|
+
import { PoseTrackerClient, type PoseTrackerClientOptions, type StartExerciseOptions } from './client';
|
|
29
|
+
import type { PoseInputFrame } from './backends/PoseBackend';
|
|
30
|
+
import type { ExerciseConfig, SdkManifest } from './types/manifest';
|
|
31
|
+
import type { PreloadOptions } from './types/preload';
|
|
32
|
+
import type {
|
|
33
|
+
ErrorEvent,
|
|
34
|
+
PoseTrackerCallbacks,
|
|
35
|
+
PoseTrackerEvent,
|
|
36
|
+
PoseTrackerEventListener,
|
|
37
|
+
PoseTrackerMode,
|
|
38
|
+
PoseTrackerStatus,
|
|
39
|
+
} from './types/events';
|
|
40
|
+
import type { Pose } from './types/pose';
|
|
41
|
+
import type { AccelerationDiagnostics, AccelerationState } from './types/acceleration';
|
|
42
|
+
import type { QualityState } from './quality/AdaptiveQualityController';
|
|
43
|
+
|
|
44
|
+
export interface PoseTrackerContextValue {
|
|
45
|
+
/**
|
|
46
|
+
* The underlying orchestrator. Escape hatch for advanced integrations
|
|
47
|
+
* (e.g. `PoseCameraView` reads the active backend and ingests poses
|
|
48
|
+
* computed in the vision-camera worklet). Stable across renders.
|
|
49
|
+
*/
|
|
50
|
+
client: PoseTrackerClient;
|
|
51
|
+
status: PoseTrackerStatus;
|
|
52
|
+
/** 'keypoints-only' (no validated key/session) or 'full-engine'. */
|
|
53
|
+
mode: PoseTrackerMode;
|
|
54
|
+
/**
|
|
55
|
+
* GPU-acceleration verdict from the warm-up health check: 'unknown' until
|
|
56
|
+
* warm-up completes, then 'gpu' | 'cpu-fallback' | 'unavailable'. Can
|
|
57
|
+
* downgrade mid-session after an Android GL context loss. See
|
|
58
|
+
* docs/ANDROID_GL_ACCELERATION.md.
|
|
59
|
+
*/
|
|
60
|
+
acceleration: AccelerationState;
|
|
61
|
+
/** Full GPU diagnostics (backend, timings, GL renderer, flags, downgrade trail). */
|
|
62
|
+
accelerationDiagnostics: AccelerationDiagnostics | null;
|
|
63
|
+
/**
|
|
64
|
+
* Adaptive camera-quality profile (AdaptiveChoice + crash-guard + FPS
|
|
65
|
+
* auto-downgrade). `capturePriority` reflects the host option (`performance`
|
|
66
|
+
* default vs `quality`). See docs/ADAPTIVE_QUALITY.md.
|
|
67
|
+
*/
|
|
68
|
+
quality: QualityState;
|
|
69
|
+
/** Last (possibly non-fatal) error; `ready` + keypoints-only can coexist with it. */
|
|
70
|
+
error: ErrorEvent | null;
|
|
71
|
+
manifest: SdkManifest | null;
|
|
72
|
+
/** Empty in keypoints-only mode. */
|
|
73
|
+
exercises: ExerciseConfig[];
|
|
74
|
+
/**
|
|
75
|
+
* Warm-up (handshake + engine + model). Default `coldStart: 'basic'` —
|
|
76
|
+
* no getUserMedia. Pass `{ coldStart: 'full' }` to also open the camera.
|
|
77
|
+
*/
|
|
78
|
+
preload: (options?: PreloadOptions) => Promise<void>;
|
|
79
|
+
/** Alias of preload(). */
|
|
80
|
+
warmup: (options?: PreloadOptions) => Promise<void>;
|
|
81
|
+
/**
|
|
82
|
+
* Hot (re)configuration: validates the token and loads the engine without
|
|
83
|
+
* touching the camera pipeline. Resolves to true on full-engine mode.
|
|
84
|
+
*/
|
|
85
|
+
configure: (apiToken?: string) => Promise<boolean>;
|
|
86
|
+
/**
|
|
87
|
+
* Start an FSM exercise (manifest) or a custom one (`jump_analysis`,
|
|
88
|
+
* `air_time_jump`). Options: `difficulty`, `userHeightCm`, `devicePitchDeg`.
|
|
89
|
+
*/
|
|
90
|
+
startExercise: (exerciseId: string, options?: StartExerciseOptions) => void;
|
|
91
|
+
stopExercise: () => void;
|
|
92
|
+
estimatePose: (frame: PoseInputFrame) => Promise<Pose | null>;
|
|
93
|
+
processFrame: (frame: PoseInputFrame) => Promise<Pose | null>;
|
|
94
|
+
addEventListener: (listener: PoseTrackerEventListener) => () => void;
|
|
95
|
+
/**
|
|
96
|
+
* Classic PoseTracker WebView JSON stream (`sendDataToNative` shape).
|
|
97
|
+
* Same as `usePoseTracker({ onMessage })`.
|
|
98
|
+
*/
|
|
99
|
+
addMessageListener: (
|
|
100
|
+
listener: (message: import('./events/classicMessage').ClassicNativeMessage) => void,
|
|
101
|
+
) => () => void;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const PoseTrackerContext = createContext<PoseTrackerContextValue | null>(null);
|
|
105
|
+
|
|
106
|
+
export interface PoseTrackerProviderProps {
|
|
107
|
+
/** Optional: without a token the SDK runs in keypoints-only mode. */
|
|
108
|
+
apiToken?: string;
|
|
109
|
+
options?: PoseTrackerClientOptions;
|
|
110
|
+
/** Start preloading as soon as the provider mounts. Default: false. */
|
|
111
|
+
autoPreload?: boolean;
|
|
112
|
+
children: React.ReactNode;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function PoseTrackerProvider({
|
|
116
|
+
apiToken,
|
|
117
|
+
options,
|
|
118
|
+
autoPreload = false,
|
|
119
|
+
children,
|
|
120
|
+
}: PoseTrackerProviderProps): React.JSX.Element {
|
|
121
|
+
const clientRef = useRef<PoseTrackerClient | null>(null);
|
|
122
|
+
if (!clientRef.current) {
|
|
123
|
+
clientRef.current = new PoseTrackerClient(apiToken, options);
|
|
124
|
+
}
|
|
125
|
+
const client = clientRef.current;
|
|
126
|
+
|
|
127
|
+
const [status, setStatus] = useState<PoseTrackerStatus>(client.getStatus());
|
|
128
|
+
const [mode, setMode] = useState<PoseTrackerMode>(client.getMode());
|
|
129
|
+
const [error, setError] = useState<ErrorEvent | null>(null);
|
|
130
|
+
const [manifest, setManifest] = useState<SdkManifest | null>(null);
|
|
131
|
+
const [exercises, setExercises] = useState<ExerciseConfig[]>([]);
|
|
132
|
+
const [acceleration, setAcceleration] = useState<AccelerationState>(client.getAcceleration());
|
|
133
|
+
const [accelerationDiagnostics, setAccelerationDiagnostics] =
|
|
134
|
+
useState<AccelerationDiagnostics | null>(client.getAccelerationDiagnostics());
|
|
135
|
+
const [quality, setQuality] = useState<QualityState>(client.getQualityState());
|
|
136
|
+
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
const offState = client.onStateChange(() => {
|
|
139
|
+
setStatus(client.getStatus());
|
|
140
|
+
setMode(client.getMode());
|
|
141
|
+
setError(client.getError());
|
|
142
|
+
setManifest(client.getManifest());
|
|
143
|
+
setExercises(client.getAvailableExercises());
|
|
144
|
+
setAcceleration(client.getAcceleration());
|
|
145
|
+
setAccelerationDiagnostics(client.getAccelerationDiagnostics());
|
|
146
|
+
setQuality(client.getQualityState());
|
|
147
|
+
});
|
|
148
|
+
if (autoPreload) {
|
|
149
|
+
client.preload().catch(() => {
|
|
150
|
+
// Error is surfaced through status/error state and the error event.
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return () => {
|
|
154
|
+
offState();
|
|
155
|
+
};
|
|
156
|
+
}, [client, autoPreload]);
|
|
157
|
+
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
return () => {
|
|
160
|
+
client.dispose().catch(() => {});
|
|
161
|
+
};
|
|
162
|
+
}, [client]);
|
|
163
|
+
|
|
164
|
+
const preload = useCallback(
|
|
165
|
+
(options?: PreloadOptions) => client.preload(options),
|
|
166
|
+
[client],
|
|
167
|
+
);
|
|
168
|
+
const configureFn = useCallback((token?: string) => client.configure(token), [client]);
|
|
169
|
+
const startExercise = useCallback(
|
|
170
|
+
(id: string, exerciseOptions?: StartExerciseOptions) => client.startExercise(id, exerciseOptions),
|
|
171
|
+
[client],
|
|
172
|
+
);
|
|
173
|
+
const stopExercise = useCallback(() => client.stopExercise(), [client]);
|
|
174
|
+
const estimatePose = useCallback(
|
|
175
|
+
(frame: PoseInputFrame) => client.estimatePose(frame),
|
|
176
|
+
[client],
|
|
177
|
+
);
|
|
178
|
+
const processFrame = useCallback(
|
|
179
|
+
(frame: PoseInputFrame) => client.processFrame(frame),
|
|
180
|
+
[client],
|
|
181
|
+
);
|
|
182
|
+
const addEventListener = useCallback(
|
|
183
|
+
(listener: PoseTrackerEventListener) => client.addEventListener(listener),
|
|
184
|
+
[client],
|
|
185
|
+
);
|
|
186
|
+
const addMessageListener = useCallback(
|
|
187
|
+
(listener: (message: import('./events/classicMessage').ClassicNativeMessage) => void) =>
|
|
188
|
+
client.addMessageListener(listener),
|
|
189
|
+
[client],
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const value = useMemo<PoseTrackerContextValue>(
|
|
193
|
+
() => ({
|
|
194
|
+
client,
|
|
195
|
+
status,
|
|
196
|
+
mode,
|
|
197
|
+
acceleration,
|
|
198
|
+
accelerationDiagnostics,
|
|
199
|
+
quality,
|
|
200
|
+
error,
|
|
201
|
+
manifest,
|
|
202
|
+
exercises,
|
|
203
|
+
preload,
|
|
204
|
+
warmup: preload,
|
|
205
|
+
configure: configureFn,
|
|
206
|
+
startExercise,
|
|
207
|
+
stopExercise,
|
|
208
|
+
estimatePose,
|
|
209
|
+
processFrame,
|
|
210
|
+
addEventListener,
|
|
211
|
+
addMessageListener,
|
|
212
|
+
}),
|
|
213
|
+
[
|
|
214
|
+
client,
|
|
215
|
+
status,
|
|
216
|
+
mode,
|
|
217
|
+
acceleration,
|
|
218
|
+
accelerationDiagnostics,
|
|
219
|
+
quality,
|
|
220
|
+
error,
|
|
221
|
+
manifest,
|
|
222
|
+
exercises,
|
|
223
|
+
preload,
|
|
224
|
+
configureFn,
|
|
225
|
+
startExercise,
|
|
226
|
+
stopExercise,
|
|
227
|
+
estimatePose,
|
|
228
|
+
processFrame,
|
|
229
|
+
addEventListener,
|
|
230
|
+
addMessageListener,
|
|
231
|
+
],
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
return <PoseTrackerContext.Provider value={value}>{children}</PoseTrackerContext.Provider>;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Access the PoseTracker pipeline and subscribe to typed events.
|
|
239
|
+
*
|
|
240
|
+
* ```tsx
|
|
241
|
+
* const { status, mode, processFrame, configure } = usePoseTracker({
|
|
242
|
+
* onKeypoints: (e) => drawSkeleton(e.keypoints), // both modes
|
|
243
|
+
* onCounter: (e) => setReps(e.count), // full-engine only
|
|
244
|
+
* });
|
|
245
|
+
* ```
|
|
246
|
+
*/
|
|
247
|
+
export function usePoseTracker(callbacks: PoseTrackerCallbacks = {}): PoseTrackerContextValue {
|
|
248
|
+
const context = useContext(PoseTrackerContext);
|
|
249
|
+
if (!context) {
|
|
250
|
+
throw new Error('usePoseTracker must be used within a <PoseTrackerProvider>.');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const callbacksRef = useRef(callbacks);
|
|
254
|
+
callbacksRef.current = callbacks;
|
|
255
|
+
|
|
256
|
+
useEffect(() => {
|
|
257
|
+
const offEvents = context.addEventListener((event: PoseTrackerEvent) => {
|
|
258
|
+
const cb = callbacksRef.current;
|
|
259
|
+
switch (event.type) {
|
|
260
|
+
case 'initialization':
|
|
261
|
+
cb.onInitialization?.(event);
|
|
262
|
+
break;
|
|
263
|
+
case 'error':
|
|
264
|
+
cb.onError?.(event);
|
|
265
|
+
break;
|
|
266
|
+
case 'warning':
|
|
267
|
+
cb.onWarning?.(event);
|
|
268
|
+
break;
|
|
269
|
+
case 'keypoints':
|
|
270
|
+
cb.onKeypoints?.(event);
|
|
271
|
+
break;
|
|
272
|
+
case 'angles':
|
|
273
|
+
cb.onAngles?.(event);
|
|
274
|
+
break;
|
|
275
|
+
case 'counter':
|
|
276
|
+
cb.onCounter?.(event);
|
|
277
|
+
break;
|
|
278
|
+
case 'posture':
|
|
279
|
+
cb.onPosture?.(event);
|
|
280
|
+
break;
|
|
281
|
+
case 'progression':
|
|
282
|
+
cb.onProgression?.(event);
|
|
283
|
+
break;
|
|
284
|
+
case 'recommendations':
|
|
285
|
+
cb.onRecommendations?.(event);
|
|
286
|
+
break;
|
|
287
|
+
case 'form_score':
|
|
288
|
+
cb.onFormScore?.(event);
|
|
289
|
+
break;
|
|
290
|
+
case 'exercise_summary':
|
|
291
|
+
cb.onExerciseSummary?.(event);
|
|
292
|
+
break;
|
|
293
|
+
case 'jump_calibration':
|
|
294
|
+
cb.onJumpCalibration?.(event);
|
|
295
|
+
break;
|
|
296
|
+
case 'jump_started':
|
|
297
|
+
cb.onJumpStarted?.(event);
|
|
298
|
+
break;
|
|
299
|
+
case 'jump_height':
|
|
300
|
+
cb.onJumpHeight?.(event);
|
|
301
|
+
break;
|
|
302
|
+
case 'jump_discarded':
|
|
303
|
+
cb.onJumpDiscarded?.(event);
|
|
304
|
+
break;
|
|
305
|
+
case 'jump_result':
|
|
306
|
+
cb.onJumpResult?.(event);
|
|
307
|
+
break;
|
|
308
|
+
case 'jump_summary':
|
|
309
|
+
cb.onJumpSummary?.(event);
|
|
310
|
+
break;
|
|
311
|
+
case 'quality_changed':
|
|
312
|
+
cb.onQualityChanged?.(event);
|
|
313
|
+
break;
|
|
314
|
+
case 'performance_warning':
|
|
315
|
+
cb.onPerformanceWarning?.(event);
|
|
316
|
+
break;
|
|
317
|
+
case 'runtime_download_progress':
|
|
318
|
+
cb.onRuntimeDownloadProgress?.(event);
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
const offMessages = context.addMessageListener((message) => {
|
|
323
|
+
callbacksRef.current.onMessage?.(message);
|
|
324
|
+
});
|
|
325
|
+
return () => {
|
|
326
|
+
offEvents();
|
|
327
|
+
offMessages();
|
|
328
|
+
};
|
|
329
|
+
}, [context]);
|
|
330
|
+
|
|
331
|
+
return context;
|
|
332
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handshake client: `configure(apiToken)` calls the Strapi backend, which
|
|
3
|
+
* validates the API token (plan/quota) and returns the `SdkManifest` —
|
|
4
|
+
* exercise configs, engine bundle descriptor, pose-runtime payload
|
|
5
|
+
* descriptor, model profiles, reference movements. All server-driven
|
|
6
|
+
* behavior flows from this single call.
|
|
7
|
+
*
|
|
8
|
+
* Keyless mode (`apiToken` null): the backend returns the PUBLIC manifest —
|
|
9
|
+
* pose-runtime descriptor only, no session/engine/exercises. This is how
|
|
10
|
+
* keypoints-only installs bootstrap the pose estimation payload.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Platform } from 'react-native';
|
|
14
|
+
|
|
15
|
+
import { SDK_VERSION } from '../sdkVersion';
|
|
16
|
+
import type { ConfigureRequest, PoseModelProfile, SdkManifest } from '../types/manifest';
|
|
17
|
+
|
|
18
|
+
export const SDK_NAME = 'posetracker-rn-light' as const;
|
|
19
|
+
export { SDK_VERSION };
|
|
20
|
+
|
|
21
|
+
/** Overridable for staging/self-hosted backends. */
|
|
22
|
+
export const DEFAULT_BASE_URL =
|
|
23
|
+
'https://movelytics-strapi-c78a339b7070.herokuapp.com';
|
|
24
|
+
|
|
25
|
+
export interface ConfigureOptions {
|
|
26
|
+
baseUrl?: string;
|
|
27
|
+
poseModelProfile?: PoseModelProfile;
|
|
28
|
+
locale?: string;
|
|
29
|
+
/** Local cache versions, so the backend can answer `upToDate` per module. */
|
|
30
|
+
localVersions?: ConfigureRequest['localVersions'];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class ConfigureError extends Error {
|
|
34
|
+
constructor(
|
|
35
|
+
readonly code: 'invalid_token' | 'quota_exceeded' | 'network' | 'internal',
|
|
36
|
+
message: string,
|
|
37
|
+
) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.name = 'ConfigureError';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function configure(
|
|
44
|
+
apiToken: string | null,
|
|
45
|
+
options: ConfigureOptions = {},
|
|
46
|
+
): Promise<SdkManifest> {
|
|
47
|
+
const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, '');
|
|
48
|
+
const body: ConfigureRequest = {
|
|
49
|
+
...(apiToken ? { apiToken } : {}),
|
|
50
|
+
sdkName: SDK_NAME,
|
|
51
|
+
sdkVersion: SDK_VERSION,
|
|
52
|
+
targetPlatform: Platform.OS === 'ios' ? 'ios' : 'android',
|
|
53
|
+
poseModelProfile: options.poseModelProfile ?? 'AdaptiveChoice',
|
|
54
|
+
locale: options.locale,
|
|
55
|
+
localVersions: options.localVersions,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
let response: Response;
|
|
59
|
+
try {
|
|
60
|
+
response = await fetch(`${baseUrl}/api/sdk/configure`, {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers: { 'Content-Type': 'application/json' },
|
|
63
|
+
body: JSON.stringify(body),
|
|
64
|
+
});
|
|
65
|
+
} catch (err) {
|
|
66
|
+
throw new ConfigureError('network', `Handshake request failed: ${String(err)}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (response.status === 401 || response.status === 403) {
|
|
70
|
+
// The 401 body carries `revoked: true` — the caller purges the sealed
|
|
71
|
+
// engine/session caches (a revoked key must not keep the engine alive).
|
|
72
|
+
throw new ConfigureError('invalid_token', 'Invalid, revoked or unauthorized API token.');
|
|
73
|
+
}
|
|
74
|
+
if (response.status === 429) {
|
|
75
|
+
throw new ConfigureError('quota_exceeded', 'API call quota exceeded for the current plan.');
|
|
76
|
+
}
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
throw new ConfigureError('internal', `Handshake failed with HTTP ${response.status}.`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return (await response.json()) as SdkManifest;
|
|
82
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch a custom skeleton overlay by Strapi `api_uuid`
|
|
3
|
+
* (WebView `?skeleton=<uuid>` parity).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { SkeletonDefinition } from '../types/skeleton';
|
|
7
|
+
import { DEFAULT_BASE_URL } from './configure';
|
|
8
|
+
|
|
9
|
+
export interface FetchSkeletonOptions {
|
|
10
|
+
baseUrl?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class SkeletonFetchError extends Error {
|
|
14
|
+
constructor(
|
|
15
|
+
readonly code: 'not_found' | 'network' | 'invalid',
|
|
16
|
+
message: string,
|
|
17
|
+
) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = 'SkeletonFetchError';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* GET /api/sdk/skeleton?uuid=…
|
|
25
|
+
* Returns the overlay definition or throws {@link SkeletonFetchError}.
|
|
26
|
+
*/
|
|
27
|
+
export async function fetchSkeletonDefinition(
|
|
28
|
+
uuid: string,
|
|
29
|
+
options: FetchSkeletonOptions = {},
|
|
30
|
+
): Promise<SkeletonDefinition> {
|
|
31
|
+
const id = uuid.trim();
|
|
32
|
+
if (!id || id === 'true' || id === 'false') {
|
|
33
|
+
throw new SkeletonFetchError('invalid', 'skeleton uuid is required');
|
|
34
|
+
}
|
|
35
|
+
const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, '');
|
|
36
|
+
let response: Response;
|
|
37
|
+
try {
|
|
38
|
+
response = await fetch(`${baseUrl}/api/sdk/skeleton?uuid=${encodeURIComponent(id)}`);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
throw new SkeletonFetchError('network', `Skeleton fetch failed: ${String(err)}`);
|
|
41
|
+
}
|
|
42
|
+
if (response.status === 404) {
|
|
43
|
+
throw new SkeletonFetchError('not_found', `Skeleton '${id}' not found`);
|
|
44
|
+
}
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
throw new SkeletonFetchError('network', `Skeleton fetch HTTP ${response.status}`);
|
|
47
|
+
}
|
|
48
|
+
const body = (await response.json()) as SkeletonDefinition & { api_uuid?: string };
|
|
49
|
+
if (!body?.keypoints || !body?.keypoint_lines || !body?.circles || !body?.lines) {
|
|
50
|
+
throw new SkeletonFetchError('invalid', 'Skeleton payload incomplete');
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
keypoints: body.keypoints,
|
|
54
|
+
keypoint_lines: body.keypoint_lines,
|
|
55
|
+
keypoint_angles: body.keypoint_angles ?? [],
|
|
56
|
+
circles: body.circles,
|
|
57
|
+
lines: body.lines,
|
|
58
|
+
angles: body.angles,
|
|
59
|
+
};
|
|
60
|
+
}
|
package/src/api/track.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage tracking client — `POST /api/sdk/track`.
|
|
3
|
+
*
|
|
4
|
+
* One call per real usage: when the camera starts with the model ready
|
|
5
|
+
* (`camera_start`), NOT on handshake and NOT on preload. Server-side, a
|
|
6
|
+
* valid API key increments the same usage counters as the WebView API
|
|
7
|
+
* (quota-checked) and inserts a `sdk-usage-event` row with the params;
|
|
8
|
+
* keyless calls insert an anonymous row.
|
|
9
|
+
*
|
|
10
|
+
* Delivery rules (see ARCHITECTURE.md §Offline):
|
|
11
|
+
* - metered (with API key): the track call MUST succeed online — offline
|
|
12
|
+
* metered sessions are refused because usage cannot be counted;
|
|
13
|
+
* - anonymous (no key): fire-and-forget with a local retry queue, flushed
|
|
14
|
+
* on the next connection.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { Platform } from 'react-native';
|
|
18
|
+
|
|
19
|
+
import { DEFAULT_BASE_URL, SDK_VERSION } from './configure';
|
|
20
|
+
import { createAsyncKeyValueStore, type KeyValueStore } from '../engine/EngineLoader';
|
|
21
|
+
|
|
22
|
+
const QUEUE_KEY = 'posetracker.track.queue';
|
|
23
|
+
const QUEUE_LIMIT = 100;
|
|
24
|
+
|
|
25
|
+
export class TrackError extends Error {
|
|
26
|
+
constructor(
|
|
27
|
+
readonly code: 'invalid_token' | 'quota_exceeded' | 'network' | 'internal',
|
|
28
|
+
message: string,
|
|
29
|
+
) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = 'TrackError';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface TrackRequest {
|
|
36
|
+
event?: string;
|
|
37
|
+
apiToken?: string | null;
|
|
38
|
+
params?: Record<string, unknown>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface TrackResponse {
|
|
42
|
+
tracked: boolean;
|
|
43
|
+
anonymous?: boolean;
|
|
44
|
+
monthly_usage_counter?: number;
|
|
45
|
+
remainingCalls?: number;
|
|
46
|
+
type?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface QueuedEvent {
|
|
50
|
+
event: string;
|
|
51
|
+
platform: string;
|
|
52
|
+
sdkVersion: string;
|
|
53
|
+
params: Record<string, unknown> | undefined;
|
|
54
|
+
queuedAtMs: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface UsageTrackerOptions {
|
|
58
|
+
baseUrl?: string;
|
|
59
|
+
keyValueStore?: KeyValueStore | null;
|
|
60
|
+
fetchFn?: typeof fetch;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class UsageTracker {
|
|
64
|
+
private readonly baseUrl: string;
|
|
65
|
+
private readonly kv: KeyValueStore | null;
|
|
66
|
+
private readonly fetchFn: typeof fetch;
|
|
67
|
+
|
|
68
|
+
constructor(options: UsageTrackerOptions = {}) {
|
|
69
|
+
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, '');
|
|
70
|
+
this.kv =
|
|
71
|
+
options.keyValueStore !== undefined ? options.keyValueStore : createAsyncKeyValueStore();
|
|
72
|
+
this.fetchFn = options.fetchFn ?? fetch;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Metered track (API key): throws on failure — the caller refuses to start
|
|
77
|
+
* a metered session it cannot count.
|
|
78
|
+
*/
|
|
79
|
+
async trackMetered(request: TrackRequest & { apiToken: string }): Promise<TrackResponse> {
|
|
80
|
+
return this.post({
|
|
81
|
+
event: request.event ?? 'camera_start',
|
|
82
|
+
apiToken: request.apiToken,
|
|
83
|
+
platform: Platform.OS,
|
|
84
|
+
sdkVersion: SDK_VERSION,
|
|
85
|
+
params: request.params,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Anonymous track (no key): never throws. Failed sends are queued locally
|
|
91
|
+
* and flushed on the next connection.
|
|
92
|
+
*/
|
|
93
|
+
async trackAnonymous(request: TrackRequest = {}): Promise<void> {
|
|
94
|
+
const payload: QueuedEvent = {
|
|
95
|
+
event: request.event ?? 'camera_start',
|
|
96
|
+
platform: Platform.OS,
|
|
97
|
+
sdkVersion: SDK_VERSION,
|
|
98
|
+
params: request.params,
|
|
99
|
+
queuedAtMs: Date.now(),
|
|
100
|
+
};
|
|
101
|
+
try {
|
|
102
|
+
await this.post({ ...payload });
|
|
103
|
+
// A send just worked: opportunistically drain older queued events.
|
|
104
|
+
void this.flushQueue();
|
|
105
|
+
} catch {
|
|
106
|
+
await this.enqueue(payload);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Send queued anonymous events (called on preload and after online sends). */
|
|
111
|
+
async flushQueue(): Promise<void> {
|
|
112
|
+
const queue = await this.readQueue();
|
|
113
|
+
if (queue.length === 0) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const remaining: QueuedEvent[] = [];
|
|
117
|
+
for (const item of queue) {
|
|
118
|
+
try {
|
|
119
|
+
await this.post({ ...item, params: { ...(item.params ?? {}), queuedAtMs: item.queuedAtMs } });
|
|
120
|
+
} catch {
|
|
121
|
+
remaining.push(item);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
await this.writeQueue(remaining);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// -------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
private async post(body: Record<string, unknown>): Promise<TrackResponse> {
|
|
130
|
+
let response: Response;
|
|
131
|
+
try {
|
|
132
|
+
response = await this.fetchFn(`${this.baseUrl}/api/sdk/track`, {
|
|
133
|
+
method: 'POST',
|
|
134
|
+
headers: { 'Content-Type': 'application/json' },
|
|
135
|
+
body: JSON.stringify(body),
|
|
136
|
+
});
|
|
137
|
+
} catch (err) {
|
|
138
|
+
throw new TrackError('network', `Usage tracking request failed: ${String(err)}`);
|
|
139
|
+
}
|
|
140
|
+
if (response.status === 401 || response.status === 403) {
|
|
141
|
+
throw new TrackError('invalid_token', 'Invalid, revoked or unauthorized API token.');
|
|
142
|
+
}
|
|
143
|
+
if (response.status === 429) {
|
|
144
|
+
throw new TrackError('quota_exceeded', 'API call quota exceeded for the current plan.');
|
|
145
|
+
}
|
|
146
|
+
if (!response.ok) {
|
|
147
|
+
throw new TrackError('internal', `Usage tracking failed with HTTP ${response.status}.`);
|
|
148
|
+
}
|
|
149
|
+
return (await response.json()) as TrackResponse;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private async readQueue(): Promise<QueuedEvent[]> {
|
|
153
|
+
try {
|
|
154
|
+
const raw = await this.kv?.getItem(QUEUE_KEY);
|
|
155
|
+
if (!raw) {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
const parsed = JSON.parse(raw) as QueuedEvent[];
|
|
159
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
160
|
+
} catch {
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private async writeQueue(queue: QueuedEvent[]): Promise<void> {
|
|
166
|
+
try {
|
|
167
|
+
if (queue.length === 0) {
|
|
168
|
+
await this.kv?.removeItem(QUEUE_KEY);
|
|
169
|
+
} else {
|
|
170
|
+
await this.kv?.setItem(QUEUE_KEY, JSON.stringify(queue.slice(-QUEUE_LIMIT)));
|
|
171
|
+
}
|
|
172
|
+
} catch {
|
|
173
|
+
// Queue persistence is best-effort.
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private async enqueue(item: QueuedEvent): Promise<void> {
|
|
178
|
+
const queue = await this.readQueue();
|
|
179
|
+
queue.push(item);
|
|
180
|
+
await this.writeQueue(queue);
|
|
181
|
+
}
|
|
182
|
+
}
|