> ## Documentation Index
> Fetch the complete documentation index at: https://docs.elata.bio/llms.txt
> Use this file to discover all available pages before exploring further.

# Add camera-based rPPG to an existing browser App

> Build a working browser rPPG flow with createRppgSession, diagnostics, and cleanup.

Use this page if you already have a browser app and want the recommended existing-app integration path.

If you want the scaffold path instead, use [Quickstart](/sdk/tutorials/first-app). If you only need the mental model first, use [rPPG In A Browser App](/sdk/guides/rppg-browser).

This tutorial shows the recommended app integration path for `@elata-biosciences/rppg-web`.

Camera rPPG is the **primary** browser integration for many Elata products. No headset required.

The core idea is simple: let `createRppgSession()` own the browser runtime, video processing loop, and diagnostics while your app owns the UI.

This is the usual **next tutorial** after [Build Your First Elata App](/sdk/tutorials/first-app) when you choose the **existing app** branch for camera rPPG. If you are **extending the scaffold** instead, stay on `rppg-demo` and use [rPPG In A Browser App](/sdk/guides/rppg-browser) plus [rppg-web](/sdk/rppg-web/getting-started).

## What You Will Build

You will:

1. install `@elata-biosciences/rppg-web`
2. request camera access
3. attach the stream to a `video` element
4. start `createRppgSession()`
5. read metrics and diagnostics
6. stop the session during cleanup

## Step 1: Install The Package

<CodeGroup>
  ```bash pnpm theme={null}
  pnpm add @elata-biosciences/rppg-web
  ```

  ```bash npm theme={null}
  npm install @elata-biosciences/rppg-web
  ```
</CodeGroup>

## Step 2: Prepare A Video Element

Your app needs a `video` element that can receive the camera stream.

```html index.html (or your component markup) theme={null}
<video id="camera" autoplay playsinline muted></video>
```

The important parts are:

* `autoplay` so playback can start once the stream is attached
* `playsinline` for mobile browser behavior
* `muted` to keep autoplay rules out of the way

## Step 3: Acquire Camera Access

```ts camera setup theme={null}
const videoEl = document.getElementById("camera") as HTMLVideoElement;

const stream = await navigator.mediaDevices.getUserMedia({
  video: { facingMode: "user" },
  audio: false,
});

videoEl.srcObject = stream;
await videoEl.play();
```

At this point your browser app should already be showing the camera preview.

## Step 4: Start `createRppgSession()`

```ts rPPG session theme={null}
import { createRppgSession } from "@elata-biosciences/rppg-web";

const session = await createRppgSession({
  video: videoEl,
  sampleRate: 30,
  backend: "auto",
  faceMesh: "off",
  onDiagnostics: (diagnostics) => {
    console.log("status", diagnostics.state.status);
    console.log("frames", diagnostics.framesSeen);
    console.log("samples", diagnostics.totalSamplesReceived);
    console.log("issues", diagnostics.issues);
  },
  onError: (error) => {
    console.error(error.code, error.message);
  },
});
```

This is the recommended starting point for most browser apps.

It handles:

* packaged WASM backend init
* frame capture
* ROI/session orchestration
* diagnostics emission
* cleanup support

## Step 5: Read Metrics In Your UI

```ts theme={null}
const metrics = session.getMetrics();
console.log(metrics);
```

In a real app you would poll or subscribe through your own UI state layer and show the values that matter to your product.

## Step 6: Clean Up Correctly

When the component, route, or page is leaving, stop the session and release the camera stream. Run these in order (same `session` and `stream` as above):

```ts theme={null}
await session.stop();
```

```ts theme={null}
for (const track of stream.getTracks()) {
  track.stop();
}
```

This matters more than it looks. It keeps later sessions from inheriting stale camera or runtime state.

## Full example (single paste)

Use this when you want one file to drop into a Vite + React app (for example replace the contents of `src/App.tsx`). It includes camera setup, session start, and cleanup on unmount.

```tsx App.tsx theme={null}
import { useEffect, useRef, useState } from "react";
import { createRppgSession, type RppgSession } from "@elata-biosciences/rppg-web";

export default function App() {
  const videoRef = useRef<HTMLVideoElement>(null);
  const sessionRef = useRef<RppgSession | null>(null);
  const streamRef = useRef<MediaStream | null>(null);
  const [line, setLine] = useState("Starting…");

  useEffect(() => {
    let cancelled = false;

    async function run() {
      const video = videoRef.current;
      if (!video) return;

      let stream: MediaStream;
      try {
        stream = await navigator.mediaDevices.getUserMedia({
          video: { facingMode: "user" },
          audio: false,
        });
      } catch {
        setLine("Camera permission denied.");
        return;
      }

      if (cancelled) {
        stream.getTracks().forEach((t) => t.stop());
        return;
      }

      streamRef.current = stream;
      video.srcObject = stream;
      await video.play().catch(() => undefined);

      const sampleRate = stream.getVideoTracks()[0]?.getSettings().frameRate ?? 30;

      try {
        const session = await createRppgSession({
          video,
          sampleRate,
          backend: "auto",
          faceMesh: "off",
          onDiagnostics: (d) => {
            setLine(`status=${d.state.status} backend=${d.backendMode}`);
          },
          onError: (e) => setLine(`${e.code}: ${e.message}`),
        });

        if (cancelled) {
          await session.dispose();
          return;
        }

        sessionRef.current = session;
      } catch (e) {
        setLine(e instanceof Error ? e.message : "Session failed");
      }
    }

    void run();

    return () => {
      cancelled = true;
      void sessionRef.current?.dispose();
      sessionRef.current = null;
      streamRef.current?.getTracks().forEach((t) => t.stop());
      streamRef.current = null;
    };
  }, []);

  return (
    <main style={{ padding: "1.5rem", maxWidth: 720 }}>
      <p>{line}</p>
      <video ref={videoRef} autoPlay playsInline muted style={{ width: "100%", borderRadius: 12 }} />
    </main>
  );
}
```

This example uses a React `ref` on `<video>` instead of `getElementById("camera")` from the steps above.

## What To Do With Diagnostics

The quickest useful app behavior is:

1. show whether the session is `running`, `degraded`, or `failed`
2. display human-readable guidance when `issues` appear
3. block publishing or scoring until your app has enough stable samples

If you want a higher-level app-facing state layer later, look at:

* `createManagedRppgSession()`
* `createRppgAppAdapter()`
* `createRppgAppMonitor()`

But start with plain `createRppgSession()` first.

## Common Problems

* `session.getDiagnostics().backendMode` is `unavailable`: your app is likely not loading the packaged WASM assets correctly
* Camera access fails: check browser permissions and `getUserMedia` support
* The session reaches terminal `failed`: recreate the session instead of trying to keep using the same poisoned processor
* You are trying to debug lower-level generated bindings first: start with `createRppgSession()` unless you are intentionally debugging the SDK itself

## Next

<CardGroup cols={3}>
  <Card title="rppg-web Reference" icon="heart-pulse" iconType="light" href="/sdk/rppg-web/getting-started">
    Package API and exports
  </Card>

  <Card title="rPPG In A Browser" icon="book-open" iconType="light" href="/sdk/guides/rppg-browser">
    Integration overview
  </Card>

  <Card title="Build Your First App" icon="rocket" iconType="light" href="/sdk/tutorials/first-app">
    Best first-time setup path
  </Card>
</CardGroup>
