Skip to content

<PoseCamera>: props ​

tsx
import { PoseCamera, type PoseCameraRef } from 'react-native-pose-detection';

<PoseCamera ref={cam} style={{ flex: 1 }} />

What runs today ​

Every prop on this page is implemented on both platforms, and the reference-parity test fails the build if one of them goes missing from the reference pages.

Layout ​

PropTypeNotes
styleStyleProp<ViewStyle>An ordinary React Native view style. The preview fills the view and the overlay is drawn inside it, so { flex: 1 } is the usual answer and a fixed height is the other one.

The preview's aspect ratio comes from the camera rather than from this style, so a view whose shape does not match it is filled edge to edge and the camera frame is cropped evenly on whichever side overflows, never stretched. The skeleton is projected the same way, so it stays on the body whatever the view's shape. Landmarks are normalized against the analysis frame either way, so nothing about the layout moves them.

Configuration ​

PropTypeDefaultNotes
profile'auto' | 'efficient' | 'balanced' | 'quality' | 'unrestricted''auto'performance
facing'auto' | 'front' | 'back''auto'auto prefers front, falls back to the other lens on the first bind
delegate'auto' | 'gpu' | 'cpu''auto'auto verifies GPU, falls back to CPU. On Android it starts on the CPU and moves to the GPU once that has built
targetFps'auto' | number'auto'a number replaces the governed rate, capped by the camera and by what the device can finish. performance
resolution'auto' | '480p' | '720p' | '1080p''auto'preview
analysisResolution'auto' | '360p' | '480p' | '720p''auto'what the model sees
thermalPolicy'adaptive' | 'critical-only' | 'off''adaptive'off stops the response, not the reporting
maxPosesnumber (1 to 5)1above 1, triggers, frames and the overlay use the primary pose: largest box, ties by distance from center. A ceiling, not a promise: pair it with minConfidence
minConfidencenumber (0.1 to 1)from maxPoseshow sure the model has to be before it calls something a body. Rebuilds the landmarker when it changes
smoothing'auto' | boolean | { minCutoff, beta }'auto'One Euro filter over x, y and z. 'auto' is off for one pose, which MediaPipe already smooths, and on for several. See below

Any explicit value pins that axis. The rest stay automatic.

Switches ​

PropTypeDefault
activebooleantrue: camera on/off
detectionbooleantrue: inference on/off
overlayboolean | OverlayConfigtrue
ts
type OverlayConfig = {
  landmarks?: boolean;       // the joints, default true
  connections?: boolean;     // the bones between them, default true
  color?: string;            // '#RRGGBB', '#AARRGGBB' or a color name, default '#00E5FF'
  lineWidth?: number;        // points, default 3
  pointRadius?: number;      // points, default 4
  minVisibility?: number;    // hide low-confidence joints, default 0.5
  only?: readonly JointName[];          // draw a subset
  angles?: readonly AngleOverlay[];     // draw angle arcs + degree labels
};

type AngleOverlay = {
  joint: AngleJointName;    // vertex of the angle
  label?: boolean;          // show the degree value, default true
  radius?: number;          // arc radius in points, default 40
  color?: string;           // defaults to the overlay color
  decimals?: number;        // 0 to 3, default 0
  minVisibility?: number;   // hide when tracking is poor, default 0.5
};

Native clamps every number here rather than trusting it, because an overlay config is the kind of thing that gets built from app state and skips the type checker. lineWidth and pointRadius cannot go below 0, radius cannot go below 1, minVisibility is clamped to 0 to 1, and decimals is capped at 3: the degree label is rebuilt on the draw path every frame, so a larger value would only make that string longer.

Angle overlay ​

tsx
<PoseCamera
  overlay={{
    angles: [
      { joint: 'leftElbow' },                    // arc + "46°"
      { joint: 'leftKnee', label: false },       // arc only
      { joint: 'rightKnee', color: '#ff5252' },
    ],
  }}
/>

Drawn natively, nothing crosses to JavaScript. Declaring a joint here is one of the three things that turn its angle on, along with an angle condition in a trigger and naming it in data.angles. Nothing else does: a joint used as a comparison bound, or listed in data.select, is a position, and computing its angle would be work nobody asked for.

AngleJointName is the 12 joints where two limb segments meet, see types. { joint: 'nose' } does not compile.

See camera control for how the three switches combine.

Data ​

angles and select are resolved in JavaScript; frames are encoded natively into a ring buffer and drained in one zero-copy read per emission. An unknown mode, or a joint name select or angles does not know, throws PoseConfigError during render with its path.

ts
data?: {
  mode?: 'off' | 'throttled' | 'batched' | 'live';   // default 'off'
  throttleMs?: number;      // 'throttled' only, default 100
  flushMs?: number;         // 'batched' only, default 500
  landmarks?: boolean;      // default true
  worldLandmarks?: boolean; // default false
  angles?: boolean | readonly AngleJointName[];
  select?: readonly JointName[];
};

