MeeeetupSDK documentationJA

Quick start (React)

@meeeetup/camera-web ships a provider that owns the camera, the detector and the capture session, plus components that read from it. Everything below runs client-side; nothing leaves the browser unless your own sink sends it.

pnpm add @meeeetup/camera-web

The shortest working camera

import { MeeeetUpCamProvider, CameraViewport, FaceGrid } from "@meeeetup/camera-web";

export function Reception() {
  return (
    <MeeeetUpCamProvider
      mode="passive"
      onBatchCapture={async (faces) => {
        await fetch("/capture", {
          method: "POST",
          headers: { "content-type": "application/json" },
          body: JSON.stringify({ images: faces.map((f) => f.dataUrl) }),
        });
      }}
    >
      <CameraViewport />
      <FaceGrid />
    </MeeeetUpCamProvider>
  );
}

That is a complete passive deployment: the viewport requests the camera and draws tracking boxes, every face gets its best frame selected, and onBatchCapture fires every ten seconds with whatever was selected.

Configuration as an object

Capture settings are plain data, so keep them in one object and spread it. This is the shape most deployments end up with — one module exporting the profile, one component consuming it:

import type { PassiveCaptureOnlyMeeeetUpCamProviderProps } from "@meeeetup/camera-web";

type CaptureConf = Omit<PassiveCaptureOnlyMeeeetUpCamProviderProps, "children">;

export const conf = {
  mode: "passive",
  minDetectionScore: 0.6,   // ignore what the detector is unsure about
  minFaceHeight: 0.18,      // ignore faces smaller than 18% of frame height
  minFrontalness: 55,       // ignore faces not turned towards the lens
  captureWindowMs: 2_000,   // search two seconds for the best frame
  cooldownMs: 30_000,       // then leave that person alone for 30s
  onBatchCapture: send,
} satisfies CaptureConf;
<MeeeetUpCamProvider {...conf}>
  <CameraViewport />
  <FaceGrid />
</MeeeetUpCamProvider>

Every field is optional except mode and the sink (onBatchCapture in passive, onCapture in interactive). Omit one and you get the shipped default — see Configuration for the full table.

Reading capture state

useMeeeetUpCam() returns the live state of the session. It is a discriminated union on mode, so narrow before touching mode-specific fields:

import { useMeeeetUpCam } from "@meeeetup/camera-web";

function CaptureStatus() {
  const cam = useMeeeetUpCam();

  if (cam.error) return <p role="alert">{cam.error}</p>;
  if (!cam.ready) return <p>Starting the camera…</p>;
  if (cam.mode !== "passive") return null;

  return (
    <div>
      <p>{cam.trackedCount} in frame · {cam.totalCount} captured this session</p>
      <button type="button" onClick={cam.flushBatch}>Send now</button>
      <button type="button" onClick={cam.rearm}>Capture everyone again</button>
    </div>
  );
}
Field Meaning
ready / error Camera and model status.
trackedCount Faces being tracked this instant.
totalCount Distinct faces selected at least once this session.
livePreviews Best crop so far per visible face — the thumbnails FaceGrid renders.
selectedFaces Faces buffered and waiting for the next batch.
flushBatch() Hand the buffer to onBatchCapture now — call it on page hide.
rearm() Restart every capture window, so faces already captured are searched again.
devices, currentDeviceId, setDeviceId, toggleCamera Camera selection, persisted in localStorage.

Components

All of them read the provider through context, so they take no props and can be placed anywhere inside it.

Component Renders
CameraViewport The <video> plus the tracking overlay canvas.
FaceGrid Live thumbnails, one per tracked face, updating as quality improves.
FaceCounter Tracked / captured counts.
DevicePicker Camera selector.
CameraToggle Front / back switch.
AlignmentRing, CaptureButton Interactive mode: the alignment guide and its shutter.

Need a layout the built-ins do not give you? Attach cam.videoRef and cam.overlayRef to your own elements and render the rest yourself — the provider does not care who owns the DOM.

Interactive mode

Same provider, different contract: one subject, one photo, taken when they hold still inside the ring.

const conf = {
  mode: "interactive",
  alignmentCaptureDelayMs: 2_000,
  onCapture: async (faceImage, fullFrame) => {
    await register(faceImage, fullFrame);
  },
} as const;

<MeeeetUpCamProvider {...conf}>
  <CameraViewport />
  <AlignmentRing />
  <CaptureButton />
</MeeeetUpCamProvider>

The capture gates in the table above are passive-mode settings: interactive capture is decided by the alignment ring, not by the tracker’s selection pipeline.