@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,39 @@
1
+ /**
2
+ * Online pose-model catalog for the light SDK.
3
+ *
4
+ * Defaults match the PoseTracker Front / Docs API product path:
5
+ * MoveNet SinglePose Lightning hosted at app.posetracker.com (same URL as
6
+ * `usePoseDetection` / tracking WebView `modelUrl`).
7
+ *
8
+ * BlazePose is accepted as a `model` alias for API parity but is not wired
9
+ * in the RN WebView yet (needs MediaPipe). Pass a TF.js `modelUrl` for any
10
+ * custom graph model.
11
+ */
12
+ /** Production MoveNet SinglePose Lightning topology (Front default). */
13
+ export declare const DEFAULT_MOVENET_LIGHTNING_URL = "https://app.posetracker.com/scripts/tmp_model_to_remove.json";
14
+ /** Docs API / Front `model` query aliases. */
15
+ export type PoseModelAlias = 'movenet' | 'movenet-singlepose-lightning' | 'lightning' | 'blazepose' | (string & {});
16
+ export interface ResolvePoseModelOptions {
17
+ /**
18
+ * Docs API `model` query parity (`movenet` default, `blazepose`, …).
19
+ * Ignored when {@link modelUrl} is set.
20
+ */
21
+ model?: PoseModelAlias;
22
+ /** Explicit TF.js graph-model topology URL (weights resolve as siblings). */
23
+ modelUrl?: string;
24
+ }
25
+ export interface ResolvedPoseModel {
26
+ /** Stable id reported in diagnostics / track params. */
27
+ modelId: string;
28
+ /** TF.js `loadGraphModel` URL, or null when unsupported without override. */
29
+ modelUrl: string | null;
30
+ /** Human-readable reason when {@link modelUrl} is null. */
31
+ unsupportedReason?: string;
32
+ }
33
+ /**
34
+ * Resolve which TF.js graph model the light WebView should fetch.
35
+ * Default: MoveNet SinglePose Lightning (product URL).
36
+ */
37
+ export declare function resolvePoseModel(options?: ResolvePoseModelOptions): ResolvedPoseModel;
38
+ /** Origin used as WebView `baseUrl` so model fetches are same-origin when possible. */
39
+ export declare function originFromModelUrl(modelUrl: string): string;
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ /**
3
+ * Online pose-model catalog for the light SDK.
4
+ *
5
+ * Defaults match the PoseTracker Front / Docs API product path:
6
+ * MoveNet SinglePose Lightning hosted at app.posetracker.com (same URL as
7
+ * `usePoseDetection` / tracking WebView `modelUrl`).
8
+ *
9
+ * BlazePose is accepted as a `model` alias for API parity but is not wired
10
+ * in the RN WebView yet (needs MediaPipe). Pass a TF.js `modelUrl` for any
11
+ * custom graph model.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.DEFAULT_MOVENET_LIGHTNING_URL = void 0;
15
+ exports.resolvePoseModel = resolvePoseModel;
16
+ exports.originFromModelUrl = originFromModelUrl;
17
+ /** Production MoveNet SinglePose Lightning topology (Front default). */
18
+ exports.DEFAULT_MOVENET_LIGHTNING_URL = 'https://app.posetracker.com/scripts/tmp_model_to_remove.json';
19
+ /**
20
+ * Resolve which TF.js graph model the light WebView should fetch.
21
+ * Default: MoveNet SinglePose Lightning (product URL).
22
+ */
23
+ function resolvePoseModel(options = {}) {
24
+ const explicit = typeof options.modelUrl === 'string' && options.modelUrl.trim().length > 0
25
+ ? options.modelUrl.trim()
26
+ : null;
27
+ if (explicit) {
28
+ const alias = typeof options.model === 'string' && options.model.trim().length > 0
29
+ ? options.model.trim()
30
+ : 'custom';
31
+ return { modelId: alias, modelUrl: explicit };
32
+ }
33
+ const key = (options.model ?? 'movenet').trim().toLowerCase();
34
+ if (key === 'movenet' || key === 'movenet-singlepose-lightning' || key === 'lightning') {
35
+ return {
36
+ modelId: 'movenet-singlepose-lightning',
37
+ modelUrl: exports.DEFAULT_MOVENET_LIGHTNING_URL,
38
+ };
39
+ }
40
+ if (key === 'blazepose') {
41
+ return {
42
+ modelId: 'blazepose',
43
+ modelUrl: null,
44
+ unsupportedReason: 'BlazePose is not wired in the light RN WebView yet (needs MediaPipe). ' +
45
+ 'Use model="movenet" or pass a TF.js graph modelUrl.',
46
+ };
47
+ }
48
+ return {
49
+ modelId: key,
50
+ modelUrl: null,
51
+ unsupportedReason: `Unknown model "${options.model}". Use "movenet" (default) or pass modelUrl.`,
52
+ };
53
+ }
54
+ /** Origin used as WebView `baseUrl` so model fetches are same-origin when possible. */
55
+ function originFromModelUrl(modelUrl) {
56
+ try {
57
+ const u = new URL(modelUrl);
58
+ return `${u.protocol}//${u.host}/`;
59
+ }
60
+ catch {
61
+ return 'https://app.posetracker.com/';
62
+ }
63
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Adaptive camera-quality controller (Sency AdaptiveChoice + RuntimeGuard).
3
+ *
4
+ * Uses a platform **minimum target FPS** (floor), not a single setpoint:
5
+ * - iOS: min 30 (acceptable band ~30–50+)
6
+ * - Android: min 15 (mid-range GPUs rarely match iOS)
7
+ *
8
+ * Flow:
9
+ * 1. Resolve an initial profile from device capability + crash-loop guard.
10
+ * 2. WebView benches MoveNet on zeros *before* opening the camera → estimated
11
+ * FPS. If estimate < min-target, drop capture profile then open webcam.
12
+ * 3. Live 1 Hz stats: if mean FPS < min-target → downgrade again.
13
+ * 4. Emit `performance_warning` when even `basic` cannot hold the floor.
14
+ */
15
+ import { type KeyValueStore } from '../engine/EngineLoader';
16
+ import type { PerformanceWarningEvent, QualityChangedEvent } from '../types/events';
17
+ import { type DeviceCapabilitySnapshot } from './deviceCapability';
18
+ import { type FpsRange, type CapturePriority, type QualityChoice, type QualityProfile, type QualityProfileId } from './profiles';
19
+ export interface QualityStatsSample {
20
+ fps: number;
21
+ medianInferenceMs: number | null;
22
+ videoSize?: string;
23
+ backend?: string;
24
+ }
25
+ export interface AdaptiveQualityControllerOptions {
26
+ /** Explicit override; default AdaptiveChoice. */
27
+ choice?: QualityChoice;
28
+ /**
29
+ * `performance` (default) vs `quality` — see {@link CapturePriority}.
30
+ * When `quality`, FPS-driven capture downgrades are skipped.
31
+ */
32
+ capturePriority?: CapturePriority;
33
+ keyValueStore?: KeyValueStore | null;
34
+ onDiagnostic?: (message: string) => void;
35
+ onQualityChanged?: (event: QualityChangedEvent) => void;
36
+ onPerformanceWarning?: (event: PerformanceWarningEvent) => void;
37
+ /** Apply constraints to the live WebView (restart getUserMedia). */
38
+ applyProfile?: (profile: QualityProfile) => void;
39
+ }
40
+ export interface QualityState {
41
+ choice: QualityChoice;
42
+ /** Host capture vs FPS trade-off (default `performance`). */
43
+ capturePriority: CapturePriority;
44
+ activeProfile: QualityProfileId;
45
+ profile: QualityProfile;
46
+ capability: DeviceCapabilitySnapshot;
47
+ meanFps: number | null;
48
+ /** Last warm-up estimate (1000/medianMs), before or at camera open. */
49
+ warmupEstimatedFps: number | null;
50
+ warmupMedianMs: number | null;
51
+ lowFpsStreak: number;
52
+ lastWarningAtMs: number | null;
53
+ /** Platform floor (iOS 30 / Android 15) — not a setpoint. */
54
+ minTargetFps: number;
55
+ /** Soft “good experience” band for docs / host UI. */
56
+ idealFpsRange: FpsRange;
57
+ /** @deprecated Use {@link minTargetFps}. */
58
+ targetFps: number;
59
+ }
60
+ export declare class AdaptiveQualityController {
61
+ private readonly choice;
62
+ private readonly capturePriority;
63
+ private readonly kv;
64
+ private readonly guard;
65
+ private readonly onDiagnostic;
66
+ private readonly onQualityChanged;
67
+ private readonly onPerformanceWarning;
68
+ private applyProfile;
69
+ private capability;
70
+ private activeProfile;
71
+ private resolved;
72
+ private fpsWindow;
73
+ private lowFpsStreak;
74
+ private lastWarningAtMs;
75
+ private downgradeInFlight;
76
+ private warmupEstimatedFps;
77
+ private warmupMedianMs;
78
+ private warmupApplied;
79
+ /** Earliest time live low-FPS downgrades are allowed (settle after camera open/swap). */
80
+ private settleUntilMs;
81
+ constructor(options?: AdaptiveQualityControllerOptions);
82
+ /** Host capture vs FPS trade-off. */
83
+ getCapturePriority(): CapturePriority;
84
+ prefersCaptureQuality(): boolean;
85
+ setApplyProfile(fn: ((profile: QualityProfile) => void) | undefined): void;
86
+ getState(): QualityState;
87
+ /** Platform minimum target FPS (floor). */
88
+ getMinTargetFps(): number;
89
+ getActiveProfile(): QualityProfile;
90
+ /**
91
+ * Resolve the profile to boot with. Call once before building WebView HTML.
92
+ * Walks down the ladder past any FAILED / crash-PROBING keys.
93
+ */
94
+ resolveInitialProfile(): Promise<QualityProfile>;
95
+ /** Call immediately before the WebView page boots this profile. */
96
+ beginSession(): Promise<void>;
97
+ /**
98
+ * Warm-up zeros bench result (before / around getUserMedia).
99
+ * Picks the safer of (warmup median → profile) and (device capability).
100
+ * May **upgrade** away from a stale last-good when the bench shows headroom
101
+ * (e.g. 15 ms median on iPhone stuck on ultralite).
102
+ */
103
+ onWarmupEstimate(options: {
104
+ medianInferenceMs: number | null;
105
+ glRenderer?: string | null;
106
+ /** Profile the HTML page already selected from the same estimate (sync). */
107
+ pageSelectedProfile?: QualityProfileId | null;
108
+ }): Promise<void>;
109
+ /** Call when the WebView posts `ready`. */
110
+ onRuntimeReady(options?: {
111
+ glRenderer?: string | null;
112
+ medianInferenceMs?: number | null;
113
+ pageSelectedProfile?: QualityProfileId | null;
114
+ }): Promise<void>;
115
+ /** Feed 1 Hz WebView stats. May auto-downgrade and/or warn. */
116
+ onStats(sample: QualityStatsSample): Promise<void>;
117
+ private downgradeTo;
118
+ private meanFps;
119
+ private persistActive;
120
+ }
@@ -0,0 +1,423 @@
1
+ "use strict";
2
+ /**
3
+ * Adaptive camera-quality controller (Sency AdaptiveChoice + RuntimeGuard).
4
+ *
5
+ * Uses a platform **minimum target FPS** (floor), not a single setpoint:
6
+ * - iOS: min 30 (acceptable band ~30–50+)
7
+ * - Android: min 15 (mid-range GPUs rarely match iOS)
8
+ *
9
+ * Flow:
10
+ * 1. Resolve an initial profile from device capability + crash-loop guard.
11
+ * 2. WebView benches MoveNet on zeros *before* opening the camera → estimated
12
+ * FPS. If estimate < min-target, drop capture profile then open webcam.
13
+ * 3. Live 1 Hz stats: if mean FPS < min-target → downgrade again.
14
+ * 4. Emit `performance_warning` when even `basic` cannot hold the floor.
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.AdaptiveQualityController = void 0;
18
+ const configure_1 = require("../api/configure");
19
+ const EngineLoader_1 = require("../engine/EngineLoader");
20
+ const deviceCapability_1 = require("./deviceCapability");
21
+ const profiles_1 = require("./profiles");
22
+ const RuntimeGuard_1 = require("./RuntimeGuard");
23
+ const ACTIVE_KEY = 'posetracker.quality.active';
24
+ class AdaptiveQualityController {
25
+ constructor(options = {}) {
26
+ this.resolved = false;
27
+ this.fpsWindow = [];
28
+ this.lowFpsStreak = 0;
29
+ this.lastWarningAtMs = null;
30
+ this.downgradeInFlight = false;
31
+ this.warmupEstimatedFps = null;
32
+ this.warmupMedianMs = null;
33
+ this.warmupApplied = false;
34
+ /** Earliest time live low-FPS downgrades are allowed (settle after camera open/swap). */
35
+ this.settleUntilMs = 0;
36
+ this.choice = options.choice ?? 'AdaptiveChoice';
37
+ this.capturePriority = options.capturePriority === 'quality' ? 'quality' : 'performance';
38
+ this.kv =
39
+ options.keyValueStore !== undefined
40
+ ? options.keyValueStore
41
+ : (0, EngineLoader_1.createAsyncKeyValueStore)();
42
+ this.guard = new RuntimeGuard_1.RuntimeGuard(this.kv, configure_1.SDK_VERSION);
43
+ this.onDiagnostic = options.onDiagnostic;
44
+ this.onQualityChanged = options.onQualityChanged;
45
+ this.onPerformanceWarning = options.onPerformanceWarning;
46
+ this.applyProfile = options.applyProfile;
47
+ this.capability = (0, deviceCapability_1.scoreDeviceCapability)();
48
+ this.activeProfile =
49
+ this.choice === 'AdaptiveChoice'
50
+ ? this.capturePriority === 'quality'
51
+ ? 'prime'
52
+ : this.capability.suggestedProfile
53
+ : this.choice;
54
+ }
55
+ /** Host capture vs FPS trade-off. */
56
+ getCapturePriority() {
57
+ return this.capturePriority;
58
+ }
59
+ prefersCaptureQuality() {
60
+ return this.capturePriority === 'quality';
61
+ }
62
+ setApplyProfile(fn) {
63
+ this.applyProfile = fn;
64
+ }
65
+ getState() {
66
+ const platform = (0, profiles_1.currentQualityPlatform)();
67
+ const minTargetFps = (0, profiles_1.getMinTargetFps)(platform);
68
+ return {
69
+ choice: this.choice,
70
+ capturePriority: this.capturePriority,
71
+ activeProfile: this.activeProfile,
72
+ profile: (0, profiles_1.getQualityProfile)(this.activeProfile),
73
+ capability: this.capability,
74
+ meanFps: this.meanFps(),
75
+ warmupEstimatedFps: this.warmupEstimatedFps,
76
+ warmupMedianMs: this.warmupMedianMs,
77
+ lowFpsStreak: this.lowFpsStreak,
78
+ lastWarningAtMs: this.lastWarningAtMs,
79
+ minTargetFps,
80
+ idealFpsRange: (0, profiles_1.getIdealFpsRange)(platform),
81
+ targetFps: minTargetFps,
82
+ };
83
+ }
84
+ /** Platform minimum target FPS (floor). */
85
+ getMinTargetFps() {
86
+ return (0, profiles_1.getMinTargetFps)((0, profiles_1.currentQualityPlatform)());
87
+ }
88
+ getActiveProfile() {
89
+ return (0, profiles_1.getQualityProfile)(this.activeProfile);
90
+ }
91
+ /**
92
+ * Resolve the profile to boot with. Call once before building WebView HTML.
93
+ * Walks down the ladder past any FAILED / crash-PROBING keys.
94
+ */
95
+ async resolveInitialProfile() {
96
+ if (this.resolved)
97
+ return this.getActiveProfile();
98
+ this.capability = (0, deviceCapability_1.scoreDeviceCapability)();
99
+ let candidate = this.choice === 'AdaptiveChoice'
100
+ ? this.capturePriority === 'quality'
101
+ ? 'prime'
102
+ : this.capability.suggestedProfile
103
+ : this.choice;
104
+ // capturePriority=quality: do not restore a low last-good (that would
105
+ // defeat the host's request for sharp preview). Crash-guard still applies.
106
+ if (this.choice === 'AdaptiveChoice' && this.kv && this.capturePriority !== 'quality') {
107
+ try {
108
+ const saved = await this.kv.getItem(ACTIVE_KEY);
109
+ if (saved && (0, profiles_1.isQualityProfileId)(saved) && isAtMost(saved, candidate)) {
110
+ // Honor last-good only when it is at most one tier below the
111
+ // capability suggestion. A bigger gap is almost always a stale
112
+ // floor (e.g. iPhone score→pro stuck on ultralite after an old
113
+ // crash / bad session). Crash-guard FAILED marks still apply below.
114
+ const gap = (0, profiles_1.qualityLadderIndex)(saved) - (0, profiles_1.qualityLadderIndex)(candidate);
115
+ if (gap <= 1) {
116
+ candidate = saved;
117
+ this.onDiagnostic?.(`[posetracker] quality: restored last-good profile=${candidate}`);
118
+ }
119
+ else {
120
+ this.onDiagnostic?.(`[posetracker] quality: ignoring stale last-good=${saved} ` +
121
+ `(suggested=${candidate}, gap=${gap} tiers) — re-probing from capability`);
122
+ }
123
+ }
124
+ }
125
+ catch {
126
+ /* ignore */
127
+ }
128
+ }
129
+ const suggested = candidate;
130
+ let safety = 0;
131
+ while (safety++ < 8) {
132
+ const crashed = await this.guard.consumeCrashIfProbing(candidate);
133
+ const failed = crashed || (await this.guard.isFailed(candidate));
134
+ if (!failed)
135
+ break;
136
+ const lower = (0, profiles_1.nextLowerQualityProfile)(candidate);
137
+ this.onDiagnostic?.(`[posetracker] quality: profile=${candidate} marked failed (crash-guard) → ` +
138
+ (lower ?? 'none'));
139
+ if (!lower)
140
+ break;
141
+ candidate = lower;
142
+ }
143
+ // Poisoned ladder: aborted getUserMedia races used to mark every tier
144
+ // FAILED and pin the device on basic. If we dropped ≥2 tiers below the
145
+ // capability suggestion, clear those marks and re-probe from suggested.
146
+ const drop = (0, profiles_1.qualityLadderIndex)(candidate) - (0, profiles_1.qualityLadderIndex)(suggested);
147
+ if (drop >= 2 && this.choice === 'AdaptiveChoice') {
148
+ this.onDiagnostic?.(`[posetracker] quality: clearing poisoned crash-guard ` +
149
+ `(${suggested}→${candidate}, drop=${drop}) — re-probing from ${suggested}`);
150
+ let walk = suggested;
151
+ while (walk) {
152
+ await this.guard.clear(walk);
153
+ if (walk === candidate)
154
+ break;
155
+ walk = (0, profiles_1.nextLowerQualityProfile)(walk);
156
+ }
157
+ candidate = suggested;
158
+ }
159
+ this.activeProfile = candidate;
160
+ this.resolved = true;
161
+ const minTarget = this.getMinTargetFps();
162
+ const band = (0, profiles_1.getIdealFpsRange)();
163
+ this.onDiagnostic?.(`[posetracker] quality: initial profile=${candidate} ` +
164
+ `minTargetFps=${minTarget} idealBand=${band.min}-${band.idealMax}+ ` +
165
+ `choice=${this.choice} capturePriority=${this.capturePriority} ` +
166
+ `score=${this.capability.score} ` +
167
+ `reasons=[${this.capability.reasons.join('; ')}]`);
168
+ return this.getActiveProfile();
169
+ }
170
+ /** Call immediately before the WebView page boots this profile. */
171
+ async beginSession() {
172
+ if (!this.resolved)
173
+ await this.resolveInitialProfile();
174
+ await this.guard.markProbing(this.activeProfile);
175
+ this.onDiagnostic?.(`[posetracker] quality: guard PROBING profile=${this.activeProfile}`);
176
+ }
177
+ /**
178
+ * Warm-up zeros bench result (before / around getUserMedia).
179
+ * Picks the safer of (warmup median → profile) and (device capability).
180
+ * May **upgrade** away from a stale last-good when the bench shows headroom
181
+ * (e.g. 15 ms median on iPhone stuck on ultralite).
182
+ */
183
+ async onWarmupEstimate(options) {
184
+ const med = options.medianInferenceMs;
185
+ const est = (0, profiles_1.estimatedFpsFromMedianMs)(med);
186
+ const minTarget = this.getMinTargetFps();
187
+ this.warmupMedianMs = med;
188
+ this.warmupEstimatedFps = est;
189
+ if (options.glRenderer) {
190
+ this.capability = (0, deviceCapability_1.scoreDeviceCapability)({ glRenderer: options.glRenderer });
191
+ }
192
+ const fromWarmup = (0, profiles_1.profileFromWarmupMedianMs)(med, minTarget);
193
+ const fromGl = this.choice === 'AdaptiveChoice' ? this.capability.suggestedProfile : this.activeProfile;
194
+ // Safer of bench vs capability — do NOT clamp to activeProfile/page
195
+ // (that permanently froze devices on a stale ultralite last-good).
196
+ let next = (0, profiles_1.lowerQualityProfile)(fromWarmup, fromGl);
197
+ if (this.capturePriority === 'quality') {
198
+ // Host opted out of FPS-driven capture cuts — keep / raise to prime.
199
+ next = this.choice === 'AdaptiveChoice' ? 'prime' : this.activeProfile;
200
+ this.onDiagnostic?.(`[posetracker] quality: capturePriority=quality — skipping FPS warm-up downgrade ` +
201
+ `(est=${est != null ? est.toFixed(1) : '?'} fps); holding profile=${next}`);
202
+ }
203
+ else {
204
+ // Never upgrade when zeros warm-up is already below the floor — camera
205
+ // preprocess only adds cost (Mali: basic~10fps, ultralite upgrade~8fps).
206
+ const belowFloor = est != null && est < minTarget;
207
+ if (belowFloor && (0, profiles_1.qualityLadderIndex)(next) < (0, profiles_1.qualityLadderIndex)(this.activeProfile)) {
208
+ this.onDiagnostic?.(`[posetracker] quality: warmup below floor (est=${est.toFixed(1)} < ${minTarget}) ` +
209
+ `— blocking upgrade ${this.activeProfile} → ${next}, staying ${this.activeProfile}`);
210
+ next = this.activeProfile;
211
+ }
212
+ }
213
+ const upgrading = (0, profiles_1.qualityLadderIndex)(next) < (0, profiles_1.qualityLadderIndex)(this.activeProfile);
214
+ this.onDiagnostic?.(`[posetracker] quality: warmup medianMs=${med != null ? med.toFixed(1) : '?'} ` +
215
+ `estimatedFps=${est != null ? est.toFixed(1) : '?'} ` +
216
+ `minTarget=${minTarget} → profile=${next} (was ${this.activeProfile}` +
217
+ `${upgrading ? ', upgrade' : ''})`);
218
+ if (next !== this.activeProfile) {
219
+ // Page posts warmup_estimate *before* getUserMedia and now opens at
220
+ // fromWarmup. Never inject setQuality during that window — it aborts
221
+ // the in-flight getUserMedia ("The operation was aborted") and fails init.
222
+ // Sync RN state only; live setQuality remains for post-ready downgrades.
223
+ const pageAlreadyApplied = options.pageSelectedProfile != null && options.pageSelectedProfile === next;
224
+ const skipApply = pageAlreadyApplied || upgrading;
225
+ const detail = upgrading
226
+ ? `Warm-up estimated inference FPS ${est != null ? est.toFixed(1) : '?'} ` +
227
+ `(median ${med != null ? med.toFixed(1) : '?'} ms) has headroom above the ` +
228
+ `minimum target of ${minTarget} FPS. Upgrading camera profile ` +
229
+ `"${this.activeProfile}" → "${next}"` +
230
+ (skipApply ? ' (page will open / already selected — no mid-boot setQuality).' : '.')
231
+ : `Warm-up estimated inference FPS ${est != null ? est.toFixed(1) : '?'} ` +
232
+ `(median ${med != null ? med.toFixed(1) : '?'} ms) is below the ` +
233
+ `platform minimum target of ${minTarget} FPS. Switching camera profile ` +
234
+ `to "${next}" before opening the webcam.`;
235
+ if (upgrading) {
236
+ // Drop stale FAILED marks for the tier we're recovering to.
237
+ await this.guard.clear(next);
238
+ }
239
+ await this.downgradeTo(next, 'warmup_estimate', detail, {
240
+ estimatedFps: est,
241
+ medianInferenceMs: med,
242
+ skipApply,
243
+ });
244
+ }
245
+ else {
246
+ this.warmupApplied = true;
247
+ await this.guard.markPassed(this.activeProfile);
248
+ await this.persistActive(this.activeProfile);
249
+ }
250
+ }
251
+ /** Call when the WebView posts `ready`. */
252
+ async onRuntimeReady(options) {
253
+ // Prefer warm-up path; ready often carries the same medianMs.
254
+ if (!this.warmupApplied && options?.medianInferenceMs != null) {
255
+ await this.onWarmupEstimate({
256
+ medianInferenceMs: options.medianInferenceMs,
257
+ glRenderer: options.glRenderer,
258
+ pageSelectedProfile: options.pageSelectedProfile,
259
+ });
260
+ return;
261
+ }
262
+ if (options?.glRenderer) {
263
+ this.capability = (0, deviceCapability_1.scoreDeviceCapability)({ glRenderer: options.glRenderer });
264
+ if (this.choice === 'AdaptiveChoice' &&
265
+ isStrictlyLower(this.capability.suggestedProfile, this.activeProfile)) {
266
+ await this.downgradeTo(this.capability.suggestedProfile, 'device_capability', 'GL renderer capability suggests a lower camera profile for stable FPS.');
267
+ return;
268
+ }
269
+ }
270
+ await this.guard.markPassed(this.activeProfile);
271
+ await this.persistActive(this.activeProfile);
272
+ this.settleUntilMs = Date.now() + profiles_1.QUALITY_SETTLE_MS;
273
+ this.lowFpsStreak = 0;
274
+ this.fpsWindow = [];
275
+ this.onDiagnostic?.(`[posetracker] quality: guard PASSED profile=${this.activeProfile} ` +
276
+ `(live downgrade settle ${profiles_1.QUALITY_SETTLE_MS}ms)`);
277
+ }
278
+ /** Feed 1 Hz WebView stats. May auto-downgrade and/or warn. */
279
+ async onStats(sample) {
280
+ if (!Number.isFinite(sample.fps) || sample.fps < 0)
281
+ return;
282
+ this.fpsWindow.push(sample.fps);
283
+ if (this.fpsWindow.length > 10)
284
+ this.fpsWindow.shift();
285
+ const profile = this.getActiveProfile();
286
+ const mean = this.meanFps();
287
+ if (mean == null)
288
+ return;
289
+ const minTarget = this.getMinTargetFps();
290
+ const critical = (0, profiles_1.getCriticalFpsThreshold)();
291
+ const now = Date.now();
292
+ // Pipeline FPS (rAF loop) is often well below 1000/medianMs because it
293
+ // includes camera + bitmap. Downgrade on **inference median** only — e.g.
294
+ // iPhone pro at fps=22 / medianMs=28 is healthy vs a 30 FPS floor (33 ms).
295
+ const budgetMs = 1000 / minTarget;
296
+ const med = sample.medianInferenceMs;
297
+ const inferenceTooSlow = med != null && Number.isFinite(med) && med > budgetMs * 1.2;
298
+ if (now < this.settleUntilMs) {
299
+ this.lowFpsStreak = 0;
300
+ return;
301
+ }
302
+ if (inferenceTooSlow) {
303
+ this.lowFpsStreak += 1;
304
+ }
305
+ else {
306
+ this.lowFpsStreak = 0;
307
+ }
308
+ // Warnings may still look at pipeline FPS (UX feel), but never after settle only.
309
+ if (mean < critical || (mean < minTarget && !(0, profiles_1.nextLowerQualityProfile)(this.activeProfile))) {
310
+ if (this.lastWarningAtMs == null || now - this.lastWarningAtMs > 30000) {
311
+ this.lastWarningAtMs = now;
312
+ const qualityNote = this.capturePriority === 'quality'
313
+ ? ' capturePriority is "quality" (sharp preview preferred over FPS) — this is expected on mid-range Android.'
314
+ : '';
315
+ const event = {
316
+ type: 'performance_warning',
317
+ code: 'device_too_slow',
318
+ message: `This device appears unsuitable for real-time pose estimation with the ` +
319
+ `current configuration (mean pipeline FPS=${mean.toFixed(1)} < ` +
320
+ `minimum target ${minTarget} FPS` +
321
+ (med != null ? `, median inference ${med.toFixed(1)} ms` : '') +
322
+ `). User experience may be significantly impacted.` +
323
+ qualityNote,
324
+ meanFps: mean,
325
+ thresholdFps: minTarget,
326
+ activeProfile: this.activeProfile,
327
+ medianInferenceMs: sample.medianInferenceMs,
328
+ videoSize: sample.videoSize,
329
+ timestampMs: now,
330
+ };
331
+ this.onDiagnostic?.(`[posetracker] WARNING: ${event.message}`);
332
+ this.onPerformanceWarning?.(event);
333
+ }
334
+ }
335
+ if (this.capturePriority !== 'quality' &&
336
+ this.lowFpsStreak >= profiles_1.LOW_FPS_STREAK_BEFORE_DOWNGRADE &&
337
+ !this.downgradeInFlight) {
338
+ const lower = (0, profiles_1.nextLowerQualityProfile)(this.activeProfile);
339
+ if (lower) {
340
+ await this.downgradeTo(lower, 'low_fps', `Median inference ${med != null ? med.toFixed(1) : '?'} ms stayed above the ` +
341
+ `budget of ${(budgetMs * 1.2).toFixed(0)} ms (min target ${minTarget} FPS) ` +
342
+ `for ${this.lowFpsStreak} samples on profile "${profile.id}". ` +
343
+ `Automatically switching to "${lower}".`, { estimatedFps: mean, medianInferenceMs: sample.medianInferenceMs });
344
+ }
345
+ }
346
+ else if (this.capturePriority === 'quality' &&
347
+ this.lowFpsStreak >= profiles_1.LOW_FPS_STREAK_BEFORE_DOWNGRADE) {
348
+ if (this.lowFpsStreak === profiles_1.LOW_FPS_STREAK_BEFORE_DOWNGRADE) {
349
+ this.onDiagnostic?.(`[posetracker] quality: capturePriority=quality — not downgrading from ` +
350
+ `"${profile.id}" despite slow median ${med != null ? med.toFixed(1) : '?'} ms`);
351
+ }
352
+ }
353
+ }
354
+ async downgradeTo(next, reason, detail, extras) {
355
+ if (next === this.activeProfile || this.downgradeInFlight)
356
+ return;
357
+ this.downgradeInFlight = true;
358
+ const previous = this.activeProfile;
359
+ try {
360
+ // Only crash_guard / hard capability caps poison a tier. Live low_fps
361
+ // must NOT markFailed — that pinned iPhones on basic after a cascade.
362
+ if (reason === 'crash_guard' || reason === 'device_capability') {
363
+ await this.guard.markFailed(previous);
364
+ }
365
+ else if ((0, profiles_1.qualityLadderIndex)(next) < (0, profiles_1.qualityLadderIndex)(previous)) {
366
+ await this.guard.clear(previous);
367
+ }
368
+ this.activeProfile = next;
369
+ this.lowFpsStreak = 0;
370
+ this.fpsWindow = [];
371
+ this.warmupApplied = true;
372
+ this.settleUntilMs = Date.now() + profiles_1.QUALITY_SETTLE_MS;
373
+ await this.guard.markProbing(next);
374
+ await this.persistActive(next);
375
+ const profile = (0, profiles_1.getQualityProfile)(next);
376
+ if (!extras?.skipApply) {
377
+ this.applyProfile?.(profile);
378
+ }
379
+ const event = {
380
+ type: 'quality_changed',
381
+ previousProfile: previous,
382
+ activeProfile: next,
383
+ reason,
384
+ detail,
385
+ profile,
386
+ estimatedFps: extras?.estimatedFps,
387
+ medianInferenceMs: extras?.medianInferenceMs,
388
+ timestampMs: Date.now(),
389
+ };
390
+ this.onDiagnostic?.(`[posetracker] quality: ${previous} → ${next} reason=${reason}` +
391
+ (extras?.skipApply ? ' (page-applied)' : '') +
392
+ ` — ${detail}`);
393
+ this.onQualityChanged?.(event);
394
+ }
395
+ finally {
396
+ this.downgradeInFlight = false;
397
+ }
398
+ }
399
+ meanFps() {
400
+ if (!this.fpsWindow.length)
401
+ return null;
402
+ const sum = this.fpsWindow.reduce((a, b) => a + b, 0);
403
+ return sum / this.fpsWindow.length;
404
+ }
405
+ async persistActive(id) {
406
+ if (!this.kv)
407
+ return;
408
+ try {
409
+ await this.kv.setItem(ACTIVE_KEY, id);
410
+ }
411
+ catch {
412
+ /* ignore */
413
+ }
414
+ }
415
+ }
416
+ exports.AdaptiveQualityController = AdaptiveQualityController;
417
+ /** True if `a` is equal or lower quality than `b`. */
418
+ function isAtMost(a, b) {
419
+ return (0, profiles_1.qualityLadderIndex)(a) >= (0, profiles_1.qualityLadderIndex)(b) && (0, profiles_1.qualityLadderIndex)(a) >= 0;
420
+ }
421
+ function isStrictlyLower(a, b) {
422
+ return (0, profiles_1.qualityLadderIndex)(a) > (0, profiles_1.qualityLadderIndex)(b);
423
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Crash-loop guard for quality / runtime profiles.
3
+ *
4
+ * Analog of Sency's `TfliteRuntimeGuard` (PROBING → PASSED | FAILED):
5
+ * 1. Before activating a profile, `markProbing(key)`.
6
+ * 2. After a healthy ready signal, `markPassed(key)`.
7
+ * 3. If the app is killed/crashes while still PROBING, the next launch
8
+ * sees PROBING → treats it as FAILED → caller must pick a lower profile.
9
+ *
10
+ * Persisted via {@link KeyValueStore} (AsyncStorage when available). Without
11
+ * a store the guard is in-memory only (still useful within one process for
12
+ * the 30s same-process timeout).
13
+ */
14
+ import type { KeyValueStore } from '../engine/EngineLoader';
15
+ export type GuardState = 'unknown' | 'probing' | 'passed' | 'failed';
16
+ export declare class RuntimeGuard {
17
+ private readonly kv;
18
+ private readonly sdkVersion;
19
+ private readonly memory;
20
+ constructor(kv: KeyValueStore | null, sdkVersion: string);
21
+ private storageKey;
22
+ getState(profileKey: string): Promise<GuardState>;
23
+ /**
24
+ * If previous launch died while probing this key, mark failed and return true.
25
+ */
26
+ consumeCrashIfProbing(profileKey: string): Promise<boolean>;
27
+ markProbing(profileKey: string): Promise<void>;
28
+ markPassed(profileKey: string): Promise<void>;
29
+ markFailed(profileKey: string): Promise<void>;
30
+ isFailed(profileKey: string): Promise<boolean>;
31
+ /** Clear persisted/memory state for a profile (recover from stale FAILED). */
32
+ clear(profileKey: string): Promise<void>;
33
+ private write;
34
+ }