mode is optional. Leaving data off entirely and leaving data.mode unset are the same thing, zero crossings per second.

angles: true computes all 12. An array computes only those. Triggers and overlay.angles add to whatever this asks for, so you do not have to repeat a joint you already referenced there.

select narrows the landmark buffer to exactly the joints you name, in the order you name them, and nothing widens it. Angles are computed natively from the full 33 landmarks before the buffer is narrowed, so asking for an angle on a joint you did not select costs nothing extra and does not smuggle that joint into the payload. Reading one that select left out throws from the accessor. See data delivery.

Triggers ​

ts
triggers?: readonly Trigger[];

Validated during render, so a bad config throws PoseConfigError at the call site rather than becoming a trigger that silently never fires. The evaluator runs on both platforms. See trigger schema.

Diagnostics ​

ts
// LogLevelConfig
logLevel?: LogLevel | Readonly<Partial<Record<LogCategory, LogLevel>>>;   // default 'off'
onLog?: (entries: readonly LogEntry[]) => void;

logLevel raises the level on top of setLogLevel() while this camera is mounted, and gives it back when the camera unmounts or the prop is removed. The level itself is global, so the raise covers everything that logs meanwhile, not only this camera; without the prop the camera leaves it alone. setLogLevel() sets the level for the whole app. Both it and this prop, during render, throw PoseConfigError on an unknown level or category rather than doing nothing, because a silently ignored level looks exactly like a bug in whatever you were trying to diagnose.

Entries reach Logcat, or os.Logger on iOS, whatever is attached, and are batched to JavaScript while a listener is: onLog on a mounted camera, or addLogListener(), which hears photo detections and exports with no camera on screen too. addLogListener() is a multiset rather than a set, so the same function registered twice needs two remove() calls. See troubleshooting.

Callbacks ​

See events.

Smoothing ​

Landmarks jitter a little from frame to frame, even on a body that is standing still. A One Euro filter trades that jitter against lag by moving its cutoff with speed: a still body is filtered hard, where jitter shows and lag does not, and a fast one is barely filtered, where lag shows and jitter does not.

With maxPoses: 1, MediaPipe already runs exactly this filter inside the landmarker, so a second one on top would only add lag. It skips its own filter when it tracks more than one body. That is what 'auto' follows: off for one pose, on for several, so the two feel the same.

ValueMeans
'auto' (default)Off at maxPoses: 1, on above it
true / falseOn or off whatever maxPoses is
{ minCutoff, beta }On, with your constants

The defaults are MediaPipe's own for pose landmarks, minCutoff: 0.05 and beta: 80, with speed measured in body spans per second rather than in frame widths, so a distant subject is smoothed like a near one. Lower minCutoff smooths a still body harder; higher beta gets out of the way of fast movement sooner. The filter starts over when the pose is lost, the camera switches, a different person becomes the largest body, or frames stop for longer than two and a half intervals at the current rate. Visibility is never smoothed: it is a confidence, not a position.

maxPoses and minConfidence ​

MediaPipe's landmarker is built around one primary subject, and the confidence a single subject wants is higher than the one that lets a second person be found at all. They are one decision, so leaving minConfidence out takes it from maxPoses:

maxPosesThreshold usedWhy
10.6one subject, tracked cleanly, with scenery not offered as a body
2 to 50.3the point where a second person appears rather than the first one twice
tsx
<PoseCamera />                                   {/* maxPoses 1, so 0.6 */}
<PoseCamera maxPoses={5} />                      {/* 0.3, because more than one was asked for */}
<PoseCamera maxPoses={5} minConfidence={0.4} />  {/* your number wins */}

Measured against pose_landmarker_full on a photo of two separated, mostly whole people:

maxPosesminConfidencePoses returned
1anything1
50.5, 0.41
50.32, one per person
50.23, the third a duplicate of the first
50.14, two of them duplicates

So 0.3 is where the default stops: below it the model starts returning the same body twice rather than finding anybody new. A person cropped by the frame with no torso or head in view is not found at any setting, because the detector that feeds the landmarker anchors on a torso.

The exact crossing point moves with the device. The same photo through the same model on an Android emulator needed 0.2 before the second person appeared, where a desktop found them at 0.3, because a body sitting near the threshold falls either side of it on small numeric differences between builds. Treat 0.3 as a starting point rather than a constant: if a body you can see is not being found, lower it and watch for the same skeleton appearing twice, which is the sign you have gone too far.

Both values are baked into the landmarker when it is built, so changing either rebuilds it. Put them in state that settles rather than state that moves.

MIT licensed. Built from the guides in the repository.