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

# Streaming over LAN

> Authenticate directly to Rhombus cameras and audio gateways on your local network for live video, recorded footage, and two-way audio.

## Overview

When your client is on the same network as a Rhombus device, it can pull live video, recorded footage, and audio straight from the device instead of through the Rhombus cloud. You get lower latency and use no internet bandwidth.

<Warning>
  **Devices on your LAN never accept API tokens.** Use your API token on your server to mint a short-lived **federated token**, then present the federated token to the device.
</Warning>

This guide covers:

* How API tokens and federated tokens differ, and which one to use where
* Minting a federated token and finding a device's LAN URIs
* Authenticating LAN requests with headers or query parameters
* Live H.264 and two-way audio over WebSocket, plus DASH/HLS for live and recorded video
* Browser limitations and troubleshooting

## API tokens vs federated tokens

These are two different credentials with different jobs.

| | API token | Federated token |
| - | - | - |
| **What it is** | Your integration's long-lived secret, created in the Rhombus Console | A short-lived token you mint with an API token via `POST /api/org/generateFederatedSessionToken` |
| **Lifetime** | Until you revoke it | The `durationSec` you request |
| **Permissions** | The role assigned to the API token | The same role as the API token that minted it, optionally limited to one device |
| **Headers** | `x-auth-scheme: api-token` and `x-auth-apikey` | `x-auth-scheme: federated-token` and `x-auth-ft` |
| **Query parameters** | Not supported | `?x-auth-scheme=federated-token&x-auth-ft=TOKEN` (media URLs and LAN devices) |
| **Rhombus cloud API** | Yes | Yes, with headers (not device-scoped tokens) |
| **Rhombus cloud media** (WAN stream URLs) | Yes, with headers (server-side clients) | Yes |
| **Devices on your LAN** | **No, never accepted** | Yes |
| **Where it may live** | Your server only | Browsers, players, and LAN clients |

A federated token is a bearer credential: anyone holding it can act with its role. **For LAN streaming, mint device-scoped tokens** with `deviceUUid`. A device-scoped token can only fetch that one device's media, and the cloud API rejects it. Also mint from an API token whose role is no broader than the viewer needs, and keep `durationSec` short.

<Note>
  The `x-auth-scheme` value is exactly `federated-token`. There is no `federated-session-token` scheme, and a federated token never goes in `x-auth-apikey`.
</Note>

## Prerequisites

<Note>
  * A Rhombus API key with permission to view the cameras or audio gateways you'll stream ([get one here](/index))
  * A client on the same network as the device, able to reach it on TCP port `8000`
  * A device that can reach the Rhombus cloud. The device checks every new token with Rhombus, so a device without internet access can't authorize new clients.
</Note>

## Implementation

