@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,51 @@
1
+ /**
2
+ * Camera component for the Apple Vision backend (iOS native builds only):
3
+ *
4
+ * vision-camera frame ──detectBodyPose plugin──▶ VNDetectHumanBodyPoseRequest
5
+ * ──19 joints (Y-flipped)──▶ Pose
6
+ *
7
+ * Everything up to the throttled `runOnJS` happens on the frame-processor
8
+ * thread. Requires a native build (NOT Expo Go) and the Vision backend
9
+ * active (`preferredBackend: 'vision'`). For every other case use
10
+ * {@link WebViewPoseView} — the SDK's default runtime.
11
+ */
12
+ import React from 'react';
13
+ import { type StyleProp, type ViewStyle } from 'react-native';
14
+ import type { Pose } from '../types/pose';
15
+ export type PoseCameraUnavailableReason =
16
+ /** Optional native modules not installed (Expo Go or missing deps). */
17
+ 'modules-missing'
18
+ /** Active backend is not Apple Vision (use WebViewPoseView instead). */
19
+ | 'native-backend-inactive'
20
+ /** Backend not initialized yet — preload()/warmup() has not completed. */
21
+ | 'backend-not-ready'
22
+ /** vision-camera found no camera device for the requested position. */
23
+ | 'no-camera-device';
24
+ export interface PoseCameraStats {
25
+ /** Synchronous inference duration for this frame, in ms. */
26
+ inferenceTimeMs: number;
27
+ /** Wall-clock timestamp of the processed frame (host clock, ms). */
28
+ timestampMs: number;
29
+ }
30
+ export interface PoseCameraViewProps {
31
+ style?: StyleProp<ViewStyle>;
32
+ /** Camera to use. Default: 'front'. */
33
+ position?: 'front' | 'back';
34
+ /** Stream frames when true (and the SDK is ready). Default: true. */
35
+ isActive?: boolean;
36
+ /** Camera fps hint passed to vision-camera (inference runs per frame). */
37
+ targetFps?: number;
38
+ /**
39
+ * Throttle for the worklet → JS pose callbacks (`runOnJS`). Inference
40
+ * still runs on every camera frame; only the JS notifications (keypoints
41
+ * events, overlays) are capped. Default: 30.
42
+ */
43
+ maxPoseCallbacksPerSecond?: number;
44
+ /** Per-pose callback (after the pose was ingested into the SDK pipeline). */
45
+ onPose?: (pose: Pose, stats: PoseCameraStats) => void;
46
+ /** Rendered when the native camera path cannot run. */
47
+ renderFallback?: (reason: PoseCameraUnavailableReason) => React.ReactElement | null;
48
+ /** Overlay content (skeleton, HUD…), rendered above the camera preview. */
49
+ children?: React.ReactNode;
50
+ }
51
+ export declare function PoseCameraView(props: PoseCameraViewProps): React.ReactElement | null;
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PoseCameraView = PoseCameraView;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ /**
6
+ * Camera component for the Apple Vision backend (iOS native builds only):
7
+ *
8
+ * vision-camera frame ──detectBodyPose plugin──▶ VNDetectHumanBodyPoseRequest
9
+ * ──19 joints (Y-flipped)──▶ Pose
10
+ *
11
+ * Everything up to the throttled `runOnJS` happens on the frame-processor
12
+ * thread. Requires a native build (NOT Expo Go) and the Vision backend
13
+ * active (`preferredBackend: 'vision'`). For every other case use
14
+ * {@link WebViewPoseView} — the SDK's default runtime.
15
+ */
16
+ const react_1 = require("react");
17
+ const react_native_1 = require("react-native");
18
+ const PoseTrackerProvider_1 = require("../PoseTrackerProvider");
19
+ const optionalModules_1 = require("../support/optionalModules");
20
+ const VisionPoseBackend_1 = require("../backends/vision/VisionPoseBackend");
21
+ const optionalVision_1 = require("../backends/vision/optionalVision");
22
+ const mapVisionJoints_1 = require("../backends/vision/mapVisionJoints");
23
+ function PoseCameraView(props) {
24
+ const { client } = (0, PoseTrackerProvider_1.usePoseTracker)();
25
+ const backend = client.getBackend();
26
+ if (!(backend instanceof VisionPoseBackend_1.VisionPoseBackend)) {
27
+ return fallback(props, 'native-backend-inactive');
28
+ }
29
+ if (!(0, optionalVision_1.isVisionPoseCameraAvailable)()) {
30
+ return fallback(props, 'modules-missing');
31
+ }
32
+ const plugin = backend.getFrameProcessorPlugin() ?? (0, optionalVision_1.getVisionBodyPosePlugin)();
33
+ if (!plugin) {
34
+ return fallback(props, 'backend-not-ready');
35
+ }
36
+ return ((0, jsx_runtime_1.jsx)(VisionPoseCameraInner, { ...props, backend: backend, plugin: plugin, visionCamera: (0, optionalModules_1.getVisionCamera)(), workletsCore: (0, optionalModules_1.getWorkletsCore)() }));
37
+ }
38
+ function fallback(props, reason) {
39
+ if (props.renderFallback) {
40
+ return props.renderFallback(reason);
41
+ }
42
+ return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.fallback, props.style], children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.fallbackText, children: FALLBACK_MESSAGES[reason] }) }));
43
+ }
44
+ const FALLBACK_MESSAGES = {
45
+ 'modules-missing': 'PoseCameraView requires an iOS native build with react-native-vision-camera + ' +
46
+ 'react-native-worklets-core. Not available in Expo Go — use WebViewPoseView.',
47
+ 'native-backend-inactive': "PoseCameraView requires the Apple Vision backend (preferredBackend: 'vision') — " +
48
+ 'the client is on the WebView runtime (use WebViewPoseView instead).',
49
+ 'backend-not-ready': 'PoseTracker is not ready yet — call preload() and wait for status "ready".',
50
+ 'no-camera-device': 'No camera device available for the requested position.',
51
+ };
52
+ function VisionPoseCameraInner(props) {
53
+ const { backend, plugin, visionCamera, workletsCore, position = 'front', isActive = true, targetFps, maxPoseCallbacksPerSecond = 30, onPose, style, children, } = props;
54
+ const { client, status } = (0, PoseTrackerProvider_1.usePoseTracker)();
55
+ const onPoseRef = (0, react_1.useRef)(onPose);
56
+ onPoseRef.current = onPose;
57
+ const device = visionCamera.useCameraDevice(position);
58
+ const lastEmitMs = workletsCore.useSharedValue(0);
59
+ const emitPose = workletsCore.useRunOnJS((pose, inferenceTimeMs) => {
60
+ client.ingestPose(pose);
61
+ backend.recordInferenceTime(inferenceTimeMs);
62
+ onPoseRef.current?.(pose, { inferenceTimeMs, timestampMs: pose.timestampMs });
63
+ }, [client, backend]);
64
+ const minEmitIntervalMs = Math.max(0, Math.floor(1000 / Math.max(1, maxPoseCallbacksPerSecond)));
65
+ const frameProcessor = visionCamera.useFrameProcessor((frame) => {
66
+ 'worklet';
67
+ const raw = plugin.call(frame);
68
+ if (!raw || !raw.joints || raw.joints.length === 0) {
69
+ return;
70
+ }
71
+ const inferenceTimeMs = typeof raw.inferenceTimeMs === 'number' ? raw.inferenceTimeMs : 0;
72
+ const pose = (0, mapVisionJoints_1.poseFromVisionPluginResult)(raw, Date.now());
73
+ if (!pose) {
74
+ return;
75
+ }
76
+ const now = Date.now();
77
+ if (now - lastEmitMs.value >= minEmitIntervalMs) {
78
+ lastEmitMs.value = now;
79
+ emitPose(pose, inferenceTimeMs);
80
+ }
81
+ }, [plugin, minEmitIntervalMs, emitPose, lastEmitMs]);
82
+ if (!device) {
83
+ return fallback(props, 'no-camera-device') ?? (0, jsx_runtime_1.jsx)(react_native_1.View, { style: style });
84
+ }
85
+ const Camera = visionCamera.Camera;
86
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.container, style], children: [(0, jsx_runtime_1.jsx)(Camera, { style: react_native_1.StyleSheet.absoluteFill, device: device, isActive: isActive && status === 'ready', frameProcessor: frameProcessor,
87
+ // Vision reads CMSampleBuffer / CVPixelBuffer directly — yuv is fine.
88
+ pixelFormat: "yuv", ...(targetFps !== undefined ? { fps: targetFps } : {}) }), children] }));
89
+ }
90
+ const styles = react_native_1.StyleSheet.create({
91
+ container: { overflow: 'hidden' },
92
+ fallback: {
93
+ alignItems: 'center',
94
+ justifyContent: 'center',
95
+ padding: 24,
96
+ backgroundColor: '#111',
97
+ },
98
+ fallbackText: { color: '#ccc', fontSize: 13, textAlign: 'center', lineHeight: 18 },
99
+ });
@@ -0,0 +1,81 @@
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
+ import React from 'react';
16
+ import { type StyleProp, type ViewStyle } from 'react-native';
17
+ import type { Pose } from '../types/pose';
18
+ import type { SkeletonDefinition } from '../types/skeleton';
19
+ export interface WebViewPoseViewProps {
20
+ style?: StyleProp<ViewStyle>;
21
+ /** 'front' | 'back' — mapped to getUserMedia facingMode. Default front. */
22
+ position?: 'front' | 'back';
23
+ /**
24
+ * Draw the skeleton overlay inside the page. Default `true` — same as the
25
+ * PoseTracker WebView `skeleton="true"` query param. Pass `false` for a
26
+ * clean camera preview (e.g. when rendering your own overlay from
27
+ * `keypoints` events).
28
+ */
29
+ drawSkeleton?: boolean;
30
+ /**
31
+ * Show a placement guide box (WebView `postureBox` parity) while the
32
+ * active exercise reports `posture.ready === false`. Default `true`.
33
+ * Drawn as an RN sibling overlay (reliable on iOS; Android WebView may
34
+ * composite above it — hosts can also render from `onPosture`).
35
+ */
36
+ drawPlacementBox?: boolean;
37
+ /**
38
+ * Placement box inset as a percentage of each edge (default `10`, same
39
+ * as the WebView / engine posture padding).
40
+ */
41
+ placementPaddingPercent?: number;
42
+ /**
43
+ * Cold-start depth for this WebView page:
44
+ * - `full` (default): open the camera after MoveNet warm-up — use on the
45
+ * visible camera screen (user expects the permission prompt).
46
+ * - `basic`: model + WebGL only — use on hidden warmers / lobby screens so
47
+ * preload never triggers getUserMedia unexpectedly.
48
+ */
49
+ coldStart?: 'basic' | 'full';
50
+ /**
51
+ * Boot overlay label (WebView `loading_message` query param parity).
52
+ * Default `"AI Loading"`. Technical progress (`accessing webcam`, backend,
53
+ * medianMs, …) is emitted as `initialization` / `diag` events — not shown
54
+ * on the branded loading UI.
55
+ */
56
+ loadingText?: string;
57
+ /**
58
+ * Force the bottom-right PoseTracker watermark on/off. When omitted, the
59
+ * SDK derives it from the configure manifest plan: shown for keyless /
60
+ * `free` / non-paid; hidden for paid plans (`developer`, `enterprise`, …).
61
+ * Always off for `coldStart: 'basic'` warmers.
62
+ */
63
+ showWatermark?: boolean;
64
+ /**
65
+ * Custom skeleton overlay (Strapi document shape). When omitted, the page
66
+ * uses the PoseTracker API default (navy `#010A73` + gold `#FFC300`).
67
+ * Takes precedence over {@link skeletonUuid}.
68
+ */
69
+ skeletonDef?: SkeletonDefinition | null;
70
+ /**
71
+ * Load a custom skeleton by Strapi `api_uuid` (WebView `?skeleton=<uuid>`).
72
+ * Fetched via `GET /api/sdk/skeleton`. Ignored if `skeletonDef` is set.
73
+ */
74
+ skeletonUuid?: string | null;
75
+ onPose?: (pose: Pose, stats: {
76
+ inferenceTimeMs: number;
77
+ timestampMs: number;
78
+ }) => void;
79
+ children?: React.ReactNode;
80
+ }
81
+ export declare function WebViewPoseView(props: WebViewPoseViewProps): React.ReactElement;
@@ -0,0 +1,312 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WebViewPoseView = WebViewPoseView;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ /**
6
+ * Camera + inference surface for {@link WebViewPoseBackend} — the SDK's
7
+ * default runtime on BOTH platforms (light / online variant).
8
+ *
9
+ * Renders a Chromium/WKWebView that owns getUserMedia + MoveNet Lightning
10
+ * (TF.js WebGL). TF.js loads from CDN; the model loads from a URL each boot
11
+ * (default: PoseTracker Front MoveNet Lightning).
12
+ *
13
+ * Camera capture resolution is driven by {@link AdaptiveQualityController}
14
+ * (AdaptiveChoice + crash-loop guard + live FPS downgrades).
15
+ *
16
+ * Requires peer `react-native-webview` (included in Expo Go) and network
17
+ * access for the first model / TF.js fetch.
18
+ */
19
+ const react_1 = require("react");
20
+ const react_native_1 = require("react-native");
21
+ const PoseTrackerProvider_1 = require("../PoseTrackerProvider");
22
+ const WebViewPoseBackend_1 = require("../backends/webview/WebViewPoseBackend");
23
+ const poseHtml_1 = require("../backends/webview/poseHtml");
24
+ const profiles_1 = require("../quality/profiles");
25
+ const features_1 = require("../types/features");
26
+ function getReactNativeWebView() {
27
+ try {
28
+ return require('react-native-webview');
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ }
34
+ function injectQuality(webRef, profile) {
35
+ const payload = JSON.stringify({
36
+ idealWidth: profile.idealWidth,
37
+ idealHeight: profile.idealHeight,
38
+ idealFrameRate: profile.idealFrameRate,
39
+ profileId: profile.id,
40
+ });
41
+ webRef.current?.injectJavaScript?.(`window.__PT_SET_QUALITY && window.__PT_SET_QUALITY(${payload}); true;`);
42
+ }
43
+ function WebViewPoseView(props) {
44
+ const { client, manifest } = (0, PoseTrackerProvider_1.usePoseTracker)();
45
+ const backend = client.getBackend();
46
+ const webviewMod = (0, react_1.useMemo)(() => getReactNativeWebView(), []);
47
+ const webRef = (0, react_1.useRef)(null);
48
+ /** Boot-time profile only — mid-session downgrades use injectJavaScript. */
49
+ const [bootProfile, setBootProfile] = (0, react_1.useState)(null);
50
+ /** Bundled pose-runtime parts (TF.js + MoveNet + page runtime). */
51
+ const [runtimeParts, setRuntimeParts] = (0, react_1.useState)(null);
52
+ const [runtimeError, setRuntimeError] = (0, react_1.useState)(null);
53
+ const isWebViewBackend = backend instanceof WebViewPoseBackend_1.WebViewPoseBackend;
54
+ const facingMode = props.position === 'back' ? 'environment' : 'user';
55
+ const drawSkeleton = props.drawSkeleton ?? true;
56
+ const drawPlacementBox = props.drawPlacementBox ?? true;
57
+ const placementPaddingPercent = props.placementPaddingPercent ?? 10;
58
+ /** Camera screens default to full; warmers must pass `basic`. */
59
+ const coldStart = props.coldStart ?? 'full';
60
+ const loadingText = typeof props.loadingText === 'string' && props.loadingText.trim().length > 0
61
+ ? props.loadingText.trim()
62
+ : poseHtml_1.DEFAULT_LOADING_TEXT;
63
+ /** Plan from configure manifest — drives watermark when prop is omitted. */
64
+ const planType = manifest?.plan?.plan ?? client.getPlanType();
65
+ /**
66
+ * Plan-gated watermark (override via prop). Recomputed when configure
67
+ * resolves a new manifest so upgrades hide the mark without remount.
68
+ */
69
+ const resolvedShowWatermark = coldStart === 'basic'
70
+ ? false
71
+ : (props.showWatermark ?? (0, features_1.shouldShowWatermark)(planType));
72
+ const [showPlacementBox, setShowPlacementBox] = (0, react_1.useState)(false);
73
+ const [resolvedSkeleton, setResolvedSkeleton] = (0, react_1.useState)(props.skeletonDef ?? null);
74
+ // Resolve custom skeleton: explicit def wins; else fetch by uuid (API parity).
75
+ (0, react_1.useEffect)(() => {
76
+ if (props.skeletonDef) {
77
+ setResolvedSkeleton(props.skeletonDef);
78
+ return;
79
+ }
80
+ const uuid = props.skeletonUuid?.trim();
81
+ if (!uuid || uuid === 'true' || uuid === 'false') {
82
+ setResolvedSkeleton(null);
83
+ return;
84
+ }
85
+ let cancelled = false;
86
+ client
87
+ .fetchSkeleton(uuid)
88
+ .then((def) => {
89
+ if (!cancelled)
90
+ setResolvedSkeleton(def);
91
+ })
92
+ .catch(() => {
93
+ if (!cancelled)
94
+ setResolvedSkeleton(null);
95
+ });
96
+ return () => {
97
+ cancelled = true;
98
+ };
99
+ }, [client, props.skeletonDef, props.skeletonUuid]);
100
+ // WebView postureBox parity: show guide while placement is not ready.
101
+ (0, react_1.useEffect)(() => {
102
+ if (!drawPlacementBox) {
103
+ setShowPlacementBox(false);
104
+ return;
105
+ }
106
+ return client.addEventListener((event) => {
107
+ if (event.type === 'posture') {
108
+ setShowPlacementBox(!event.ready);
109
+ }
110
+ else if (event.type === 'exercise_summary') {
111
+ setShowPlacementBox(false);
112
+ }
113
+ });
114
+ }, [client, drawPlacementBox]);
115
+ (0, react_1.useEffect)(() => {
116
+ let cancelled = false;
117
+ (async () => {
118
+ const resolved = await client.resolveQualityProfile();
119
+ if (cancelled)
120
+ return;
121
+ await client.beginQualitySession();
122
+ if (cancelled)
123
+ return;
124
+ setBootProfile(resolved);
125
+ })().catch(() => {
126
+ // Fall back to UltraLite constraints embedded in buildPoseHtml defaults.
127
+ if (!cancelled) {
128
+ setBootProfile({
129
+ id: 'ultralite',
130
+ label: 'UltraLite (fallback)',
131
+ idealWidth: 480,
132
+ idealHeight: 360,
133
+ idealFrameRate: 24,
134
+ minStableFps: (0, profiles_1.getMinTargetFps)(),
135
+ });
136
+ }
137
+ });
138
+ return () => {
139
+ cancelled = true;
140
+ };
141
+ }, [client]);
142
+ // Resolve online runtime descriptor (CDN + model URL + thin page runtime).
143
+ (0, react_1.useEffect)(() => {
144
+ let cancelled = false;
145
+ client
146
+ .getRuntimeParts()
147
+ .then((parts) => {
148
+ if (!cancelled)
149
+ setRuntimeParts(parts);
150
+ })
151
+ .catch((err) => {
152
+ if (!cancelled) {
153
+ setRuntimeError(err instanceof Error ? err.message : String(err));
154
+ }
155
+ });
156
+ return () => {
157
+ cancelled = true;
158
+ };
159
+ }, [client]);
160
+ // Watermark plan upgrades inject `__PT_SET_WATERMARK` (no remount); snapshot
161
+ // `resolvedShowWatermark` into the initial HTML only.
162
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- watermark via inject
163
+ const html = (0, react_1.useMemo)(() => {
164
+ if (!bootProfile || !runtimeParts)
165
+ return null;
166
+ return (0, poseHtml_1.buildPoseHtml)(runtimeParts, {
167
+ facingMode,
168
+ idealWidth: bootProfile.idealWidth,
169
+ idealHeight: bootProfile.idealHeight,
170
+ idealFrameRate: bootProfile.idealFrameRate,
171
+ profileId: bootProfile.id,
172
+ minTargetFps: (0, profiles_1.getMinTargetFps)(),
173
+ drawSkeleton,
174
+ coldStart,
175
+ loadingText,
176
+ showWatermark: resolvedShowWatermark,
177
+ // Default API skeleton is baked into the page runtime; customs are
178
+ // injected via __PT_SET_SKELETON (avoids remounting on uuid fetch).
179
+ skeletonDef: props.skeletonDef ?? null,
180
+ capturePriority: client.getQualityState().capturePriority,
181
+ });
182
+ }, [
183
+ facingMode,
184
+ bootProfile,
185
+ runtimeParts,
186
+ client,
187
+ drawSkeleton,
188
+ coldStart,
189
+ loadingText,
190
+ props.skeletonDef,
191
+ ]);
192
+ // Apply custom skeleton (uuid fetch or prop) without remounting the page.
193
+ (0, react_1.useEffect)(() => {
194
+ if (!isWebViewBackend || !html)
195
+ return;
196
+ const payload = JSON.stringify(resolvedSkeleton);
197
+ webRef.current?.injectJavaScript?.(`window.__PT_SET_SKELETON && window.__PT_SET_SKELETON(${payload}); true;`);
198
+ }, [isWebViewBackend, html, resolvedSkeleton]);
199
+ // Plan / prop watermark updates without remounting.
200
+ (0, react_1.useEffect)(() => {
201
+ if (!isWebViewBackend || !html)
202
+ return;
203
+ webRef.current?.injectJavaScript?.(`window.__PT_SET_WATERMARK && window.__PT_SET_WATERMARK(${resolvedShowWatermark ? 'true' : 'false'}); true;`);
204
+ }, [isWebViewBackend, html, resolvedShowWatermark]);
205
+ // loadingText prop updates without remounting (html rebuild still covers
206
+ // first paint via useMemo).
207
+ (0, react_1.useEffect)(() => {
208
+ if (!isWebViewBackend || !html)
209
+ return;
210
+ const payload = JSON.stringify(loadingText);
211
+ webRef.current?.injectJavaScript?.(`window.__PT_SET_LOADING_TEXT && window.__PT_SET_LOADING_TEXT(${payload}); true;`);
212
+ }, [isWebViewBackend, html, loadingText]);
213
+ (0, react_1.useEffect)(() => {
214
+ if (!(backend instanceof WebViewPoseBackend_1.WebViewPoseBackend)) {
215
+ return;
216
+ }
217
+ backend.setAttached(true);
218
+ backend.setOnPose((pose, inferenceTimeMs) => {
219
+ client.ingestPose(pose);
220
+ props.onPose?.(pose, { inferenceTimeMs, timestampMs: pose.timestampMs });
221
+ });
222
+ backend.setOpenCameraHandler(() => {
223
+ webRef.current?.injectJavaScript?.('window.__PT_OPEN_CAMERA && window.__PT_OPEN_CAMERA(); true;');
224
+ });
225
+ // Live downgrades restart getUserMedia without remounting the WebView.
226
+ client.setQualityApplyHandler((next) => {
227
+ injectQuality(webRef, next);
228
+ });
229
+ return () => {
230
+ backend.setOnPose(undefined);
231
+ backend.setOpenCameraHandler(undefined);
232
+ backend.setAttached(false);
233
+ client.setQualityApplyHandler(undefined);
234
+ };
235
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- attach once per backend identity
236
+ }, [backend, client]);
237
+ // Battery / thermal safety: release the in-page camera and halt the
238
+ // inference loop while the app is backgrounded, reacquire on foreground.
239
+ // Skip resume injection for basic (model-only) pages — no camera to reopen.
240
+ (0, react_1.useEffect)(() => {
241
+ if (!isWebViewBackend)
242
+ return;
243
+ const sub = react_native_1.AppState.addEventListener('change', (state) => {
244
+ if (state === 'background') {
245
+ webRef.current?.injectJavaScript?.('window.__PT_SUSPEND && window.__PT_SUSPEND(); true;');
246
+ }
247
+ else if (state === 'active' && coldStart === 'full') {
248
+ webRef.current?.injectJavaScript?.('window.__PT_RESUME && window.__PT_RESUME(); true;');
249
+ }
250
+ // 'inactive' (iOS control center / app switcher) is intentionally
251
+ // ignored: brief overlays shouldn't tear the camera down.
252
+ });
253
+ return () => sub.remove();
254
+ }, [isWebViewBackend, coldStart]);
255
+ if (!webviewMod) {
256
+ return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.fill, styles.center, props.style], children: (0, jsx_runtime_1.jsxs)(react_native_1.Text, { style: styles.error, children: ["react-native-webview is required for the Chromium pose backend. Run:", '\n', "npm install react-native-webview (or npx expo install react-native-webview)"] }) }));
257
+ }
258
+ if (!isWebViewBackend) {
259
+ return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.fill, styles.center, props.style], children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.error, children: "WebViewPoseView requires the WebView backend (preferredBackend 'auto' or 'webview') \u2014 the client is on Apple Vision (use PoseCameraView instead)." }) }));
260
+ }
261
+ if (runtimeError) {
262
+ return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.fill, styles.center, props.style], children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.error, children: runtimeError }) }));
263
+ }
264
+ if (!html || !bootProfile) {
265
+ return (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.fill, props.style] });
266
+ }
267
+ const WebView = webviewMod.WebView;
268
+ const wvBackend = backend;
269
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.fill, props.style], collapsable: false, children: [(0, jsx_runtime_1.jsx)(WebView, { ref: webRef, style: react_native_1.StyleSheet.absoluteFill, originWhitelist: ['*'],
270
+ // Same synthetic origin as the offline SDK. getUserMedia in RN
271
+ // WebView is reliable on https://localhost/; pointing baseUrl at a
272
+ // real product host (app.posetracker.com) can leave the page stuck
273
+ // on the boot overlay with a 0×0 / never-granted camera stream.
274
+ // Model + weights use absolute URLs with Access-Control-Allow-Origin: *.
275
+ source: { html, baseUrl: 'https://localhost/' }, allowFileAccess: true, mixedContentMode: "always", mediaPlaybackRequiresUserAction: false, mediaCapturePermissionGrantType: "grant", allowsInlineMediaPlayback: true, javaScriptEnabled: true, domStorageEnabled: true,
276
+ // Android: needed for getUserMedia in WebView
277
+ setSupportMultipleWindows: false,
278
+ // NOTE: never set androidLayerType — 'hardware'/'software' force the
279
+ // WebView into an Android View layer instead of its native SurfaceView
280
+ // path and destroy WebGL/video performance. Default ('none') is correct.
281
+ // Skeleton is drawn inside the HTML — Android WebView often composites
282
+ // above RN siblings, so PoseOverlay children are not visible there.
283
+ onMessage: (event) => {
284
+ wvBackend.handleMessage(event.nativeEvent.data);
285
+ }, onError: (e) => {
286
+ wvBackend.handleMessage(JSON.stringify({
287
+ type: 'error',
288
+ message: e.nativeEvent.description ?? 'WebView error',
289
+ }));
290
+ } }), showPlacementBox ? ((0, jsx_runtime_1.jsx)(react_native_1.View, { pointerEvents: "none", style: [
291
+ styles.placementBox,
292
+ {
293
+ top: `${placementPaddingPercent}%`,
294
+ bottom: `${placementPaddingPercent}%`,
295
+ left: `${placementPaddingPercent}%`,
296
+ right: `${placementPaddingPercent}%`,
297
+ },
298
+ ] })) : null, props.children] }));
299
+ }
300
+ const styles = react_native_1.StyleSheet.create({
301
+ fill: { flex: 1, backgroundColor: '#000' },
302
+ center: { alignItems: 'center', justifyContent: 'center', padding: 16 },
303
+ error: { color: '#FE8370', textAlign: 'center', fontSize: 13 },
304
+ /** Front drawPostureBox stroke `#FE8370`. */
305
+ placementBox: {
306
+ position: 'absolute',
307
+ borderWidth: 3,
308
+ borderColor: '#FE8370',
309
+ borderRadius: 8,
310
+ backgroundColor: 'transparent',
311
+ },
312
+ });