MeeeetupSDK documentationJA

Types and schema

Every exported type of @meeeetup/camera-core@0.6.0, plus the React surface on top of it. Defaults are the values the constructor resolves when a field is omitted, not suggestions.

Session

class FaceCaptureSession {
  constructor(opts: FaceCaptureSessionOptions);

  readonly trackedCount: number;
  readonly pendingFaces: SelectedFace[];
  readonly activeTracks: readonly TrackedFace[];

  processDetections(detections: Detection[], frame: FrameBuffer): void;
  tick(): void;
  rearm(): void;
  flushBatch(): Promise<void>;
  dispose(): void;
}
Member When to call it
processDetections Once per video frame, with that frame’s detections and pixels.
tick Once per animation frame or timer: reaps tracks unseen for TRACK_STALE_MS, finalising anything they still held.
rearm Re-arms every capture window. No-op without captureWindowMs.
flushBatch Hands buffered faces to onBatchCapture now — call it on page hide. Passive mode also calls it on batchIntervalMs.
dispose Stops timers, drops tracks and buffer. Call on unmount.

FaceCaptureSessionOptions

interface FaceCaptureSessionOptions {
  sessionType: "interactive" | "passive";

  batchIntervalMs?: number;     // 10_000
  maxBufferedFaces?: number;    // 200
  maxFacesPerRequest?: number;  // 50
  cooldownMs?: number;          // 10_000
  minDetectionScore?: number;   // 0.5
  minFaceHeight?: number;       // 0
  minFrontalness?: number;      // 50
  captureWindowMs?: number;     // 0 — off
  noseLandmark?: NoseLandmark;  // "noseTip"

  onSelect?: (face: SelectedFace) => void;
  onTrackRemoved?: (trackId: string) => void;
  onBatchSent?: (count: number) => void;
  onBatchError?: (error: unknown) => void;
  onBatchCapture?: (faces: SelectedFace[]) => void | Promise<void>;
}
Field Type Default Meaning
sessionType "interactive" | "passive" required Passive buffers and batches; interactive hands you one face at a time.
batchIntervalMs number 10_000 Passive send cadence.
maxBufferedFaces number 200 Buffer ceiling while offline; oldest drop first.
maxFacesPerRequest number 50 Batch size; must not exceed the backend’s cap.
cooldownMs number 10_000 Minimum gap between captures of the same track; the re-arm delay with a window.
minDetectionScore number 0.5 Detector confidence floor, 0–1.
minFaceHeight number 0 Face-box height floor as a fraction of frame height.
minFrontalness number 50 Pose floor, 0–100.
captureWindowMs number 0 Best-shot window per track; 0 keeps the settle gate.
noseLandmark "noseTip" | "noseBase" "noseTip" Which landmark sits in keypoint slot [2].

Callbacks all default to no-ops; onBatchCapture is required in practice for passive mode, since it is the only output path.

Detector contract

Everything entering the pipeline is normalised to 0–1 against the frame, whatever the detector natively reports.

interface Detection {
  boundingBox: {
    originX: number;  // left edge,  0–1
    originY: number;  // top edge,   0–1
    width: number;    // 0–1
    height: number;   // 0–1
  };
  /** [0] rightEye · [1] leftEye · [2] noseTip (or noseBase on ML Kit) */
  keypoints: Array<{ x: number; y: number }>;
  score: number;      // detector confidence, 0–1
}

The keypoint order is a contract, not a convention: frontalness and direction both index it directly.

Frame contract

FrameBuffer is the only platform seam — the core never touches a DOM or native image.

interface FrameBuffer {
  readonly width: number;
  readonly height: number;
  toJpegBase64(quality?: number): string;
  cropFaceJpeg(
    cx: number, cy: number,
    bw: number, bh: number,
    quality?: number,
  ): string | null;
}

@meeeetup/camera-web implements it over a canvas and @meeeetup/camera-react-native over a cached snapshot file; implement it yourself only when wiring a detector or surface the SDK does not ship.

What you receive

interface SelectedFace {
  trackId: string;
  dataUrl: string;      // 256×256 JPEG data URI
  frontalness: number;  // 0–100
  lastSentAt: number;   // Unix ms
  createdAt: number;    // Unix ms, when the face was first seen
  final: boolean;       // false while a window may still replace it
}

interface LiveFacePreview {
  trackId: string;
  dataUrl: string;      // best crop so far
  frontalness: number;  // smoothed, 0–100
}

Tracked-face state

session.activeTracks exposes the tracker’s own state — read-only, and stable only within a frame. Overlays and progress indicators are what it is for.

interface TrackedFace {
  id: string;
  createdAt: number;
  cx: number; cy: number;                    // centroid, normalised
  dispCx: number; dispCy: number;            // last drawn position
  dispW: number; dispH: number;
  bestJpeg: string | null;                   // all-time best crop
  bestFrontalness: number;
  selectedJpeg: string | null;               // what the current window holds
  selectedFrontalness: number;
  currentFrontalness: number;                // this frame
  smoothedFrontalness: number;               // EMA, settle path only
  lastSeenAt: number; lastSentAt: number;
  pendingJpeg: string | null;                // settle path only
  pendingFrontalness: number;
  pendingStaleFrames: number;
  pendingConfirmedFrames: number;
  windowState: "idle" | "open" | "closed";
  windowElapsedMs: number;                   // window time actually accrued
  windowTickAt: number;
}

A windowed track fills selectedJpeg and leaves pendingJpeg null; a settle-path track does the opposite.

Geometry

function getFrontalness(
  kps: Array<{ x: number; y: number }>,
  faceWidth: number,
  faceHeight: number,
  opts?: { noseLandmark?: "noseTip" | "noseBase" },
): number; // 0–100

