# useWebcam

useWebcam [#usewebcam]

`useWebcam` is a React hook that owns a `getUserMedia` video stream. `start()`
acquires the camera (or re-acquires after a denial — the retry path), `stop()`
releases every track, and unmount cleanup guarantees the camera light goes off
with the surface. A runtime permission denial surfaces as a recoverable `error`
(`permission` / `hardware` / `unknown`) rather than a thrown exception, so you can
render a retry action.

It returns `{ stream, isActive, error, start, stop, clearError }` and exports the
`WebcamError` / `WebcamErrorKind` types. It imports only React and browser APIs —
no dependencies, no `@localmode/*`.

> **Pairs with [Video Canvas](/docs/media-vision/video-canvas).** The Video Canvas
> is deliberately a shell ("the app owns acquisition + permissions"); this hook
> owns acquisition. Feed the returned `stream` into your `<video>` and drive an
> overlay from a vision model.

**When to use it:** any webcam-driven surface — object detection on a still, live
landmark/skeleton tracking, or a camera preview — where you want a small,
portable acquisition lifecycle with honest error handling.

Preview [#preview]

```tsx
'use client';

import { useEffect, useRef } from 'react';

import { useWebcam } from '@/components/use-webcam';

/**
 * Demo for useWebcam, used by the docs live preview behind a Run gate — nothing
 * touches `getUserMedia` until you click Start, so no camera prompt fires on
 * mount. Start acquires the stream; Stop releases every track (camera light off).
 */
export default function UseWebcamDemo() {
  const { stream, isActive, error, start, stop, clearError } = useWebcam({
    width: 640,
    height: 480,
  });
  const videoRef = useRef<HTMLVideoElement | null>(null);

  useEffect(() => {
    if (videoRef.current) videoRef.current.srcObject = stream;
  }, [stream]);

  return (
    <div className="flex w-full max-w-md flex-col items-start gap-3">
      <div className="flex items-center gap-2 text-xs">
        <button
          type="button"
          onClick={() => void start()}
          disabled={isActive}
          className="rounded-md border border-border px-3 py-1.5 font-medium hover:bg-muted disabled:opacity-50"
        >
          Start camera
        </button>
        <button
          type="button"
          onClick={stop}
          disabled={!isActive}
          className="rounded-md border border-border px-3 py-1.5 font-medium hover:bg-muted disabled:opacity-50"
        >
          Stop
        </button>
      </div>

      {error && (
        <p
          role="alert"
          className="flex items-center gap-2 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-xs text-destructive"
        >
          {error.message}
          <button type="button" onClick={clearError} className="font-medium underline">
            Dismiss
          </button>
        </p>
      )}

      <video
        ref={videoRef}
        autoPlay
        muted
        playsInline
        className="aspect-video w-full max-w-sm -scale-x-100 rounded-md border border-border bg-muted"
      />
    </div>
  );
}
```

Installation [#installation]

```bash
npx shadcn@latest add @localmode/ui/media-vision/use-webcam
```

The hook installs to `hooks/use-webcam.ts` (the `registry:hook` install target).

Dependencies [#dependencies]

* **Data source:** none — it reads `navigator.mediaDevices.getUserMedia` directly. Works in any React app in a secure context.
* No npm or registry dependencies.

Files installed [#files-installed]

* `hooks/use-webcam.ts` — the hook

API [#api]

**UseWebcamOptions**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `width` | `number` | `1280` | Ideal capture width in pixels. |
| `height` | `number` | `720` | Ideal capture height in pixels. |

**WebcamError**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `kind` | `WebcamErrorKind` | — | **Required.** |
| `message` | `string` | — | **Required.** |

Examples [#examples]

Acquire and render a preview [#acquire-and-render-a-preview]

```tsx
import { useEffect, useRef } from 'react';
import { useWebcam } from '@/hooks/use-webcam';

export function CameraPreview() {
  const { stream, isActive, error, start, stop, clearError } = useWebcam({ width: 640 });
  const videoRef = useRef<HTMLVideoElement | null>(null);
  useEffect(() => {
    if (videoRef.current) videoRef.current.srcObject = stream;
  }, [stream]);

  return (
    <div>
      <button onClick={() => start()} disabled={isActive}>Start</button>
      <button onClick={stop} disabled={!isActive}>Stop</button>
      {error && (
        <p role="alert">
          {error.message} <button onClick={clearError}>Dismiss</button>
        </p>
      )}
      <video ref={videoRef} autoPlay muted playsInline />
    </div>
  );
}
```

Retry after a permission denial [#retry-after-a-permission-denial]

Because `start()` re-acquires, wiring the error's retry to `start()` is enough:

```tsx
{error?.kind === 'permission' && <button onClick={() => start()}>Retry</button>}
```

Customization [#customization]

The hook maps `NotAllowedError` / `SecurityError` to `permission`,
`NotFoundError` / `NotReadableError` / `OverconstrainedError` to `hardware`, and
everything else to `unknown`. Because you own the copied file, change the
constraints (resolution, `facingMode`), the error copy, or add audio capture.