Skip to main content
Use this when the app already owns the camera. PoseTracker stays the data layer: it runs pose estimation and, once you start an exercise, tracks that movement across frames. You decide what to draw. This is an SDK API on all four packages. It is not the hosted iframe, and it is not a new camera view. The default path is unchanged: 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.
You keep the camera and the preview. PoseTracker does not open a camera in this mode, and it does not draw a skeleton. Send one image, wait until the call resolves, then send the next. A second call that arrives early is dropped. The result for that frame is the pose, plus any exercise events if you already started one.
  • 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.
That mapping is exact when the view shows the whole frame, same aspect ratio. If the preview crops (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.
A 1080p JPEG per frame is not a real-time path. On React Native the bytes cross the WebView bridge, then get decoded. Resize in the camera pipeline, then send the small JPEG. The SDK does not downscale for you.
Do not mix the two flows on one client. 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

No WebViewPoseView in this tree. Your camera component is the preview. PoseTracker only returns data.
Wrap Screen in <PoseTrackerProvider apiToken="YOUR_API_KEY">. Omit the token for keypoints only.
During an exercise, raw keypoints events are paused unless options.features.keypoints is on (paid plans). counter, posture, and the other exercise events still fire. Keypoints-only mode (no startExercise) always streams keypoints.

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: takeSnapshot plus expo-image-manipulator to 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-processor shape (real-time), once a native encoder gives you a small JPEG:
PoseTracker does not read 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

No mount(), no PoseCamera, no overlay. Draw your own <video> (CSS-mirrored if you want a selfie) and a canvas for the skeleton.
Prefer 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 and result.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" and setSource({ type: 'image' }) are one shot, and setSource resets 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.

Next

Last modified on October 16, 2026