function getFaceDirection(
  kps: Array<{ x: number; y: number }>,
  faceWidth: number,
  opts?: { deadzone?: number }, // default 0.35
): FaceDirection;

interface FaceDirection {
  yaw: number;    // −1 viewer's left … +1 viewer's right
  pitch: number;  // −1 chin up … +1 chin down
  label: FaceDirectionLabel;
}

type FaceDirectionLabel =
  | "front" | "left" | "right" | "up" | "down"
  | "up-left" | "up-right" | "down-left" | "down-right";

Frontalness answers how usable is this frame; direction answers which way do I ask them to turn. Frontalness is pure pose and is never multiplied by Detection.score.

Constants

Exported so a consumer can reason about the pipeline without hard-coding numbers.

Constant Value Meaning
TRACK_MATCH_THRESHOLD 0.20 Max normalised centroid distance for a detection to join a track.
TRACK_STALE_MS 3000 Unseen longer than this and the track is reaped.
TRACK_CONFIRM_MS 300 A track must exist this long before it can be captured.
BOX_DISPLAY_MS 150 How long an overlay box outlives its last sighting.
PENDING_CONFIRM_THRESHOLD 50 Default minFrontalness.
MIN_PENDING_CONFIRMED_FRAMES 3 Frames above the floor before a capture may happen.
DEFAULT_MIN_DETECTION_SCORE 0.5 Default minDetectionScore.
TRACK_COOLDOWN_MS 10_000 Default cooldownMs.
WINDOW_RESUME_GAP_MS 250 Longest frame gap that still counts as continuous window time.
DEFAULT_MAX_BUFFERED_FACES 200 Default maxBufferedFaces.
DEFAULT_MAX_FACES_PER_REQUEST 50 Default maxFacesPerRequest.

Tracker primitives

Below the session, for consumers running their own loop:

function updateTracks(
  detections: Detection[],
  frame: FrameBuffer,
  tracks: TrackedFace[],
  onCommit: (track: TrackedFace) => void,
  options?: UpdateTracksOptions,
): void;

interface UpdateTracksOptions {
  cooldownMs?: number;       // TRACK_COOLDOWN_MS
  noseLandmark?: NoseLandmark;
  minFrontalness?: number;   // PENDING_CONFIRM_THRESHOLD
  captureWindowMs?: number;  // 0
  onSelect?: (face: SelectedFace) => void; // required with a window
}

function flushStale(
  tracks: TrackedFace[],
  onCommit: (track: TrackedFace) => void,
  onRemove: (trackId: string) => void,
): TrackedFace[];

function commitPending(track: TrackedFace, onSelect: (face: SelectedFace) => void): void;
function resetCaptureWindow(track: TrackedFace): void;
function finalizeCaptureWindow(track: TrackedFace, onSelect: (face: SelectedFace) => void): void;
function nmsFilter(faces: Detection[], iouThreshold?: number): Detection[];
function boxIoU(a: Detection["boundingBox"], b: Detection["boundingBox"]): number;

React surface

type MeeeetUpCamProviderProps =
  | InteractiveMeeeetUpCamProviderProps
  | PassiveCaptureOnlyMeeeetUpCamProviderProps;

interface PassiveCaptureOnlyMeeeetUpCamProviderProps {
  mode: "passive";
  children: React.ReactNode;
  onBatchCapture: (faces: SelectedFace[]) => void | Promise<void>;
  minDetectionScore?: number;
  minFaceHeight?: number;
  minFrontalness?: number;
  captureWindowMs?: number;
  cooldownMs?: number;
  mediapipeWasmUrl?: string;
}

interface InteractiveMeeeetUpCamProviderProps {
  mode: "interactive";
  children: React.ReactNode;
  onCapture: (faceImage: string, fullCircleImage: string) => void | Promise<void>;
  alignmentCaptureDelayMs?: number; // 2000
  devFaces?: string[];
}

useMeeeetUpCam() returns a union discriminated on mode:

interface PassiveState {
  mode: "passive";
  ready: boolean;
  error: string | null;
  videoRef: React.RefObject<HTMLVideoElement | null>;
  overlayRef: React.RefObject<HTMLCanvasElement | null>;
  devices: MediaDeviceInfo[];
  currentDeviceId: string | undefined;
  setDeviceId: (id: string | undefined) => void;
  currentFacingMode: "user" | "environment";
  toggleCamera: () => void;
  trackedCount: number;
  totalCount: number;
  selectedFaces: SelectedFace[];
  livePreviews: LiveFacePreview[];
  flushBatch: () => Promise<void>;
  rearm: () => void;
}

The interactive half carries the alignment-ring state instead: isFaceAligned, captureProgress, alignmentCountdown, faceBoundingBox, captureImage(), toggleFlip().

React Native surface

interface UseFaceCaptureSessionOptions
  extends Omit<FaceCaptureSessionOptions, "noseLandmark"> {
  cameraRef: React.RefObject<CameraLike | null>;
  detectionFps?: number;        // 15
  photoCadenceMs?: number;      // 1000
  snapshotQuality?: number;     // 60
  windowWidth?: number;
  windowHeight?: number;
  cameraFacing?: "front" | "back" | "external"; // "front"
}

interface UseFaceCaptureSessionReturn {
  ready: boolean;
  error: string | null;
  trackedCount: number;
  lastDetections: Detection[];
  lastSnapshot: SnapshotInfo | null;
  lastTimings: PipelineTimings | null;
  frameProcessor: ReadonlyFrameProcessor;
  flushBatch: () => Promise<void>;
}

noseLandmark is omitted on purpose: the hook pins it to "noseBase" because ML Kit has no nose tip, and overriding it would break score parity with the web SDK.