@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.
Files changed (125) hide show
  1. package/LICENSE +45 -0
  2. package/PoseTrackerVision.podspec +28 -0
  3. package/README.md +161 -0
  4. package/THIRD_PARTY_NOTICES.md +19 -0
  5. package/ios/PoseTrackerVision/PoseTrackerBodyPosePlugin.m +18 -0
  6. package/ios/PoseTrackerVision/PoseTrackerBodyPosePlugin.swift +108 -0
  7. package/lib/PoseTrackerProvider.d.ts +102 -0
  8. package/lib/PoseTrackerProvider.js +204 -0
  9. package/lib/api/configure.d.ts +29 -0
  10. package/lib/api/configure.js +64 -0
  11. package/lib/api/skeleton.d.ts +17 -0
  12. package/lib/api/skeleton.js +53 -0
  13. package/lib/api/track.d.ts +61 -0
  14. package/lib/api/track.js +146 -0
  15. package/lib/backends/PoseBackend.d.ts +50 -0
  16. package/lib/backends/PoseBackend.js +11 -0
  17. package/lib/backends/vision/VisionPoseBackend.d.ts +72 -0
  18. package/lib/backends/vision/VisionPoseBackend.js +177 -0
  19. package/lib/backends/vision/mapVisionJoints.d.ts +35 -0
  20. package/lib/backends/vision/mapVisionJoints.js +162 -0
  21. package/lib/backends/vision/optionalVision.d.ts +32 -0
  22. package/lib/backends/vision/optionalVision.js +76 -0
  23. package/lib/backends/webview/WebViewPoseBackend.d.ts +223 -0
  24. package/lib/backends/webview/WebViewPoseBackend.js +382 -0
  25. package/lib/backends/webview/brandAssets.d.ts +5 -0
  26. package/lib/backends/webview/brandAssets.js +8 -0
  27. package/lib/backends/webview/onlineRuntime.d.ts +45 -0
  28. package/lib/backends/webview/onlineRuntime.js +57 -0
  29. package/lib/backends/webview/poseHtml.d.ts +71 -0
  30. package/lib/backends/webview/poseHtml.js +188 -0
  31. package/lib/backends/webview/poseRuntimeSource.d.ts +3 -0
  32. package/lib/backends/webview/poseRuntimeSource.js +4 -0
  33. package/lib/cache/obfuscate.d.ts +18 -0
  34. package/lib/cache/obfuscate.js +90 -0
  35. package/lib/camera/PoseCameraView.d.ts +51 -0
  36. package/lib/camera/PoseCameraView.js +99 -0
  37. package/lib/camera/WebViewPoseView.d.ts +81 -0
  38. package/lib/camera/WebViewPoseView.js +312 -0
  39. package/lib/client.d.ts +361 -0
  40. package/lib/client.js +1046 -0
  41. package/lib/diagnostics/logReport.d.ts +23 -0
  42. package/lib/diagnostics/logReport.js +102 -0
  43. package/lib/engine/EngineLoader.d.ts +95 -0
  44. package/lib/engine/EngineLoader.js +347 -0
  45. package/lib/engine/types.d.ts +83 -0
  46. package/lib/engine/types.js +11 -0
  47. package/lib/events/classicMessage.d.ts +31 -0
  48. package/lib/events/classicMessage.js +259 -0
  49. package/lib/exercises/aliases.d.ts +18 -0
  50. package/lib/exercises/aliases.js +48 -0
  51. package/lib/index.d.ts +72 -0
  52. package/lib/index.js +170 -0
  53. package/lib/models/poseModels.d.ts +39 -0
  54. package/lib/models/poseModels.js +63 -0
  55. package/lib/quality/AdaptiveQualityController.d.ts +120 -0
  56. package/lib/quality/AdaptiveQualityController.js +423 -0
  57. package/lib/quality/RuntimeGuard.d.ts +34 -0
  58. package/lib/quality/RuntimeGuard.js +105 -0
  59. package/lib/quality/captureMode.d.ts +82 -0
  60. package/lib/quality/captureMode.js +75 -0
  61. package/lib/quality/deviceCapability.d.ts +37 -0
  62. package/lib/quality/deviceCapability.js +177 -0
  63. package/lib/quality/profiles.d.ts +125 -0
  64. package/lib/quality/profiles.js +202 -0
  65. package/lib/runtime/RuntimeCache.d.ts +75 -0
  66. package/lib/runtime/RuntimeCache.js +230 -0
  67. package/lib/sdkVersion.d.ts +2 -0
  68. package/lib/sdkVersion.js +5 -0
  69. package/lib/support/optionalModules.d.ts +46 -0
  70. package/lib/support/optionalModules.js +82 -0
  71. package/lib/types/acceleration.d.ts +86 -0
  72. package/lib/types/acceleration.js +15 -0
  73. package/lib/types/events.d.ts +345 -0
  74. package/lib/types/events.js +10 -0
  75. package/lib/types/features.d.ts +90 -0
  76. package/lib/types/features.js +122 -0
  77. package/lib/types/manifest.d.ts +197 -0
  78. package/lib/types/manifest.js +10 -0
  79. package/lib/types/pose.d.ts +36 -0
  80. package/lib/types/pose.js +41 -0
  81. package/lib/types/preload.d.ts +17 -0
  82. package/lib/types/preload.js +10 -0
  83. package/lib/types/skeleton.d.ts +29 -0
  84. package/lib/types/skeleton.js +52 -0
  85. package/package.json +92 -0
  86. package/react-native.config.js +12 -0
  87. package/src/PoseTrackerProvider.tsx +332 -0
  88. package/src/api/configure.ts +82 -0
  89. package/src/api/skeleton.ts +60 -0
  90. package/src/api/track.ts +182 -0
  91. package/src/backends/PoseBackend.ts +59 -0
  92. package/src/backends/vision/VisionPoseBackend.ts +238 -0
  93. package/src/backends/vision/mapVisionJoints.ts +185 -0
  94. package/src/backends/vision/optionalVision.ts +94 -0
  95. package/src/backends/webview/WebViewPoseBackend.ts +582 -0
  96. package/src/backends/webview/brandAssets.ts +5 -0
  97. package/src/backends/webview/onlineRuntime.ts +94 -0
  98. package/src/backends/webview/poseHtml.ts +259 -0
  99. package/src/backends/webview/poseRuntimeSource.d.ts +3 -0
  100. package/src/cache/obfuscate.ts +94 -0
  101. package/src/camera/PoseCameraView.tsx +223 -0
  102. package/src/camera/WebViewPoseView.tsx +466 -0
  103. package/src/client.ts +1295 -0
  104. package/src/diagnostics/logReport.ts +131 -0
  105. package/src/engine/EngineLoader.ts +428 -0
  106. package/src/engine/types.ts +93 -0
  107. package/src/events/classicMessage.ts +271 -0
  108. package/src/exercises/aliases.ts +50 -0
  109. package/src/index.ts +243 -0
  110. package/src/models/poseModels.ts +93 -0
  111. package/src/quality/AdaptiveQualityController.ts +579 -0
  112. package/src/quality/RuntimeGuard.ts +119 -0
  113. package/src/quality/captureMode.ts +97 -0
  114. package/src/quality/deviceCapability.ts +187 -0
  115. package/src/quality/profiles.ts +253 -0
  116. package/src/runtime/RuntimeCache.ts +300 -0
  117. package/src/sdkVersion.ts +2 -0
  118. package/src/support/optionalModules.ts +114 -0
  119. package/src/types/acceleration.ts +94 -0
  120. package/src/types/events.ts +437 -0
  121. package/src/types/features.ts +176 -0
  122. package/src/types/manifest.ts +230 -0
  123. package/src/types/pose.ts +85 -0
  124. package/src/types/preload.ts +19 -0
  125. package/src/types/skeleton.ts +77 -0
