@pose-tracker/react-native-pose-estimation-light 0.3.5 → 0.3.6
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/README.md +47 -0
- package/lib/PoseTrackerProvider.d.ts +17 -1
- package/lib/PoseTrackerProvider.js +36 -2
- package/lib/backends/webview/WebViewPoseBackend.d.ts +30 -3
- package/lib/backends/webview/WebViewPoseBackend.js +79 -3
- package/lib/backends/webview/poseRuntimeSource.js +2 -2
- package/lib/camera/WebViewPoseView.js +5 -0
- package/lib/client.d.ts +29 -3
- package/lib/client.js +76 -3
- package/lib/index.d.ts +2 -1
- package/lib/index.js +1 -0
- package/lib/sdkVersion.d.ts +1 -1
- package/lib/sdkVersion.js +1 -1
- package/lib/types/externalFrame.d.ts +41 -0
- package/lib/types/externalFrame.js +16 -0
- package/package.json +2 -1
- package/src/PoseTrackerProvider.tsx +74 -3
- package/src/backends/webview/WebViewPoseBackend.ts +100 -3
- package/src/camera/WebViewPoseView.tsx +7 -0
- package/src/client.ts +98 -4
- package/src/index.ts +6 -1
- package/src/sdkVersion.ts +1 -1
- package/src/types/externalFrame.ts +53 -0
package/README.md
CHANGED
|
@@ -133,6 +133,50 @@ Same contract as the offline SDK / web tracking URL:
|
|
|
133
133
|
</PoseTrackerProvider>
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
+
## External frames (your camera, our data)
|
|
137
|
+
|
|
138
|
+
Full guide, including Vision Camera and expo-camera: https://docs.posetracker.com/external-frames
|
|
139
|
+
|
|
140
|
+
Optional. Use it when your app already owns the camera (VisionCamera, a
|
|
141
|
+
custom pipeline, a recorded file) and you only need the data. PoseTracker
|
|
142
|
+
runs pose estimation and the exercise engine on every frame you push, and
|
|
143
|
+
returns the same events as the camera flow: keypoints, posture/placement,
|
|
144
|
+
counter, form score, progression. **Nothing is drawn**, so you render what you want.
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
const { warmupExternal, startExercise, processFrame } = usePoseTracker({
|
|
148
|
+
onCounter: (e) => setReps(e.count), // listeners still fire
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
await warmupExternal(); // no camera permission, no getUserMedia
|
|
152
|
+
startExercise('squat');
|
|
153
|
+
|
|
154
|
+
// For each frame from your camera:
|
|
155
|
+
const { dropped, pose, events } = await processFrame({
|
|
156
|
+
base64: jpegBase64, // or uri: 'file:///…' / 'data:image/jpeg;base64,…'
|
|
157
|
+
width: 256,
|
|
158
|
+
height: 192,
|
|
159
|
+
timestampMs: Date.now(),
|
|
160
|
+
mirrored: true, // front camera (default). false for the back camera.
|
|
161
|
+
});
|
|
162
|
+
if (!dropped) {
|
|
163
|
+
// pose.keypoints are normalized (0..1) to the frame you sent
|
|
164
|
+
// events: everything emitted for this frame (counter, posture, …)
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
- The Provider mounts a hidden 1×1 warmer after `warmupExternal()`. It is not
|
|
169
|
+
needed if a `<WebViewPoseView />` is already on screen. The light package
|
|
170
|
+
still needs network for the model.
|
|
171
|
+
- One frame in flight at a time: an overlapping call resolves at once with
|
|
172
|
+
`{ dropped: true, pose: lastPose, events: [] }`. Await before pushing the next one.
|
|
173
|
+
- Keep the longest side of each frame at about 256 px or less. A 1080p JPEG per
|
|
174
|
+
frame is not real-time over the bridge.
|
|
175
|
+
- The engine is temporal: push frames in order and keep the same session
|
|
176
|
+
across frames.
|
|
177
|
+
- Don't mix the two flows. `processFrame` throws while the SDK camera is open.
|
|
178
|
+
- `processFrame` before `warmupExternal()` throws.
|
|
179
|
+
|
|
136
180
|
## Cold-start
|
|
137
181
|
|
|
138
182
|
| Mode | API | Camera permission |
|
|
@@ -169,6 +213,9 @@ offline MoveNet.
|
|
|
169
213
|
**Who sees the watermark?**
|
|
170
214
|
Keyless and free plans. Hidden for paid plans.
|
|
171
215
|
|
|
216
|
+
**How does Claude or Cursor integrate this SDK?**
|
|
217
|
+
Add the connector `https://mcp.posetracker.com/api/mcp`, sign in, and click Allow at `https://app.posetracker.com/oauth/authorize`. The agent writes the snippet on your account. Docs: https://docs.posetracker.com/ai/mcp-connector.
|
|
218
|
+
|
|
172
219
|
## License
|
|
173
220
|
|
|
174
221
|
**Proprietary** — Movelytics SAS / PoseTracker. See [`LICENSE`](./LICENSE).
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import React from 'react';
|
|
18
18
|
import { PoseTrackerClient, type PoseTrackerClientOptions, type StartExerciseOptions } from './client';
|
|
19
19
|
import type { PoseInputFrame } from './backends/PoseBackend';
|
|
20
|
+
import type { ExternalFrame, ExternalFrameResult } from './types/externalFrame';
|
|
20
21
|
import type { ExerciseConfig, SdkManifest } from './types/manifest';
|
|
21
22
|
import type { PreloadOptions } from './types/preload';
|
|
22
23
|
import type { ErrorEvent, PoseTrackerCallbacks, PoseTrackerEventListener, PoseTrackerMode, PoseTrackerStatus } from './types/events';
|
|
@@ -72,7 +73,18 @@ export interface PoseTrackerContextValue {
|
|
|
72
73
|
startExercise: (exerciseId: string, options?: StartExerciseOptions) => void;
|
|
73
74
|
stopExercise: () => void;
|
|
74
75
|
estimatePose: (frame: PoseInputFrame) => Promise<Pose | null>;
|
|
75
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Pass `{ base64 | uri, width, height, timestampMs }` from your own camera
|
|
78
|
+
* after {@link warmupExternal}: resolves `{ dropped, pose, events }`.
|
|
79
|
+
* Any other frame keeps the original pose + engine behaviour.
|
|
80
|
+
*/
|
|
81
|
+
processFrame: ProcessFrameFn;
|
|
82
|
+
/**
|
|
83
|
+
* Opt-in: load the model for frames from your own camera. Mounts a hidden
|
|
84
|
+
* 1×1 basic WebView (no camera, nothing drawn). Not needed with
|
|
85
|
+
* `WebViewPoseView`.
|
|
86
|
+
*/
|
|
87
|
+
warmupExternal: () => Promise<void>;
|
|
76
88
|
addEventListener: (listener: PoseTrackerEventListener) => () => void;
|
|
77
89
|
/**
|
|
78
90
|
* Classic PoseTracker WebView JSON stream (`sendDataToNative` shape).
|
|
@@ -80,6 +92,10 @@ export interface PoseTrackerContextValue {
|
|
|
80
92
|
*/
|
|
81
93
|
addMessageListener: (listener: (message: import('./events/classicMessage').ClassicNativeMessage) => void) => () => void;
|
|
82
94
|
}
|
|
95
|
+
export interface ProcessFrameFn {
|
|
96
|
+
(frame: ExternalFrame): Promise<ExternalFrameResult>;
|
|
97
|
+
(frame: PoseInputFrame): Promise<Pose | null>;
|
|
98
|
+
}
|
|
83
99
|
export interface PoseTrackerProviderProps {
|
|
84
100
|
/** Optional: without a token the SDK runs in keypoints-only mode. */
|
|
85
101
|
apiToken?: string;
|
|
@@ -20,7 +20,9 @@ const jsx_runtime_1 = require("react/jsx-runtime");
|
|
|
20
20
|
* full-engine without restarting the camera pipeline.
|
|
21
21
|
*/
|
|
22
22
|
const react_1 = require("react");
|
|
23
|
+
const react_native_1 = require("react-native");
|
|
23
24
|
const client_1 = require("./client");
|
|
25
|
+
const WebViewPoseBackend_1 = require("./backends/webview/WebViewPoseBackend");
|
|
24
26
|
const PoseTrackerContext = (0, react_1.createContext)(null);
|
|
25
27
|
function PoseTrackerProvider({ apiToken, engine, options, autoPreload = false, children, }) {
|
|
26
28
|
const clientRef = (0, react_1.useRef)(null);
|
|
@@ -39,8 +41,22 @@ function PoseTrackerProvider({ apiToken, engine, options, autoPreload = false, c
|
|
|
39
41
|
const [acceleration, setAcceleration] = (0, react_1.useState)(client.getAcceleration());
|
|
40
42
|
const [accelerationDiagnostics, setAccelerationDiagnostics] = (0, react_1.useState)(client.getAccelerationDiagnostics());
|
|
41
43
|
const [quality, setQuality] = (0, react_1.useState)(client.getQualityState());
|
|
44
|
+
/** Hidden warmer for external frames; false unless warmupExternal() ran. */
|
|
45
|
+
const [mountExternalWarmer, setMountExternalWarmer] = (0, react_1.useState)(false);
|
|
42
46
|
(0, react_1.useEffect)(() => {
|
|
43
47
|
const offState = client.onStateChange(() => {
|
|
48
|
+
if (client.isExternalWarmerRequested()) {
|
|
49
|
+
setMountExternalWarmer((mounted) => {
|
|
50
|
+
if (mounted)
|
|
51
|
+
return true;
|
|
52
|
+
const backend = client.getBackend();
|
|
53
|
+
// Reuse a WebViewPoseView the host already mounted.
|
|
54
|
+
return backend instanceof WebViewPoseBackend_1.WebViewPoseBackend && !backend.isAttached();
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
setMountExternalWarmer(false);
|
|
59
|
+
}
|
|
44
60
|
setStatus(client.getStatus());
|
|
45
61
|
setMode(client.getMode());
|
|
46
62
|
setError(client.getError());
|
|
@@ -69,7 +85,8 @@ function PoseTrackerProvider({ apiToken, engine, options, autoPreload = false, c
|
|
|
69
85
|
const startExercise = (0, react_1.useCallback)((id, exerciseOptions) => client.startExercise(id, exerciseOptions), [client]);
|
|
70
86
|
const stopExercise = (0, react_1.useCallback)(() => client.stopExercise(), [client]);
|
|
71
87
|
const estimatePose = (0, react_1.useCallback)((frame) => client.estimatePose(frame), [client]);
|
|
72
|
-
const processFrame = (0, react_1.useCallback)((frame) => client.processFrame(frame), [client]);
|
|
88
|
+
const processFrame = (0, react_1.useCallback)(((frame) => client.processFrame(frame)), [client]);
|
|
89
|
+
const warmupExternal = (0, react_1.useCallback)(() => client.warmupExternal(), [client]);
|
|
73
90
|
const addEventListener = (0, react_1.useCallback)((listener) => client.addEventListener(listener), [client]);
|
|
74
91
|
const addMessageListener = (0, react_1.useCallback)((listener) => client.addMessageListener(listener), [client]);
|
|
75
92
|
const value = (0, react_1.useMemo)(() => ({
|
|
@@ -89,6 +106,7 @@ function PoseTrackerProvider({ apiToken, engine, options, autoPreload = false, c
|
|
|
89
106
|
stopExercise,
|
|
90
107
|
estimatePose,
|
|
91
108
|
processFrame,
|
|
109
|
+
warmupExternal,
|
|
92
110
|
addEventListener,
|
|
93
111
|
addMessageListener,
|
|
94
112
|
}), [
|
|
@@ -107,11 +125,27 @@ function PoseTrackerProvider({ apiToken, engine, options, autoPreload = false, c
|
|
|
107
125
|
stopExercise,
|
|
108
126
|
estimatePose,
|
|
109
127
|
processFrame,
|
|
128
|
+
warmupExternal,
|
|
110
129
|
addEventListener,
|
|
111
130
|
addMessageListener,
|
|
112
131
|
]);
|
|
113
|
-
return (0, jsx_runtime_1.
|
|
132
|
+
return ((0, jsx_runtime_1.jsxs)(PoseTrackerContext.Provider, { value: value, children: [children, mountExternalWarmer ? (0, jsx_runtime_1.jsx)(ExternalFramesWarmer, {}) : null] }));
|
|
133
|
+
}
|
|
134
|
+
/** Hidden basic page for external frames: no camera, nothing drawn. */
|
|
135
|
+
function ExternalFramesWarmer() {
|
|
136
|
+
// Required lazily: WebViewPoseView itself imports this module.
|
|
137
|
+
const { WebViewPoseView } = require('./camera/WebViewPoseView');
|
|
138
|
+
return ((0, jsx_runtime_1.jsx)(react_native_1.View, { pointerEvents: "none", style: styles.externalWarmer, children: (0, jsx_runtime_1.jsx)(WebViewPoseView, { coldStart: "basic", drawSkeleton: false, drawPlacementBox: false, showWatermark: false }) }));
|
|
114
139
|
}
|
|
140
|
+
const styles = react_native_1.StyleSheet.create({
|
|
141
|
+
externalWarmer: {
|
|
142
|
+
position: 'absolute',
|
|
143
|
+
width: 1,
|
|
144
|
+
height: 1,
|
|
145
|
+
opacity: 0,
|
|
146
|
+
overflow: 'hidden',
|
|
147
|
+
},
|
|
148
|
+
});
|
|
115
149
|
/**
|
|
116
150
|
* Access the PoseTracker pipeline and subscribe to typed events.
|
|
117
151
|
*
|
|
@@ -7,14 +7,16 @@
|
|
|
7
7
|
* only one real-time across the whole Android park in Expo Go. Fully
|
|
8
8
|
* offline: TF.js and the model ship inside the npm package (poseHtml.ts).
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* By default the WebView owns the camera (`getUserMedia`) and
|
|
11
|
+
* `estimatePose()` returns the latest pose posted by the page. Mount
|
|
12
|
+
* {@link WebViewPoseView} (or a 1×1 warmer) to attach the runtime.
|
|
13
|
+
* Opt-in: {@link pushFrame} infers host-provided frames on a basic warmer.
|
|
13
14
|
*/
|
|
14
15
|
import type { PoseBackend, PoseBackendInitOptions, PoseInputFrame } from '../PoseBackend';
|
|
15
16
|
import type { Pose } from '../../types/pose';
|
|
16
17
|
import type { AccelerationDiagnostics, AccelerationState, DiagnosticListener } from '../../types/acceleration';
|
|
17
18
|
import type { PoseTrackerEvent } from '../../types/events';
|
|
19
|
+
import type { ExternalFrame } from '../../types/externalFrame';
|
|
18
20
|
export type WebViewPoseMessage = {
|
|
19
21
|
type: 'ready';
|
|
20
22
|
backend: string;
|
|
@@ -59,6 +61,13 @@ export type WebViewPoseMessage = {
|
|
|
59
61
|
score: number;
|
|
60
62
|
inferenceTimeMs: number;
|
|
61
63
|
timestampMs: number;
|
|
64
|
+
/** Set when the pose answers a pushed external frame. */
|
|
65
|
+
frameId?: number;
|
|
66
|
+
} | {
|
|
67
|
+
type: 'frame_result';
|
|
68
|
+
id: number;
|
|
69
|
+
dropped: boolean;
|
|
70
|
+
error?: string;
|
|
62
71
|
} | {
|
|
63
72
|
type: 'stats';
|
|
64
73
|
fps: number;
|
|
@@ -172,6 +181,9 @@ export declare class WebViewPoseBackend implements PoseBackend {
|
|
|
172
181
|
/** True after the page successfully opened getUserMedia. */
|
|
173
182
|
private cameraOpened;
|
|
174
183
|
private openCameraHandler;
|
|
184
|
+
private pushFrameHandler;
|
|
185
|
+
private frameSeq;
|
|
186
|
+
private readonly pendingFrames;
|
|
175
187
|
private lastPose;
|
|
176
188
|
private inferenceTimesMs;
|
|
177
189
|
private medianInferenceMs;
|
|
@@ -192,11 +204,26 @@ export declare class WebViewPoseBackend implements PoseBackend {
|
|
|
192
204
|
constructor(options?: WebViewPoseBackendOptions);
|
|
193
205
|
/** Host view calls this when the WebView mounts / unmounts. */
|
|
194
206
|
setAttached(attached: boolean): void;
|
|
207
|
+
isAttached(): boolean;
|
|
208
|
+
/** Model warm in the attached page (basic or full cold-start). */
|
|
209
|
+
isWarm(): boolean;
|
|
210
|
+
getLastPose(): Pose | null;
|
|
195
211
|
setOnPose(handler: ((pose: Pose, inferenceTimeMs: number) => void) | undefined): void;
|
|
196
212
|
setOnReady(handler: WebViewPoseBackendOptions['onReady']): void;
|
|
197
213
|
setOnStats(handler: WebViewPoseBackendOptions['onStats']): void;
|
|
198
214
|
/** Feed a message from the WebView `onMessage` handler. */
|
|
199
215
|
handleMessage(raw: string): void;
|
|
216
|
+
/** Host view injects `__PT_PUSH_FRAME` with the JSON payload. */
|
|
217
|
+
setPushFrameHandler(handler: ((payload: string) => void) | undefined): void;
|
|
218
|
+
/**
|
|
219
|
+
* Infer one host-provided frame in the attached page. Resolves after the
|
|
220
|
+
* page posted the pose (already forwarded to `onPose`, hence the engine).
|
|
221
|
+
*/
|
|
222
|
+
pushFrame(frame: ExternalFrame): Promise<{
|
|
223
|
+
dropped: boolean;
|
|
224
|
+
pose: Pose | null;
|
|
225
|
+
}>;
|
|
226
|
+
private rejectPendingFrames;
|
|
200
227
|
init(_options: PoseBackendInitOptions): Promise<void>;
|
|
201
228
|
warmup(): Promise<void>;
|
|
202
229
|
/** Whether getUserMedia has already run in the attached page. */
|
|
@@ -8,13 +8,15 @@
|
|
|
8
8
|
* only one real-time across the whole Android park in Expo Go. Fully
|
|
9
9
|
* offline: TF.js and the model ship inside the npm package (poseHtml.ts).
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* By default the WebView owns the camera (`getUserMedia`) and
|
|
12
|
+
* `estimatePose()` returns the latest pose posted by the page. Mount
|
|
13
|
+
* {@link WebViewPoseView} (or a 1×1 warmer) to attach the runtime.
|
|
14
|
+
* Opt-in: {@link pushFrame} infers host-provided frames on a basic warmer.
|
|
14
15
|
*/
|
|
15
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
17
|
exports.WebViewPoseBackend = void 0;
|
|
17
18
|
exports.isWebViewPoseBackend = isWebViewPoseBackend;
|
|
19
|
+
const PUSH_FRAME_TIMEOUT_MS = 10000;
|
|
18
20
|
class WebViewPoseBackend {
|
|
19
21
|
constructor(options = {}) {
|
|
20
22
|
this.name = 'webview-movenet';
|
|
@@ -24,6 +26,8 @@ class WebViewPoseBackend {
|
|
|
24
26
|
this.warm = false;
|
|
25
27
|
/** True after the page successfully opened getUserMedia. */
|
|
26
28
|
this.cameraOpened = false;
|
|
29
|
+
this.frameSeq = 0;
|
|
30
|
+
this.pendingFrames = new Map();
|
|
27
31
|
this.lastPose = null;
|
|
28
32
|
this.inferenceTimesMs = [];
|
|
29
33
|
this.medianInferenceMs = null;
|
|
@@ -54,8 +58,20 @@ class WebViewPoseBackend {
|
|
|
54
58
|
// Drop stale GPU verdict from a previous warmer/page so the host does
|
|
55
59
|
// not show acceleration=gpu while the next WebView is still booting.
|
|
56
60
|
this.setAcceleration('unknown');
|
|
61
|
+
this.pushFrameHandler = undefined;
|
|
62
|
+
this.rejectPendingFrames('WebView detached while a frame was in flight.');
|
|
57
63
|
}
|
|
58
64
|
}
|
|
65
|
+
isAttached() {
|
|
66
|
+
return this.attached;
|
|
67
|
+
}
|
|
68
|
+
/** Model warm in the attached page (basic or full cold-start). */
|
|
69
|
+
isWarm() {
|
|
70
|
+
return this.warm;
|
|
71
|
+
}
|
|
72
|
+
getLastPose() {
|
|
73
|
+
return this.lastPose;
|
|
74
|
+
}
|
|
59
75
|
setOnPose(handler) {
|
|
60
76
|
this.onPose = handler;
|
|
61
77
|
}
|
|
@@ -222,8 +238,66 @@ class WebViewPoseBackend {
|
|
|
222
238
|
timestampMs: msg.timestampMs,
|
|
223
239
|
};
|
|
224
240
|
this.lastPose = pose;
|
|
241
|
+
if (msg.frameId != null) {
|
|
242
|
+
const pending = this.pendingFrames.get(msg.frameId);
|
|
243
|
+
if (pending)
|
|
244
|
+
pending.pose = pose;
|
|
245
|
+
}
|
|
225
246
|
this.onPose?.(pose, msg.inferenceTimeMs);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (msg.type === 'frame_result') {
|
|
250
|
+
const pending = this.pendingFrames.get(msg.id);
|
|
251
|
+
if (!pending)
|
|
252
|
+
return;
|
|
253
|
+
this.pendingFrames.delete(msg.id);
|
|
254
|
+
clearTimeout(pending.timer);
|
|
255
|
+
if (msg.error) {
|
|
256
|
+
pending.reject(new Error(`processFrame: ${msg.error}`));
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
pending.resolve({ dropped: msg.dropped === true, pose: pending.pose });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/** Host view injects `__PT_PUSH_FRAME` with the JSON payload. */
|
|
264
|
+
setPushFrameHandler(handler) {
|
|
265
|
+
this.pushFrameHandler = handler;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Infer one host-provided frame in the attached page. Resolves after the
|
|
269
|
+
* page posted the pose (already forwarded to `onPose`, hence the engine).
|
|
270
|
+
*/
|
|
271
|
+
pushFrame(frame) {
|
|
272
|
+
const handler = this.pushFrameHandler;
|
|
273
|
+
if (!handler) {
|
|
274
|
+
return Promise.reject(new Error('processFrame: no WebViewPoseView is attached. Call warmupExternal() first.'));
|
|
275
|
+
}
|
|
276
|
+
const id = ++this.frameSeq;
|
|
277
|
+
return new Promise((resolve, reject) => {
|
|
278
|
+
const timer = setTimeout(() => {
|
|
279
|
+
this.pendingFrames.delete(id);
|
|
280
|
+
reject(new Error('processFrame: timed out waiting for the pose runtime.'));
|
|
281
|
+
}, PUSH_FRAME_TIMEOUT_MS);
|
|
282
|
+
this.pendingFrames.set(id, { pose: null, resolve, reject, timer });
|
|
283
|
+
handler(JSON.stringify({
|
|
284
|
+
id,
|
|
285
|
+
base64: frame.base64 ?? null,
|
|
286
|
+
uri: frame.uri ?? null,
|
|
287
|
+
mime: frame.mime ?? null,
|
|
288
|
+
width: frame.width,
|
|
289
|
+
height: frame.height,
|
|
290
|
+
timestampMs: frame.timestampMs,
|
|
291
|
+
mirrored: frame.mirrored !== false,
|
|
292
|
+
}));
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
rejectPendingFrames(message) {
|
|
296
|
+
for (const pending of this.pendingFrames.values()) {
|
|
297
|
+
clearTimeout(pending.timer);
|
|
298
|
+
pending.reject(new Error(`processFrame: ${message}`));
|
|
226
299
|
}
|
|
300
|
+
this.pendingFrames.clear();
|
|
227
301
|
}
|
|
228
302
|
async init(_options) {
|
|
229
303
|
this.onDiagnostic?.('[posetracker] WebViewPoseBackend.init — waiting for Chromium WebView to mount (WebViewPoseView)');
|
|
@@ -312,6 +386,8 @@ class WebViewPoseBackend {
|
|
|
312
386
|
this.cameraOpened = false;
|
|
313
387
|
this.attached = false;
|
|
314
388
|
this.openCameraHandler = undefined;
|
|
389
|
+
this.pushFrameHandler = undefined;
|
|
390
|
+
this.rejectPendingFrames('client disposed.');
|
|
315
391
|
this.lastPose = null;
|
|
316
392
|
this.acceleration = 'unknown';
|
|
317
393
|
this.readyWaiters = [];
|