@pose-tracker/react-native-pose-estimation-light 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +45 -0
- package/PoseTrackerVision.podspec +28 -0
- package/README.md +161 -0
- package/THIRD_PARTY_NOTICES.md +19 -0
- package/ios/PoseTrackerVision/PoseTrackerBodyPosePlugin.m +18 -0
- package/ios/PoseTrackerVision/PoseTrackerBodyPosePlugin.swift +108 -0
- package/lib/PoseTrackerProvider.d.ts +102 -0
- package/lib/PoseTrackerProvider.js +204 -0
- package/lib/api/configure.d.ts +29 -0
- package/lib/api/configure.js +64 -0
- package/lib/api/skeleton.d.ts +17 -0
- package/lib/api/skeleton.js +53 -0
- package/lib/api/track.d.ts +61 -0
- package/lib/api/track.js +146 -0
- package/lib/backends/PoseBackend.d.ts +50 -0
- package/lib/backends/PoseBackend.js +11 -0
- package/lib/backends/vision/VisionPoseBackend.d.ts +72 -0
- package/lib/backends/vision/VisionPoseBackend.js +177 -0
- package/lib/backends/vision/mapVisionJoints.d.ts +35 -0
- package/lib/backends/vision/mapVisionJoints.js +162 -0
- package/lib/backends/vision/optionalVision.d.ts +32 -0
- package/lib/backends/vision/optionalVision.js +76 -0
- package/lib/backends/webview/WebViewPoseBackend.d.ts +223 -0
- package/lib/backends/webview/WebViewPoseBackend.js +382 -0
- package/lib/backends/webview/brandAssets.d.ts +5 -0
- package/lib/backends/webview/brandAssets.js +8 -0
- package/lib/backends/webview/onlineRuntime.d.ts +45 -0
- package/lib/backends/webview/onlineRuntime.js +57 -0
- package/lib/backends/webview/poseHtml.d.ts +71 -0
- package/lib/backends/webview/poseHtml.js +188 -0
- package/lib/backends/webview/poseRuntimeSource.d.ts +3 -0
- package/lib/backends/webview/poseRuntimeSource.js +4 -0
- package/lib/cache/obfuscate.d.ts +18 -0
- package/lib/cache/obfuscate.js +90 -0
- package/lib/camera/PoseCameraView.d.ts +51 -0
- package/lib/camera/PoseCameraView.js +99 -0
- package/lib/camera/WebViewPoseView.d.ts +81 -0
- package/lib/camera/WebViewPoseView.js +312 -0
- package/lib/client.d.ts +361 -0
- package/lib/client.js +1046 -0
- package/lib/diagnostics/logReport.d.ts +23 -0
- package/lib/diagnostics/logReport.js +102 -0
- package/lib/engine/EngineLoader.d.ts +95 -0
- package/lib/engine/EngineLoader.js +347 -0
- package/lib/engine/types.d.ts +83 -0
- package/lib/engine/types.js +11 -0
- package/lib/events/classicMessage.d.ts +31 -0
- package/lib/events/classicMessage.js +259 -0
- package/lib/exercises/aliases.d.ts +18 -0
- package/lib/exercises/aliases.js +48 -0
- package/lib/index.d.ts +72 -0
- package/lib/index.js +170 -0
- package/lib/models/poseModels.d.ts +39 -0
- package/lib/models/poseModels.js +63 -0
- package/lib/quality/AdaptiveQualityController.d.ts +120 -0
- package/lib/quality/AdaptiveQualityController.js +423 -0
- package/lib/quality/RuntimeGuard.d.ts +34 -0
- package/lib/quality/RuntimeGuard.js +105 -0
- package/lib/quality/captureMode.d.ts +82 -0
- package/lib/quality/captureMode.js +75 -0
- package/lib/quality/deviceCapability.d.ts +37 -0
- package/lib/quality/deviceCapability.js +177 -0
- package/lib/quality/profiles.d.ts +125 -0
- package/lib/quality/profiles.js +202 -0
- package/lib/runtime/RuntimeCache.d.ts +75 -0
- package/lib/runtime/RuntimeCache.js +230 -0
- package/lib/sdkVersion.d.ts +2 -0
- package/lib/sdkVersion.js +5 -0
- package/lib/support/optionalModules.d.ts +46 -0
- package/lib/support/optionalModules.js +82 -0
- package/lib/types/acceleration.d.ts +86 -0
- package/lib/types/acceleration.js +15 -0
- package/lib/types/events.d.ts +345 -0
- package/lib/types/events.js +10 -0
- package/lib/types/features.d.ts +90 -0
- package/lib/types/features.js +122 -0
- package/lib/types/manifest.d.ts +197 -0
- package/lib/types/manifest.js +10 -0
- package/lib/types/pose.d.ts +36 -0
- package/lib/types/pose.js +41 -0
- package/lib/types/preload.d.ts +17 -0
- package/lib/types/preload.js +10 -0
- package/lib/types/skeleton.d.ts +29 -0
- package/lib/types/skeleton.js +52 -0
- package/package.json +92 -0
- package/react-native.config.js +12 -0
- package/src/PoseTrackerProvider.tsx +332 -0
- package/src/api/configure.ts +82 -0
- package/src/api/skeleton.ts +60 -0
- package/src/api/track.ts +182 -0
- package/src/backends/PoseBackend.ts +59 -0
- package/src/backends/vision/VisionPoseBackend.ts +238 -0
- package/src/backends/vision/mapVisionJoints.ts +185 -0
- package/src/backends/vision/optionalVision.ts +94 -0
- package/src/backends/webview/WebViewPoseBackend.ts +582 -0
- package/src/backends/webview/brandAssets.ts +5 -0
- package/src/backends/webview/onlineRuntime.ts +94 -0
- package/src/backends/webview/poseHtml.ts +259 -0
- package/src/backends/webview/poseRuntimeSource.d.ts +3 -0
- package/src/cache/obfuscate.ts +94 -0
- package/src/camera/PoseCameraView.tsx +223 -0
- package/src/camera/WebViewPoseView.tsx +466 -0
- package/src/client.ts +1295 -0
- package/src/diagnostics/logReport.ts +131 -0
- package/src/engine/EngineLoader.ts +428 -0
- package/src/engine/types.ts +93 -0
- package/src/events/classicMessage.ts +271 -0
- package/src/exercises/aliases.ts +50 -0
- package/src/index.ts +243 -0
- package/src/models/poseModels.ts +93 -0
- package/src/quality/AdaptiveQualityController.ts +579 -0
- package/src/quality/RuntimeGuard.ts +119 -0
- package/src/quality/captureMode.ts +97 -0
- package/src/quality/deviceCapability.ts +187 -0
- package/src/quality/profiles.ts +253 -0
- package/src/runtime/RuntimeCache.ts +300 -0
- package/src/sdkVersion.ts +2 -0
- package/src/support/optionalModules.ts +114 -0
- package/src/types/acceleration.ts +94 -0
- package/src/types/events.ts +437 -0
- package/src/types/features.ts +176 -0
- package/src/types/manifest.ts +230 -0
- package/src/types/pose.ts +85 -0
- package/src/types/preload.ts +19 -0
- package/src/types/skeleton.ts +77 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Camera capture + Android perf experiment knobs (WebView getUserMedia / MoveNet).
|
|
3
|
+
*
|
|
4
|
+
* iOS is never mutated by the Android-only flags below — {@link buildPoseHtml}
|
|
5
|
+
* forces frame-skip / soft-cap / preprocess overrides off on iOS.
|
|
6
|
+
*
|
|
7
|
+
* ## Experiment stack 2026-08-06
|
|
8
|
+
* 1. `device-native` cold open (Front-aligned) + optional Android soft-cap
|
|
9
|
+
* 2. Android frame-skip between inferences
|
|
10
|
+
* 3. Android lighter preprocess (`canvas-direct`)
|
|
11
|
+
* 4. Android min-target floor 10 FPS (downgrade only when truly needed)
|
|
12
|
+
* 5. Verbose perf debug in WebView stats / HUD
|
|
13
|
+
*
|
|
14
|
+
* ## REVERT to previous stable behaviour
|
|
15
|
+
* 1. `CAPTURE_CONSTRAINT_MODE = 'profile-constrained'`
|
|
16
|
+
* 2. `ENABLE_MALI_HARD_CAP = true`
|
|
17
|
+
* 3. `ANDROID_INFER_FRAME_SKIP = 0`
|
|
18
|
+
* 4. `ANDROID_PREPROCESS_PATH = 'imagebitmap'`
|
|
19
|
+
* 5. `ANDROID_SOFT_CAP_PROFILE = null` (experiment currently pins `'basic'`)
|
|
20
|
+
* 6. `ANDROID_MIN_TARGET_FPS = 15`
|
|
21
|
+
* 7. `ANDROID_PERF_DEBUG = false`
|
|
22
|
+
* 8. Rebuild: `npm run build:runtime-payload && npm run build`
|
|
23
|
+
* 9. Refresh testapp link + `npx expo start -c`
|
|
24
|
+
*
|
|
25
|
+
* You also have a zip archive of the previous SDK as a full rollback.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** How getUserMedia chooses resolution on cold open. */
|
|
29
|
+
export type CaptureConstraintMode = 'device-native' | 'profile-constrained';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Letterbox preprocess path inside the WebView (Android experiment only).
|
|
33
|
+
* - `imagebitmap` — draw 192² canvas → createImageBitmap → fromPixels (legacy)
|
|
34
|
+
* - `canvas-direct` — draw 192² canvas → fromPixels(canvas) (skips ImageBitmap)
|
|
35
|
+
*/
|
|
36
|
+
export type AndroidPreprocessPath = 'imagebitmap' | 'canvas-direct';
|
|
37
|
+
|
|
38
|
+
/** Soft-cap ladder ids (mirrors QualityProfileId; kept local to avoid import cycles). */
|
|
39
|
+
export type AndroidSoftCapProfile =
|
|
40
|
+
| 'prime'
|
|
41
|
+
| 'pro'
|
|
42
|
+
| 'lite'
|
|
43
|
+
| 'ultralite'
|
|
44
|
+
| 'basic';
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* - `device-native` — facingMode only (like PoseTrackerFront). Preview = device default (often HD).
|
|
48
|
+
* - `profile-constrained` — ideal width/height/frameRate from the AdaptiveChoice ladder.
|
|
49
|
+
*/
|
|
50
|
+
export const CAPTURE_CONSTRAINT_MODE: CaptureConstraintMode = 'device-native';
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* When true, Mali GL renderers are hard-capped to UltraLite at capability score
|
|
54
|
+
* time (previous SDK default). Disable for the Front-aligned experiment so RN
|
|
55
|
+
* does not immediately setQuality() back down to 480×360 after a native HD open.
|
|
56
|
+
*/
|
|
57
|
+
export const ENABLE_MALI_HARD_CAP = false;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Android-only: ready rAF ticks to skip between MoveNet inferences.
|
|
61
|
+
* Preview stays at full capture quality.
|
|
62
|
+
*
|
|
63
|
+
* - `0` — off
|
|
64
|
+
* - `1` — every other ready tick
|
|
65
|
+
* - `2` — every 3rd, etc.
|
|
66
|
+
*/
|
|
67
|
+
export const ANDROID_INFER_FRAME_SKIP = 1;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Android-only preprocess path. iOS always uses `imagebitmap` (unchanged).
|
|
71
|
+
*/
|
|
72
|
+
export const ANDROID_PREPROCESS_PATH: AndroidPreprocessPath = 'canvas-direct';
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Android-only cold-open soft cap. When set, getUserMedia uses this profile’s
|
|
76
|
+
* width/height as ideal+max even in `device-native` mode (so the device cannot
|
|
77
|
+
* open unrestricted 1080p). Set `null` for fully unconstrained facingMode-only.
|
|
78
|
+
*
|
|
79
|
+
* Mali-G52 evidence (2026-08-06): `basic` 240×320 ≈ 10 FPS median; `ultralite`
|
|
80
|
+
* 360×480 ≈ 8 FPS — pin soft-cap to `basic` to hold the 10 FPS floor.
|
|
81
|
+
* Live `setQuality` cannot go lower than basic.
|
|
82
|
+
*/
|
|
83
|
+
export const ANDROID_SOFT_CAP_PROFILE: AndroidSoftCapProfile | null = 'basic';
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Android AdaptiveChoice floor (min target FPS). Downgrade when median
|
|
87
|
+
* inference ms stays above ~1.2 × (1000/floor). iOS stays on
|
|
88
|
+
* {@link MIN_TARGET_FPS_IOS} (30).
|
|
89
|
+
*/
|
|
90
|
+
export const ANDROID_MIN_TARGET_FPS = 10;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* When true (Android), WebView posts rich `experiments` + `leverHints` on each
|
|
94
|
+
* 1 Hz stats tick and mirrors a compact line on the HUD. RN logs the same via
|
|
95
|
+
* the existing diagnostic listener. iOS ignores this flag.
|
|
96
|
+
*/
|
|
97
|
+
export const ANDROID_PERF_DEBUG = true;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight device capability score → initial quality profile.
|
|
3
|
+
*
|
|
4
|
+
* Sency's AdaptiveChoice uses RAM / CPU freq / first API level. In Expo Go we
|
|
5
|
+
* only have a subset of signals without extra native deps; we combine:
|
|
6
|
+
* - Platform (iOS is typically 3–5× faster on WebGL MoveNet)
|
|
7
|
+
* - Android API level
|
|
8
|
+
* - Optional RAM via expo-device / react-native DeviceInfo when present
|
|
9
|
+
* - GL renderer hint (Mali mid-range) once known from the WebView
|
|
10
|
+
*
|
|
11
|
+
* Score is 0–100; mapped to a QualityProfileId. Brand/year caps from Sency
|
|
12
|
+
* are approximated via API level + Mali detection.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { Platform } from 'react-native';
|
|
16
|
+
|
|
17
|
+
import { ENABLE_MALI_HARD_CAP } from './captureMode';
|
|
18
|
+
import type { QualityProfileId } from './profiles';
|
|
19
|
+
|
|
20
|
+
export interface DeviceCapabilitySnapshot {
|
|
21
|
+
platform: 'ios' | 'android' | 'other';
|
|
22
|
+
/** Android API level or iOS major version when available. */
|
|
23
|
+
osVersion: number | null;
|
|
24
|
+
/** Total device RAM in GiB when a probe is available. */
|
|
25
|
+
totalMemoryGiB: number | null;
|
|
26
|
+
/** WebGL UNMASKED_RENDERER when known (after first WebView ready). */
|
|
27
|
+
glRenderer: string | null;
|
|
28
|
+
/** 0–100 composite score. */
|
|
29
|
+
score: number;
|
|
30
|
+
/** Profile suggested by the score (before crash-guard / caps). */
|
|
31
|
+
suggestedProfile: QualityProfileId;
|
|
32
|
+
reasons: string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function probeTotalMemoryGiB(): number | null {
|
|
36
|
+
// Optional peers — never required.
|
|
37
|
+
try {
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
39
|
+
const Device = require('expo-device');
|
|
40
|
+
const bytes = Device.totalMemory as number | null | undefined;
|
|
41
|
+
if (typeof bytes === 'number' && bytes > 0) return bytes / (1024 * 1024 * 1024);
|
|
42
|
+
} catch {
|
|
43
|
+
/* ignore */
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
47
|
+
const DeviceInfo = require('react-native-device-info');
|
|
48
|
+
// sync API may exist
|
|
49
|
+
if (typeof DeviceInfo.getTotalMemorySync === 'function') {
|
|
50
|
+
const bytes = DeviceInfo.getTotalMemorySync() as number;
|
|
51
|
+
if (typeof bytes === 'number' && bytes > 0) return bytes / (1024 * 1024 * 1024);
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
/* ignore */
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function parseOsVersion(): number | null {
|
|
60
|
+
const v = Platform.Version;
|
|
61
|
+
if (typeof v === 'number') return v;
|
|
62
|
+
if (typeof v === 'string') {
|
|
63
|
+
const major = parseInt(v.split('.')[0] ?? '', 10);
|
|
64
|
+
return Number.isFinite(major) ? major : null;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function isMaliRenderer(renderer: string | null | undefined): boolean {
|
|
70
|
+
return !!(renderer && /mali/i.test(renderer));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Map capability score → profile (before Mali / crash-guard caps).
|
|
75
|
+
* Thresholds calibrated so mid-range Android (Mali-G52 era) lands on UltraLite.
|
|
76
|
+
*/
|
|
77
|
+
export function profileFromScore(score: number): QualityProfileId {
|
|
78
|
+
if (score >= 78) return 'prime';
|
|
79
|
+
if (score >= 62) return 'pro';
|
|
80
|
+
if (score >= 48) return 'lite';
|
|
81
|
+
if (score >= 32) return 'ultralite';
|
|
82
|
+
return 'basic';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function scoreDeviceCapability(options?: {
|
|
86
|
+
glRenderer?: string | null;
|
|
87
|
+
}): DeviceCapabilitySnapshot {
|
|
88
|
+
const reasons: string[] = [];
|
|
89
|
+
const platform =
|
|
90
|
+
Platform.OS === 'ios' ? 'ios' : Platform.OS === 'android' ? 'android' : 'other';
|
|
91
|
+
const osVersion = parseOsVersion();
|
|
92
|
+
const totalMemoryGiB = probeTotalMemoryGiB();
|
|
93
|
+
const glRenderer = options?.glRenderer ?? null;
|
|
94
|
+
|
|
95
|
+
let score = 0;
|
|
96
|
+
|
|
97
|
+
if (platform === 'ios') {
|
|
98
|
+
// Apple GPU WebGL MoveNet is typically <20 ms — start high.
|
|
99
|
+
score += 55;
|
|
100
|
+
reasons.push('platform=ios (+55)');
|
|
101
|
+
if (osVersion != null) {
|
|
102
|
+
if (osVersion >= 17) {
|
|
103
|
+
score += 15;
|
|
104
|
+
reasons.push(`ios>=17 (+15)`);
|
|
105
|
+
} else if (osVersion >= 15) {
|
|
106
|
+
score += 10;
|
|
107
|
+
reasons.push(`ios>=15 (+10)`);
|
|
108
|
+
} else {
|
|
109
|
+
score += 5;
|
|
110
|
+
reasons.push(`ios legacy (+5)`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
} else if (platform === 'android') {
|
|
114
|
+
score += 18;
|
|
115
|
+
reasons.push('platform=android (+18)');
|
|
116
|
+
if (osVersion != null) {
|
|
117
|
+
if (osVersion >= 34) {
|
|
118
|
+
score += 14;
|
|
119
|
+
reasons.push(`api>=34 (+14)`);
|
|
120
|
+
} else if (osVersion >= 31) {
|
|
121
|
+
score += 10;
|
|
122
|
+
reasons.push(`api>=31 (+10)`);
|
|
123
|
+
} else if (osVersion >= 28) {
|
|
124
|
+
score += 6;
|
|
125
|
+
reasons.push(`api>=28 (+6)`);
|
|
126
|
+
} else {
|
|
127
|
+
reasons.push(`api=${osVersion} (+0)`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
score += 20;
|
|
132
|
+
reasons.push('platform=other (+20)');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (totalMemoryGiB != null) {
|
|
136
|
+
if (totalMemoryGiB >= 8) {
|
|
137
|
+
score += 20;
|
|
138
|
+
reasons.push(`ram>=8GiB (+20)`);
|
|
139
|
+
} else if (totalMemoryGiB >= 6) {
|
|
140
|
+
score += 14;
|
|
141
|
+
reasons.push(`ram>=6GiB (+14)`);
|
|
142
|
+
} else if (totalMemoryGiB >= 4) {
|
|
143
|
+
score += 8;
|
|
144
|
+
reasons.push(`ram>=4GiB (+8)`);
|
|
145
|
+
} else {
|
|
146
|
+
reasons.push(`ram=${totalMemoryGiB.toFixed(1)}GiB (+0)`);
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
// Neutral default when RAM unknown: don't assume high-end.
|
|
150
|
+
score += 6;
|
|
151
|
+
reasons.push('ram=unknown (+6)');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (isMaliRenderer(glRenderer)) {
|
|
155
|
+
// Mali mid-range: HD camera preprocess alone can cost ~100 ms/frame.
|
|
156
|
+
score = Math.min(score, 40);
|
|
157
|
+
reasons.push('gl=Mali cap score≤40');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
score = Math.max(0, Math.min(100, score));
|
|
161
|
+
let suggested = profileFromScore(score);
|
|
162
|
+
|
|
163
|
+
// Hard caps (Sency-style).
|
|
164
|
+
if (platform === 'android' && suggested === 'prime') {
|
|
165
|
+
suggested = 'pro';
|
|
166
|
+
reasons.push('android cap: prime→pro');
|
|
167
|
+
}
|
|
168
|
+
// Mali → ultralite hard cap: gated by captureMode (see captureMode.ts REVERT).
|
|
169
|
+
if (
|
|
170
|
+
ENABLE_MALI_HARD_CAP &&
|
|
171
|
+
isMaliRenderer(glRenderer) &&
|
|
172
|
+
(suggested === 'prime' || suggested === 'pro')
|
|
173
|
+
) {
|
|
174
|
+
suggested = 'ultralite';
|
|
175
|
+
reasons.push('mali cap: →ultralite');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
platform,
|
|
180
|
+
osVersion,
|
|
181
|
+
totalMemoryGiB,
|
|
182
|
+
glRenderer,
|
|
183
|
+
score,
|
|
184
|
+
suggestedProfile: suggested,
|
|
185
|
+
reasons,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Camera / preprocess quality tiers for the WebView MoveNet runtime.
|
|
3
|
+
*
|
|
4
|
+
* Inspired by Sency's PoseModelChoice ladder (Prime → … → Basic), adapted to
|
|
5
|
+
* our stack: MoveNet Lightning always runs at 192×192; what changes is the
|
|
6
|
+
* **camera capture resolution**.
|
|
7
|
+
*
|
|
8
|
+
* FPS policy uses a **minimum target** (floor), not a single setpoint:
|
|
9
|
+
* - iOS: min 30 — acceptable/ideal band roughly 30–50+ FPS
|
|
10
|
+
* - Android: min from {@link ANDROID_MIN_TARGET_FPS} (experiment default 10)
|
|
11
|
+
*
|
|
12
|
+
* Warm-up benches estimate execute cost; AdaptiveChoice picks a capture
|
|
13
|
+
* profile before getUserMedia when the estimate is below the platform floor.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { Platform } from 'react-native';
|
|
17
|
+
|
|
18
|
+
import { ANDROID_MIN_TARGET_FPS } from './captureMode';
|
|
19
|
+
|
|
20
|
+
export type QualityProfileId = 'prime' | 'pro' | 'lite' | 'ultralite' | 'basic';
|
|
21
|
+
|
|
22
|
+
/** Host-facing choice, mirrors Sency's AdaptiveChoice + explicit override. */
|
|
23
|
+
export type QualityChoice = 'AdaptiveChoice' | QualityProfileId;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Host trade-off between pose FPS and camera preview sharpness.
|
|
27
|
+
*
|
|
28
|
+
* - `performance` (default) — AdaptiveChoice may lower capture (esp. Android
|
|
29
|
+
* soft-cap / warm-up / live downgrade) so inference stays near the platform
|
|
30
|
+
* FPS floor. Preview can look soft on mid-range Android.
|
|
31
|
+
* - `quality` — keep high capture / skip FPS-driven capture downgrades.
|
|
32
|
+
* Preview stays sharp; pose FPS may drop well below the floor. The SDK still
|
|
33
|
+
* emits `performance_warning` when slow.
|
|
34
|
+
*
|
|
35
|
+
* Explicit {@link QualityChoice} profile ids still pin the starting tier;
|
|
36
|
+
* `quality` only disables automatic FPS-driven drops.
|
|
37
|
+
*/
|
|
38
|
+
export type CapturePriority = 'performance' | 'quality';
|
|
39
|
+
|
|
40
|
+
export type QualityPlatform = 'ios' | 'android' | 'other';
|
|
41
|
+
|
|
42
|
+
export interface FpsRange {
|
|
43
|
+
/** Soft lower bound of the “good experience” band (same as min target on that OS). */
|
|
44
|
+
min: number;
|
|
45
|
+
/**
|
|
46
|
+
* Soft upper reference for docs / UI (“we’re happy around here”).
|
|
47
|
+
* Not a hard cap — exceeding it is fine.
|
|
48
|
+
*/
|
|
49
|
+
idealMax: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface QualityProfile {
|
|
53
|
+
id: QualityProfileId;
|
|
54
|
+
/** Human label for diagnostics / docs. */
|
|
55
|
+
label: string;
|
|
56
|
+
/** getUserMedia ideal width (CSS pixels / sensor). */
|
|
57
|
+
idealWidth: number;
|
|
58
|
+
/** getUserMedia ideal height. */
|
|
59
|
+
idealHeight: number;
|
|
60
|
+
/** getUserMedia ideal frameRate. */
|
|
61
|
+
idealFrameRate: number;
|
|
62
|
+
/**
|
|
63
|
+
* Per-profile floor used when AdaptiveChoice has not overridden via platform.
|
|
64
|
+
* Live controller uses {@link getMinTargetFps} instead.
|
|
65
|
+
*/
|
|
66
|
+
minStableFps: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Ordered highest → lowest. `nextLowerQualityProfile` walks this ladder.
|
|
71
|
+
*/
|
|
72
|
+
export const QUALITY_LADDER: readonly QualityProfileId[] = [
|
|
73
|
+
'prime',
|
|
74
|
+
'pro',
|
|
75
|
+
'lite',
|
|
76
|
+
'ultralite',
|
|
77
|
+
'basic',
|
|
78
|
+
] as const;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Android minimum acceptable inference FPS (floor).
|
|
82
|
+
* Sourced from {@link ANDROID_MIN_TARGET_FPS} so the experiment flag is the
|
|
83
|
+
* single knob (see captureMode.ts).
|
|
84
|
+
*/
|
|
85
|
+
export const MIN_TARGET_FPS_ANDROID = ANDROID_MIN_TARGET_FPS;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* iOS minimum acceptable inference FPS (floor).
|
|
89
|
+
* Acceptable/ideal experience is typically 30–50+ on Apple GPU.
|
|
90
|
+
*/
|
|
91
|
+
export const MIN_TARGET_FPS_IOS = 30;
|
|
92
|
+
|
|
93
|
+
/** Fallback floor for unknown platforms. */
|
|
94
|
+
export const MIN_TARGET_FPS_DEFAULT = MIN_TARGET_FPS_ANDROID;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Documented “good experience” bands (not hard caps).
|
|
98
|
+
* Downgrades / warnings use the **min** only.
|
|
99
|
+
*/
|
|
100
|
+
export const IDEAL_FPS_RANGE_IOS: FpsRange = { min: MIN_TARGET_FPS_IOS, idealMax: 50 };
|
|
101
|
+
export const IDEAL_FPS_RANGE_ANDROID: FpsRange = { min: MIN_TARGET_FPS_ANDROID, idealMax: 30 };
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @deprecated Use {@link getMinTargetFps} / {@link MIN_TARGET_FPS_ANDROID}.
|
|
105
|
+
* Kept as the Android floor so older imports keep a sensible number.
|
|
106
|
+
*/
|
|
107
|
+
export const TARGET_FPS = MIN_TARGET_FPS_ANDROID;
|
|
108
|
+
|
|
109
|
+
/** @deprecated Use {@link minMedianMsForMinTarget}. */
|
|
110
|
+
export const TARGET_MEDIAN_MS = 1000 / MIN_TARGET_FPS_ANDROID;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Critical alert when FPS stays well below the platform minimum target
|
|
114
|
+
* (even on `basic`). Android: <10; iOS: <20.
|
|
115
|
+
*/
|
|
116
|
+
export function getCriticalFpsThreshold(platform: QualityPlatform = currentQualityPlatform()): number {
|
|
117
|
+
if (platform === 'ios') return 20;
|
|
118
|
+
return 10;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** @deprecated Prefer {@link getCriticalFpsThreshold}. */
|
|
122
|
+
export const CRITICAL_FPS_THRESHOLD = 10;
|
|
123
|
+
|
|
124
|
+
/** Consecutive samples below min-target before auto-downgrade. */
|
|
125
|
+
/** Sustained samples below the inference budget before a live downgrade. */
|
|
126
|
+
export const LOW_FPS_STREAK_BEFORE_DOWNGRADE = 4;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Ignore live downgrade decisions for this long after ready / a quality swap
|
|
130
|
+
* (camera restart tanks the first 1 Hz stats samples).
|
|
131
|
+
*/
|
|
132
|
+
export const QUALITY_SETTLE_MS = 5_000;
|
|
133
|
+
|
|
134
|
+
export const QUALITY_PROFILES: Record<QualityProfileId, QualityProfile> = {
|
|
135
|
+
prime: {
|
|
136
|
+
id: 'prime',
|
|
137
|
+
label: 'Prime (1280×720 @30)',
|
|
138
|
+
idealWidth: 1280,
|
|
139
|
+
idealHeight: 720,
|
|
140
|
+
idealFrameRate: 30,
|
|
141
|
+
minStableFps: MIN_TARGET_FPS_ANDROID,
|
|
142
|
+
},
|
|
143
|
+
pro: {
|
|
144
|
+
id: 'pro',
|
|
145
|
+
label: 'Pro (960×540 @30)',
|
|
146
|
+
idealWidth: 960,
|
|
147
|
+
idealHeight: 540,
|
|
148
|
+
idealFrameRate: 30,
|
|
149
|
+
minStableFps: MIN_TARGET_FPS_ANDROID,
|
|
150
|
+
},
|
|
151
|
+
lite: {
|
|
152
|
+
id: 'lite',
|
|
153
|
+
label: 'Lite (640×480 @30)',
|
|
154
|
+
idealWidth: 640,
|
|
155
|
+
idealHeight: 480,
|
|
156
|
+
idealFrameRate: 30,
|
|
157
|
+
minStableFps: MIN_TARGET_FPS_ANDROID,
|
|
158
|
+
},
|
|
159
|
+
ultralite: {
|
|
160
|
+
id: 'ultralite',
|
|
161
|
+
label: 'UltraLite (480×360 @24)',
|
|
162
|
+
idealWidth: 480,
|
|
163
|
+
idealHeight: 360,
|
|
164
|
+
idealFrameRate: 24,
|
|
165
|
+
minStableFps: MIN_TARGET_FPS_ANDROID,
|
|
166
|
+
},
|
|
167
|
+
basic: {
|
|
168
|
+
id: 'basic',
|
|
169
|
+
label: 'Basic (320×240 @20)',
|
|
170
|
+
idealWidth: 320,
|
|
171
|
+
idealHeight: 240,
|
|
172
|
+
idealFrameRate: 20,
|
|
173
|
+
minStableFps: MIN_TARGET_FPS_ANDROID,
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
export function currentQualityPlatform(): QualityPlatform {
|
|
178
|
+
if (Platform.OS === 'ios') return 'ios';
|
|
179
|
+
if (Platform.OS === 'android') return 'android';
|
|
180
|
+
return 'other';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Platform minimum target FPS (floor — not a setpoint). */
|
|
184
|
+
export function getMinTargetFps(platform: QualityPlatform = currentQualityPlatform()): number {
|
|
185
|
+
if (platform === 'ios') return MIN_TARGET_FPS_IOS;
|
|
186
|
+
if (platform === 'android') return ANDROID_MIN_TARGET_FPS;
|
|
187
|
+
return MIN_TARGET_FPS_DEFAULT;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function getIdealFpsRange(platform: QualityPlatform = currentQualityPlatform()): FpsRange {
|
|
191
|
+
if (platform === 'ios') return IDEAL_FPS_RANGE_IOS;
|
|
192
|
+
if (platform === 'android') return IDEAL_FPS_RANGE_ANDROID;
|
|
193
|
+
return { min: MIN_TARGET_FPS_DEFAULT, idealMax: 30 };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Max warm-up median ms that still meets a given min-target FPS. */
|
|
197
|
+
export function minMedianMsForMinTarget(minTargetFps: number): number {
|
|
198
|
+
return 1000 / minTargetFps;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function getQualityProfile(id: QualityProfileId): QualityProfile {
|
|
202
|
+
return QUALITY_PROFILES[id];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function nextLowerQualityProfile(id: QualityProfileId): QualityProfileId | null {
|
|
206
|
+
const idx = QUALITY_LADDER.indexOf(id);
|
|
207
|
+
if (idx < 0 || idx >= QUALITY_LADDER.length - 1) return null;
|
|
208
|
+
return QUALITY_LADDER[idx + 1]!;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function isQualityProfileId(value: string): value is QualityProfileId {
|
|
212
|
+
return Object.prototype.hasOwnProperty.call(QUALITY_PROFILES, value);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function qualityLadderIndex(id: QualityProfileId): number {
|
|
216
|
+
return QUALITY_LADDER.indexOf(id);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Lower or equal quality (higher index) wins. */
|
|
220
|
+
export function lowerQualityProfile(
|
|
221
|
+
a: QualityProfileId,
|
|
222
|
+
b: QualityProfileId,
|
|
223
|
+
): QualityProfileId {
|
|
224
|
+
return qualityLadderIndex(a) >= qualityLadderIndex(b) ? a : b;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Map warm-up execute median (zeros, no camera) → max safe capture profile.
|
|
229
|
+
*
|
|
230
|
+
* Thresholds scale with the platform **minimum target** FPS. Warm-up ignores
|
|
231
|
+
* bitmap cost, so anything over the floor budget must stay on `basic` —
|
|
232
|
+
* the old `≤ 1.3× budget → ultralite` band upgraded Mali devices that were
|
|
233
|
+
* already below the floor (e.g. 120 ms zeros → ultralite → live ~8 FPS).
|
|
234
|
+
*/
|
|
235
|
+
export function profileFromWarmupMedianMs(
|
|
236
|
+
medianMs: number | null,
|
|
237
|
+
minTargetFps: number = getMinTargetFps(),
|
|
238
|
+
): QualityProfileId {
|
|
239
|
+
if (medianMs == null || !Number.isFinite(medianMs)) return 'basic';
|
|
240
|
+
const budget = minMedianMsForMinTarget(minTargetFps);
|
|
241
|
+
// Over the floor on zeros alone → basic only (camera adds more cost).
|
|
242
|
+
if (medianMs > budget) return 'basic';
|
|
243
|
+
// At / under the floor with headroom → higher capture ok
|
|
244
|
+
if (medianMs <= budget * 0.6) return 'prime';
|
|
245
|
+
if (medianMs <= budget * 0.75) return 'pro';
|
|
246
|
+
if (medianMs <= budget * 0.9) return 'lite';
|
|
247
|
+
return 'ultralite';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function estimatedFpsFromMedianMs(medianMs: number | null): number | null {
|
|
251
|
+
if (medianMs == null || !Number.isFinite(medianMs) || medianMs <= 0) return null;
|
|
252
|
+
return 1000 / medianMs;
|
|
253
|
+
}
|