Skip to main content

Capture result & errors

onCapture always fires once per attempt

As of v0.16, onCapture fires exactly once for every capture attempt — success or failure. The result is a discriminated union keyed first on ok, then on type:

  • ok: true — the output is present (an uri/width/height to read).
  • ok: false — the attempt failed; the error is the same CameraError object handed to onError.

Every variant also carries a warnings array — a successful stitch can still come back with warnings worth surfacing.

Breaking change in v0.16

Before v0.16, onCapture fired on success only. Now it fires on failure too. Gate on ok before reading uri/width/height:

onCapture={(result) => {
if (!result.ok) {
handle(result.error); // CameraError
return;
}
save(result.uri); // narrowed to ok:true here
}}

onError still fires on failure as an unchanged mirror of the ok: false result, so existing error handling keeps working — you can adopt the new ok: false branch at your own pace.

CameraCaptureResult

type CameraCaptureResult =
// VARIANT 1 — single photo succeeded
| {
ok: true;
type: 'photo';
uri: string;
width: number;
height: number;
depthPath?: string; // iOS-only, captureDepthData opt-in
warnings: CaptureWarning[];
}
// VARIANT 2 — panorama succeeded
| {
ok: true;
type: 'panorama';
uri: string;
width: number;
height: number;
framesRequested: number;
framesIncluded: number;
framesDropped: number;
finalConfidenceThresh: number;
durationMs: number;
/** Which cv::Stitcher pipeline the batch finalize actually ran
* (after auto-resolution). Useful for a "Stitched as: scans" pill. */
stitchModeResolved?: 'panorama' | 'scans';
rRadians?: number; // DEV-only
tMeters?: number; // DEV-only
decisionRatio?: number; // DEV-only
debugSummary?: string; // DEV-only
keyframePaths?: string[]; // iOS-only
captureOrientation?: string; // iOS-only
warnings: CaptureWarning[];
}
// VARIANT 3 — the attempt failed (photo or panorama)
| {
ok: false;
type: 'photo' | 'panorama';
error: CameraError;
warnings: CaptureWarning[];
};
PanoramaCaptureResult

The success-panorama variant is also exported on its own:

import type { PanoramaCaptureResult } from 'react-native-image-stitcher';
// = Extract<CameraCaptureResult, { ok: true; type: 'panorama' }>

Field notes

  • framesRequested / framesIncluded / framesDropped — how many candidate frames the engine took in, how many survived the confidence filter, and how many it dropped. A large gap usually pairs with a LOW_FRAME_UTILIZATION warning.
  • finalConfidenceThresh — the confidence threshold the C+D progressive-retry loop settled on.
  • durationMs — wall-clock time from start to finalize.
  • stitchModeResolved — present once defaultStitchMode was 'auto' and the engine picked 'panorama' vs 'scans' at finalize.
  • rRadians / tMeters / decisionRatio / debugSummaryDEV-only diagnostics (the rotation/translation pose and the mode-decision ratio). Don't depend on them in production.
  • keyframePaths / captureOrientationiOS-only. The on-disk keyframe paths and the device orientation at capture.
  • depthPathiOS-only, present when the captureDepthData prop is on, the capture was non-AR, and the device delivered a depth map. Points at the <photo>.depth.bin sidecar saved next to uri (float32 metres + JSON header — see Photo depth sidecar for the format). Never present on Android or for AR captures.

Branch on ok first, then on type. Surface any warnings regardless of which success branch you land in.

import {
Camera,
type CameraCaptureResult,
} from 'react-native-image-stitcher';

function onCapture(result: CameraCaptureResult) {
// 1. Failure path — `error` mirrors what onError received.
if (!result.ok) {
reportFailure(result.error); // result.error: CameraError
return;
}

// 2. Success — surface any warnings (a clean stitch can still warn).
if (result.warnings.length > 0) {
showWarnings(result.warnings); // CaptureWarning[]
}

// 3. Branch on the output type.
if (result.type === 'photo') {
save(result.uri, result.width, result.height);
return;
}

// result.type === 'panorama' here
console.log(
`panorama ${result.framesIncluded}/${result.framesRequested} frames, ` +
`${result.framesDropped} dropped, ${result.durationMs}ms, ` +
`mode=${result.stitchModeResolved ?? 'n/a'}`,
);
save(result.uri, result.width, result.height);
}

<Camera onCapture={onCapture} />;

CaptureWarning

A warning means the capture succeeded but something about it is worth telling the user. Warnings ride on every CameraCaptureResult (including ok: false) as warnings: CaptureWarning[].

interface CaptureWarning {
code: CaptureWarningCode;
message: string;
// The three below appear only on LOW_FRAME_UTILIZATION:
framesRequested?: number;
framesIncluded?: number;
utilization?: number;
}
codeWhen it fires
LOW_FRAME_UTILIZATIONFewer than the threshold (default 70%) of captured frames survived the confidence filter — the panorama may be incomplete. Carries framesRequested / framesIncluded / utilization; the message is a filled-in template.
LATERAL_DRIFT_FINALIZEThe capture was auto-finalized early because the phone drifted sideways — only the pre-drift portion was stitched.
HIGH_PAN_SPEEDThe pan exceeded the recommended pace at some point (the live "too fast" cue fired); motion blur / thin overlap may have hurt the result.
Customising warning copy