<Steps>
  <Step title="Mint a federated token on your server">
    Call `generateFederatedSessionToken` with your API key. Your API key never leaves your server.

    <CodeGroup>
      ```python Python theme={null}
      import requests

      API_HEADERS = {
          "x-auth-scheme": "api-token",
          "x-auth-apikey": "YOUR_API_KEY",
          "Content-Type": "application/json",
      }

      response = requests.post(
          "https://api2.rhombussystems.com/api/org/generateFederatedSessionToken",
          headers=API_HEADERS,
          json={
              "durationSec": 3600,               # token lifetime in seconds
              "deviceUUid": "YOUR_CAMERA_UUID",  # recommended: only this device's media
          },
      )
      response.raise_for_status()
      federated_token = response.json()["federatedSessionToken"]
      ```

      ```javascript Node.js theme={null}
      const response = await fetch(
        'https://api2.rhombussystems.com/api/org/generateFederatedSessionToken',
        {
          method: 'POST',
          headers: {
            'x-auth-scheme': 'api-token',
            'x-auth-apikey': process.env.RHOMBUS_API_KEY,
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({ durationSec: 3600, deviceUUid: 'YOUR_CAMERA_UUID' })
        }
      );
      const { federatedSessionToken } = await response.json();
      ```

      ```bash cURL theme={null}
      curl -X POST "https://api2.rhombussystems.com/api/org/generateFederatedSessionToken" \
        -H "x-auth-scheme: api-token" \
        -H "x-auth-apikey: YOUR_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{"durationSec": 3600, "deviceUUid": "YOUR_CAMERA_UUID"}'
      ```
    </CodeGroup>

    Example response:

    ```json theme={null}
    {
      "federatedSessionToken": "Q2gP7m1kTnWb3sXyZ4aJ8w"
    }
    ```

    | Field | Notes |
    | - | - |
    | `durationSec` | Required. Request only as long as the session needs, and mint a new token before it expires. |
    | `deviceUUid` | Optional, recommended for LAN. Limits the token to media (LAN and WAN) for one device; the token can't call the cloud API. Note the casing: `deviceUUid`. |
    | `domain` | Optional. The browser origin allowed to use the token against Rhombus cloud media, as a full URL such as `https://app.example.com`. LAN devices don't check it, so use `deviceUUid` and a short `durationSec` to limit a LAN token. |

    The token can never do more than the API token that minted it. If that role only allows live video, the device serves live streams and refuses recorded footage.
  </Step>

  <Step title="Get the device's LAN URIs">
    On your server, call `POST /api/camera/getMediaUris` with `cameraUuid`, or `/api/audiogateway/getMediaUris` with `gatewayUuid`, using your API key. A device-scoped federated token can't call the cloud API.

    ```python Python theme={null}
    response = requests.post(
        "https://api2.rhombussystems.com/api/camera/getMediaUris",
        headers=API_HEADERS,
        json={"cameraUuid": "YOUR_CAMERA_UUID"},
    )
    response.raise_for_status()
    uris = response.json()
    ```

    | Field | What it is | Example |
    | - | - | - |
    | `lanLiveH264Uris` | Live H.264 over WebSocket (lowest latency) | `wss://192-168-1-50.lan.rhombussystems.com:8000/DEVICE_UUID/ws` |
    | `lanLiveOpusUris` | Live two-way audio over WebSocket | `wss://192-168-1-50.lan.rhombussystems.com:8000/DEVICE_UUID/audio` |
    | `lanLiveMpdUris` / `lanLiveM3u8Uris` | Live DASH / HLS | `https://192-168-1-50.lan.rhombussystems.com:8000/DEVICE_UUID/live/live.mpd` |
    | `lanVodMpdUrisTemplates` / `lanVodM3u8UrisTemplates` | Recorded footage from the device's own storage | `https://192-168-1-50.lan.rhombussystems.com:8000/DEVICE_UUID/store/{START_TIME}_{DURATION}/clip.mpd` |
    | `lanCheckUrls` | Reachability check | `https://192-168-1-50.lan.rhombussystems.com:8000/DEVICE_UUID/` |

    * Each field is an array because a device can have more than one LAN address. Use the first one your client can reach.
    * In the VOD templates, replace `{START_TIME}` with a Unix timestamp in **seconds** and `{DURATION}` with a length in seconds.
    * The LAN hostname is public DNS that resolves to the device's private IP, and the device serves a publicly trusted certificate for it. Connect using the hostname exactly as returned, not the raw IP, and keep normal TLS verification on. Devices in the EU region use `.lan.eu.rhombussystems.com` hostnames ([API Regions](/api-regions)).
  </Step>

  <Step title="Present the federated token to the device">
    Send the federated token on **every** request to the device, including each DASH/HLS segment and the WebSocket upgrade. Use whichever form your client supports:

    <Tabs>
      <Tab title="Headers">
        For native apps, servers, curl, and ffmpeg:

        ```text theme={null}
        x-auth-scheme: federated-token
        x-auth-ft: YOUR_FEDERATED_TOKEN
        ```
      </Tab>

      <Tab title="Query parameters">
        For browser `WebSocket` connections and players that can't set headers:

        ```text theme={null}
        ?x-auth-scheme=federated-token&x-auth-ft=YOUR_FEDERATED_TOKEN
        ```
      </Tab>
    </Tabs>

    The device checks the token with Rhombus: it must be unexpired, belong to the device's organization, and carry a role that can view this device. The device caches a successful check for up to 60 seconds, so an expired or revoked token can keep working for up to a minute.
  </Step>
</Steps>

## Code examples

### Live DASH manifest

