> ## 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.

# Stream EEG over Web Bluetooth

> Build a browser EEG flow that discovers a supported device, connects, and starts receiving frames.

Use this page if you already have a browser app and need the recommended
existing-app BLE transport path.

If you want the scaffold path instead, use [Quickstart](/sdk/tutorials/first-app). If you
only need the transport model first, use [Web Bluetooth With Supported Devices](/sdk/guides/web-bluetooth).

This tutorial builds on the browser EEG package and adds live Web Bluetooth
transport with `@elata-biosciences/eeg-web-ble`.

Use this when your app needs real headset data in Chrome or Edge.

This step **turns on Web Bluetooth** for a Muse-compatible headset on top of the
EEG stack. It comes **after** you are committed to EEG (and usually **after**
[rPPG](/sdk/guides/rppg-browser) if your product also uses camera biosignals).

## Before You Start

You need:

* Chrome or Edge
* `https://` or `localhost`
* Bluetooth enabled on the machine
* a supported Muse-compatible device
* `@elata-biosciences/eeg-web` installed alongside `@elata-biosciences/eeg-web-ble`

Safari and iOS are not supported for this browser BLE workflow.

## What You Will Build

You will:

1. install the BLE transport package
2. create a `BleTransport`
3. subscribe to frame and status events
4. connect and start streaming

## Step 1: Install The Packages

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

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

## Step 2: Transport module + connect (single paste)

Use one module so imports, handlers, and lifecycle stay together (for example
`src/headbandBle.ts`). Call `connectHeadband()` from a **button** click (browser
BLE requires a user gesture).

```ts headbandBle.ts theme={null}
import { AthenaWasmDecoder } from "@elata-biosciences/eeg-web";
import { BleTransport } from "@elata-biosciences/eeg-web-ble";

let transport: BleTransport | null = null;

export function createHeadbandTransport() {
  const next = new BleTransport({
    deviceOptions: {
      athenaDecoderFactory: () => new AthenaWasmDecoder(),
    },
  });

  next.onStatus = (status) => {
    console.log("status", status.state, status.reason);
  };

  next.onFrame = (frame) => {
    console.log("eeg samples", frame.eeg.samples.length);
  };

  return next;
}

export async function connectHeadband() {
  transport = createHeadbandTransport();
  try {
    await transport.startStreaming();
  } catch (error) {
    console.error("BLE start failed", error);
  }
}
```

Why include `athenaDecoderFactory` up front:

* it keeps Athena-compatible devices working
* it uses the supported decoder path from `@elata-biosciences/eeg-web`
* it avoids a common failure mode later when testing across device variants

`startStreaming()` is the recommended default because it handles the common
`connect()` plus `start()` sequence in one call and avoids a frequent mistake
where apps connect successfully but never actually begin streaming.

## Step 3: Turn Frames Into App State

Once `onFrame` starts firing, move the data into your own app state instead of
leaving it only in `console.log`.

The usual pattern is:

1. receive `HeadbandFrameV1` frames
2. extract EEG samples or metadata
3. compute or forward the values your app cares about
4. render charts, scores, or adaptation logic

## Step 4: Clean Up On Route Or Component Exit

When the current view is leaving, stop streaming so later reconnect attempts
start from a clean state. Add this next to the helpers above (same module as
`transport`). Append to **headbandBle.ts**:

```ts theme={null}
export async function stopHeadband() {
  if (!transport) return;
  await transport.stop();
  transport = null;
}
```

If you prefer finer-grained lifecycle control, you can still call
`connect()` and `start()` separately. The tutorial uses `startStreaming()`
because it is the safest default for most app integrations.

## Step 5: Handle Browser And Platform Constraints

If the chooser never appears or `navigator.bluetooth` is missing, check these
first:

* the page is running on `https://` or `localhost`
* you are using Chrome or Edge
* Bluetooth is enabled
* the target device is powered on and available

## Athena And Classic Devices

This repo supports:

* Muse 2 and Muse S classic BLE devices
* Muse S Athena protocol v2 devices

Including `athenaDecoderFactory` is the simplest supported way to keep both
flows covered.

## Common Problems

* `navigator.bluetooth` is undefined: unsupported browser or insecure context
* No device chooser appears: Bluetooth disabled or page not running on a secure origin
* Athena decoding fails: make sure you passed `athenaDecoderFactory`
* You need Safari or iOS support: this browser package is not the right path; use a native or bridge strategy

## Next

<CardGroup cols={3}>
  <Card title="eeg-web-ble Reference" icon="bluetooth" iconType="light" href="/sdk/eeg-web-ble/getting-started">
    Transport API and options
  </Card>

  <Card title="eeg-web Reference" icon="brain" iconType="light" href="/sdk/eeg-web/getting-started">
    EEG runtime and models
  </Card>

  <Card title="Web Bluetooth Guide" icon="book-open" iconType="light" href="/sdk/guides/web-bluetooth">
    Transport model overview
  </Card>
</CardGroup>