The default warning strings live in DEFAULT_CAPTURE_WARNING_COPY and can be localised. See Internationalisation.

CameraError

onError receives a CameraError, and the ok: false CameraCaptureResult carries the same object on error. It's a class extending Error:

class CameraError extends Error {
code: CameraErrorCode;
cause?: unknown;
name: 'CameraError';
message: string;
}

Branch deterministically on code (toast vs retry vs report):

CodeMeaning
CAMERA_PERMISSION_DENIEDCamera permission was denied by the user.
CAMERA_DEVICE_UNAVAILABLENo usable camera device is available.
PHOTO_CAPTURE_FAILEDA tap-shutter single-photo capture failed.
PANORAMA_START_FAILEDThe panorama capture failed to start.
PANORAMA_FINALIZE_FAILEDThe panorama finalize step failed.
STITCH_NEED_MORE_IMGScv::Stitcher needs more input images to stitch.
STITCH_HOMOGRAPHY_FAILHomography estimation failed during stitching.
STITCH_CAMERA_PARAMS_FAILCamera parameter estimation/adjustment failed during stitching.
STITCH_LOW_QUALITYv0.16 — the native post-stitch validator rejected the output as disjoint / fragmented / mis-proportioned. Recoverable by re-capturing (carries "try again" copy).
STITCH_OOMOut of memory during stitching.
OUTPUT_WRITE_FAILEDWriting the output file to disk failed (e.g. a bad outputDir).
VISION_CAMERA_RUNTIMEvision-camera surfaced a non-transient runtime error (e.g. invalid format, recording cancelled, microphone-permission denied). The full underlying error is on .cause.
UNKNOWNAn unclassified failure.
onError={(err) => {
if (err.code === 'STITCH_NEED_MORE_IMGS') retryWithGuidance();
else report(err.code, err.message);
}}

onCaptureAbandoned — no output at all

Some in-progress captures are auto-abandoned by the SDK without producing output. When that happens, onCaptureAbandoned(reason) fires and onCapture does NOT fire for that attempt.

onCaptureAbandoned={(reason) => {
// reason: 'orientation-drift' | 'lateral-drift'
switch (reason) {
case 'orientation-drift':
// The device rotated across the accepted hold mid-capture.
return toast('Keep the phone in one orientation while panning.');
case 'lateral-drift':
// v0.16 — the phone moved sideways before enough frames to stitch.
return toast('Pan in one straight line — try again.');
}
}}
ReasonMeaning
'orientation-drift'A cross-mode rotation happened mid-capture (the device left the accepted hold).
'lateral-drift'v0.16 — the phone moved sideways before enough frames were captured to stitch.
Abandon vs lateral-drift finalize

There are two distinct lateral-drift outcomes. If the drift happens after enough frames exist, the capture is finalized early — you get a normal ok: true result with a LATERAL_DRIFT_FINALIZE warning. If the drift happens before enough frames, the capture is abandoned — you get onCaptureAbandoned('lateral-drift') and no onCapture.

Friendly copy for recoverable failures — userFacingStitchError

The STITCH_* codes are recoverable — the user can usually fix them by re-capturing. The SDK exports userFacingStitchError(code), which returns ready-to-show { title, message } copy for a host Alert/toast instead of the raw cv::Stitcher diagnostic, and returns null for every non-recoverable code (permission denied, device unavailable, generic finalize failure, unknown, …) so you fall back to your generic error UI.

import {
userFacingStitchError,
type UserFacingStitchError,
} from 'react-native-image-stitcher';

// UserFacingStitchError = { title: string; message: string }
function userFacingStitchError(
code: CameraErrorCode,
): UserFacingStitchError | null;
err.codeuserFacingStitchError(code)
STITCH_NEED_MORE_IMGS"Please pan more slowly" — not enough overlap; each frame needs to overlap the one before it.
STITCH_CAMERA_PARAMS_FAIL"Please pan more slowly" — the view moved too much; pivot in one spot, the 0.5× lens is especially sensitive (try 1×).
STITCH_HOMOGRAPHY_FAIL"Please pan more slowly" — frames couldn't be aligned; keep the phone level with more overlap.
STITCH_LOW_QUALITY"That didn't come out right" — the frames stitched but didn't form one clean image; try again, panning slowly in one direction.
STITCH_OOM"Try a shorter sweep" — try a shorter, narrower sweep (or 1× for wide scenes).
any non-recoverable codenull (show your own generic error UI)
import { userFacingStitchError } from 'react-native-image-stitcher';
import { Alert } from 'react-native';

onError={(err) => {
const friendly = userFacingStitchError(err.code);
if (friendly) Alert.alert(friendly.title, friendly.message);
else report(err.code, err.message);
}}

The mapping lives in the SDK so every consumer shows the same vetted guidance for the same failure. See Recipes → Friendly recoverable-stitch alerts.