> ## Documentation index
> Fetch the complete documentation index at: https://developer.rhombus.com/llms.txt
> Use it to discover all available pages before exploring further.

# Stream Live and Recorded Video from Cameras

> Embed live and recorded video from Rhombus cameras in your applications using HLS streams, thumbnails, and shared playback URLs delivered through the API.

## Overview

Rhombus cameras expose multiple ways to access video content through the public API:

- **Live streaming** for real-time monitoring, embedded as a Rhombus-hosted iframe
- **HLS manifests** (`.m3u8`) for direct playback in your own player
- **Thumbnail images** for dashboards and quick previews
- **Frame captures** at any historical timestamp

For downloading recorded footage as MP4 files, see [Recorded Video Access](https://developer.rhombus.com/implementations/streaming-video#recorded-video-access).

## Getting camera thumbnails

All Rhombus cameras automatically upload thumbnail images. These are perfect for dashboards, monitoring interfaces, and quick status checks.

### Thumbnail endpoint

Retrieve camera thumbnails using a GET request to:

```text
https://media.rhombussystems.com/media/{cameraUuid}/{mediaRegion}/snapshot.jpeg
```

> **Note**: Media URLs carry no credential, so authenticate every `media.rhombussystems.com` request. From a server, send your API key headers (`x-auth-scheme: api-token` and `x-auth-apikey`), or `Authorization: Bearer <token>` for an OAuth access token whose grant includes the `read` scope. In a browser, append a [federated token](https://developer.rhombus.com/authentication/federated-tokens) as query parameters instead: `?x-auth-scheme=federated-token&x-auth-ft=<token>` (clients that can set headers may send the same names as headers). See [Media and streams](https://developer.rhombus.com/authentication/media).

To get the `cameraUuid` and `mediaRegion` for a camera, call [`POST /api/camera/getMinimalCameraStateList`](https://developer.rhombus.com/api-reference/camera-webservice/get-minimal-camera-state-list). The response contains a `cameraStates` array; each entry has `uuid` and `mediaRegion` fields.

### Basic implementation

```bash cURL
# 1. List cameras to get UUIDs and media regions
curl -X POST \
  "https://api2.rhombussystems.com/api/camera/getMinimalCameraStateList" \
  -H "x-auth-scheme: api-token" \
  -H "x-auth-apikey: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{}'
```

```bash cURL
# 2. Fetch the thumbnail using a camera's UUID and media region
curl -X GET \
  "https://media.rhombussystems.com/media/{CAMERA_UUID}/{MEDIA_REGION}/snapshot.jpeg" \
  -H "x-auth-scheme: api-token" \
  -H "x-auth-apikey: YOUR_API_KEY" \
  --output thumbnail.jpg
```

### Getting frames at specific times

To get a JPEG image at a specific historical timestamp, call [`POST /api/video/getExactFrameUri`](https://developer.rhombus.com/api-reference/video-webservice/get-exact-frame-uri). The response contains a `frameUri`. It isn't signed and carries no credential: download it with the same authentication headers from a server, or append the federated-token query parameters in a browser.

```javascript
// Server-side: this uses your API key
async function getFrameAtTime(cameraUuid, timestampMs, apiKey) {
  // Get the frame URI (it isn't signed, so the download needs auth too)
  const frameUriResponse = await fetch('https://api2.rhombussystems.com/api/video/getExactFrameUri', {
    method: 'POST',
    headers: {
      'x-auth-scheme': 'api-token',
      'x-auth-apikey': apiKey,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      cameraUuid: cameraUuid,
      timestampMs: timestampMs // epoch milliseconds
    })
  });

  const { frameUri } = await frameUriResponse.json();

  // Fetch the actual JPEG
  const frameResponse = await fetch(frameUri, {
    headers: {
      'x-auth-scheme': 'api-token',
      'x-auth-apikey': apiKey
    }
  });

  return frameResponse.blob();
}
```

The request also accepts optional `downscaleFactor`, `jpgQuality`, and `permyriadCropX`/`permyriadCropY`/`permyriadCropWidth`/`permyriadCropHeight` fields (each in 1/10000ths of the image dimensions, where `10000` = 100%).

## Live video streaming

Rhombus offers two ways to play live video in your application:

1. **Embed the Rhombus player as an iframe** — easiest path; analytics and timeline events render automatically.
2. **Play the HLS manifest directly** — use your own player (`hls.js`, Safari native, etc.) for full UI control.

Both come from the same API call: [`POST /api/camera/createSharedLiveVideoStream`](https://developer.rhombus.com/api-reference/camera-webservice/create-shared-live-video-stream). The response contains both URLs:

| Field                       | Description                                                |
| --------------------------- | ---------------------------------------------------------- |
| `sharedLiveVideoStreamUrl`  | Hosted Rhombus player URL — embed in an iframe             |
| `sharedLiveM3U8StreamUrl`   | HLS (`.m3u8`) manifest — play directly with any HLS player |
| `sharedLiveVideoStreamUuid` | UUID of the created share, useful for revoking later       |

### Embedding shared streams as iframes (recommended)

Create a shared stream on your server, then drop the returned URL into an iframe. The browser needs no Rhombus credential because the link itself grants access: anyone who has it can watch, unless you create it with [`createProtectedSharedLiveVideoStream`](https://developer.rhombus.com/api-reference/camera-webservice/create-protected-shared-live-video-stream) (password-protected). Set `expirationTimeSecs` and delete the share when you no longer need it. See [Shared stream links](https://developer.rhombus.com/authentication/media#shared-stream-links).

```javascript
// Server-side: this uses your API key
async function createSharedStream(cameraUuid, apiKey) {
  const response = await fetch('https://api2.rhombussystems.com/api/camera/createSharedLiveVideoStream', {
    method: 'POST',
    headers: {
      'x-auth-scheme': 'api-token',
      'x-auth-apikey': apiKey,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ cameraUuid })
  });

  const data = await response.json();
  return data.sharedLiveVideoStreamUrl;
}
```

Embed the returned URL:

```html
<iframe
  allow="fullscreen"
  frameborder="0"
  height="100%"
  width="100%"
  src="SHARED_STREAM_URL_GOES_HERE">
</iframe>
```

### React component example

The component calls `/api/shared-stream`, a route on your own backend that calls `createSharedLiveVideoStream` with your API key and returns the response.

```jsx
import React, { useEffect, useState } from 'react';

const RhombusSharedStream = ({
  cameraUuid,
  width = "100%",
  height = "400px",
  options = {}
}) => {
  const [streamUrl, setStreamUrl] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

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

    (async () => {
      try {
        setLoading(true);
        // Your backend creates the share with its API key and returns the response
        const response = await fetch('/api/shared-stream', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ cameraUuid })
        });

        if (!response.ok) throw new Error('Failed to create shared stream');
        const data = await response.json();

        const url = new URL(data.sharedLiveVideoStreamUrl);
        Object.entries(options).forEach(([key, value]) => {
          url.searchParams.set(key, value);
        });

        if (!cancelled) {
          setStreamUrl(url.toString());
          setError(null);
        }
      } catch (err) {
        if (!cancelled) setError(err.message);
      } finally {
        if (!cancelled) setLoading(false);
      }
    })();

    return () => { cancelled = true; };
  }, [cameraUuid, options]);

  if (loading) return <div>Loading stream...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <iframe
      src={streamUrl}
      width={width}
      height={height}
      frameBorder="0"
      allow="fullscreen"
      title={`Camera ${cameraUuid} Stream`}
    />
  );
};

export default RhombusSharedStream;
```

### URL parameters for enhanced control

Customize the embedded player with URL parameters. All parameters can be set to `true` or `false`:

| Parameter         | Description                                             |
| ----------------- | ------------------------------------------------------- |
| `disableautoplay` | Enable or disable video autoplay upon loading           |
| `hideevents`      | Enable or disable the timeline and related events       |
| `realtime`        | Enable or disable real-time streaming by default        |
| `showheader`      | Enable or disable zoom & streaming buttons at the top   |
| `showfooter`      | Enable or disable camera name and timestamp at bottom   |
| `lowRes`          | Force low resolution on the video stream                |
| `performanceMode` | Force performance mode allowing GPU usage               |
| `t`               | Timestamp (epoch milliseconds) where video should start |

#### Parameter usage

Append parameters to the shared stream URL: `https://{{url}}/?{{variable}}=true&{{variable}}=false`

**Example:**

```text
https://console.rhombussystems.com/share/live/ehrF58ABSj2VT8QzXh7GVA?disableautoplay=true&hideevents=true&realtime=true
```

#### React usage with parameters

```jsx
<RhombusSharedStream
  cameraUuid="your-camera-uuid"
  options={{
    disableautoplay: 'true',
    hideevents: 'true',
    realtime: 'true',
    showheader: 'false',
    lowRes: 'false'
  }}
/>
```

### Embedding HLS streams directly

If you want to render live video in your own player rather than the hosted iframe, use `sharedLiveM3U8StreamUrl` from the same `createSharedLiveVideoStream` response. The manifest is a standard HLS `.m3u8`. It isn't signed: the URL contains only the share's ID, and anyone who has it can watch, so no `x-auth-apikey` header is needed. For a password-protected share, send `Authorization: Basic <base64(":" + password)>` (an empty user name).

> **Note**: The HLS URL is a bearer link to one camera: unless the share is password-protected, anyone who has it can watch until it expires or you delete the share. It expires only if you set `expirationTimeSecs` (a Unix time in seconds, not a duration). Treat it as a credential.

Safari plays HLS natively. For other browsers, use [hls.js](https://github.com/video-dev/hls.js):

```html
<video id="rhombus-live" controls autoplay muted style="width: 100%;"></video>

<script type="module">
  import Hls from 'https://cdn.jsdelivr.net/npm/hls.js@latest/dist/hls.min.js';

  async function startLive(cameraUuid) {
    // Your backend creates the share with its API key and returns the response
    const response = await fetch('/api/shared-stream', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ cameraUuid })
    });

    const { sharedLiveM3U8StreamUrl } = await response.json();
    const video = document.getElementById('rhombus-live');

    if (Hls.isSupported()) {
      const hls = new Hls();
      hls.loadSource(sharedLiveM3U8StreamUrl);
      hls.attachMedia(video);
    } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
      video.src = sharedLiveM3U8StreamUrl;
    }
  }

  startLive('YOUR_CAMERA_UUID');
</script>
```

To play a camera's own streams instead of a share, call [`getMediaUris`](https://developer.rhombus.com/api-reference/camera-webservice/get-camera-media-uris) on your server and give the player `wanLiveM3u8Uri` (or the DASH `wanLiveMpdUri`) with a federated token appended as query parameters. On the cloud hosts, HLS playlists and DASH manifests (live and recorded) carry that query string to every segment URL they list, so the one URL works in any player. API keys and `x-auth-org` are never copied into segment URLs. See [Media and streams](https://developer.rhombus.com/authentication/media#live-and-recorded-video).

Because creating a shared stream requires an API key, `/api/shared-stream` is a route on a server you control: it calls `createSharedLiveVideoStream` and passes only the resulting `sharedLiveM3U8StreamUrl` (or `sharedLiveVideoStreamUrl`) to the browser. Keep the API key out of client code.

## Recorded video access

To export recorded footage as a clip, use the public clip API. Clip creation is **asynchronous**:

1. Call [`POST /api/video/spliceV3`](https://developer.rhombus.com/api-reference/video-webservice/splice-v3) with the cameras and time range you want. Provide `deviceUuids` (array), `startTimeMillis` (epoch milliseconds), and `durationSec`. Set `audioIncluded: true` to mux in audio from an associated audio gateway. The response returns a `clipUuid`.
2. Poll [`POST /api/event/getClipsWithProgress`](https://developer.rhombus.com/api-reference/event-webservice/get-list-of-saved-clips-in-organization-with-current-progress) to track processing — each clip reports `percentComplete` and `status` — and retrieve the finished clip once it is ready.

```bash cURL
# 1. Request a clip for a time range
curl -X POST \
  "https://api2.rhombussystems.com/api/video/spliceV3" \
  -H "x-auth-scheme: api-token" \
  -H "x-auth-apikey: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{
    "deviceUuids": ["CAMERA_UUID"],
    "startTimeMillis": 1700000000000,
    "durationSec": 60,
    "title": "Incident clip"
  }'
```

```bash cURL
# 2. Poll for progress until the clip is ready
curl -X POST \
  "https://api2.rhombussystems.com/api/event/getClipsWithProgress" \
  -H "x-auth-scheme: api-token" \
  -H "x-auth-apikey: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{}'
```

As a convenience, the [Rhombus CLI](https://developer.rhombus.com/rhombus-cli) can also play recorded footage and download alert clips without wiring up this flow yourself:

```bash
# Open a camera's recorded footage in your browser, starting an hour ago
rhombus footage CAMERA_UUID --start "1h ago"

# Download the clip associated with a specific alert
rhombus alert download ALERT_UUID --output alert.mp4
```

For alert-driven workflows, configure a [webhook](https://developer.rhombus.com/webhooks) to deliver alert events to your service, then create a clip for the alert's time range.

To list recent alerts programmatically, use [`POST /api/event/getPolicyAlerts`](https://developer.rhombus.com/api-reference/event-webservice/get-recent-policy-alerts-for-organization) — the previous `getPolicyAlertsV2` endpoint is deprecated.

## Complete implementation example

A React component supporting both thumbnail and live-stream view types. Your API key stays on your backend, which serves three routes, each wrapping one Rhombus call: `/api/camera-states` (`getMinimalCameraStateList`), `/api/federated-token` (`generateFederatedSessionToken`, as in [Federated tokens](https://developer.rhombus.com/authentication/federated-tokens#mint-a-token), returning the mint response with its `expiresAtMs`; `durationSec` defaults to one hour and can't exceed 24 hours) and `/api/shared-stream` (`createSharedLiveVideoStream`):

```jsx
import React, { useEffect, useRef, useState, useCallback } from 'react';

const RhombusCameraViewer = ({
  cameraUuid,
  viewType = 'thumbnail', // 'thumbnail' | 'stream'
  autoRefresh = true,
  refreshInterval = 5000,
  streamOptions = {},
  width = "100%",
  height = "400px"
}) => {
  const [thumbnailUrl, setThumbnailUrl] = useState(null);
  const [streamUrl, setStreamUrl] = useState(null);
  const [cameraDetails, setCameraDetails] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);
  const lastObjectUrl = useRef(null);

  const getCameraDetails = useCallback(async () => {
    // Your backend calls getMinimalCameraStateList with its API key
    const response = await fetch('/api/camera-states', { method: 'POST' });

    if (!response.ok) throw new Error('Failed to list cameras');
    const data = await response.json();
    const camera = (data.cameraStates || []).find(c => c.uuid === cameraUuid);
    if (!camera) throw new Error('Camera not found');
    return camera;
  }, [cameraUuid]);

  // Your backend mints a short-lived federated token for this camera and
  // returns the mint response, including expiresAtMs
  const getFederatedToken = useCallback(async () => {
    const response = await fetch('/api/federated-token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ cameraUuid })
    });
    if (!response.ok) throw new Error('Failed to get a token');
    const { federatedSessionToken, expiresAtMs } = await response.json();
    return { token: federatedSessionToken, expiresAtMs };
  }, [cameraUuid]);

  const loadThumbnail = useCallback(async (camera, token) => {
    const auth = `x-auth-scheme=federated-token&x-auth-ft=${encodeURIComponent(token)}`;
    const response = await fetch(
      `https://media.rhombussystems.com/media/${camera.uuid}/${camera.mediaRegion}/snapshot.jpeg?${auth}`,
      { cache: 'no-store' }
    );
    if (!response.ok) throw new Error('Failed to load thumbnail');

    const blob = await response.blob();
    const url = URL.createObjectURL(blob);
    if (lastObjectUrl.current) URL.revokeObjectURL(lastObjectUrl.current);
    lastObjectUrl.current = url;
    setThumbnailUrl(url);
  }, []);

  const createSharedStream = useCallback(async () => {
    // Your backend creates the share with its API key and returns the response
    const response = await fetch('/api/shared-stream', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ cameraUuid })
    });
    if (!response.ok) throw new Error('Failed to create shared stream');

    const data = await response.json();
    const url = new URL(data.sharedLiveVideoStreamUrl);
    Object.entries(streamOptions).forEach(([key, value]) => {
      url.searchParams.set(key, value);
    });
    setStreamUrl(url.toString());
  }, [cameraUuid, streamOptions]);

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

    (async () => {
      try {
        setIsLoading(true);
        const camera = await getCameraDetails();
        if (cancelled) return;
        setCameraDetails(camera);

        if (viewType === 'thumbnail') {
          let auth = await getFederatedToken();
          await loadThumbnail(camera, auth.token);
          if (autoRefresh) {
            interval = setInterval(async () => {
              try {
                // Mint a new token a minute before this one expires
                if (Date.now() > auth.expiresAtMs - 60_000) auth = await getFederatedToken();
                await loadThumbnail(camera, auth.token);
              } catch (err) {
                setError(err.message);
              }
            }, refreshInterval);
          }
        } else if (viewType === 'stream') {
          await createSharedStream();
        }

        setError(null);
      } catch (err) {
        if (!cancelled) setError(err.message);
      } finally {
        if (!cancelled) setIsLoading(false);
      }
    })();

    return () => {
      cancelled = true;
      if (interval) clearInterval(interval);
      if (lastObjectUrl.current) {
        URL.revokeObjectURL(lastObjectUrl.current);
        lastObjectUrl.current = null;
      }
    };
  }, [viewType, autoRefresh, refreshInterval, getCameraDetails, getFederatedToken, loadThumbnail, createSharedStream]);

  if (error) {
    return (
      <div style={{ padding: '20px', border: '1px solid #ff6b6b', borderRadius: '4px' }}>
        <p>Error: {error}</p>
      </div>
    );
  }

  if (isLoading) {
    return <div style={{ padding: '20px', textAlign: 'center' }}>Loading camera...</div>;
  }

  return (
    <div style={{ width, height }}>
      {viewType === 'thumbnail' && thumbnailUrl && (
        <img
          src={thumbnailUrl}
          alt={cameraDetails?.name || `Camera ${cameraUuid}`}
          style={{ width: '100%', height: '100%', objectFit: 'cover' }}
        />
      )}
      {viewType === 'stream' && streamUrl && (
        <iframe
          src={streamUrl}
          width="100%"
          height="100%"
          frameBorder="0"
          allow="fullscreen"
          title={`Camera ${cameraUuid} Stream`}
        />
      )}
    </div>
  );
};

export default RhombusCameraViewer;
```

### Usage examples

```jsx
// Thumbnail view with auto-refresh
<RhombusCameraViewer
  cameraUuid="your-camera-uuid"
  viewType="thumbnail"
  autoRefresh={true}
  refreshInterval={3000}
  width="640px"
  height="480px"
/>

// Live stream with custom options
<RhombusCameraViewer
  cameraUuid="your-camera-uuid"
  viewType="stream"
  streamOptions={{
    disableautoplay: 'false',
    hideevents: 'false',
    realtime: 'true',
    showheader: 'true',
    showfooter: 'true'
  }}
  width="100%"
  height="600px"
/>
```

## Best practices

### Performance optimization

- **Thumbnail caching**: Cache thumbnails client-side and refresh on a sensible interval rather than polling continuously.
- **Lazy loading**: Load video content only when needed or when in viewport.
- **Quality selection**: Use `lowRes=true` on the iframe player for grids and dashboards.

### Security considerations

- **Keep API keys server-side**: Never expose `x-auth-apikey` in browser code. Call `createSharedLiveVideoStream` from your backend and pass only the resulting URLs to the client. For thumbnails and frames in the browser, use a [federated token](https://developer.rhombus.com/authentication/federated-tokens).
- **Keep federated tokens short-lived**: Mint them for minutes or hours (never more than 24 hours), read `expiresAtMs` from the mint response, and mint a new one before it passes. Revoke a token you no longer need with `POST /api/org/revokeFederatedSessionToken`.
- **HTTPS only**: Always use HTTPS for API calls and shared stream URLs.
- **Scope keys narrowly**: Limit each API key to the minimum permissions required.
- **Set expirations on shared streams**: Pass `expirationTimeSecs`, the Unix time in seconds when the link stops working (not a duration), so URLs don't outlive the use case. Without it, a shared link never expires.

### Error handling

```javascript
class RhombusVideoError extends Error {
  constructor(message, code, cameraUuid) {
    super(message);
    this.name = 'RhombusVideoError';
    this.code = code;
    this.cameraUuid = cameraUuid;
  }
}

async function fetchWithRetry(url, init, retries = 3) {
  for (let attempt = 1; attempt <= retries; attempt++) {
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), 10000);
    try {
      const response = await fetch(url, { ...init, signal: controller.signal });
      clearTimeout(timer);
      if (!response.ok) {
        throw new RhombusVideoError(`HTTP ${response.status}`, response.status, init?.cameraUuid);
      }
      return response;
    } catch (error) {
      clearTimeout(timer);
      if (attempt === retries) throw error;
      await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000));
    }
  }
}
```

### Accessibility

```jsx
<video
  controls
  aria-label={`Live feed from camera ${cameraName}`}
  poster={thumbnailUrl}
>
  <track kind="captions" src="captions.vtt" srcLang="en" label="English" />
  Your browser does not support the video tag.
</video>
```

## Troubleshooting

**Thumbnail not loading**

- Verify `cameraUuid` and `mediaRegion` came from a recent `getMinimalCameraStateList` response — `mediaRegion` can change.
- Check that the credential's role has access to the camera's location.
- In a browser, check the token: an expired or revoked federated token gets 401, and a token minted with `domain` gets 403 `DOMAIN_NOT_ALLOWED` on pages from any other origin.
- Confirm the camera is online (`connectionStatus` in the camera state response).

**Stream connection failures**

- Check network connectivity to `*.rhombussystems.com`.
- For HLS playback, confirm the browser supports HLS natively or that `hls.js` is loaded.
- Inspect firewall/proxy rules — corporate proxies frequently block media segment requests.

**Poor video quality**

- Pass `lowRes=true` for bandwidth-constrained clients.
- Check upstream bandwidth at the camera site.

## Next steps

- **[Video Player](https://developer.rhombus.com/implementations/video-player)**: Build a custom video player with HLS playback and timeline controls.
- **[LAN Realtime Detection](https://developer.rhombus.com/implementations/lan-realtime-detection-overlay)**: Parse AI bounding boxes from the LAN H.264 WebSocket stream and overlay them on live video.
- **[Streaming over LAN](https://developer.rhombus.com/implementations/lan-streaming)**: Stream live video, recorded footage, and audio directly from devices on your network.
- **[Backup Footage](https://developer.rhombus.com/implementations/backup-footage-local-storage)**: Download and archive camera footage to local storage.
