Petnow LogoPetnow

Basic Usage

How to integrate the camera UI with the usePetnowCamera hook and CameraView.

Full Example

import { useCallback, useState } from 'react';
import {
  usePetnowCamera,
  CameraView,
  type DetectionStatus,
  type CameraResult,
} from '@petnow/react-native-camera-ui';

function Scan({ apiKey, captureSessionId }) {
  const camera = usePetnowCamera({ apiKey });
  const [status, setStatus] = useState<DetectionStatus | null>(null);
  const [progress, setProgress] = useState(0);

  const onFinished = useCallback((r: CameraResult) => {
    if (r.success) upload(r.fingerprintImages, r.appearanceImages);
  }, []);

  // Mount CameraView only when ready.
  if (camera.state.status !== 'ready') {
    return <Loading state={camera.state} />;
  }

  return (
    <CameraView
      camera={camera}
      species="DOG"
      purpose="PET_PROFILE_REGISTRATION"
      captureSessionId={captureSessionId}
      style={{ flex: 1 }}
      onDetectionStatus={setStatus}
      onDetectionProgress={setProgress}
      onDetectionFinished={onFinished}
    />
  );
}

For how to layer UI such as guide text and buttons on top of <CameraView>, see Customization.

usePetnowCamera

const camera = usePetnowCamera({ apiKey, isDebugMode });

Stores apiKey in the native SDK and returns a controller handle with detection commands. When apiKey changes, it is stored again.

camera.state

The SDK readiness state. Mount <CameraView> only when status === 'ready'.

statusMeaning
idleapiKey is not set yet
initializingStoring the license in the native SDK
readyStorage complete — the camera can be mounted
errorThe native initialize call failed (message in state.error)

state tracks license storage, not validation. License validation is performed at view mount time (initializeCamera) on iOS, and a validation failure surfaces as failed in onDetectionStatus. Android has no license server validation (the key is retained for monitoring purposes). Mounting <CameraView> before ready is not supported.

Commands

Commands work after the bound <CameraView> has mounted.

MethodDescription
camera.startDetection()Start a detection session (or restart from the beginning — retake)
camera.pauseDetection()Pause detection (keeps the camera, preserves progress)
camera.resumeDetection()Resume paused detection
camera.switchCamera()Switch between front and rear cameras
camera.retry()Retry native initialization with the same apiKey (for recovering from a transient error)

CameraView

Binds to the controller and renders the native preview + detection overlay. Mounting initializes the camera, and unmounting cleans it up.

Props

PropTypeDescription
cameraPetnowCameraThe controller returned by usePetnowCamera
species'DOG' | 'CAT'Pet species (determines the detection pipeline)
purpose'PET_PROFILE_REGISTRATION' | 'PET_IDENTIFICATION' | 'PET_VERIFICATION'Capture purpose (determines the number of required images)
captureSessionIdstringServer-issued UUID (Server API)
difficultyMode?'EASY' | 'NORMAL' | 'HARD'If unspecified, the server selects it
enableFakeDetection?booleanFake-image detection (default false)
bracketingMode?booleanCapture bracketing — briefly burst-shoots around the adopted frame to collect a better fingerprint image (default false). Can be toggled at runtime (no remount needed)
styleViewStyleStandard RN view style

Changing the session settings (species/purpose/difficultyMode/enableFakeDetection) or captureSessionId automatically triggers a re-initialization flow in the native side on the same view — changing species/purpose/difficultyMode/enableFakeDetection recreates the controller, and changing captureSessionId reconfigures the capture runtime and detector. If you want a clearer transition in terms of UX, you can also re-mount the view with a React key.

Events

onDetectionStatus={(s: DetectionStatus) => { /* {type} or {type:'failed', reason} */ }}
onDetectionProgress={(p: number) => { /* 0 ~ 100 */ }}
onDetectionFinished={(r: CameraResult) => { /* {success, fingerprintImages, appearanceImages} */ }}
  • DetectionStatus.type: noObject | processing | detected | finished | failed (with reason in this case).
  • CameraResult: fingerprintImages / appearanceImages are arrays of local file:// URIs.

Two-Layer Lifecycle

The camera (view lifetime) is the outer layer, and detection is a sub-lifecycle that runs within it.

Camera:  mount ────────────────────────────── unmount
         (initializeCamera)                  (finalize)
Detection:   └ auto start → pause ⇄ resume → finished → retake(startDetection) ┘
  • The camera follows the view lifetime (mount = initializeCamera, unmount = finalize).
  • Detection starts automatically on mount; pause, resume, and retake (startDetection) afterward are controlled by hook commands (pauseDetection preserves progress).

Error / Permission Handling

There is no separate error channel. Failures surface in two places.

  • Initialization (license storage) failurecamera.state becomes error. Retry with camera.retry().
  • Camera/detection stage failure (invalid license (iOS), permission denied, camera open failure, capture failure) → { type: 'failed', reason } in onDetectionStatus.
function guideMessage(status: DetectionStatus | null): string {
  if (!status) return 'Initializing camera...';
  switch (status.type) {
    case 'failed':
      return `Recognition failed: ${status.reason}`;
    case 'noObject':
      return 'Center your pet on the screen';
    case 'finished':
      return 'Capture complete!';
    default:
      return '';
  }
}

When permission is denied, failed is delivered. Guide the user to grant the permission in Settings, then re-mount <CameraView>.

Retake / Continuous Capture

After onDetectionFinished, calling camera.startDetection() on the same view detects again from the beginning (retake). For a continuous flow, simply restart in the completion callback.

const onFinished = useCallback((r: CameraResult) => {
  upload(r);
  if (continuousMode) {
    setTimeout(() => camera.startDetection(), 1200); // restart after a short pause
  }
}, [camera, continuousMode]);

Result Handling / Upload

The fingerprintImages / appearanceImages from onDetectionFinished are arrays of local file:// URIs. Upload them from JS to your app server, then the server performs registration, verification, and identification via the Server API. The client only handles capture and upload (the flow is identical to the native test app).

Session Lifetime Automation

The camera is a single hardware resource, but RN views are frequently destroyed and recreated, so the session is managed by the package as a singleton rather than by the view. With a 1.5-second grace period after the last detach, the session is reused across transitions and remounts, and when captureSessionId changes, the capture runtime and detector are reconfigured with the new session ID while keeping the controller/session ownership structure intact. For details, see Introduction.

Attention Sounds (PetnowSound)

You can play attention sounds to draw a pet's attention independently of the camera (corresponding to the native SoundPlayer). It works even without a camera view.

import { PetnowSound } from '@petnow/react-native-camera-ui';

await PetnowSound.prepare();                 // Load samples (first time only)
const handle = PetnowSound.play('random', 'DOG'); // Play → returns a stop handle
// ...
PetnowSound.stop(handle);                    // Stop with the handle
PetnowSound.release();                        // Release native audio resources
MethodDescription
prepare()Load samples (Promise). Once before play. iOS resolves immediately (on-demand load)
play(sound, species)Returns a stop handle (number, ≥0) after playing. -1 if not prepared/not mapped
stop(handle)Stop with the handle
release()Release native audio resources
  • sound: 'random' (selects a candidate suitable for the species) | 'pouringFood' | 'bagOfChips' | 'openingCan' | 'squeakyToy' | 'pspsps' | 'shakers'
  • species: 'DOG' | 'CAT' — used for 'random' candidate selection (ignored for other sounds)

Next Steps

  • Customization — Layering guide, buttons, and result UI on top of <CameraView>

On this page