@@ -0,0 +1,466 @@
1
+ /**
2
+ * Camera + inference surface for {@link WebViewPoseBackend} — the SDK's
3
+ * default runtime on BOTH platforms (light / online variant).
4
+ *
5
+ * Renders a Chromium/WKWebView that owns getUserMedia + MoveNet Lightning
6
+ * (TF.js WebGL). TF.js loads from CDN; the model loads from a URL each boot
7
+ * (default: PoseTracker Front MoveNet Lightning).
8
+ *
9
+ * Camera capture resolution is driven by {@link AdaptiveQualityController}
10
+ * (AdaptiveChoice + crash-loop guard + live FPS downgrades).
11
+ *
12
+ * Requires peer `react-native-webview` (included in Expo Go) and network
13
+ * access for the first model / TF.js fetch.
14
+ */
15
+
16
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
17
+ import {
18
+ AppState,
19
+ StyleSheet,
20
+ Text,
21
+ View,
22
+ type StyleProp,
23
+ type ViewStyle,
24
+ } from 'react-native';
25
+
26
+ import { usePoseTracker } from '../PoseTrackerProvider';
27
+ import { WebViewPoseBackend } from '../backends/webview/WebViewPoseBackend';
28
+ import {
29
+ buildPoseHtml,
30
+ DEFAULT_LOADING_TEXT,
31
+ } from '../backends/webview/poseHtml';
32
+ import type { OnlineRuntimeParts } from '../backends/webview/onlineRuntime';
33
+ import { getMinTargetFps, type QualityProfile } from '../quality/profiles';
34
+ import { shouldShowWatermark } from '../types/features';
35
+ import type { Pose } from '../types/pose';
36
+ import type { SkeletonDefinition } from '../types/skeleton';
37
+
38
+ declare function require(name: string): unknown;
39
+
40
+ function getReactNativeWebView(): {
41
+ WebView: React.ComponentType<Record<string, unknown>>;
42
+ } | null {
43
+ try {
44
+ return require('react-native-webview') as {
45
+ WebView: React.ComponentType<Record<string, unknown>>;
46
+ };
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ function injectQuality(
53
+ webRef: React.RefObject<{ injectJavaScript?: (js: string) => void } | null>,
54
+ profile: QualityProfile,
55
+ ): void {
56
+ const payload = JSON.stringify({
57
+ idealWidth: profile.idealWidth,
58
+ idealHeight: profile.idealHeight,
59
+ idealFrameRate: profile.idealFrameRate,
60
+ profileId: profile.id,
61
+ });
62
+ webRef.current?.injectJavaScript?.(
63
+ `window.__PT_SET_QUALITY && window.__PT_SET_QUALITY(${payload}); true;`,
64
+ );
65
+ }
66
+
67
+ export interface WebViewPoseViewProps {
68
+ style?: StyleProp<ViewStyle>;
69
+ /** 'front' | 'back' — mapped to getUserMedia facingMode. Default front. */
70
+ position?: 'front' | 'back';
71
+ /**
72
+ * Draw the skeleton overlay inside the page. Default `true` — same as the
73
+ * PoseTracker WebView `skeleton="true"` query param. Pass `false` for a
74
+ * clean camera preview (e.g. when rendering your own overlay from
75
+ * `keypoints` events).
76
+ */
77
+ drawSkeleton?: boolean;
78
+ /**
79
+ * Show a placement guide box (WebView `postureBox` parity) while the
80
+ * active exercise reports `posture.ready === false`. Default `true`.
81
+ * Drawn as an RN sibling overlay (reliable on iOS; Android WebView may
82
+ * composite above it — hosts can also render from `onPosture`).
83
+ */
84
+ drawPlacementBox?: boolean;
85
+ /**
86
+ * Placement box inset as a percentage of each edge (default `10`, same
87
+ * as the WebView / engine posture padding).
88
+ */
89
+ placementPaddingPercent?: number;
90
+ /**
91
+ * Cold-start depth for this WebView page:
92
+ * - `full` (default): open the camera after MoveNet warm-up — use on the
93
+ * visible camera screen (user expects the permission prompt).
94
+ * - `basic`: model + WebGL only — use on hidden warmers / lobby screens so
95
+ * preload never triggers getUserMedia unexpectedly.
96
+ */
97
+ coldStart?: 'basic' | 'full';
98
+ /**
99
+ * Boot overlay label (WebView `loading_message` query param parity).
100
+ * Default `"AI Loading"`. Technical progress (`accessing webcam`, backend,
101
+ * medianMs, …) is emitted as `initialization` / `diag` events — not shown
102
+ * on the branded loading UI.
103
+ */
104
+ loadingText?: string;
105
+ /**
106
+ * Force the bottom-right PoseTracker watermark on/off. When omitted, the
107
+ * SDK derives it from the configure manifest plan: shown for keyless /
108
+ * `free` / non-paid; hidden for paid plans (`developer`, `enterprise`, …).
109
+ * Always off for `coldStart: 'basic'` warmers.
110
+ */
111
+ showWatermark?: boolean;
112
+ /**
113
+ * Custom skeleton overlay (Strapi document shape). When omitted, the page
114
+ * uses the PoseTracker API default (navy `#010A73` + gold `#FFC300`).
115
+ * Takes precedence over {@link skeletonUuid}.
116
+ */
117
+ skeletonDef?: SkeletonDefinition | null;
118
+ /**
119
+ * Load a custom skeleton by Strapi `api_uuid` (WebView `?skeleton=<uuid>`).
120
+ * Fetched via `GET /api/sdk/skeleton`. Ignored if `skeletonDef` is set.
121
+ */
122
+ skeletonUuid?: string | null;
123
+ onPose?: (pose: Pose, stats: { inferenceTimeMs: number; timestampMs: number }) => void;
124
+ children?: React.ReactNode;
125
+ }
126
+
127
+ export function WebViewPoseView(props: WebViewPoseViewProps): React.ReactElement {
128
+ const { client, manifest } = usePoseTracker();
129
+ const backend = client.getBackend();
130
+ const webviewMod = useMemo(() => getReactNativeWebView(), []);
131
+ const webRef = useRef<{ injectJavaScript?: (js: string) => void } | null>(null);
132
+ /** Boot-time profile only — mid-session downgrades use injectJavaScript. */
133
+ const [bootProfile, setBootProfile] = useState<QualityProfile | null>(null);
134
+
135
+ /** Bundled pose-runtime parts (TF.js + MoveNet + page runtime). */
136
+ const [runtimeParts, setRuntimeParts] = useState<OnlineRuntimeParts | null>(null);
137
+ const [runtimeError, setRuntimeError] = useState<string | null>(null);
138
+
139
+ const isWebViewBackend = backend instanceof WebViewPoseBackend;
140
+ const facingMode = props.position === 'back' ? 'environment' : 'user';
141
+ const drawSkeleton = props.drawSkeleton ?? true;
142
+ const drawPlacementBox = props.drawPlacementBox ?? true;
143
+ const placementPaddingPercent = props.placementPaddingPercent ?? 10;
144
+ /** Camera screens default to full; warmers must pass `basic`. */
145
+ const coldStart = props.coldStart ?? 'full';
146
+ const loadingText =
147
+ typeof props.loadingText === 'string' && props.loadingText.trim().length > 0
148
+ ? props.loadingText.trim()
149
+ : DEFAULT_LOADING_TEXT;
150
+ /** Plan from configure manifest — drives watermark when prop is omitted. */
151
+ const planType = manifest?.plan?.plan ?? client.getPlanType();
152
+ /**
153
+ * Plan-gated watermark (override via prop). Recomputed when configure
154
+ * resolves a new manifest so upgrades hide the mark without remount.
155
+ */
156
+ const resolvedShowWatermark =
157
+ coldStart === 'basic'
158
+ ? false
159
+ : (props.showWatermark ?? shouldShowWatermark(planType));
160
+ const [showPlacementBox, setShowPlacementBox] = useState(false);
161
+ const [resolvedSkeleton, setResolvedSkeleton] = useState<SkeletonDefinition | null>(
162
+ props.skeletonDef ?? null,
163
+ );
164
+
165
+ // Resolve custom skeleton: explicit def wins; else fetch by uuid (API parity).
166
+ useEffect(() => {
167
+ if (props.skeletonDef) {
168
+ setResolvedSkeleton(props.skeletonDef);
169
+ return;
170
+ }
171
+ const uuid = props.skeletonUuid?.trim();
172
+ if (!uuid || uuid === 'true' || uuid === 'false') {
173
+ setResolvedSkeleton(null);
174
+ return;
175
+ }
176
+ let cancelled = false;
177
+ client
178
+ .fetchSkeleton(uuid)
179
+ .then((def) => {
180
+ if (!cancelled) setResolvedSkeleton(def);
181
+ })
182
+ .catch(() => {
183
+ if (!cancelled) setResolvedSkeleton(null);
184
+ });
185
+ return () => {
186
+ cancelled = true;
187
+ };
188
+ }, [client, props.skeletonDef, props.skeletonUuid]);
189
+
190
+ // WebView postureBox parity: show guide while placement is not ready.
191
+ useEffect(() => {
192
+ if (!drawPlacementBox) {
193
+ setShowPlacementBox(false);
194
+ return;
195
+ }
196
+ return client.addEventListener((event) => {
197
+ if (event.type === 'posture') {
198
+ setShowPlacementBox(!event.ready);
199
+ } else if (event.type === 'exercise_summary') {
200
+ setShowPlacementBox(false);
201
+ }
202
+ });
203
+ }, [client, drawPlacementBox]);
204
+
205
+ useEffect(() => {
206
+ let cancelled = false;
207
+ (async () => {
208
+ const resolved = await client.resolveQualityProfile();
209
+ if (cancelled) return;
210
+ await client.beginQualitySession();
211
+ if (cancelled) return;
212
+ setBootProfile(resolved);
213
+ })().catch(() => {
214
+ // Fall back to UltraLite constraints embedded in buildPoseHtml defaults.
215
+ if (!cancelled) {
216
+ setBootProfile({
217
+ id: 'ultralite',
218
+ label: 'UltraLite (fallback)',
219
+ idealWidth: 480,
220
+ idealHeight: 360,
221
+ idealFrameRate: 24,
222
+ minStableFps: getMinTargetFps(),
223
+ });
224
+ }
225
+ });
226
+ return () => {
227
+ cancelled = true;
228
+ };
229
+ }, [client]);
230
+
231
+ // Resolve online runtime descriptor (CDN + model URL + thin page runtime).
232
+ useEffect(() => {
233
+ let cancelled = false;
234
+ client
235
+ .getRuntimeParts()
236
+ .then((parts) => {
237
+ if (!cancelled) setRuntimeParts(parts);
238
+ })
239
+ .catch((err: unknown) => {
240
+ if (!cancelled) {
241
+ setRuntimeError(err instanceof Error ? err.message : String(err));
242
+ }
243
+ });
244
+ return () => {
245
+ cancelled = true;
246
+ };
247
+ }, [client]);
248
+
249
+ // Watermark plan upgrades inject `__PT_SET_WATERMARK` (no remount); snapshot
250
+ // `resolvedShowWatermark` into the initial HTML only.
251
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- watermark via inject
252
+ const html = useMemo(() => {
253
+ if (!bootProfile || !runtimeParts) return null;
254
+ return buildPoseHtml(runtimeParts, {
255
+ facingMode,
256
+ idealWidth: bootProfile.idealWidth,
257
+ idealHeight: bootProfile.idealHeight,
258
+ idealFrameRate: bootProfile.idealFrameRate,
259
+ profileId: bootProfile.id,
260
+ minTargetFps: getMinTargetFps(),
261
+ drawSkeleton,
262
+ coldStart,
263
+ loadingText,
264
+ showWatermark: resolvedShowWatermark,
265
+ // Default API skeleton is baked into the page runtime; customs are
266
+ // injected via __PT_SET_SKELETON (avoids remounting on uuid fetch).
267
+ skeletonDef: props.skeletonDef ?? null,
268
+ capturePriority: client.getQualityState().capturePriority,
269
+ });
270
+ }, [
271
+ facingMode,
272
+ bootProfile,
273
+ runtimeParts,
274
+ client,
275
+ drawSkeleton,
276
+ coldStart,
277
+ loadingText,
278
+ props.skeletonDef,
279
+ ]);
280
+
281
+ // Apply custom skeleton (uuid fetch or prop) without remounting the page.
282
+ useEffect(() => {
283
+ if (!isWebViewBackend || !html) return;
284
+ const payload = JSON.stringify(resolvedSkeleton);
285
+ webRef.current?.injectJavaScript?.(
286
+ `window.__PT_SET_SKELETON && window.__PT_SET_SKELETON(${payload}); true;`,
287
+ );
288
+ }, [isWebViewBackend, html, resolvedSkeleton]);
289
+
290
+ // Plan / prop watermark updates without remounting.
291
+ useEffect(() => {
292
+ if (!isWebViewBackend || !html) return;
293
+ webRef.current?.injectJavaScript?.(
294
+ `window.__PT_SET_WATERMARK && window.__PT_SET_WATERMARK(${
295
+ resolvedShowWatermark ? 'true' : 'false'
296
+ }); true;`,
297
+ );
298
+ }, [isWebViewBackend, html, resolvedShowWatermark]);
299
+
300
+ // loadingText prop updates without remounting (html rebuild still covers
301
+ // first paint via useMemo).
302
+ useEffect(() => {
303
+ if (!isWebViewBackend || !html) return;
304
+ const payload = JSON.stringify(loadingText);
305
+ webRef.current?.injectJavaScript?.(
306
+ `window.__PT_SET_LOADING_TEXT && window.__PT_SET_LOADING_TEXT(${payload}); true;`,
307
+ );
308
+ }, [isWebViewBackend, html, loadingText]);
309
+
310
+ useEffect(() => {
311
+ if (!(backend instanceof WebViewPoseBackend)) {
312
+ return;
313
+ }
314
+ backend.setAttached(true);
315
+ backend.setOnPose((pose, inferenceTimeMs) => {
316
+ client.ingestPose(pose);
317
+ props.onPose?.(pose, { inferenceTimeMs, timestampMs: pose.timestampMs });
318
+ });
319
+ backend.setOpenCameraHandler(() => {
320
+ webRef.current?.injectJavaScript?.(
321
+ 'window.__PT_OPEN_CAMERA && window.__PT_OPEN_CAMERA(); true;',
322
+ );
323
+ });
324
+ // Live downgrades restart getUserMedia without remounting the WebView.
325
+ client.setQualityApplyHandler((next) => {
326
+ injectQuality(webRef, next);
327
+ });
328
+ return () => {
329
+ backend.setOnPose(undefined);
330
+ backend.setOpenCameraHandler(undefined);
331
+ backend.setAttached(false);
332
+ client.setQualityApplyHandler(undefined);
333
+ };
334
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- attach once per backend identity
335
+ }, [backend, client]);
336
+
337
+ // Battery / thermal safety: release the in-page camera and halt the
338
+ // inference loop while the app is backgrounded, reacquire on foreground.
339
+ // Skip resume injection for basic (model-only) pages — no camera to reopen.
340
+ useEffect(() => {
341
+ if (!isWebViewBackend) return;
342
+ const sub = AppState.addEventListener('change', (state) => {
343
+ if (state === 'background') {
344
+ webRef.current?.injectJavaScript?.(
345
+ 'window.__PT_SUSPEND && window.__PT_SUSPEND(); true;',
346
+ );
347
+ } else if (state === 'active' && coldStart === 'full') {
348
+ webRef.current?.injectJavaScript?.(
349
+ 'window.__PT_RESUME && window.__PT_RESUME(); true;',
350
+ );
351
+ }
352
+ // 'inactive' (iOS control center / app switcher) is intentionally
353
+ // ignored: brief overlays shouldn't tear the camera down.
354
+ });
355
+ return () => sub.remove();
356
+ }, [isWebViewBackend, coldStart]);
357
+
358
+ if (!webviewMod) {
359
+ return (
360
+ <View style={[styles.fill, styles.center, props.style]}>
361
+ <Text style={styles.error}>
362
+ react-native-webview is required for the Chromium pose backend. Run:{'\n'}
363
+ npm install react-native-webview (or npx expo install react-native-webview)
364
+ </Text>
365
+ </View>
366
+ );
367
+ }
368
+
369
+ if (!isWebViewBackend) {
370
+ return (
371
+ <View style={[styles.fill, styles.center, props.style]}>
372
+ <Text style={styles.error}>
373
+ WebViewPoseView requires the WebView backend (preferredBackend
374
+ &apos;auto&apos; or &apos;webview&apos;) — the client is on Apple Vision
375
+ (use PoseCameraView instead).
376
+ </Text>
377
+ </View>
378
+ );
379
+ }
380
+
381
+ if (runtimeError) {
382
+ return (
383
+ <View style={[styles.fill, styles.center, props.style]}>
384
+ <Text style={styles.error}>{runtimeError}</Text>
385
+ </View>
386
+ );
387
+ }
388
+
389
+ if (!html || !bootProfile) {
390
+ return <View style={[styles.fill, props.style]} />;
391
+ }
392
+
393
+ const WebView = webviewMod.WebView;
394
+ const wvBackend = backend as WebViewPoseBackend;
395
+
396
+ return (
397
+ <View style={[styles.fill, props.style]} collapsable={false}>
398
+ <WebView
399
+ ref={webRef}
400
+ style={StyleSheet.absoluteFill}
401
+ originWhitelist={['*']}
402
+ // Same synthetic origin as the offline SDK. getUserMedia in RN
403
+ // WebView is reliable on https://localhost/; pointing baseUrl at a
404
+ // real product host (app.posetracker.com) can leave the page stuck
405
+ // on the boot overlay with a 0×0 / never-granted camera stream.
406
+ // Model + weights use absolute URLs with Access-Control-Allow-Origin: *.
407
+ source={{ html, baseUrl: 'https://localhost/' }}
408
+ allowFileAccess
409
+ mixedContentMode="always"
410
+ mediaPlaybackRequiresUserAction={false}
411
+ mediaCapturePermissionGrantType="grant"
412
+ allowsInlineMediaPlayback
413
+ javaScriptEnabled
414
+ domStorageEnabled
415
+ // Android: needed for getUserMedia in WebView
416
+ setSupportMultipleWindows={false}
417
+ // NOTE: never set androidLayerType — 'hardware'/'software' force the
418
+ // WebView into an Android View layer instead of its native SurfaceView
419
+ // path and destroy WebGL/video performance. Default ('none') is correct.
420
+ // Skeleton is drawn inside the HTML — Android WebView often composites
421
+ // above RN siblings, so PoseOverlay children are not visible there.
422
+ onMessage={(event: { nativeEvent: { data: string } }) => {
423
+ wvBackend.handleMessage(event.nativeEvent.data);
424
+ }}
425
+ onError={(e: { nativeEvent: { description?: string } }) => {
426
+ wvBackend.handleMessage(
427
+ JSON.stringify({
428
+ type: 'error',
429
+ message: e.nativeEvent.description ?? 'WebView error',
430
+ }),
431
+ );
432
+ }}
433
+ />
434
+ {showPlacementBox ? (
435
+ <View
436
+ pointerEvents="none"
437
+ style={[
438
+ styles.placementBox,
439
+ {
440
+ top: `${placementPaddingPercent}%`,
441
+ bottom: `${placementPaddingPercent}%`,
442
+ left: `${placementPaddingPercent}%`,
443
+ right: `${placementPaddingPercent}%`,
444
+ },
445
+ ]}
446
+ />
447
+ ) : null}
448
+ {/* Optional RN overlay (works on iOS; usually hidden under Android WebView surface). */}
449
+ {props.children}
450
+ </View>
451
+ );
452
+ }
453
+
454
+ const styles = StyleSheet.create({
455
+ fill: { flex: 1, backgroundColor: '#000' },
456
+ center: { alignItems: 'center', justifyContent: 'center', padding: 16 },
457
+ error: { color: '#FE8370', textAlign: 'center', fontSize: 13 },
458
+ /** Front drawPostureBox stroke `#FE8370`. */
459
+ placementBox: {
460
+ position: 'absolute',
461
+ borderWidth: 3,
462
+ borderColor: '#FE8370',
463
+ borderRadius: 8,
464
+ backgroundColor: 'transparent',
465
+ },
466
+ });