@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,582 @@
1
+ /**
2
+ * WebView inference backend — the SDK's base runtime on BOTH platforms.
3
+ *
4
+ * Runs MoveNet SinglePose Lightning (17 keypoints, 192×192) inside a real
5
+ * browser WebGL context (Chromium ANGLE on Android, WKWebView on iOS) — the
6
+ * same approach the PoseTracker WebView product uses in production, and the
7
+ * only one real-time across the whole Android park in Expo Go. Fully
8
+ * offline: TF.js and the model ship inside the npm package (poseHtml.ts).
9
+ *
10
+ * The WebView owns the camera (`getUserMedia`). Frames are NOT pushed from
11
+ * React Native — `estimatePose()` returns the latest pose posted by the page.
12
+ * Mount {@link WebViewPoseView} (or a 1×1 warmer) to attach the runtime.
13
+ */
14
+
15
+ import type { PoseBackend, PoseBackendInitOptions, PoseInputFrame } from '../PoseBackend';
16
+ import type { KeypointName, Pose } from '../../types/pose';
17
+ import type {
18
+ AccelerationDiagnostics,
19
+ AccelerationState,
20
+ DiagnosticListener,
21
+ } from '../../types/acceleration';
22
+ import type { PoseTrackerEvent } from '../../types/events';
23
+
24
+ export type WebViewPoseMessage =
25
+ | {
26
+ type: 'ready';
27
+ backend: string;
28
+ medianInferenceMs: number | null;
29
+ warmUpRunsMs: number[];
30
+ estimatedFps?: number | null;
31
+ profileId?: string;
32
+ /** false after basic (model-only) cold-start; true once getUserMedia ran. */
33
+ cameraOpened?: boolean;
34
+ coldStart?: 'basic' | 'full';
35
+ gl: { renderer: string | null; vendor: string | null; version: string | null } | null;
36
+ note?: string | null;
37
+ }
38
+ | {
39
+ type: 'warmup_estimate';
40
+ medianInferenceMs: number | null;
41
+ estimatedFps: number | null;
42
+ /** Platform floor (iOS 30 / Android 15). */
43
+ minTargetFps: number;
44
+ /** @deprecated Use {@link minTargetFps}. */
45
+ targetFps?: number;
46
+ profileId: string;
47
+ previousProfileId?: string;
48
+ idealWidth?: number;
49
+ idealHeight?: number;
50
+ gl?: { renderer: string | null; vendor: string | null; version: string | null } | null;
51
+ }
52
+ | {
53
+ type: 'pose';
54
+ keypoints: Array<{ name: string; x: number; y: number; score: number }>;
55
+ score: number;
56
+ inferenceTimeMs: number;
57
+ timestampMs: number;
58
+ }
59
+ | {
60
+ type: 'stats';
61
+ fps: number;
62
+ medianInferenceMs: number | null;
63
+ estimatedFpsFromMedian?: number | null;
64
+ frames: number;
65
+ backend: string;
66
+ breakdown?: {
67
+ drawMs: number | null;
68
+ fromPixelsMs: number | null;
69
+ executeMs: number | null;
70
+ totalPipelineMs?: number | null;
71
+ };
72
+ windowCounts?: { inferred: number; skipped: number };
73
+ experiments?: Record<string, unknown>;
74
+ leverHints?: {
75
+ dominantStage?: string;
76
+ dominantMs?: number;
77
+ bitmapShare?: number;
78
+ fromPixelsShare?: number;
79
+ executeShare?: number;
80
+ hints?: string[];
81
+ } | null;
82
+ videoSize?: string;
83
+ mode?: string;
84
+ }
85
+ | {
86
+ type: 'initialization';
87
+ message: string;
88
+ ready?: boolean;
89
+ step?: 'accessing_webcam' | 'loading_pose_model' | 'ready';
90
+ }
91
+ | { type: 'warning'; message: string; code?: string }
92
+ | { type: 'diag'; message: string }
93
+ | { type: 'error'; message: string; code?: string };
94
+
95
+ export interface WebViewPoseStats {
96
+ fps: number;
97
+ medianInferenceMs: number | null;
98
+ estimatedFpsFromMedian?: number | null;
99
+ frames: number;
100
+ backend: string;
101
+ videoSize?: string;
102
+ mode?: string;
103
+ breakdown?: {
104
+ drawMs: number | null;
105
+ fromPixelsMs: number | null;
106
+ executeMs: number | null;
107
+ totalPipelineMs?: number | null;
108
+ };
109
+ windowCounts?: { inferred: number; skipped: number };
110
+ experiments?: Record<string, unknown>;
111
+ leverHints?: {
112
+ dominantStage?: string;
113
+ dominantMs?: number;
114
+ bitmapShare?: number;
115
+ fromPixelsShare?: number;
116
+ executeShare?: number;
117
+ hints?: string[];
118
+ } | null;
119
+ }
120
+
121
+ export interface WebViewPoseBackendOptions {
122
+ onDiagnostic?: DiagnosticListener;
123
+ onAccelerationChange?: (state: AccelerationState) => void;
124
+ /** Fired on every pose from the WebView (used by WebViewPoseView / ingest). */
125
+ onPose?: (pose: Pose, inferenceTimeMs: number) => void;
126
+ /** Fired when the Chromium page reports ready (model + backend warmed). */
127
+ onReady?: (info: {
128
+ backend: string;
129
+ medianInferenceMs: number | null;
130
+ estimatedFps?: number | null;
131
+ profileId?: string | null;
132
+ glRenderer: string | null;
133
+ }) => void;
134
+ /**
135
+ * Fired after zeros warm-up, typically *before* getUserMedia — used to pick
136
+ * a capture profile targeting ≥15 FPS.
137
+ */
138
+ onWarmupEstimate?: (info: {
139
+ medianInferenceMs: number | null;
140
+ estimatedFps: number | null;
141
+ profileId: string;
142
+ glRenderer: string | null;
143
+ }) => void;
144
+ /** Fired ~1 Hz with inference FPS / breakdown (adaptive quality input). */
145
+ onStats?: (stats: WebViewPoseStats) => void;
146
+ /**
147
+ * Promotes page-level init / warning / error into typed {@link PoseTrackerEvent}s
148
+ * so the host can listen the same way as the classic PoseTracker WebView.
149
+ */
150
+ onTrackerEvent?: (event: PoseTrackerEvent) => void;
151
+ }
152
+
153
+ type ReadyResolver = {
154
+ resolve: () => void;
155
+ reject: (err: Error) => void;
156
+ };
157
+
158
+ export class WebViewPoseBackend implements PoseBackend {
159
+ readonly name = 'webview-movenet';
160
+
161
+ private acceleration: AccelerationState = 'unknown';
162
+ private attached = false;
163
+ private ready = false;
164
+ private warm = false;
165
+ /** True after the page successfully opened getUserMedia. */
166
+ private cameraOpened = false;
167
+ private openCameraHandler: (() => void) | undefined;
168
+ private lastPose: Pose | null = null;
169
+ private inferenceTimesMs: number[] = [];
170
+ private medianInferenceMs: number | null = null;
171
+ private glRenderer: string | null = null;
172
+ private glVendor: string | null = null;
173
+ private glVersion: string | null = null;
174
+ private tfjsBackend: string | null = null;
175
+ private reasons: string[] = [];
176
+ private readyWaiters: ReadyResolver[] = [];
177
+ private cameraWaiters: ReadyResolver[] = [];
178
+ private readonly onDiagnostic: DiagnosticListener | undefined;
179
+ private readonly onAccelerationChange: ((state: AccelerationState) => void) | undefined;
180
+ private onPose: ((pose: Pose, inferenceTimeMs: number) => void) | undefined;
181
+ private onReady: WebViewPoseBackendOptions['onReady'];
182
+ private onWarmupEstimate: WebViewPoseBackendOptions['onWarmupEstimate'];
183
+ private onStats: WebViewPoseBackendOptions['onStats'];
184
+ private onTrackerEvent: WebViewPoseBackendOptions['onTrackerEvent'];
185
+
186
+ constructor(options: WebViewPoseBackendOptions = {}) {
187
+ this.onDiagnostic = options.onDiagnostic;
188
+ this.onAccelerationChange = options.onAccelerationChange;
189
+ this.onPose = options.onPose;
190
+ this.onReady = options.onReady;
191
+ this.onWarmupEstimate = options.onWarmupEstimate;
192
+ this.onStats = options.onStats;
193
+ this.onTrackerEvent = options.onTrackerEvent;
194
+ }
195
+
196
+ /** Host view calls this when the WebView mounts / unmounts. */
197
+ setAttached(attached: boolean): void {
198
+ this.attached = attached;
199
+ this.onDiagnostic?.(
200
+ `[posetracker] WebViewPoseBackend attached=${attached} ready=${this.ready}`,
201
+ );
202
+ if (!attached) {
203
+ this.ready = false;
204
+ this.warm = false;
205
+ this.cameraOpened = false;
206
+ this.openCameraHandler = undefined;
207
+ // Drop stale GPU verdict from a previous warmer/page so the host does
208
+ // not show acceleration=gpu while the next WebView is still booting.
209
+ this.setAcceleration('unknown');
210
+ }
211
+ }
212
+
213
+ setOnPose(handler: ((pose: Pose, inferenceTimeMs: number) => void) | undefined): void {
214
+ this.onPose = handler;
215
+ }
216
+
217
+ setOnReady(handler: WebViewPoseBackendOptions['onReady']): void {
218
+ this.onReady = handler;
219
+ }
220
+
221
+ setOnStats(handler: WebViewPoseBackendOptions['onStats']): void {
222
+ this.onStats = handler;
223
+ }
224
+
225
+ /** Feed a message from the WebView `onMessage` handler. */
226
+ handleMessage(raw: string): void {
227
+ let msg: WebViewPoseMessage;
228
+ try {
229
+ msg = JSON.parse(raw) as WebViewPoseMessage;
230
+ } catch {
231
+ return;
232
+ }
233
+
234
+ if (msg.type === 'warmup_estimate') {
235
+ this.glRenderer = msg.gl?.renderer ?? this.glRenderer;
236
+ this.medianInferenceMs = msg.medianInferenceMs;
237
+ const minTarget = msg.minTargetFps ?? msg.targetFps;
238
+ this.onDiagnostic?.(
239
+ `[posetracker] WebView warmup_estimate fps=${
240
+ msg.estimatedFps != null ? msg.estimatedFps.toFixed(1) : '?'
241
+ } medianMs=${msg.medianInferenceMs != null ? msg.medianInferenceMs.toFixed(1) : '?'} ` +
242
+ `profile=${msg.profileId} minTarget=${minTarget}`,
243
+ );
244
+ this.onWarmupEstimate?.({
245
+ medianInferenceMs: msg.medianInferenceMs,
246
+ estimatedFps: msg.estimatedFps,
247
+ profileId: msg.profileId,
248
+ glRenderer: this.glRenderer,
249
+ });
250
+ return;
251
+ }
252
+
253
+ if (msg.type === 'ready') {
254
+ this.ready = true;
255
+ this.warm = true;
256
+ if (msg.cameraOpened === true) {
257
+ this.cameraOpened = true;
258
+ this.flushCameraWaiters();
259
+ }
260
+ this.tfjsBackend = msg.backend;
261
+ this.medianInferenceMs = msg.medianInferenceMs;
262
+ this.inferenceTimesMs = msg.warmUpRunsMs ?? [];
263
+ this.glRenderer = msg.gl?.renderer ?? null;
264
+ this.glVendor = msg.gl?.vendor ?? null;
265
+ this.glVersion = msg.gl?.version ?? null;
266
+ const med = msg.medianInferenceMs;
267
+ // Chromium WebGL on Android mid-range is typically 15–80 ms — treat as GPU.
268
+ this.setAcceleration(med != null && med > 200 ? 'cpu-fallback' : 'gpu');
269
+ this.onDiagnostic?.(
270
+ `[posetracker] WebView Chromium ready backend=${msg.backend} ` +
271
+ `medianMs=${med != null ? med.toFixed(1) : 'n/a'} ` +
272
+ `estFps=${msg.estimatedFps != null ? msg.estimatedFps.toFixed(1) : 'n/a'} ` +
273
+ `profile=${msg.profileId ?? 'n/a'} coldStart=${msg.coldStart ?? '?'} ` +
274
+ `camera=${msg.cameraOpened === true ? 'on' : 'deferred'} ` +
275
+ `renderer=${this.glRenderer ?? 'n/a'}` +
276
+ (msg.note ? ` note=${msg.note}` : ''),
277
+ );
278
+ this.onReady?.({
279
+ backend: msg.backend,
280
+ medianInferenceMs: med,
281
+ estimatedFps: msg.estimatedFps,
282
+ profileId: msg.profileId,
283
+ glRenderer: this.glRenderer,
284
+ });
285
+ this.onTrackerEvent?.({
286
+ type: 'initialization',
287
+ step: 'ready',
288
+ message: 'running',
289
+ ready: true,
290
+ });
291
+ this.flushReadyWaiters();
292
+ return;
293
+ }
294
+
295
+ if (msg.type === 'initialization') {
296
+ const step = msg.step ?? (msg.ready ? 'ready' : 'loading_pose_model');
297
+ this.onDiagnostic?.(
298
+ `[posetracker] WebView init: ${msg.message} ready=${Boolean(msg.ready)}`,
299
+ );
300
+ this.onTrackerEvent?.({
301
+ type: 'initialization',
302
+ step,
303
+ message: msg.message,
304
+ ready: Boolean(msg.ready),
305
+ });
306
+ return;
307
+ }
308
+
309
+ if (msg.type === 'warning') {
310
+ this.onDiagnostic?.(`[posetracker] WebView warning: ${msg.message}`);
311
+ this.onTrackerEvent?.({
312
+ type: 'warning',
313
+ code: 'webview',
314
+ message: msg.message,
315
+ timestampMs: Date.now(),
316
+ });
317
+ return;
318
+ }
319
+
320
+ if (msg.type === 'error') {
321
+ // Mid-boot quality swaps abort getUserMedia; ignore so warmup can finish.
322
+ if (/abort/i.test(msg.message)) {
323
+ this.onDiagnostic?.(
324
+ `[posetracker] WebView pose error ignored (abort during quality swap): ${msg.message}`,
325
+ );
326
+ return;
327
+ }
328
+ this.reasons.push(msg.message);
329
+ this.onDiagnostic?.(`[posetracker] WebView pose error: ${msg.message}`);
330
+ this.onTrackerEvent?.({
331
+ type: 'error',
332
+ code: 'webview_error',
333
+ message: msg.message,
334
+ });
335
+ const err = new Error(msg.message);
336
+ for (const w of this.readyWaiters) w.reject(err);
337
+ this.readyWaiters = [];
338
+ return;
339
+ }
340
+
341
+ if (msg.type === 'diag') {
342
+ this.onDiagnostic?.(`[posetracker] WebView diag: ${msg.message}`);
343
+ return;
344
+ }
345
+
346
+ if (msg.type === 'stats') {
347
+ this.medianInferenceMs = msg.medianInferenceMs;
348
+ this.tfjsBackend = msg.backend;
349
+ const b = msg.breakdown;
350
+ const fmt = (v: number | null | undefined) => (v != null ? v.toFixed(1) : 'n/a');
351
+ const wc = msg.windowCounts;
352
+ const lh = msg.leverHints;
353
+ this.onDiagnostic?.(
354
+ `[posetracker] WebView stats fps=${msg.fps} medianMs=${
355
+ msg.medianInferenceMs != null ? Math.round(msg.medianInferenceMs) : 'n/a'
356
+ }` +
357
+ (msg.estimatedFpsFromMedian != null
358
+ ? ` estFps=${msg.estimatedFpsFromMedian.toFixed(1)}`
359
+ : '') +
360
+ ` backend=${msg.backend}` +
361
+ (msg.mode ? ` mode=${msg.mode}` : '') +
362
+ (msg.videoSize ? ` video=${msg.videoSize}` : '') +
363
+ (b
364
+ ? ` bitmap=${fmt(b.drawMs)} fromPixels=${fmt(b.fromPixelsMs)} execute=${fmt(b.executeMs)}` +
365
+ (b.totalPipelineMs != null ? ` total=${fmt(b.totalPipelineMs)}` : '')
366
+ : '') +
367
+ (wc ? ` inferred/skipped=${wc.inferred}/${wc.skipped}` : ''),
368
+ );
369
+ if (lh?.hints?.length) {
370
+ this.onDiagnostic?.(
371
+ `[posetracker] WebView leverHints dominant=${lh.dominantStage ?? '?'} ` +
372
+ `shares bmp=${lh.bitmapShare ?? '?'} px=${lh.fromPixelsShare ?? '?'} ex=${lh.executeShare ?? '?'} ` +
373
+ `→ ${lh.hints.join(' | ')}`,
374
+ );
375
+ }
376
+ this.onStats?.({
377
+ fps: msg.fps,
378
+ medianInferenceMs: msg.medianInferenceMs,
379
+ estimatedFpsFromMedian: msg.estimatedFpsFromMedian,
380
+ frames: msg.frames,
381
+ backend: msg.backend,
382
+ videoSize: msg.videoSize,
383
+ mode: msg.mode,
384
+ breakdown: msg.breakdown,
385
+ windowCounts: msg.windowCounts,
386
+ experiments: msg.experiments,
387
+ leverHints: msg.leverHints,
388
+ });
389
+ return;
390
+ }
391
+
392
+ if (msg.type === 'pose') {
393
+ const pose: Pose = {
394
+ keypoints: msg.keypoints.map((k) => ({
395
+ name: k.name as KeypointName,
396
+ x: k.x,
397
+ y: k.y,
398
+ score: k.score,
399
+ })),
400
+ score: msg.score,
401
+ timestampMs: msg.timestampMs,
402
+ };
403
+ this.lastPose = pose;
404
+ this.onPose?.(pose, msg.inferenceTimeMs);
405
+ }
406
+ }
407
+
408
+ async init(_options: PoseBackendInitOptions): Promise<void> {
409
+ this.onDiagnostic?.(
410
+ '[posetracker] WebViewPoseBackend.init — waiting for Chromium WebView to mount (WebViewPoseView)',
411
+ );
412
+ // Actual model load happens inside the HTML page once the view attaches.
413
+ }
414
+
415
+ async warmup(): Promise<void> {
416
+ if (this.warm) return;
417
+ this.onDiagnostic?.(
418
+ '[posetracker] WebViewPoseBackend.warmup — awaiting page ready (model cold-start)…',
419
+ );
420
+ await this.waitUntilReady(60_000);
421
+ }
422
+
423
+ /** Whether getUserMedia has already run in the attached page. */
424
+ isCameraOpened(): boolean {
425
+ return this.cameraOpened;
426
+ }
427
+
428
+ /**
429
+ * Host injects `__PT_OPEN_CAMERA` via {@link setOpenCameraHandler}.
430
+ * Used when upgrading basic → full cold-start without remounting the page.
431
+ */
432
+ setOpenCameraHandler(handler: (() => void) | undefined): void {
433
+ this.openCameraHandler = handler;
434
+ }
435
+
436
+ /**
437
+ * Open getUserMedia after a basic (model-only) ready. No-op if the page
438
+ * already opened the camera (coldStart=full boot).
439
+ */
440
+ async openCamera(): Promise<void> {
441
+ if (this.cameraOpened) return;
442
+ if (!this.ready) {
443
+ await this.waitUntilReady(60_000);
444
+ }
445
+ if (this.cameraOpened) return;
446
+ if (!this.openCameraHandler) {
447
+ throw new Error(
448
+ 'WebViewPoseBackend.openCamera: no WebViewPoseView attached to inject __PT_OPEN_CAMERA.',
449
+ );
450
+ }
451
+ this.onDiagnostic?.('[posetracker] WebViewPoseBackend.openCamera — requesting getUserMedia…');
452
+ this.openCameraHandler();
453
+ await this.waitUntilCameraOpened(30_000);
454
+ }
455
+
456
+ async estimatePose(_frame: PoseInputFrame): Promise<Pose | null> {
457
+ // Camera lives inside the WebView; return last pose for API compatibility.
458
+ return this.lastPose;
459
+ }
460
+
461
+ getAcceleration(): AccelerationDiagnostics | null {
462
+ if (this.acceleration === 'unknown' && !this.ready) {
463
+ return null;
464
+ }
465
+ return {
466
+ state: this.acceleration,
467
+ tfjsBackend: this.tfjsBackend,
468
+ runtime: 'webview',
469
+ medianInferenceMs: this.medianInferenceMs,
470
+ inferenceTimesMs: [...this.inferenceTimesMs],
471
+ maxAcceptableInferenceMs: 200,
472
+ capabilities: this.glRenderer
473
+ ? {
474
+ glVersion: this.glVersion,
475
+ renderer: this.glRenderer,
476
+ vendor: this.glVendor,
477
+ maxTextureSize: null,
478
+ colorBufferFloat: true,
479
+ colorBufferHalfFloat: true,
480
+ textureFloat: true,
481
+ textureHalfFloat: true,
482
+ extensionQueriesShimmed: false,
483
+ }
484
+ : null,
485
+ flags: {
486
+ WEBVIEW_CHROMIUM: true,
487
+ note:
488
+ this.tfjsBackend === 'wasm'
489
+ ? 'Inference runs in WebView WASM (slow fallback)'
490
+ : 'Inference runs in WebView main-thread WebGL MoveNet (camera capped, createImageBitmap 192)',
491
+ },
492
+ contextLossCount: 0,
493
+ reasons: [
494
+ ...this.reasons,
495
+ 'backend=webview-movenet (Chromium getUserMedia + TF.js WebGL MoveNet Lightning)',
496
+ ],
497
+ };
498
+ }
499
+
500
+ async dispose(): Promise<void> {
501
+ this.ready = false;
502
+ this.warm = false;
503
+ this.cameraOpened = false;
504
+ this.attached = false;
505
+ this.openCameraHandler = undefined;
506
+ this.lastPose = null;
507
+ this.acceleration = 'unknown';
508
+ this.readyWaiters = [];
509
+ this.cameraWaiters = [];
510
+ }
511
+
512
+ private waitUntilReady(timeoutMs: number): Promise<void> {
513
+ if (this.ready) return Promise.resolve();
514
+ return new Promise((resolve, reject) => {
515
+ const timer = setTimeout(() => {
516
+ reject(
517
+ new Error(
518
+ 'WebViewPoseBackend: timed out waiting for Chromium page ready. ' +
519
+ 'Mount <WebViewPoseView coldStart="basic" /> (or full) so MoveNet can warm up.',
520
+ ),
521
+ );
522
+ }, timeoutMs);
523
+ this.readyWaiters.push({
524
+ resolve: () => {
525
+ clearTimeout(timer);
526
+ resolve();
527
+ },
528
+ reject: (err) => {
529
+ clearTimeout(timer);
530
+ reject(err);
531
+ },
532
+ });
533
+ });
534
+ }
535
+
536
+ private waitUntilCameraOpened(timeoutMs: number): Promise<void> {
537
+ if (this.cameraOpened) return Promise.resolve();
538
+ return new Promise((resolve, reject) => {
539
+ const timer = setTimeout(() => {
540
+ reject(
541
+ new Error(
542
+ 'WebViewPoseBackend: timed out waiting for getUserMedia. ' +
543
+ 'Ensure the user grants camera permission when prompted.',
544
+ ),
545
+ );
546
+ }, timeoutMs);
547
+ this.cameraWaiters.push({
548
+ resolve: () => {
549
+ clearTimeout(timer);
550
+ resolve();
551
+ },
552
+ reject: (err) => {
553
+ clearTimeout(timer);
554
+ reject(err);
555
+ },
556
+ });
557
+ });
558
+ }
559
+
560
+ private flushReadyWaiters(): void {
561
+ const waiters = this.readyWaiters;
562
+ this.readyWaiters = [];
563
+ for (const w of waiters) w.resolve();
564
+ }
565
+
566
+ private flushCameraWaiters(): void {
567
+ const waiters = this.cameraWaiters;
568
+ this.cameraWaiters = [];
569
+ for (const w of waiters) w.resolve();
570
+ }
571
+
572
+ private setAcceleration(state: AccelerationState): void {
573
+ if (this.acceleration !== state) {
574
+ this.acceleration = state;
575
+ this.onAccelerationChange?.(state);
576
+ }
577
+ }
578
+ }
579
+
580
+ export function isWebViewPoseBackend(backend: PoseBackend): backend is WebViewPoseBackend {
581
+ return backend.name === 'webview-movenet';
582
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * PoseTracker join logo (icon + wordmark) for WebView overlays.
3
+ * Generated from assets/PoseTracker_logo.png — do not edit by hand.
4
+ */
5
+ export const POSETRACKER_LOGO_DATA_URL = "data:image/png;base64,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";