```bash cURL theme={null}
TOKEN="YOUR_FEDERATED_TOKEN"

curl -H "x-auth-scheme: federated-token" -H "x-auth-ft: $TOKEN" \
  "https://192-168-1-50.lan.rhombussystems.com:8000/YOUR_CAMERA_UUID/live/live.mpd"
```

A valid token returns the DASH manifest (`200`). A missing, expired, or unauthorized token returns `401`.

### Recorded footage with ffmpeg

ffmpeg passes `-headers` on to every playlist and segment request. This saves 60 seconds of footage starting at `1767225600` (2026-01-01 00:00:00 UTC):

```bash ffmpeg theme={null}
ffmpeg -headers $'x-auth-scheme: federated-token\r\nx-auth-ft: '"$TOKEN"$'\r\n' \
  -i "https://192-168-1-50.lan.rhombussystems.com:8000/YOUR_CAMERA_UUID/store/1767225600_60/clip.m3u8" \
  -c copy clip.mp4
```

### Live H.264 over WebSocket

<CodeGroup>
  ```python Python theme={null}
  import asyncio
  import json
  from urllib.parse import urlencode

  import requests
  import websockets  # pip install websockets

  API_HEADERS = {
      "x-auth-scheme": "api-token",
      "x-auth-apikey": "YOUR_API_KEY",
      "Content-Type": "application/json",
  }
  CAMERA_UUID = "YOUR_CAMERA_UUID"


  def mint_device_token(device_uuid: str) -> str:
      response = requests.post(
          "https://api2.rhombussystems.com/api/org/generateFederatedSessionToken",
          headers=API_HEADERS,
          json={"durationSec": 3600, "deviceUUid": device_uuid},
      )
      response.raise_for_status()
      return response.json()["federatedSessionToken"]


  def lan_h264_uri(camera_uuid: str) -> str:
      response = requests.post(
          "https://api2.rhombussystems.com/api/camera/getMediaUris",
          headers=API_HEADERS,
          json={"cameraUuid": camera_uuid},
      )
      response.raise_for_status()
      uris = response.json().get("lanLiveH264Uris") or []
      if not uris:
          raise SystemExit("The camera reported no LAN address")
      return uris[0]


  async def watch(uri: str, token: str, max_frames: int = 30) -> None:
      auth = urlencode({"x-auth-scheme": "federated-token", "x-auth-ft": token})
      async with websockets.connect(f"{uri}?{auth}") as ws:
          frames = 0
          async for message in ws:
              if isinstance(message, str):  # one text init message comes first
                  print("init:", json.loads(message))
                  continue
              timestamp_ms, offset = None, 0
              while offset + 4 <= len(message):
                  tlv_type = message[offset]
                  length = int.from_bytes(message[offset + 1 : offset + 4], "big")
                  value = message[offset + 4 : offset + 4 + length]
                  offset += 4 + length
                  if tlv_type == 0x02:
                      timestamp_ms = int.from_bytes(value, "big")
                  elif tlv_type in (0x00, 0x01):  # frame data is always last
                      kind = "keyframe" if tlv_type == 0x00 else "delta"
                      print(f"{kind}: {len(value)} bytes at {timestamp_ms}")
                      frames += 1
                      break
              if frames >= max_frames:
                  break


  asyncio.run(watch(lan_h264_uri(CAMERA_UUID), mint_device_token(CAMERA_UUID)))
  ```

  ```javascript Browser theme={null}
  // federatedToken comes from your server (Step 1), never your API key
  const url = new URL(lanLiveH264Uri);
  url.searchParams.set('x-auth-scheme', 'federated-token');
  url.searchParams.set('x-auth-ft', federatedToken);

  const ws = new WebSocket(url);
  ws.binaryType = 'arraybuffer';
  ws.onmessage = (event) => {
    if (typeof event.data === 'string') {
      console.log('init', JSON.parse(event.data));
      return;
    }
    handleH264Message(event.data); // see the TLV format linked below
  };
  ```
</CodeGroup>

Example output from the Python script:

```text theme={null}
init: {'action': 'init', 'width': 1920, 'height': 1080, 'codec': 'h264', 'framerate': 15}
keyframe: 48213 bytes at 1791158465584
delta: 3120 bytes at 1791158465651
```

