WebViewPoseView on React Native and PoseCamera on the web still open the camera for you.
A still image or a video file is a different API. Use Media sources (
source="image" / setSource). External frames are a live pump: you send the next image only after the previous call resolves.- Call
warmupExternal()once, before the first frame. - Call
startExercise()once for the movement, not on every frame. - Keep the longest side at 256 pixels or less.
- Put the capture clock in
timestampMs, not the time the promise resolves.
Contract
mirrored defaults to true. mime defaults to image/jpeg.
events is only what this frame produced (keypoints, posture, counter, …), in emission order. The same events are delivered to usePoseTracker / on() before the promise resolves. A dropped frame returns { dropped: true, pose: lastPose, events: [] } and emits nothing new.
With an API key, warmupExternal() counts one session, the same way opening the SDK camera does. Later frames are not counted again. Calling startExercise('plank') after startExercise('squat') while you keep sending frames is still the same call: placement and counter restart, nothing is re-counted. A new client (or a new warmupExternal() session after teardown) counts a new call.
Coordinates
Keypoints are normalized 0–1 on the image you sent, origin top-left, after the mirror flag.object-fit: cover, Vision Camera’s default crop), map through that crop. Do not assume the view size is the frame size.
timestampMs is the capture time. The exercise engine uses it for rep timing. Pass the clock at capture, not the time processFrame resolves.
Rules
1
Warm up once
await warmupExternal() before any processFrame. A second call is a no-op. On React Native the Provider mounts a hidden 1×1 page for this (coldStart="basic"). You do not add that view yourself. If a WebViewPoseView is already on screen, that page is reused.2
Start the exercise once
startExercise('squat') uses the same engine as the camera path. Frame N+1 continues that squat. Do not call it again per frame. stopExercise() ends the session.3
Send one small frame at a time
Await the promise, then send the next image. An overlapping call is dropped. Keep the longest side at 256 px or less.
processFrame throws while the SDK camera is open (WebViewPoseView with the camera running, or web start()). Close that session first, or use a client that never mounts the SDK camera.
The light React Native package and the web packages still need network to load MoveNet. The offline React Native package does not, for keypoints. Exercises follow the usual API-key rules — API key.
On React Native this mode does not emit stats, quality_changed, or performance_warning. Those events drive the SDK camera (resolution changes, reopening getUserMedia). The hidden page must not touch a camera. On the web, stats still fires about once a second.
React Native
NoWebViewPoseView in this tree. Your camera component is the preview. PoseTracker only returns data.
Screen in <PoseTrackerProvider apiToken="YOUR_API_KEY">. Omit the token for keypoints only.
Vision Camera
react-native-vision-camera owns the preview. Two ways to feed PoseTracker:
- Real-time: in a frame processor, resize so the longest side is ≤ 256, JPEG-encode, then hop to JS and
await processFrame. Run the processor around 12 fps (runAtTargetFps). The encode step is native code (a frame-processor plugin). PoseTracker accepts the JPEG, not the raw camera buffer. - Simple loop:
takeSnapshotplusexpo-image-manipulatorto resize. This is a few frames per second, not a 30 fps path. Useful to wire the screen before the encoder exists.
takeSnapshot returns pixels that may already be mirrored (photo.isMirrored). Pass mirrored: !photo.isMirrored when you draw on the Vision Camera preview (the preview is mirrored for the front camera; a mirrored file already matches it).
frame itself. A raw RGB buffer or a vision-camera-resize-plugin array is not a processFrame input.
expo-camera
CameraView is the preview. takePictureAsync goes through the shutter path, so treat it as a slow loop and still resize. Set mirror on CameraView so the saved JPEG matches the front-camera preview, then pass mirrored: false. (mirror on takePictureAsync is deprecated.)
expo-camera here is the camera, not only the permission API. The SDK quickstart uses expo-camera only to ask for permission, then lets WebViewPoseView capture. Do not mount both capture paths.
CameraX, AVFoundation, or another native pipeline
The SDK call stays in JavaScript. The native layer’s job is to write a JPEG (longest side ≤ 256) and hand it to JS:
Call
processFrame from JS and await it. A worklet or a camera callback that fires again before the promise resolves should drop that frame locally. The SDK drops it too, but you still pay for the bridge if you already injected the JPEG.
Web
Nomount(), no PoseCamera, no overlay. Draw your own <video> (CSS-mirrored if you want a selfie) and a canvas for the skeleton.
image (the canvas or an ImageBitmap) over base64. fetch + createImageBitmap on a data URL is the slow path.
requestAnimationFrame only schedules the next frame after the await, so a single loop stays at one frame in flight. A second loop calling processFrame at the same time receives dropped: true.
Stop with pt.stopExercise() and stream.getTracks().forEach((t) => t.stop()). Do not call pt.start() on this client while the pump is running.
React
Same client, no<PoseCamera />.
What comes back
Listeners andresult.events use the same event types as the camera path.
pose is { keypoints, score, timestampMs }. keypoints[i] is { name, x, y, score } with x and y in 0–1. Seventeen COCO points, same order as the camera path.
Without startExercise, you get keypoints and no counter. The engine is not reset between frames: two accepted frames of one squat can advance counter from 1 to 2.
Errors
A frame that arrives while one is running does not throw. It resolves
{ dropped: true, pose: lastPose, events: [] }.
What this is not
- Not
WebViewPoseView/PoseCamera. Those open the camera and can draw the skeleton. - Not Media sources.
source="image"andsetSource({ type: 'image' })are one shot, andsetSourceresets temporal state. - Not the hosted tracking URL, and not the deprecated pixel (
app.posetracker.com/pose_tracker/pixel). Nothing on that URL is part of this mode.