Each binary message is a sequence of TLV records (1-byte type, 3-byte big-endian length, value) ending with the H.264 frame data. The full type table, including embedded AI detections, is in [LAN Realtime Detection Overlay](/implementations/lan-realtime-detection-overlay). For a ready-made browser player, use `RhombusRealtimePlayer` with `connectionMode="lan"` from the [React SDK](/implementations/react-sdk).

## Audio over WebSocket

The `lanLiveOpusUris` socket carries audio in both directions and uses the same federated token authentication.

### Listening

Each binary message from the device holds three TLV records. Each record header is 1 bit (echo flag), 7 bits (type), and a 24-bit big-endian length, followed by the value.

| Type | Value |
| - | - |
| `0` | Sample count (2 bytes, big-endian) |
| `1` | Unix timestamp in milliseconds (8 bytes, big-endian) |
| `4` | One Opus packet, 48 kHz mono. The echo flag is set if the device was playing audio when this was captured. |

### Talking

To play audio on the device's speaker, send binary messages on the same socket. Each record is a 4-byte ASCII type, a 4-byte big-endian length, then the payload. Each audio record carries exactly 20 ms of audio.

| Type | Payload |
| - | - |
| `opus` | One Opus packet, 48 kHz mono |
| `pcm ` (trailing space) | 960 samples of 48 kHz mono signed 16-bit little-endian PCM (length 1920) |
| `ctrl` | 8-byte big-endian Unix timestamp in milliseconds to start playback. Send `0` to play every packet even if delayed, or `-1` to drop late audio and stay real time. |

By default the device plays every Opus packet, even late ones, and drops late PCM to stay real time.

## Browser apps

* **WebSocket streams** (`lanLiveH264Uris`, `lanLiveOpusUris`) work from any origin with the token in the query string.
* **LAN DASH and HLS** (`lanLive*` and `lanVod*` URLs) can't be loaded by browser JavaScript such as dash.js or hls.js on your own domain, because devices only allow Rhombus origins for cross-origin requests. Play LAN DASH/HLS from a native app or server, or use the WebSocket stream in the browser.
* Never put an API key in browser code. Mint federated tokens on your server and hand only those to the browser.

## Devices on older firmware

Devices running firmware released before mid-April 2026 only read the token from a cookie. If a native client must support them, also send:

```text theme={null}
Cookie: RSESSIONID=RFT:YOUR_FEDERATED_TOKEN
```

Current firmware accepts this cookie too, so a native client can send it alongside the headers.

## Troubleshooting

<AccordionGroup>
  <Accordion title="401 on every request">
    No token reached the device, `x-auth-scheme` isn't exactly `federated-token`, or you sent an API key instead of a federated token. Devices never accept API keys.
  </Accordion>

  <Accordion title="401 after working for a while">
    The federated token expired. Mint a new one before `durationSec` runs out.
  </Accordion>

  <Accordion title="401 for one device, fine for others">
    The token was minted with `deviceUUid` for a different device, or its role can't view this device.
  </Accordion>

  <Accordion title="Live works, recorded footage is refused">
    The token's role, inherited from the API key that minted it, only allows live video.
  </Accordion>

  <Accordion title="Connection closes with no response">
    The device couldn't reach the Rhombus cloud to check the token.
  </Accordion>

  <Accordion title="Works in curl, fails in a browser">
    Browsers block cross-origin DASH/HLS requests to the device. See [Browser apps](#browser-apps).
  </Accordion>

  <Accordion title="TLS or hostname errors">
    Use the LAN hostname from `getMediaUris`, not the raw IP address.
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="React SDK" icon="react" href="/implementations/react-sdk">
    Drop-in LAN and WAN players that handle federated tokens for you.
  </Card>

  <Card title="LAN Realtime Detection" icon="vector-square" href="/implementations/lan-realtime-detection-overlay">
    Full H.264 TLV format and AI bounding-box overlays.
  </Card>

  <Card title="Streaming Video" icon="video" href="/implementations/streaming-video">
    Thumbnails, shared streams, and recorded clips through the cloud.
  </Card>

  <Card title="Retrieving Audio" icon="microphone" href="/implementations/retrieving-audio">
    Audio gateway streams and recordings.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.